Created
May 31, 2015 09:54
-
-
Save rasjones/f347f148b9a8787049a6 to your computer and use it in GitHub Desktop.
Swagger Akka HTTP MicroServices Example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import akka.actor.ActorSystem | |
import akka.event.{Logging, LoggingAdapter} | |
import akka.http.scaladsl.Http | |
import akka.http.scaladsl.client.RequestBuilding | |
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ | |
import akka.http.scaladsl.marshalling._ | |
import akka.http.scaladsl.model._ | |
import akka.http.scaladsl.model.StatusCodes._ | |
import akka.http.scaladsl.server.Directives | |
import akka.http.scaladsl.server.Directives._ | |
import akka.http.scaladsl.unmarshalling.Unmarshaller.UnsupportedContentTypeException | |
import akka.http.scaladsl.unmarshalling._ | |
import akka.http.scaladsl.util.FastFuture | |
import akka.stream.{ActorFlowMaterializer, FlowMaterializer} | |
import akka.stream.scaladsl.{Flow, Sink, Source} | |
import com.typesafe.config.Config | |
import com.typesafe.config.ConfigFactory | |
import java.io.IOException | |
import org.json4s.{DefaultFormats, Formats} | |
import com.tecsisa.akka.http.swagger.SwaggerHttpService | |
import com.wordnik.swagger.model._ | |
import com.wordnik.swagger.annotations._ | |
import scala.annotation.meta._ | |
import scala.concurrent.{ExecutionContext, ExecutionContextExecutor, Future} | |
import scala.math._ | |
import spray.json.DefaultJsonProtocol | |
case class IpInfo( | |
@(ApiModelProperty @field)(value = "an ip address", required = true) | |
ip: String, | |
@(ApiModelProperty @field)(value = "country", required = false) | |
country: Option[String], | |
@(ApiModelProperty @field)(value = "city", required = false) | |
city: Option[String], | |
@(ApiModelProperty @field)(value = "latitude", required = false) | |
latitude: Option[Double], | |
@(ApiModelProperty @field)(value = "longtiude", required = false) | |
longitude: Option[Double] | |
) | |
case class IpPairSummaryRequest( | |
@(ApiModelProperty @field)(value = "ip1", required = true) | |
ip1: String, | |
@(ApiModelProperty @field)(value = "ip2", required = true) | |
ip2: String) | |
case class IpPairSummary( | |
@(ApiModelProperty @field)(value = "distance", required = false) | |
distance: Option[Double], | |
@(ApiModelProperty @field)(value = "ip info 1", required = true) | |
ip1Info: IpInfo, | |
@(ApiModelProperty @field)(value = "ip info 2", required = true) | |
ip2Info: IpInfo) | |
object IpPairSummary { | |
def apply(ip1Info: IpInfo, ip2Info: IpInfo): IpPairSummary = IpPairSummary(calculateDistance(ip1Info, ip2Info), ip1Info, ip2Info) | |
private def calculateDistance(ip1Info: IpInfo, ip2Info: IpInfo): Option[Double] = { | |
(ip1Info.latitude, ip1Info.longitude, ip2Info.latitude, ip2Info.longitude) match { | |
case (Some(lat1), Some(lon1), Some(lat2), Some(lon2)) => | |
// see http://www.movable-type.co.uk/scripts/latlong.html | |
val φ1 = toRadians(lat1) | |
val φ2 = toRadians(lat2) | |
val Δφ = toRadians(lat2 - lat1) | |
val Δλ = toRadians(lon2 - lon1) | |
val a = pow(sin(Δφ / 2), 2) + cos(φ1) * cos(φ2) * pow(sin(Δλ / 2), 2) | |
val c = 2 * atan2(sqrt(a), sqrt(1 - a)) | |
Option(EarthRadius * c) | |
case _ => None | |
} | |
} | |
private val EarthRadius = 6371.0 | |
} | |
trait IPService extends Directives { | |
def config: Config | |
val logger: LoggingAdapter | |
implicit def exec: ExecutionContextExecutor | |
implicit def fm: FlowMaterializer | |
implicit def sys: ActorSystem | |
import DefaultJsonProtocol._ | |
implicit val ipInfoFormat = jsonFormat5(IpInfo.apply) | |
implicit val ipPairSummaryRequestFormat = jsonFormat2(IpPairSummaryRequest.apply) | |
implicit val ipPairSummaryFormat = jsonFormat3(IpPairSummary.apply) | |
lazy val telizeConnectionFlow: Flow[HttpRequest, HttpResponse, Any] = | |
Http().outgoingConnection(config.getString("services.telizeHost"), config.getInt("services.telizePort")) | |
def telizeRequest(request: HttpRequest): Future[HttpResponse] = Source.single(request).via(telizeConnectionFlow).runWith(Sink.head) | |
def fetchIpInfo(ip: String): Future[Either[String, IpInfo]] = { | |
telizeRequest(RequestBuilding.Get(s"/geoip/$ip")).flatMap { response => | |
response.status match { | |
case OK => Unmarshal(response.entity).to[IpInfo].map(Right(_)) | |
case BadRequest => Future.successful(Left(s"$ip: incorrect IP format")) | |
case _ => Unmarshal(response.entity).to[String].flatMap { entity => | |
val error = s"Telize request failed with status code ${response.status} and entity $entity" | |
logger.error(error) | |
Future.failed(new IOException(error)) | |
} | |
} | |
} | |
} | |
@ApiOperation(httpMethod = "GET", response = classOf[IpPairSummary], value = "Fetch IP Address") | |
@ApiImplicitParams(Array( | |
new ApiImplicitParam(name = "ip", required = true, dataType = "String", paramType = "path", value = "IP Address") | |
)) | |
@ApiResponses(Array( | |
new ApiResponse(code = 400, message = "Bad request"))) | |
def getIpInfo(ip: String) = { | |
complete { | |
fetchIpInfo(ip).map[ToResponseMarshallable] { | |
case Right(ipInfo) => ipInfo | |
case Left(errorMessage) => BadRequest -> errorMessage | |
} | |
} | |
} | |
@ApiOperation(httpMethod = "POST", consumes="application/json", response = classOf[IpPairSummary], value = "Fetch Multiple IP Addresses") | |
@ApiImplicitParams(Array( | |
new ApiImplicitParam(name = "ipPairSummaryRequest", required = true, dataType = "IpPairSummaryRequest", paramType = "body", value = "IP Pair Summary Request") | |
)) | |
@ApiResponses(Array( | |
new ApiResponse(code = 400, message = "Bad Request"))) | |
def postIpInfo(ipPairSummaryRequest: IpPairSummaryRequest) = { | |
complete { | |
val ip1InfoFuture = fetchIpInfo(ipPairSummaryRequest.ip1) | |
val ip2InfoFuture = fetchIpInfo(ipPairSummaryRequest.ip2) | |
ip1InfoFuture.zip(ip2InfoFuture).map[ToResponseMarshallable] { | |
case (Right(info1), Right(info2)) => IpPairSummary(info1, info2) | |
case (Left(errorMessage), _) => BadRequest -> errorMessage | |
case (_, Left(errorMessage)) => BadRequest -> errorMessage | |
} | |
} | |
} | |
val routes = { | |
logRequestResult("akka-http-microservice") { | |
pathPrefix("ip") { | |
(get & path(Segment)) { ip => | |
getIpInfo(ip) | |
} ~ | |
(post & entity(as[IpPairSummaryRequest])) { ipPairSummaryRequest => | |
postIpInfo(ipPairSummaryRequest) | |
} | |
} | |
} | |
} | |
} | |
trait JsonMarshalling { | |
implicit def feum[A: Manifest](implicit formats: Formats, m: FlowMaterializer, ec: ExecutionContext): FromEntityUnmarshaller[A] = | |
PredefinedFromEntityUnmarshallers.stringUnmarshaller.flatMapWithInput { (entity, s) => | |
if (entity.contentType().mediaType == MediaTypes.`application/json`) | |
FastFuture.successful(org.json4s.native.Serialization.read[A](s)) | |
else | |
FastFuture.failed( | |
UnsupportedContentTypeException(ContentTypeRange(MediaRange(MediaTypes.`application/json`))) | |
) | |
} | |
implicit def tem[A <: AnyRef](implicit formats: Formats): ToEntityMarshaller[A] = { | |
val stringMarshaller = PredefinedToEntityMarshallers.stringMarshaller(MediaTypes.`application/json`) | |
stringMarshaller.compose(org.json4s.native.Serialization.writePretty[A]) | |
} | |
} | |
@Api(value = "/ip", description = "IPOperations") | |
class Service(implicit system: ActorSystem, executor: ExecutionContextExecutor, materializer: FlowMaterializer, val config: Config, val logger: LoggingAdapter) extends IPService { | |
override implicit def exec: ExecutionContextExecutor = executor | |
override implicit def fm: FlowMaterializer = materializer | |
override implicit def sys: ActorSystem = system | |
} | |
object AkkaHttpMicroservice extends App { | |
implicit val sys = ActorSystem() | |
implicit val exec = sys.dispatcher | |
implicit val materializer = ActorFlowMaterializer() | |
implicit val config = ConfigFactory.load() | |
implicit val logger: LoggingAdapter = Logging(sys, getClass) | |
val service = new Service | |
val apiviewerRoutes = | |
get { | |
pathPrefix("swagger") { | |
pathEndOrSingleSlash { | |
getFromResource("swagger/index.html") | |
} | |
} ~ | |
getFromResourceDirectory("swagger") | |
} | |
import scala.reflect.runtime.universe._ | |
val swaggerService = new SwaggerHttpService with JsonMarshalling { | |
override def apiTypes = Seq(typeOf[Service]) | |
override def apiVersion = "1.0" | |
override def baseUrl = "/" | |
override def docsPath = "api-docs" | |
override def apiInfo = Some(new ApiInfo("Spray-Swagger Sample", "Description", "Terms of Service", "Contact", "License", "License URL")) | |
override implicit def executor: ExecutionContextExecutor = scala.concurrent.ExecutionContext.global | |
override implicit val system: ActorSystem = sys | |
override implicit val formats: Formats = DefaultFormats | |
} | |
Http().bindAndHandle(swaggerService.swaggerRoutes ~ apiviewerRoutes | |
~ service.routes, config.getString("http.interface"), config.getInt("http.port")) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment