Skip to content

Commit

Permalink
Merge pull request #59 from blast-hardcheese/text-plain
Browse files Browse the repository at this point in the history
Support for text/plain, as well as a bugfix for root path matching
  • Loading branch information
blast-hardcheese authored May 25, 2018
2 parents feff57d + 9c72428 commit dea4a99
Show file tree
Hide file tree
Showing 11 changed files with 246 additions and 15 deletions.
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ fullRunTask(runExample, Test, "com.twilio.guardrail.CLI", """
--client --specPath modules/sample/src/main/resources/petstore.json --outputPath modules/sample/src/main/scala --packageName clients.akkaHttp --framework akka-http
--server --specPath modules/sample/src/main/resources/petstore.json --outputPath modules/sample/src/main/scala --packageName servers
--client --specPath modules/sample/src/main/resources/plain.json --outputPath modules/sample/src/main/scala --packageName tests.dtos
--client --specPath modules/sample/src/main/resources/contentType-textPlain.yaml --outputPath modules/sample/src/main/scala --packageName tests.contentTypes.textPlain
--server --specPath modules/sample/src/main/resources/raw-response.yaml --outputPath modules/sample/src/main/scala --packageName raw.server
--server --specPath modules/sample/src/test/resources/server1.yaml --outputPath modules/sample/src/main/scala --packageName tracer.servers --tracing
--client --specPath modules/sample/src/test/resources/server1.yaml --outputPath modules/sample/src/main/scala --packageName tracer.clients --tracing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ object ReadSwagger {
swagger <- Option(new SwaggerParser().read(absolutePath))
} yield rs.next(swagger))
.getOrElse {
println(s"${AnsiColor.RED}Requested json definition file ${rs.path}...${AnsiColor.RESET} is not found")
println(s"${AnsiColor.RED}Spec file ${rs.path} is either incorrectly formatted or missing.${AnsiColor.RESET}")
Monoid.empty[T]
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,13 @@ object AkkaHttpClientGenerator {
q"scala.collection.immutable.Seq[Option[HttpHeader]](..$args).flatten"
}

def build(methodName: String, httpMethod: HttpMethod, urlWithParams: Term, formDataParams: Option[Term], formDataNeedsMultipart: Boolean, headerParams: Term, responseTypeRef: Type, tracing: Boolean
def build(methodName: String, httpMethod: HttpMethod, urlWithParams: Term, formDataParams: Option[Term], textPlain: Boolean, formDataNeedsMultipart: Boolean, headerParams: Term, responseTypeRef: Type, tracing: Boolean
)(tracingArgsPre: List[ScalaParameter], tracingArgsPost: List[ScalaParameter], pathArgs: List[ScalaParameter], qsArgs: List[ScalaParameter], formArgs: List[ScalaParameter], body: Option[ScalaParameter], headerArgs: List[ScalaParameter], extraImplicits: List[Term.Param]
): Defn = {
val implicitParams = Option(extraImplicits).filter(_.nonEmpty)
val defaultHeaders = param"headers: scala.collection.immutable.Seq[HttpHeader] = Nil"
val fallbackHttpBody: Option[(Term, Type)] = if (Set(HttpMethod.PUT, HttpMethod.POST) contains httpMethod) Some((q"HttpEntity.Empty", t"HttpEntity.Strict")) else None
val textPlainBody: Option[Term] = if (textPlain) body.map(sp => q"TextPlain(${if (sp.required) sp.paramName else q"""${sp.paramName}.getOrElse("")"""})") else None
val safeBody: Option[(Term, Type)] = body.map(sp => (sp.paramName, sp.argType)).orElse(fallbackHttpBody)

val formEntity: Option[Term] = formDataParams.map { formDataParams =>
Expand All @@ -135,7 +136,7 @@ object AkkaHttpClientGenerator {
}
}

val entity: Term = formEntity.orElse(safeBody.map(_._1)).getOrElse(q"HttpEntity.Empty")
val entity: Term = formEntity.orElse(textPlainBody).orElse(safeBody.map(_._1)).getOrElse(q"HttpEntity.Empty")
val methodBody: Term = if (tracing) {
val tracingLabel = q"""s"$${clientName}:$${methodName}""""
q"""
Expand Down Expand Up @@ -172,7 +173,9 @@ object AkkaHttpClientGenerator {
// Placeholder for when more functions get logging
_ <- Target.pure(())

formDataNeedsMultipart = Option(operation.getConsumes).exists(_.contains("multipart/form-data"))
consumes = Option(operation.getConsumes).fold(List.empty[String])(_.asScala.toList)
textPlain = consumes.contains("text/plain")
formDataNeedsMultipart = consumes.contains("multipart/form-data")

// Get the response type
unresolvedResponseTypeRef <- SwaggerUtil.getResponseType(httpMethod, operation)
Expand Down Expand Up @@ -209,7 +212,7 @@ object AkkaHttpClientGenerator {
tracingArgsPre = if (tracing) List(ScalaParameter.fromParam(param"traceBuilder: TraceBuilder")) else List.empty
tracingArgsPost = if (tracing) List(ScalaParameter.fromParam(param"methodName: String = ${Lit.String(toDashedCase(methodName))}")) else List.empty
extraImplicits = List.empty
defn = build(methodName, httpMethod, urlWithParams, formDataParams, formDataNeedsMultipart, headerParams, responseTypeRef, tracing)(tracingArgsPre, tracingArgsPost, pathArgs, qsArgs, formArgs, bodyArgs, headerArgs, extraImplicits)
defn = build(methodName, httpMethod, urlWithParams, formDataParams, textPlain, formDataNeedsMultipart, headerParams, responseTypeRef, tracing)(tracingArgsPre, tracingArgsPost, pathArgs, qsArgs, formArgs, bodyArgs, headerArgs, extraImplicits)
} yield defn
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ object AkkaHttpGenerator {
type HttpClient = HttpRequest => Future[HttpResponse]
type TraceBuilder = String => HttpClient => HttpClient

class TextPlain(val value: String)
object TextPlain {
def apply(value: String): TextPlain = new TextPlain(value)
implicit final def textTEM: ToEntityMarshaller[TextPlain] =
Marshaller.withFixedContentType(ContentTypes.${Term.Name("`text/plain(UTF-8)`")}) { text =>
HttpEntity(ContentTypes.${Term.Name("`text/plain(UTF-8)`")}, text.value)
}
}

implicit final def jsonMarshaller(
implicit printer: Printer = Printer.noSpaces
): ToEntityMarshaller[${jsonType}] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ object AkkaHttpServerGenerator {
else term

(basePath.getOrElse("") + path).stripPrefix("/") match {
case "" => Target.pure(q"pathEnd")
case "" => Target.pure(q"pathEndOrSingleSlash")
case path =>
for {
pathDirective <- SwaggerUtil.paths.generateUrlAkkaPathExtractors(path, pathArgs)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import cats.syntax.traverse._
import cats.instances.all._

case class RawParameterName private[generators] (value: String) { def toLit: Lit.String = Lit.String(value) }
class ScalaParameter private[generators] (val in: Option[String], val param: Term.Param, val paramName: Term.Name, val argName: RawParameterName, val argType: Type) {
class ScalaParameter private[generators] (val in: Option[String], val param: Term.Param, val paramName: Term.Name, val argName: RawParameterName, val argType: Type, val required: Boolean) {
override def toString: String = s"ScalaParameter(${in}, ${param}, ${paramName}, ${argName}, ${argType})"
}
object ScalaParameter {
Expand All @@ -18,12 +18,13 @@ object ScalaParameter {
def fromParam: Term.Param => ScalaParameter = { param => fromParam(param.name.value)(param) }
def fromParam(argName: String): Term.Param => ScalaParameter = fromParam(RawParameterName(argName))
def fromParam(argName: RawParameterName): Term.Param => ScalaParameter = { case param@Term.Param(mods, name, decltype, default) =>
val tpe: Type = decltype.flatMap({
case Type.ByName(tpe) => Some(tpe)
case tpe@Type.Name(_) => Some(tpe)
val (tpe, required): (Type, Boolean) = decltype.flatMap({
case tpe@Type.Apply(Type.Name("Option"), List(_)) => Some((tpe, false))
case Type.ByName(tpe) => Some((tpe, true))
case tpe@Type.Name(_) => Some((tpe, true))
case _ => None
}).getOrElse(t"Nothing")
new ScalaParameter(None, param, Term.Name(name.value), argName, tpe)
}).getOrElse((t"Nothing", true))
new ScalaParameter(None, param, Term.Name(name.value), argName, tpe, required)
}

def fromParameter(protocolElems: List[StrictProtocolElems]): Parameter => Target[ScalaParameter] = { parameter =>
Expand Down Expand Up @@ -127,7 +128,7 @@ object ScalaParameter {

val paramName = Term.Name(toCamelCase(parameter.getName))
val param = param"${paramName}: ${declType}".copy(default=defaultValue)
new ScalaParameter(Option(parameter.getIn), param, paramName, RawParameterName(parameter.getName), declType)
new ScalaParameter(Option(parameter.getIn), param, paramName, RawParameterName(parameter.getName), declType, required)
}
}

Expand All @@ -144,7 +145,8 @@ object ScalaParameter {
param.param.copy(name=escapedName),
escapedName,
param.argName,
param.argType
param.argType,
param.required
)
}
else param
Expand Down
39 changes: 39 additions & 0 deletions modules/sample/src/main/resources/contentType-textPlain.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
swagger: "2.0"
info:
title: 'Content-Type: text/plain spec'
version: 1.0.0
host: localhost:1234
paths:
/foo:
post:
operationId: doFoo
x-scala-package: foo
consumes:
- text/plain
produces:
- text/plain
parameters:
- in: body
name: body
schema:
type: string
responses:
'201':
description: "Created"
/bar:
post:
operationId: doBar
x-scala-package: foo
consumes:
- text/plain
produces:
- text/plain
parameters:
- in: body
name: body
required: true
schema:
type: string
responses:
'201':
description: "Created"
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package tests.generators.AkkaHttp.Client.contentType

import _root_.tests.contentTypes.textPlain.Implicits.IgnoredEntity
import _root_.tests.contentTypes.textPlain.foo.FooClient
import akka.http.scaladsl.model._
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
import akka.http.scaladsl.testkit.ScalatestRouteTest
import cats.instances.future._
import org.scalatest.concurrent.ScalaFutures
import org.scalatest.{EitherValues, FunSuite, Matchers}
import scala.concurrent.Future
import scala.concurrent.duration._

import _root_.tests.scalatest.EitherTValues

class TextPlainTest extends FunSuite with Matchers with EitherValues with EitherTValues with ScalaFutures with ScalatestRouteTest {
override implicit val patienceConfig = PatienceConfig(1000.millis, 1000.millis)
test("Plain text should be emitted for optional parameters") {
val route: Route = (path("foo") & extractRequestEntity & entity(as[String])) { (entity, value) =>
complete({
if (entity.contentType == ContentTypes.`text/plain(UTF-8)` && value == "sample") {
StatusCodes.OK
} else {
StatusCodes.BadRequest
}
})
}
val client: HttpRequest => Future[HttpResponse] = Route.asyncHandler(route)
val fooClient = FooClient.httpClient(client)
new EitherTValuable(fooClient.doFoo(Some("sample"))).rightValue.futureValue shouldBe IgnoredEntity.empty
}

test("Plain text should be emitted for required parameters") {
val route: Route = (path("bar") & extractRequestEntity & entity(as[String])) { (entity, value) =>
complete({
if (entity.contentType == ContentTypes.`text/plain(UTF-8)` && value == "sample") {
StatusCodes.OK
} else {
StatusCodes.BadRequest
}
})
}
val client: HttpRequest => Future[HttpResponse] = Route.asyncHandler(route)
val fooClient = FooClient.httpClient(client)
new EitherTValuable(fooClient.doBar("sample")).rightValue.futureValue shouldBe IgnoredEntity.empty
}
}
28 changes: 28 additions & 0 deletions modules/sample/src/test/scala/scalatest/EitherTValues.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package tests.scalatest

import cats.Functor
import cats.data.EitherT
import org.scalactic.source
import org.scalatest._
import org.scalatest.exceptions.{StackDepthException, TestFailedException}
import scala.language.implicitConversions

trait EitherTValues {

implicit def convertEitherTToValuable[F[_]: Functor, L, R](eitherT: EitherT[F, L, R]) = new EitherTValuable(eitherT)

class EitherTValuable[F[_]: Functor, L, R](eitherT: EitherT[F, L, R]) {
def leftValue(implicit pos: source.Position): F[L] = {
eitherT.fold(identity, { _ =>
throw new TestFailedException((_: StackDepthException) => Option.empty[String], Option.empty[Throwable], pos)
})
}

def rightValue(implicit pos: source.Position): F[R] = {
eitherT.fold({ _ =>
throw new TestFailedException((_: StackDepthException) => Option.empty[String], Option.empty[Throwable], pos)
}, identity)
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package tests.generators.akkaHttp.client.contentType

import _root_.io.swagger.parser.SwaggerParser
import cats.instances.all._
import com.twilio.swagger._
import com.twilio.guardrail.generators.AkkaHttp
import com.twilio.guardrail.{ClassDefinition, Client, Clients, Context, ClientGenerator, ProtocolGenerator, ProtocolDefinitions, RandomType, CodegenApplication, Target}
import org.scalatest.{FunSuite, Matchers}
import scala.meta._

class TextPlainTest extends FunSuite with Matchers {
val swagger = s"""
|swagger: "2.0"
|info:
| title: Whatever
| version: 1.0.0
|host: localhost:1234
|schemes:
| - http
|paths:
| /foo:
| put:
| operationId: putFoo
| consumes:
| - text/plain
| parameters:
| - name: body
| in: body
| required: true
| schema:
| type: string
| responses:
| 200:
| description: Success
|""".stripMargin

test("Properly handle all methods") {
val (
_,
Clients(Client(tags, className, statements) :: _),
_
) = runSwaggerSpec(swagger)(Context.empty, AkkaHttp)
val List(cmp, cls) = statements.dropWhile(_.isInstanceOf[Import])

val companion = q"""
object Client {
def apply(host: String = "http://localhost:1234")(implicit httpClient: HttpRequest => Future[HttpResponse], ec: ExecutionContext, mat: Materializer): Client = new Client(host = host)(httpClient = httpClient, ec = ec, mat = mat)
def httpClient(httpClient: HttpRequest => Future[HttpResponse], host: String = "http://localhost:1234")(implicit ec: ExecutionContext, mat: Materializer): Client = new Client(host = host)(httpClient = httpClient, ec = ec, mat = mat)
}
"""
val client = q"""
class Client(host: String = "http://localhost:1234")(implicit httpClient: HttpRequest => Future[HttpResponse], ec: ExecutionContext, mat: Materializer) {
val basePath: String = ""
private[this] def wrap[T: FromEntityUnmarshaller](resp: Future[HttpResponse]): EitherT[Future, Either[Throwable, HttpResponse], T] = {
EitherT(resp.flatMap(resp => if (resp.status.isSuccess) {
Unmarshal(resp.entity).to[T].map(Right.apply _)
} else {
FastFuture.successful(Left(Right(resp)))
}).recover({
case e: Throwable =>
Left(Left(e))
}))
}
def putFoo(body: String, headers: scala.collection.immutable.Seq[HttpHeader] = Nil): EitherT[Future, Either[Throwable, HttpResponse], IgnoredEntity] = {
val allHeaders = headers ++ scala.collection.immutable.Seq[Option[HttpHeader]]().flatten
wrap[IgnoredEntity](Marshal(TextPlain(body)).to[RequestEntity].flatMap {
entity => httpClient(HttpRequest(method = HttpMethods.PUT, uri = host + basePath + "/foo", entity = entity, headers = allHeaders))
})
}
}
"""

cmp.structure should equal(companion.structure)
cls.structure should equal(client.structure)
}
}
27 changes: 26 additions & 1 deletion src/test/scala/generators/AkkaHttp/Server.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ class AkkaHttpServerTest extends FunSuite with Matchers {
|swagger: '2.0'
|host: petstore.swagger.io
|paths:
| /:
| get:
| x-scala-package: store
| operationId: getRoot
| responses:
| 200:
| description: Successful
| "/store/order/{order_id}":
| get:
| tags:
Expand Down Expand Up @@ -127,6 +134,7 @@ class AkkaHttpServerTest extends FunSuite with Matchers {

val handler = q"""
trait StoreHandler {
def getRoot(respond: StoreResource.getRootResponse.type)(): scala.concurrent.Future[StoreResource.getRootResponse]
def getOrderById(respond: StoreResource.getOrderByIdResponse.type)(orderId: Long, status: OrderStatus = OrderStatus.Placed): scala.concurrent.Future[StoreResource.getOrderByIdResponse]
def getFoo(respond: StoreResource.getFooResponse.type)(): scala.concurrent.Future[StoreResource.getFooResponse]
def getFooBar(respond: StoreResource.getFooBarResponse.type)(bar: Long): scala.concurrent.Future[StoreResource.getFooBarResponse]
Expand All @@ -144,7 +152,9 @@ class AkkaHttpServerTest extends FunSuite with Matchers {
string => io.circe.Json.fromString(string).as[T].left.flatMap(err => io.circe.jawn.parse(string).flatMap(_.as[T])).fold(scala.concurrent.Future.failed _, scala.concurrent.Future.successful _)
}
def routes(handler: StoreHandler)(implicit mat: akka.stream.Materializer): Route = {
(get & path("store" / "order" / LongNumber) & parameter(Symbol("status").as[OrderStatus]) & discardEntity) {
(get & pathEndOrSingleSlash & discardEntity) {
complete(handler.getRoot(getRootResponse)())
} ~ (get & path("store" / "order" / LongNumber) & parameter(Symbol("status").as[OrderStatus]) & discardEntity) {
(orderId, status) => complete(handler.getOrderById(getOrderByIdResponse)(orderId, status))
} ~ (get & (pathPrefix("foo") & pathEndOrSingleSlash) & discardEntity) {
complete(handler.getFoo(getFooResponse)())
Expand All @@ -154,6 +164,21 @@ class AkkaHttpServerTest extends FunSuite with Matchers {
bar => complete(handler.putBar(putBarResponse)(bar))
}
}
sealed abstract class getRootResponse(val statusCode: StatusCode)
case object getRootResponseOK extends getRootResponse(StatusCodes.OK)
object getRootResponse {
implicit val getRootTRM: ToResponseMarshaller[getRootResponse] = Marshaller { implicit ec =>
resp => getRootTR(resp)
}
implicit def getRootTR(value: getRootResponse)(implicit ec: scala.concurrent.ExecutionContext): scala.concurrent.Future[List[Marshalling[HttpResponse]]] = value match {
case r: getRootResponseOK.type =>
scala.concurrent.Future.successful(Marshalling.Opaque {
() => HttpResponse(r.statusCode)
} :: Nil)
}
def apply[T](value: T)(implicit ev: T => getRootResponse): getRootResponse = ev(value)
def OK: getRootResponse = getRootResponseOK
}
sealed abstract class getOrderByIdResponse(val statusCode: StatusCode)
case class getOrderByIdResponseOK(value: Order) extends getOrderByIdResponse(StatusCodes.OK)
case object getOrderByIdResponseBadRequest extends getOrderByIdResponse(StatusCodes.BadRequest)
Expand Down

0 comments on commit dea4a99

Please sign in to comment.