Skip to content

Instantly share code, notes, and snippets.

@rasjones
Created May 31, 2015 09:54
Show Gist options
  • Save rasjones/f347f148b9a8787049a6 to your computer and use it in GitHub Desktop.
Save rasjones/f347f148b9a8787049a6 to your computer and use it in GitHub Desktop.
Swagger Akka HTTP MicroServices Example
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