diff --git a/build.sbt b/build.sbt
index 2903e33b..1f312631 100644
--- a/build.sbt
+++ b/build.sbt
@@ -1,6 +1,6 @@
import sbt.Compile
import sbt.Keys.cleanFiles
-val releaseVersion = sys.env.getOrElse("TAG", "0.4.4-Delta")
+val releaseVersion = sys.env.getOrElse("TAG", "1.0.0-Gamma")
addCommandAlias("publishSmithy4Play", "smithy4play/publish")
addCommandAlias("publishLocalSmithy4Play", "smithy4play/publishLocal")
addCommandAlias("generateCoverage", "clean; coverage; test; coverageReport")
@@ -29,15 +29,15 @@ val defaultProjectSettings = Seq(
version := releaseVersion
) ++ githubSettings
-val sharedSettings = defaultProjectSettings
-
+val sharedSettings = defaultProjectSettings
lazy val smithy4play = project
.in(file("smithy4play"))
.enablePlugins(Smithy4sCodegenPlugin)
.settings(
sharedSettings,
+ addCompilerPlugin("org.typelevel" % "kind-projector" % "0.13.2" cross CrossVersion.full),
scalaVersion := Dependencies.scalaVersion,
- Compile / smithy4sAllowedNamespaces := List("smithy.smithy4play"),
+ Compile / smithy4sAllowedNamespaces := List("smithy.smithy4play", "aws.protocols"),
Compile / smithy4sInputDirs := Seq(
(ThisBuild / baseDirectory).value / "smithy4play" / "src" / "resources" / "META_INF" / "smithy"
),
@@ -53,14 +53,15 @@ lazy val smithy4playTest = project
.enablePlugins(Smithy4sCodegenPlugin, PlayScala)
.settings(
sharedSettings,
- scalaVersion := Dependencies.scalaVersion,
- name := "smithy4playTest",
+ scalaVersion := Dependencies.scalaVersion,
+ name := "smithy4playTest",
scalacOptions += "-Ymacro-annotations",
Compile / compile / wartremoverWarnings ++= Warts.unsafe,
cleanKeepFiles += (ThisBuild / baseDirectory).value / "smithy4playTest" / "app",
- cleanFiles += (ThisBuild / baseDirectory).value / "smithy4playTest" / "app" / "testDefinitions" / "test",
- Compile / smithy4sInputDirs := Seq((ThisBuild / baseDirectory).value / "smithy4playTest" / "testSpecs"),
- Compile / smithy4sOutputDir := (ThisBuild / baseDirectory).value / "smithy4playTest" / "app",
+ cleanFiles += (ThisBuild / baseDirectory).value / "smithy4playTest" / "app" / "specs" / "testDefinitions" / "test",
+ Compile / smithy4sInputDirs := Seq((ThisBuild / baseDirectory).value / "smithy4playTest" / "testSpecs"),
+ Compile / smithy4sOutputDir := (ThisBuild / baseDirectory).value / "smithy4playTest" / "app" / "specs",
+ Compile / smithy4sAllowedNamespaces := List("aws.protocols", "testDefinitions.test"),
libraryDependencies ++= Seq(
guice,
Dependencies.cats,
diff --git a/project/Dependencies.scala b/project/Dependencies.scala
index 460050b7..6d685c4d 100644
--- a/project/Dependencies.scala
+++ b/project/Dependencies.scala
@@ -7,9 +7,10 @@ object Dependencies {
val typesafePlay = "com.typesafe.play" %% "play" % playVersion
val scalaVersion = "2.13.12"
- val smithy4sVersion = "0.17.19"
+ val smithy4sVersion = "0.18.8"
val smithyCore = "com.disneystreaming.smithy4s" %% "smithy4s-core" % smithy4sVersion
val smithyJson = "com.disneystreaming.smithy4s" %% "smithy4s-json" % smithy4sVersion
+ val smithyXml = "com.disneystreaming.smithy4s" %% "smithy4s-xml" % smithy4sVersion
val smithy4sCompliance = "com.disneystreaming.smithy4s" %% "smithy4s-compliance-tests" % smithy4sVersion
val alloyCore = "com.disneystreaming.alloy" % "alloy-core" % "0.2.8"
val alloyOpenapi = "com.disneystreaming.alloy" %% "alloy-openapi" % "0.2.8"
@@ -26,6 +27,7 @@ object Dependencies {
lazy val list = Seq(
smithyCore,
smithyJson,
+ smithyXml,
alloyCore,
alloyOpenapi,
smithy4sCompliance,
diff --git a/project/plugins.sbt b/project/plugins.sbt
index 0505db9d..8d4bdd00 100644
--- a/project/plugins.sbt
+++ b/project/plugins.sbt
@@ -1,7 +1,7 @@
addSbtPlugin("com.codecommit" %% "sbt-github-packages" % "0.5.3")
addSbtPlugin("org.wartremover" %% "sbt-wartremover" % "3.1.5")
addSbtPlugin("org.scalameta" %% "sbt-scalafmt" % "2.5.2")
-addSbtPlugin("com.disneystreaming.smithy4s" %% "smithy4s-sbt-codegen" % "0.17.19")
+addSbtPlugin("com.disneystreaming.smithy4s" %% "smithy4s-sbt-codegen" % "0.18.8")
addSbtPlugin("com.typesafe.play" %% "sbt-plugin" % "2.9.1")
addSbtPlugin("org.scoverage" %% "sbt-scoverage" % "2.0.9")
diff --git a/smithy4play/src/main/scala/de/innfactory/smithy4play/CodecDecider.scala b/smithy4play/src/main/scala/de/innfactory/smithy4play/CodecDecider.scala
new file mode 100644
index 00000000..f6b9c481
--- /dev/null
+++ b/smithy4play/src/main/scala/de/innfactory/smithy4play/CodecDecider.scala
@@ -0,0 +1,138 @@
+package de.innfactory.smithy4play
+
+import play.api.http.MimeTypes
+import smithy4s.capability.instances.either._
+import smithy4s.codecs.Writer.CachedCompiler
+import smithy4s.codecs._
+import smithy4s.http.{ HttpResponse, HttpRestSchema, Metadata, MetadataError }
+import smithy4s.json.Json
+import smithy4s.kinds.PolyFunction
+import smithy4s.schema.CachedSchemaCompiler
+import smithy4s.xml.Xml
+import smithy4s.{ codecs, Blob }
+
+object CodecDecider {
+
+ private val jsonCodecs = Json.payloadCodecs
+ .withJsoniterCodecCompiler(
+ Json.jsoniter
+ )
+
+ private val jsonEncoder: BlobEncoder.Compiler = jsonCodecs.encoders
+ private val jsonDecoder: BlobDecoder.Compiler = jsonCodecs.decoders
+ private val metadataEncoder = Metadata.Encoder
+ private val metadataDecoder: CachedSchemaCompiler[Metadata.Decoder] = Metadata.Decoder
+
+ def encoder(
+ contentType: Seq[String]
+ ): CachedSchemaCompiler[codecs.BlobEncoder] =
+ contentType match {
+ case Seq(MimeTypes.JSON) => jsonEncoder
+ case Seq(MimeTypes.XML) => Xml.encoders
+ case _ =>
+ CachedSchemaCompiler
+ .getOrElse(smithy4s.codecs.StringAndBlobCodecs.encoders, jsonEncoder)
+ }
+
+ def requestDecoder(
+ contentType: Seq[String]
+ ): CachedSchemaCompiler[Decoder[Either[Throwable, *], PlayHttpRequest[Blob], *]] =
+ HttpRestSchema.combineDecoderCompilers[Either[Throwable, *], PlayHttpRequest[Blob]](
+ metadataDecoder
+ .mapK(
+ Decoder.in[Either[MetadataError, *]].composeK[Metadata, PlayHttpRequest[Blob]](_.metadata)
+ )
+ .asInstanceOf[CachedSchemaCompiler[Decoder[Either[Throwable, *], PlayHttpRequest[Blob], *]]],
+ decoder(contentType)
+ .mapK(
+ Decoder.in[Either[PayloadError, *]].composeK[Blob, PlayHttpRequest[Blob]](_.body)
+ )
+ .asInstanceOf[CachedSchemaCompiler[Decoder[Either[Throwable, *], PlayHttpRequest[Blob], *]]],
+ _ => Right(())
+ )(eitherZipper)
+
+ def requestEncoder(
+ contentType: Seq[String]
+ ): CachedCompiler[EndpointRequest] =
+ HttpRestSchema.combineWriterCompilers(
+ metadataEncoder.mapK(
+ metadataPipe
+ ),
+ encoder(contentType).mapK(
+ blobPipe
+ ),
+ _ => false
+ )
+
+ def httpResponseDecoder(
+ contentType: Seq[String]
+ ): CachedSchemaCompiler[Decoder[Either[Throwable, *], HttpResponse[Blob], *]] =
+ HttpRestSchema.combineDecoderCompilers[Either[Throwable, *], HttpResponse[Blob]](
+ metadataDecoder
+ .mapK(
+ Decoder
+ .in[Either[MetadataError, *]]
+ .composeK[Metadata, HttpResponse[Blob]](r =>
+ Metadata(Map.empty, Map.empty, headers = r.headers, statusCode = Some(r.statusCode))
+ )
+ )
+ .asInstanceOf[CachedSchemaCompiler[Decoder[Either[Throwable, *], HttpResponse[Blob], *]]],
+ decoder(contentType)
+ .mapK(
+ Decoder.in[Either[PayloadError, *]].composeK[Blob, HttpResponse[Blob]](_.body)
+ )
+ .asInstanceOf[CachedSchemaCompiler[Decoder[Either[Throwable, *], HttpResponse[Blob], *]]],
+ _ => Right(())
+ )(eitherZipper)
+
+ def httpMessageEncoder(
+ contentType: Seq[String]
+ ): CachedCompiler[HttpResponse[Blob]] =
+ HttpRestSchema.combineWriterCompilers(
+ metadataEncoder.mapK(
+ httpRequestMetadataPipe
+ ),
+ encoder(contentType).mapK(
+ httpRequestBlobPipe
+ ),
+ _ => false
+ )
+
+ private val httpRequestBodyLift: Writer[HttpResponse[Blob], Blob] =
+ Writer.lift[HttpResponse[Blob], Blob]((res, blob) => res.copy(body = blob))
+ private val httpRequestMetadataLift: Writer[HttpResponse[Blob], Metadata] =
+ Writer.lift[HttpResponse[Blob], Metadata]((res, metadata) =>
+ res.addHeaders(metadata.headers.map { case (insensitive, value) =>
+ (insensitive, value)
+ })
+ )
+ private val httpRequestBlobPipe: PolyFunction[Encoder[Blob, *], Writer[HttpResponse[Blob], *]] =
+ smithy4s.codecs.Encoder.pipeToWriterK[HttpResponse[Blob], Blob](httpRequestBodyLift)
+ private val httpRequestMetadataPipe: PolyFunction[Encoder[Metadata, *], Writer[HttpResponse[Blob], *]] =
+ smithy4s.codecs.Encoder.pipeToWriterK(httpRequestMetadataLift)
+
+ private val blobLift: Writer[EndpointRequest, Blob] =
+ Writer.lift[EndpointRequest, Blob]((res, blob) => res.copy(body = blob))
+ private val metadataLift: Writer[EndpointRequest, Metadata] =
+ Writer.lift[EndpointRequest, Metadata]((res, metadata) =>
+ res.addHeaders(metadata.headers.map { case (insensitive, value) =>
+ (insensitive, value)
+ })
+ )
+ private val blobPipe: PolyFunction[Encoder[Blob, *], Writer[EndpointRequest, *]] =
+ smithy4s.codecs.Encoder.pipeToWriterK[EndpointRequest, Blob](blobLift)
+ private val metadataPipe: PolyFunction[Encoder[Metadata, *], Writer[EndpointRequest, *]] =
+ smithy4s.codecs.Encoder.pipeToWriterK(metadataLift)
+
+ def decoder(
+ contentType: Seq[String]
+ ): CachedSchemaCompiler[BlobDecoder] =
+ contentType match {
+ case Seq(MimeTypes.JSON) => jsonDecoder
+ case Seq(MimeTypes.XML) => Xml.decoders
+ case _ =>
+ CachedSchemaCompiler
+ .getOrElse(smithy4s.codecs.StringAndBlobCodecs.decoders, jsonDecoder)
+ }
+
+}
diff --git a/smithy4play/src/main/scala/de/innfactory/smithy4play/CodecUtils.scala b/smithy4play/src/main/scala/de/innfactory/smithy4play/CodecUtils.scala
deleted file mode 100644
index f3c1b330..00000000
--- a/smithy4play/src/main/scala/de/innfactory/smithy4play/CodecUtils.scala
+++ /dev/null
@@ -1,35 +0,0 @@
-package de.innfactory.smithy4play
-
-import smithy4s.{ HintMask, Schema }
-import smithy4s.http.{ CodecAPI, PayloadError }
-import smithy4s.http.json.codecs
-import smithy4s.internals.InputOutput
-
-object CodecUtils {
-
- private val codecs: codecs =
- smithy4s.http.json.codecs(alloy.SimpleRestJson.protocol.hintMask ++ HintMask(InputOutput))
-
- def writeInputToBody[I](input: I, inputSchema: Schema[I], codecAPI: CodecAPI): Array[Byte] = {
- val codec = codecAPI.compileCodec(inputSchema)
- codecAPI.writeToArray(codec, input)
- }
-
- def readFromBytes[E](data: Array[Byte], inputSchema: Schema[E], codecAPI: CodecAPI): Either[PayloadError, E] =
- codecAPI.decodeFromByteArray(codecAPI.compileCodec(inputSchema), data)
-
- def writeEntityToJsonBytes[E](e: E, schema: Schema[E]) = writeInputToBody[E](e, schema, codecs)
-
- def readFromJsonBytes[E](bytes: Array[Byte], schema: Schema[E]): Option[E] =
- readFromBytes(bytes, schema, codecs).toOption
-
- def extractCodec(headers: Map[String, Seq[String]]): CodecAPI = {
- val contentType =
- headers.getOrElse("content-type", List("application/json"))
- val codecApi = contentType match {
- case List("application/json") => codecs
- case _ => CodecAPI.nativeStringsAndBlob(codecs)
- }
- codecApi
- }
-}
diff --git a/smithy4play/src/main/scala/de/innfactory/smithy4play/SmithyPlayEndpoint.scala b/smithy4play/src/main/scala/de/innfactory/smithy4play/SmithyPlayEndpoint.scala
index 2d2c6258..9ca51aeb 100644
--- a/smithy4play/src/main/scala/de/innfactory/smithy4play/SmithyPlayEndpoint.scala
+++ b/smithy4play/src/main/scala/de/innfactory/smithy4play/SmithyPlayEndpoint.scala
@@ -1,15 +1,17 @@
package de.innfactory.smithy4play
-import akka.util.ByteString
+import alloy.SimpleRestJson
+import aws.protocols.RestXml
import cats.data.{ EitherT, Kleisli }
-import cats.implicits.toBifunctorOps
import de.innfactory.smithy4play
import de.innfactory.smithy4play.middleware.MiddlewareBase
+import play.api.http.MimeTypes
import play.api.mvc._
-import smithy4s.http.{ CodecAPI, HttpEndpoint, Metadata, PathParams }
+import smithy4s.codecs.PayloadError
+import smithy4s.http._
import smithy4s.kinds.FunctorInterpreter
import smithy4s.schema.Schema
-import smithy4s.{ ByteArray, Endpoint, Service }
+import smithy4s.{ Blob, Endpoint, Service }
import scala.concurrent.{ ExecutionContext, Future }
@@ -23,23 +25,17 @@ class SmithyPlayEndpoint[Alg[_[_, _, _, _, _]], F[_] <: ContextRoute[_], Op[
service: Service[Alg],
impl: FunctorInterpreter[Op, F],
middleware: Seq[MiddlewareBase],
- endpoint: Endpoint[Op, I, E, O, SI, SO],
- codecs: CodecAPI
+ endpoint: Endpoint[Op, I, E, O, SI, SO]
)(implicit cc: ControllerComponents, ec: ExecutionContext)
extends AbstractController(cc) {
- private val httpEndpoint: Either[HttpEndpoint.HttpEndpointError, HttpEndpoint[I]] = HttpEndpoint.cast(endpoint)
+ private val httpEndpoint: Either[HttpEndpoint.HttpEndpointError, HttpEndpoint[I]] = HttpEndpoint.cast(endpoint.schema)
private val serviceHints = service.hints
private val endpointHints = endpoint.hints
+ private val serviceContentType: String = serviceHints.toMimeType
- private val inputSchema: Schema[I] = endpoint.input
- private val outputSchema: Schema[O] = endpoint.output
-
- private val inputMetadataDecoder: Metadata.PartialDecoder[I] =
- Metadata.PartialDecoder.fromSchema(inputSchema)
-
- private val outputMetadataEncoder: Metadata.Encoder[O] =
- Metadata.Encoder.fromSchema(outputSchema)
+ private implicit val inputSchema: Schema[I] = endpoint.input
+ private implicit val outputSchema: Schema[O] = endpoint.output
def handler(v1: RequestHeader): Handler =
httpEndpoint.map { httpEp =>
@@ -52,14 +48,14 @@ class SmithyPlayEndpoint[Alg[_[_, _, _, _, _]], F[_] <: ContextRoute[_], Op[
)
}
- val result = for {
- pathParams <- getPathParams(v1, httpEp)
- metadata = getMetadata(pathParams, v1)
- input <- getInput(request, metadata)
- endpointLogic = impl(endpoint.wrap(input))
- .asInstanceOf[Kleisli[RouteResult, RoutingContext, O]]
- .map(mapToEndpointResult(httpEp.code))
-
+ implicit val epContentType: ContentType = ContentType(request.contentType.getOrElse(serviceContentType))
+ val result = for {
+ pathParams <- getPathParams(v1, httpEp)
+ metadata = getMetadata(pathParams, v1)
+ input <- getInput(request, metadata)
+ endpointLogic = impl(endpoint.wrap(input))
+ .asInstanceOf[Kleisli[RouteResult, RoutingContext, O]]
+ .map(mapToEndpointResult(httpEp.code))
chainedMiddlewares = middleware.foldRight(endpointLogic)((a, b) => a.middleware(b.run))
res <-
chainedMiddlewares.run(RoutingContext.fromRequest(request, serviceHints, endpointHints, v1))
@@ -72,39 +68,33 @@ class SmithyPlayEndpoint[Alg[_[_, _, _, _, _]], F[_] <: ContextRoute[_], Op[
}
.getOrElse(Action(NotFound("404")))
- private def mapToEndpointResult(statusCode: Int)(o: O): EndpointResult = {
- val outputMetadata = outputMetadataEncoder.encode(o)
- val outputHeaders = outputMetadata.headers.map { case (k, v) =>
- (k.toString.toLowerCase, v.mkString(""))
- }
- val contentType =
- outputHeaders.getOrElse("content-type", "application/json")
- val codecApi = contentType match {
- case "application/json" => codecs
- case _ => CodecAPI.nativeStringsAndBlob(codecs)
- }
- logger.debug(s"[SmithyPlayEndpoint] Headers: ${outputHeaders.mkString("|")}")
-
- val codec = codecApi.compileCodec(outputSchema)
- val expectBody = Metadata.PartialDecoder
+ private def mapToEndpointResult(
+ statusCode: Int
+ )(output: O)(implicit defaultContentType: ContentType): HttpResponse[Blob] =
+ CodecDecider
+ .httpMessageEncoder(Seq(defaultContentType.value))
.fromSchema(outputSchema)
- .total
- .isEmpty // expect body if metadata decoder is not total
- val body = if (expectBody) Some(codecApi.writeToArray(codec, o)) else None
- EndpointResult(body, status = smithy4play.Status(outputHeaders, statusCode))
- }
+ .write(
+ HttpResponse(
+ statusCode = statusCode,
+ headers = Map.empty,
+ body = Blob.empty
+ ),
+ output
+ )
private def getPathParams(
v1: RequestHeader,
httpEp: HttpEndpoint[I]
- ): EitherT[Future, ContextRouteError, Map[String, String]] =
+ )(implicit defaultContentType: ContentType): EitherT[Future, ContextRouteError, Map[String, String]] =
EitherT(
Future(
matchRequestPath(v1, httpEp)
.toRight[ContextRouteError](
Smithy4PlayError(
"Error in extracting PathParams",
- smithy4play.Status(Map.empty, 400)
+ smithy4play.Status(Map.empty, 400),
+ contentType = defaultContentType.value
)
)
)
@@ -113,84 +103,34 @@ class SmithyPlayEndpoint[Alg[_[_, _, _, _, _]], F[_] <: ContextRoute[_], Op[
private def getInput(
request: Request[RawBuffer],
metadata: Metadata
- ): EitherT[Future, ContextRouteError, I] =
- EitherT(
- Future(inputMetadataDecoder.total match {
- case Some(value) =>
- value
- .decode(metadata)
- .leftMap { e =>
- logger.info(e.getMessage())
- Smithy4PlayError(
- "Error decoding Input",
- smithy4play.Status(Map.empty, 500)
- )
- }
- case None =>
- request.contentType.getOrElse("application/json") match {
- case "application/json" => parseJson(request, metadata)
- case _ => parseRaw(request, metadata)
- }
-
- })
- )
-
- private def parseJson(request: Request[RawBuffer], metadata: Metadata): Either[ContextRouteError, I] = {
- val codec = codecs.compileCodec(inputSchema)
- for {
- metadataPartial <- inputMetadataDecoder
- .decode(metadata)
- .leftMap { e =>
- logger.info(e.getMessage())
- Smithy4PlayError(
- "Error decoding Input Metadata",
- smithy4play.Status(Map.empty, 500)
- )
- }
- c <-
- codecs
- .decodeFromByteBufferPartial(
- codec,
- request.body.asBytes().getOrElse(ByteString.empty).toByteBuffer
- )
- .leftMap(e =>
- Smithy4PlayError(
- s"expected: ${e.expected}",
- smithy4play.Status(Map.empty, 500),
- additionalInformation = Some(e.getMessage())
- )
- )
- } yield metadataPartial.combine(c)
- }
-
- private def parseRaw(request: Request[RawBuffer], metadata: Metadata): Either[ContextRouteError, I] = {
- val nativeCodec: CodecAPI = CodecAPI.nativeStringsAndBlob(codecs)
- val input = ByteArray(request.body.asBytes().getOrElse(ByteString.empty).toArray)
- val codec = nativeCodec
- .compileCodec(inputSchema)
- for {
- metadataPartial <- inputMetadataDecoder
- .decode(metadata)
- .leftMap { e =>
- logger.info(e.getMessage())
- Smithy4PlayError(
- "Error decoding Input Metadata",
- additionalInformation = Some(e.getMessage()),
- status = smithy4play.Status(Map.empty, 500)
- )
- }
- bodyPartial <-
- nativeCodec
- .decodeFromByteArrayPartial(codec, input.array)
- .leftMap(e =>
- Smithy4PlayError(
- s"expected: ${e.expected}",
- smithy4play.Status(Map.empty, 400),
- additionalInformation = Some(e.getMessage())
+ )(implicit defaultContentType: ContentType): EitherT[Future, ContextRouteError, I] =
+ EitherT {
+ Future {
+ val codec = CodecDecider.requestDecoder(Seq(defaultContentType.value))
+ codec
+ .fromSchema(inputSchema)
+ .decode({
+ val body = request.body.asBytes().map(b => Blob(b.toByteBuffer)).getOrElse(Blob.empty)
+ PlayHttpRequest(
+ metadata = metadata,
+ body = body
)
- )
- } yield metadataPartial.combine(bodyPartial)
- }
+ })
+ }
+ }.leftMap {
+ case error: PayloadError =>
+ Smithy4PlayError(
+ error.filterMessage,
+ smithy4play.Status(Map.empty, 400),
+ contentType = defaultContentType.value
+ )
+ case error: MetadataError =>
+ Smithy4PlayError(
+ error.filterMessage,
+ smithy4play.Status(Map.empty, 400),
+ contentType = defaultContentType.value
+ )
+ }
private def getMetadata(pathParams: PathParams, request: RequestHeader): Metadata =
Metadata(
@@ -200,20 +140,25 @@ class SmithyPlayEndpoint[Alg[_[_, _, _, _, _]], F[_] <: ContextRoute[_], Op[
)
private def handleFailure(error: ContextRouteError): Result =
- Results.Status(error.status.statusCode)(error.toJson).withHeaders(error.status.headers.toList: _*)
-
- private def handleSuccess(output: EndpointResult): Result = {
- val status = Results.Status(output.status.statusCode)
- val outputHeadersWithoutContentType = output.status.headers.-("content-type").toList
+ Results
+ .Status(error.status.statusCode)(error.parse)
+ .withHeaders(error.status.headers.toList: _*)
+ .as(error.contentType)
+
+ private def handleSuccess(output: HttpResponse[Blob])(implicit defaultContentType: ContentType): Result = {
+ val status = Results.Status(output.statusCode)
+ val contentTypeKey = CaseInsensitive("content-type")
+ val outputHeadersWithoutContentType =
+ output.headers.-(contentTypeKey).toList.map(h => (h._1.toString, h._2.head))
val contentType =
- output.status.headers.getOrElse("content-type", "application/json")
-
- output.body match {
- case Some(value) =>
- status(value)
- .as(contentType)
- .withHeaders(outputHeadersWithoutContentType: _*)
- case None => status("").withHeaders(outputHeadersWithoutContentType: _*)
+ output.headers.getOrElse(contentTypeKey, Seq(defaultContentType.value))
+
+ if (!output.body.isEmpty) {
+ status(output.body.toArray)
+ .as(contentType.head)
+ .withHeaders(outputHeadersWithoutContentType: _*)
+ } else {
+ status("").withHeaders(outputHeadersWithoutContentType: _*)
}
}
diff --git a/smithy4play/src/main/scala/de/innfactory/smithy4play/SmithyPlayRouter.scala b/smithy4play/src/main/scala/de/innfactory/smithy4play/SmithyPlayRouter.scala
index 2a785540..d4a0b588 100644
--- a/smithy4play/src/main/scala/de/innfactory/smithy4play/SmithyPlayRouter.scala
+++ b/smithy4play/src/main/scala/de/innfactory/smithy4play/SmithyPlayRouter.scala
@@ -1,14 +1,15 @@
package de.innfactory.smithy4play
-import cats.data.{ EitherT, Kleisli }
import cats.implicits.toTraverseOps
import de.innfactory.smithy4play.middleware.MiddlewareBase
import play.api.mvc.{ AbstractController, ControllerComponents, Handler, RequestHeader }
import play.api.routing.Router.Routes
-import smithy4s.HintMask
+import smithy4s.codecs.{ BlobEncoder, PayloadDecoder, PayloadEncoder }
import smithy4s.http.{ HttpEndpoint, PathSegment }
-import smithy4s.internals.InputOutput
+import smithy4s.json.{ Json, JsonPayloadCodecCompiler }
import smithy4s.kinds.{ FunctorAlgebra, Kind1, PolyFunction5 }
+import smithy4s.schema.CachedSchemaCompiler
+import smithy4s.xml.Xml
import scala.concurrent.ExecutionContext
@@ -25,7 +26,7 @@ class SmithyPlayRouter[Alg[_[_, _, _, _, _]], F[
val interpreter: PolyFunction5[service.Operation, Kind1[F]#toKind5] = service.toPolyFunction[Kind1[F]#toKind5](impl)
val endpoints: Seq[service.Endpoint[_, _, _, _, _]] = service.endpoints
val httpEndpoints: Seq[Either[HttpEndpoint.HttpEndpointError, HttpEndpoint[_]]] =
- endpoints.map(HttpEndpoint.cast(_))
+ endpoints.map(ep => HttpEndpoint.cast(ep.schema))
new PartialFunction[RequestHeader, Handler] {
override def isDefinedAt(x: RequestHeader): Boolean = {
@@ -36,7 +37,7 @@ class SmithyPlayRouter[Alg[_[_, _, _, _, _]], F[
override def apply(v1: RequestHeader): Handler = {
logger.debug("[SmithyPlayRouter] calling apply on: " + service.id.name)
for {
- zippedEndpoints <- endpoints.map(ep => HttpEndpoint.cast(ep).map((ep, _))).sequence
+ zippedEndpoints <- endpoints.map(ep => HttpEndpoint.cast(ep.schema).map((ep, _))).sequence
endpointAndHttpEndpoint <-
zippedEndpoints
.find(ep => checkIfRequestHeaderMatchesEndpoint(v1, ep._2))
@@ -47,8 +48,7 @@ class SmithyPlayRouter[Alg[_[_, _, _, _, _]], F[
service,
interpreter,
middlewares,
- endpointAndHttpEndpoint._1,
- smithy4s.http.json.codecs(alloy.SimpleRestJson.protocol.hintMask ++ HintMask(InputOutput))
+ endpointAndHttpEndpoint._1
).handler(v1)
} match {
case Right(value) => value
@@ -61,7 +61,7 @@ class SmithyPlayRouter[Alg[_[_, _, _, _, _]], F[
private def checkIfRequestHeaderMatchesEndpoint(
x: RequestHeader,
ep: HttpEndpoint[_]
- ) = {
+ ): Boolean = {
ep.path.map {
case PathSegment.StaticSegment(value) => value
case PathSegment.LabelSegment(value) => value
diff --git a/smithy4play/src/main/scala/de/innfactory/smithy4play/client/GenericAPIClient.scala b/smithy4play/src/main/scala/de/innfactory/smithy4play/client/GenericAPIClient.scala
index 783d76ce..c9e98dca 100644
--- a/smithy4play/src/main/scala/de/innfactory/smithy4play/client/GenericAPIClient.scala
+++ b/smithy4play/src/main/scala/de/innfactory/smithy4play/client/GenericAPIClient.scala
@@ -3,7 +3,9 @@ package de.innfactory.smithy4play.client
import cats.data.Kleisli
import de.innfactory.smithy4play.{ ClientResponse, RunnableClientRequest }
import smithy4s.Service
+import smithy4s.http.CaseInsensitive
import smithy4s.kinds.{ FunctorK, FunctorK5, Kind1, PolyFunction5 }
+
import scala.concurrent.ExecutionContext
private class GenericAPIClient[Alg[_[_, _, _, _, _]]](
@@ -18,7 +20,9 @@ private class GenericAPIClient[Alg[_[_, _, _, _, _]]](
private def transformer(): Alg[Kind1[RunnableClientRequest]#toKind5] =
smithyPlayClient.service.fromPolyFunction(this.opToResponse())
- private def transformer(additionalHeaders: Option[Map[String, Seq[String]]]): Alg[Kind1[ClientResponse]#toKind5] =
+ private def transformer(
+ additionalHeaders: Option[Map[CaseInsensitive, Seq[String]]]
+ ): Alg[Kind1[ClientResponse]#toKind5] =
smithyPlayClient.service.fromPolyFunction(this.opToResponse(additionalHeaders))
/* uses the SmithyPlayClient to transform a Operation to a ClientResponse */
@@ -26,14 +30,14 @@ private class GenericAPIClient[Alg[_[_, _, _, _, _]]](
new PolyFunction5[smithyPlayClient.service.Operation, Kind1[RunnableClientRequest]#toKind5] {
override def apply[I, E, O, SI, SO](
fa: smithyPlayClient.service.Operation[I, E, O, SI, SO]
- ): Kleisli[ClientResponse, Option[Map[String, Seq[String]]], O] =
- Kleisli[ClientResponse, Option[Map[String, Seq[String]]], O] { additionalHeaders =>
+ ): Kleisli[ClientResponse, Option[Map[CaseInsensitive, Seq[String]]], O] =
+ Kleisli[ClientResponse, Option[Map[CaseInsensitive, Seq[String]]], O] { additionalHeaders =>
smithyPlayClient.send(fa, additionalHeaders)
}
}
private def opToResponse(
- additionalHeaders: Option[Map[String, Seq[String]]]
+ additionalHeaders: Option[Map[CaseInsensitive, Seq[String]]]
): PolyFunction5[smithyPlayClient.service.Operation, Kind1[ClientResponse]#toKind5] =
new PolyFunction5[smithyPlayClient.service.Operation, Kind1[ClientResponse]#toKind5] {
override def apply[I, E, O, SI, SO](
@@ -49,7 +53,7 @@ object GenericAPIClient {
def withClientAndHeaders(
client: RequestClient,
- additionalHeaders: Option[Map[String, Seq[String]]],
+ additionalHeaders: Option[Map[CaseInsensitive, Seq[String]]],
additionalSuccessCodes: List[Int] = List.empty
)(implicit ec: ExecutionContext): Alg[Kind1[ClientResponse]#toKind5] =
apply(service, additionalHeaders, additionalSuccessCodes, client)
@@ -71,7 +75,7 @@ object GenericAPIClient {
def apply[Alg[_[_, _, _, _, _]]](
serviceI: Service[Alg],
- additionalHeaders: Option[Map[String, Seq[String]]],
+ additionalHeaders: Option[Map[CaseInsensitive, Seq[String]]],
additionalSuccessCodes: List[Int],
client: RequestClient
)(implicit ec: ExecutionContext): Alg[Kind1[ClientResponse]#toKind5] =
diff --git a/smithy4play/src/main/scala/de/innfactory/smithy4play/client/RequestClient.scala b/smithy4play/src/main/scala/de/innfactory/smithy4play/client/RequestClient.scala
index 569848ff..45919432 100644
--- a/smithy4play/src/main/scala/de/innfactory/smithy4play/client/RequestClient.scala
+++ b/smithy4play/src/main/scala/de/innfactory/smithy4play/client/RequestClient.scala
@@ -1,16 +1,17 @@
package de.innfactory.smithy4play.client
+import de.innfactory.smithy4play.EndpointRequest
import play.api.mvc.Headers
+import smithy4s.Blob
+import smithy4s.http.{ CaseInsensitive, HttpResponse }
import scala.concurrent.Future
-case class SmithyClientResponse(body: Option[Array[Byte]], headers: Map[String, Seq[String]], statusCode: Int)
-
trait RequestClient {
def send(
method: String,
path: String,
- headers: Map[String, Seq[String]],
- body: Option[Array[Byte]]
- ): Future[SmithyClientResponse]
+ headers: Map[CaseInsensitive, Seq[String]],
+ request: EndpointRequest
+ ): Future[HttpResponse[Blob]]
}
diff --git a/smithy4play/src/main/scala/de/innfactory/smithy4play/client/SmithyPlayClient.scala b/smithy4play/src/main/scala/de/innfactory/smithy4play/client/SmithyPlayClient.scala
index ecfae1b5..6311dd95 100644
--- a/smithy4play/src/main/scala/de/innfactory/smithy4play/client/SmithyPlayClient.scala
+++ b/smithy4play/src/main/scala/de/innfactory/smithy4play/client/SmithyPlayClient.scala
@@ -1,9 +1,11 @@
package de.innfactory.smithy4play.client
+import cats.implicits.toBifunctorOps
import de.innfactory.smithy4play.ClientResponse
-import smithy4s.http.HttpEndpoint
+import smithy4s.Blob
+import smithy4s.http.{ CaseInsensitive, HttpEndpoint }
-import scala.concurrent.ExecutionContext
+import scala.concurrent.{ ExecutionContext, Future }
class SmithyPlayClient[Alg[_[_, _, _, _, _]], F[_]](
baseUri: String,
@@ -14,12 +16,12 @@ class SmithyPlayClient[Alg[_[_, _, _, _, _]], F[_]](
def send[I, E, O, SI, SO](
op: service.Operation[I, E, O, SI, SO],
- additionalHeaders: Option[Map[String, Seq[String]]]
+ additionalHeaders: Option[Map[CaseInsensitive, Seq[String]]]
): ClientResponse[O] = {
- val (input, endpoint) = service.endpoint(op)
+ val endpoint = service.endpoint(op)
HttpEndpoint
- .cast(endpoint)
+ .cast(endpoint.schema)
.map(httpEndpoint =>
new SmithyPlayClientEndpoint(
endpoint = endpoint,
@@ -27,7 +29,8 @@ class SmithyPlayClient[Alg[_[_, _, _, _, _]], F[_]](
additionalHeaders = additionalHeaders,
additionalSuccessCodes = additionalSuccessCodes,
httpEndpoint = httpEndpoint,
- input = input,
+ input = service.input(op),
+ serviceHints = service.hints,
client = client
).send()
)
diff --git a/smithy4play/src/main/scala/de/innfactory/smithy4play/client/SmithyPlayClientEndpoint.scala b/smithy4play/src/main/scala/de/innfactory/smithy4play/client/SmithyPlayClientEndpoint.scala
index 06368ee1..fb632569 100644
--- a/smithy4play/src/main/scala/de/innfactory/smithy4play/client/SmithyPlayClientEndpoint.scala
+++ b/smithy4play/src/main/scala/de/innfactory/smithy4play/client/SmithyPlayClientEndpoint.scala
@@ -1,88 +1,92 @@
package de.innfactory
package smithy4play
package client
-import smithy4s.{ Endpoint, Schema }
-import smithy4s.http.{ CaseInsensitive, CodecAPI, HttpEndpoint, Metadata, MetadataError, PayloadError }
+
+import alloy.SimpleRestJson
+import aws.protocols.RestXml
import cats.implicits._
+import play.api.http.MimeTypes
+import smithy4s.codecs.PayloadError
+import smithy4s.http._
+import smithy4s.{ Blob, Endpoint, Hints, Schema }
import scala.concurrent.{ ExecutionContext, Future }
private[smithy4play] class SmithyPlayClientEndpoint[Op[_, _, _, _, _], I, E, O, SI, SO](
endpoint: Endpoint[Op, I, E, O, SI, SO],
+ serviceHints: Hints,
baseUri: String,
- additionalHeaders: Option[Map[String, Seq[String]]],
- additionalSuccessCodes: List[Int] = List.empty,
+ additionalHeaders: Option[Map[CaseInsensitive, Seq[String]]],
+ additionalSuccessCodes: List[Int],
httpEndpoint: HttpEndpoint[I],
input: I,
client: RequestClient
)(implicit executionContext: ExecutionContext) {
- private val inputSchema: Schema[I] = endpoint.input
- private val outputSchema: Schema[O] = endpoint.output
+ private implicit val inputSchema: Schema[I] = endpoint.input
+ private implicit val outputSchema: Schema[O] = endpoint.output
- private val inputMetadataEncoder =
- Metadata.Encoder.fromSchema(inputSchema)
- private val outputMetadataDecoder =
- Metadata.PartialDecoder.fromSchema(outputSchema)
- private val inputHasBody =
- Metadata.TotalDecoder.fromSchema(inputSchema).isEmpty
+ private val serviceContentType: String = serviceHints.toMimeType
+ private val inputMetadataEncoder =
+ Metadata.Encoder.fromSchema(HttpRestSchema.OnlyMetadata(inputSchema).schema)
+ private val contentTypeKey = CaseInsensitive("content-type")
def send(
): ClientResponse[O] = {
- val metadata = inputMetadataEncoder.encode(input)
- val path = buildPath(metadata)
- val headers = metadata.headers.map(x => (x._1.toString.toLowerCase, x._2))
- val headersWithAuth = if (additionalHeaders.isDefined) headers.combine(additionalHeaders.get) else headers
- val code = httpEndpoint.code
- val codecApi: CodecAPI = CodecUtils.extractCodec(headers)
- val send = client.send(httpEndpoint.method.toString, path, headersWithAuth, _)
- val response = if (inputHasBody) {
- val codec = codecApi.compileCodec(inputSchema)
- val bodyEncoded = codecApi.writeToArray(codec, input)
- send(Some(bodyEncoded))
- } else send(None)
+ val metadata = inputMetadataEncoder.encode(input)
+ val path = buildPath(metadata)
+ val headers = metadata.headers
+ val contentTypeOpt = headers.get(contentTypeKey)
+ val contentType = contentTypeOpt.getOrElse(Seq(serviceContentType))
+ val headersWithAuth = if (additionalHeaders.isDefined) headers.combine(additionalHeaders.get) else headers
+ val code = httpEndpoint.code
+ val response =
+ client.send(
+ httpEndpoint.method.toString,
+ path,
+ headersWithAuth.updated(contentTypeKey, contentType),
+ writeInputToBlob(input, contentType)
+ )
decodeResponse(response, code)
}
+ private def writeInputToBlob(input: I, contentType: Seq[String]): EndpointRequest = {
+ val codecs = CodecDecider.requestEncoder(contentType)
+ codecs.fromSchema(inputSchema).write(PlayHttpRequest(Blob.empty, Metadata.empty), input)
+ }
+
private def decodeResponse(
- response: Future[SmithyClientResponse],
+ response: Future[HttpResponse[Blob]],
expectedCode: Int
): ClientResponse[O] =
for {
- res <- response
- metadata = Metadata(headers = res.headers.map(headers => (CaseInsensitive(headers._1), headers._2)))
- output <- if ((additionalSuccessCodes :+ expectedCode).contains(res.statusCode))
- handleSuccess(metadata, res)
- else handleError(res)
+ res <- response
+ output <- if ((additionalSuccessCodes :+ expectedCode).contains(res.statusCode)) {
+ handleSuccess(res)
+ } else handleError(res)
} yield output
- def handleSuccess(metadata: Metadata, response: SmithyClientResponse) = {
- val headers = response.headers.map(x => (x._1.toLowerCase, x._2))
- val output = outputMetadataDecoder.total match {
- case Some(totalDecoder) =>
- totalDecoder.decode(metadata)
- case None =>
- for {
- metadataPartial <- outputMetadataDecoder.decode(metadata)
- codecApi = CodecUtils.extractCodec(headers)
- bodyPartial <-
- codecApi.decodeFromByteArrayPartial(codecApi.compileCodec(outputSchema), response.body.get)
- output <- metadataPartial.combineCatch(bodyPartial)
- } yield output
+ def handleSuccess(response: HttpResponse[Blob]): ClientResponse[O] =
+ Future {
+ val headers = response.headers.map(x => (x._1, x._2))
+ val contentType = headers.getOrElse(contentTypeKey, Seq(serviceContentType))
+ val codec = CodecDecider.httpResponseDecoder(contentType)
+
+ codec
+ .fromSchema(outputSchema)
+ .decode(response)
+ .map(o => HttpResponse(response.statusCode, headers, o))
+ .leftMap {
+ case error: PayloadError =>
+ SmithyPlayClientEndpointErrorResponse(error.expected.getBytes(), response.statusCode)
+ case error: MetadataError =>
+ SmithyPlayClientEndpointErrorResponse(error.getMessage().getBytes(), response.statusCode)
+ }
}
- Future(
- output.map(o => SmithyPlayClientEndpointResponse(Some(o), headers, response.statusCode)).left.map {
- case error: PayloadError =>
- SmithyPlayClientEndpointErrorResponse(error.expected.getBytes, response.statusCode)
- case error: MetadataError =>
- SmithyPlayClientEndpointErrorResponse(error.getMessage().getBytes(), response.statusCode)
- }
- )
- }
- def handleError(response: SmithyClientResponse) = Future(
+ private def handleError(response: HttpResponse[Blob]): ClientResponse[O] = Future(
Left {
SmithyPlayClientEndpointErrorResponse(
- response.body.getOrElse(Array.emptyByteArray),
+ response.body.toArray,
response.statusCode
)
}
diff --git a/smithy4play/src/main/scala/de/innfactory/smithy4play/client/SmithyPlayClientEndpointResponse.scala b/smithy4play/src/main/scala/de/innfactory/smithy4play/client/SmithyPlayClientEndpointResponse.scala
deleted file mode 100644
index 1989a172..00000000
--- a/smithy4play/src/main/scala/de/innfactory/smithy4play/client/SmithyPlayClientEndpointResponse.scala
+++ /dev/null
@@ -1,9 +0,0 @@
-package de.innfactory.smithy4play.client
-
-import de.innfactory.smithy4play.Showable
-
-case class SmithyPlayClientEndpointResponse[O](
- body: Option[O],
- headers: Map[String, Seq[String]],
- statusCode: Int
-) extends Showable
diff --git a/smithy4play/src/main/scala/de/innfactory/smithy4play/client/SmithyPlayTestUtils.scala b/smithy4play/src/main/scala/de/innfactory/smithy4play/client/SmithyPlayTestUtils.scala
index 2bd04165..c90c2f5d 100644
--- a/smithy4play/src/main/scala/de/innfactory/smithy4play/client/SmithyPlayTestUtils.scala
+++ b/smithy4play/src/main/scala/de/innfactory/smithy4play/client/SmithyPlayTestUtils.scala
@@ -2,6 +2,7 @@ package de.innfactory.smithy4play.client
import de.innfactory.smithy4play.{ logger, ClientResponse }
import play.api.libs.json.{ Json, Reads }
+import smithy4s.http.HttpResponse
import scala.concurrent.duration.{ Duration, DurationInt }
import scala.concurrent.{ Await, ExecutionContext }
@@ -12,10 +13,13 @@ object SmithyPlayTestUtils {
def awaitRight(implicit
ec: ExecutionContext,
timeout: Duration = 5.seconds
- ): SmithyPlayClientEndpointResponse[O] =
+ ): HttpResponse[O] =
Await.result(
response.map { res =>
- if (res.isLeft) logger.error(s"Expected Right, got Left: ${res.left.toOption.get.toString}")
+ if (res.isLeft)
+ logger.error(
+ s"Expected Right, got Left: ${res.left.toOption.get.toString} Error: ${res.left.toOption.get.error.toErrorString}"
+ )
res.toOption.get
},
timeout
@@ -23,8 +27,7 @@ object SmithyPlayTestUtils {
def awaitLeft(implicit
ec: ExecutionContext,
- timeout: Duration = 5.seconds,
- errorAsString: Boolean = true
+ timeout: Duration = 5.seconds
): SmithyPlayClientEndpointErrorResponse =
Await.result(
response.map { res =>
diff --git a/smithy4play/src/main/scala/de/innfactory/smithy4play/compliancetests/ComplianceClient.scala b/smithy4play/src/main/scala/de/innfactory/smithy4play/compliancetests/ComplianceClient.scala
index 7119ee20..87c0377e 100644
--- a/smithy4play/src/main/scala/de/innfactory/smithy4play/compliancetests/ComplianceClient.scala
+++ b/smithy4play/src/main/scala/de/innfactory/smithy4play/compliancetests/ComplianceClient.scala
@@ -1,9 +1,9 @@
package de.innfactory.smithy4play.compliancetests
import de.innfactory.smithy4play.ClientResponse
-import de.innfactory.smithy4play.client.{ SmithyPlayClientEndpointErrorResponse, SmithyPlayClientEndpointResponse }
+import de.innfactory.smithy4play.client.SmithyPlayClientEndpointErrorResponse
import play.api.libs.json.Json
-import smithy4s.http.HttpEndpoint
+import smithy4s.http.{ HttpEndpoint, HttpResponse }
import smithy4s.kinds.{ FunctorAlgebra, Kind1 }
import smithy4s.{ Document, Endpoint, Service }
import smithy.test._
@@ -37,12 +37,12 @@ class ComplianceClient[
}
private def matchResponse[I, E, O, SE, SO](
- response: Either[SmithyPlayClientEndpointErrorResponse, SmithyPlayClientEndpointResponse[O]],
+ response: Either[SmithyPlayClientEndpointErrorResponse, HttpResponse[O]],
endpoint: Endpoint[service.Operation, I, E, O, SE, SO],
responseTestCase: Option[HttpResponseTestCase]
) = {
- val httpEp = HttpEndpoint.cast(endpoint).toOption.get
+ val httpEp = HttpEndpoint.cast(endpoint.schema).toOption.get
val responseStatusCode = response match {
case Left(value) => value.statusCode
case Right(value) => value.statusCode
@@ -59,7 +59,7 @@ class ComplianceClient[
expectedCode = expectedStatusCode,
receivedCode = responseStatusCode,
expectedBody = expectedOutput,
- receivedBody = response.toOption.flatMap(_.body),
+ receivedBody = response.toOption.map(_.body),
expectedError = responseTestCase match {
case Some(value) => value.body.getOrElse("")
case None => ""
diff --git a/smithy4play/src/main/scala/de/innfactory/smithy4play/middleware/MiddlewareBase.scala b/smithy4play/src/main/scala/de/innfactory/smithy4play/middleware/MiddlewareBase.scala
index 57441b7f..43767590 100644
--- a/smithy4play/src/main/scala/de/innfactory/smithy4play/middleware/MiddlewareBase.scala
+++ b/smithy4play/src/main/scala/de/innfactory/smithy4play/middleware/MiddlewareBase.scala
@@ -1,8 +1,10 @@
package de.innfactory.smithy4play.middleware
import cats.data.Kleisli
-import de.innfactory.smithy4play.{ EndpointResult, RouteResult, RoutingContext }
+import de.innfactory.smithy4play.{ RouteResult, RoutingContext }
import play.api.Logger
+import smithy4s.Blob
+import smithy4s.http.HttpResponse
trait MiddlewareBase {
@@ -10,14 +12,14 @@ trait MiddlewareBase {
protected def logic(
r: RoutingContext,
- next: RoutingContext => RouteResult[EndpointResult]
- ): RouteResult[EndpointResult]
+ next: RoutingContext => RouteResult[HttpResponse[Blob]]
+ ): RouteResult[HttpResponse[Blob]]
protected def skipMiddleware(r: RoutingContext): Boolean = false
def middleware(
- f: RoutingContext => RouteResult[EndpointResult]
- ): Kleisli[RouteResult, RoutingContext, EndpointResult] =
+ f: RoutingContext => RouteResult[HttpResponse[Blob]]
+ ): Kleisli[RouteResult, RoutingContext, HttpResponse[Blob]] =
Kleisli { r =>
if (skipMiddleware(r)) f(r)
else logic(r, f)
diff --git a/smithy4play/src/main/scala/de/innfactory/smithy4play/middleware/ValidateAuthMiddleware.scala b/smithy4play/src/main/scala/de/innfactory/smithy4play/middleware/ValidateAuthMiddleware.scala
index b95f6802..73f1bc6e 100644
--- a/smithy4play/src/main/scala/de/innfactory/smithy4play/middleware/ValidateAuthMiddleware.scala
+++ b/smithy4play/src/main/scala/de/innfactory/smithy4play/middleware/ValidateAuthMiddleware.scala
@@ -2,8 +2,11 @@ package de.innfactory.smithy4play.middleware
import cats.data.EitherT
import de.innfactory.smithy4play
-import de.innfactory.smithy4play.{ EndpointResult, RouteResult, RoutingContext, Smithy4PlayError }
+import de.innfactory.smithy4play.{ RouteResult, RoutingContext, Smithy4PlayError }
+import smithy.api
import smithy.api.{ Auth, HttpBearerAuth }
+import smithy4s.Blob
+import smithy4s.http.HttpResponse
import javax.inject.{ Inject, Singleton }
import scala.concurrent.{ ExecutionContext, Future }
@@ -14,17 +17,24 @@ class ValidateAuthMiddleware @Inject() (implicit
) extends MiddlewareBase {
override protected def skipMiddleware(r: RoutingContext): Boolean = {
- val serviceAuthHints = r.serviceHints.get(HttpBearerAuth.tagInstance).map(_ => Auth(Set(HttpBearerAuth.id.show)))
+ val serviceAuthHints: Option[api.Auth.Type] =
+ r.serviceHints
+ .get(HttpBearerAuth.tagInstance)
+ .map(_ =>
+ Auth(Set(smithy.api.AuthTraitReference(smithy4s.ShapeId(namespace = "smithy.api", name = "httpBearerAuth"))))
+ )
for {
authSet <- r.endpointHints.get(Auth.tag) orElse serviceAuthHints
- _ <- authSet.value.find(_.value == HttpBearerAuth.id.show)
+ _ <- authSet.value.find(_.value.name == HttpBearerAuth.id.name)
} yield r.headers.contains("Authorization")
}.getOrElse(true)
override def logic(
r: RoutingContext,
- next: RoutingContext => RouteResult[EndpointResult]
- ): RouteResult[EndpointResult] =
- EitherT.leftT[Future, EndpointResult](Smithy4PlayError("Unauthorized", status = smithy4play.Status(Map.empty, 401)))
+ next: RoutingContext => RouteResult[HttpResponse[Blob]]
+ ): RouteResult[HttpResponse[Blob]] =
+ EitherT.leftT[Future, HttpResponse[Blob]](
+ Smithy4PlayError("Unauthorized", status = smithy4play.Status(Map.empty, 401), contentType = "application/json")
+ )
}
diff --git a/smithy4play/src/main/scala/de/innfactory/smithy4play/package.scala b/smithy4play/src/main/scala/de/innfactory/smithy4play/package.scala
index 99c6eb7a..921c6f32 100644
--- a/smithy4play/src/main/scala/de/innfactory/smithy4play/package.scala
+++ b/smithy4play/src/main/scala/de/innfactory/smithy4play/package.scala
@@ -1,26 +1,40 @@
package de.innfactory
+import alloy.SimpleRestJson
+import aws.protocols.RestXml
import cats.data.{ EitherT, Kleisli }
-import de.innfactory.smithy4play.client.{ SmithyPlayClientEndpointErrorResponse, SmithyPlayClientEndpointResponse }
+import de.innfactory.smithy4play.client.SmithyPlayClientEndpointErrorResponse
import org.slf4j
import play.api.Logger
-import play.api.libs.json.{ JsValue, Json }
+import play.api.http.MimeTypes
+import play.api.libs.json.{ JsValue, Json, OFormat }
import play.api.mvc.{ Headers, RequestHeader }
-import smithy4s.http.{ CaseInsensitive, HttpEndpoint }
+import smithy4s.{ Blob, Hints }
+import smithy4s.http.{ CaseInsensitive, HttpEndpoint, HttpResponse, Metadata }
import scala.annotation.{ compileTimeOnly, StaticAnnotation }
import scala.concurrent.Future
import scala.language.experimental.macros
+import scala.util.matching.Regex
+import scala.xml.Elem
package object smithy4play {
trait ContextRouteError extends StatusResult[ContextRouteError] {
+ def contentType: String = "application/json"
def message: String
+ def toXml: Elem = {message}
+ def parse: String = contentType match {
+ case "application/json" => toJson.toString()
+ case "application/xml" => toXml.toString()
+ }
def toJson: JsValue
}
- type ClientResponse[O] = Future[Either[SmithyPlayClientEndpointErrorResponse, SmithyPlayClientEndpointResponse[O]]]
- type RunnableClientRequest[O] = Kleisli[ClientResponse, Option[Map[String, Seq[String]]], O]
+ case class ContentType(value: String)
+
+ type ClientResponse[O] = Future[Either[SmithyPlayClientEndpointErrorResponse, HttpResponse[O]]]
+ type RunnableClientRequest[O] = Kleisli[ClientResponse, Option[Map[CaseInsensitive, Seq[String]]], O]
type RouteResult[O] = EitherT[Future, ContextRouteError, O]
type ContextRoute[O] = Kleisli[RouteResult, RoutingContext, O]
@@ -31,21 +45,46 @@ package object smithy4play {
case class Status(headers: Map[String, String], statusCode: Int)
object Status {
- implicit val format = Json.format[Status]
+ implicit val format: OFormat[Status] = Json.format[Status]
}
- case class EndpointResult(body: Option[Array[Byte]], status: Status) extends StatusResult[EndpointResult] {
- override def addHeaders(headers: Map[String, String]): EndpointResult = this.copy(
- status = status.copy(
- headers = status.headers ++ headers
+ type EndpointRequest = PlayHttpRequest[Blob]
+ case class PlayHttpRequest[Body](body: Body, metadata: Metadata) {
+ def addHeaders(headers: Map[CaseInsensitive, Seq[String]]): PlayHttpRequest[Body] = this.copy(
+ metadata = metadata.copy(
+ headers = metadata.headers ++ headers
)
)
}
+ implicit class EnhancedHints(hints: Hints) {
+ def toMimeType: String =
+ (hints.get(RestXml.getTag), hints.get(SimpleRestJson.getTag)) match {
+ case (Some(_), None) => MimeTypes.XML
+ case _ => MimeTypes.JSON
+ }
+ }
+
+ implicit class EnhancedThrowable(throwable: Throwable) {
+ private val regex1: Regex = """(?s), offset: (?:0x)?[0-9a-fA-F]+, buf:.*""".r
+ private val regex2: Regex = """(.*), offset: .*, buf:.* (\(path:.*\))""".r
+ def filterMessage: String =
+ regex2.replaceAllIn(
+ throwable.getMessage.filter(_ >= ' '),
+ m =>
+ m.matched match {
+ case regex2(initialMessage, endMessage) => s"$initialMessage: $endMessage"
+ case msg => regex1.replaceAllIn(msg, "")
+ }
+ )
+
+ }
+
private[smithy4play] case class Smithy4PlayError(
message: String,
status: Status,
- additionalInformation: Option[String] = None
+ additionalInformation: Option[String] = None,
+ override val contentType: String
) extends ContextRouteError {
override def toJson: JsValue = Json.toJson(this)(Smithy4PlayError.format)
override def addHeaders(headers: Map[String, String]): Smithy4PlayError = this.copy(
diff --git a/smithy4playTest/.gitignore b/smithy4playTest/.gitignore
index 716038bf..90223970 100644
--- a/smithy4playTest/.gitignore
+++ b/smithy4playTest/.gitignore
@@ -1 +1 @@
-*/testDefinitions/*
\ No newline at end of file
+*/specs/*
\ No newline at end of file
diff --git a/smithy4playTest/app/controller/TestController.scala b/smithy4playTest/app/controller/TestController.scala
index ee60b420..20ec6951 100644
--- a/smithy4playTest/app/controller/TestController.scala
+++ b/smithy4playTest/app/controller/TestController.scala
@@ -4,7 +4,7 @@ import cats.data.{ EitherT, Kleisli }
import controller.models.TestError
import de.innfactory.smithy4play.{ AutoRouting, ContextRoute, ContextRouteError }
import play.api.mvc.ControllerComponents
-import smithy4s.ByteArray
+import smithy4s.Blob
import testDefinitions.test._
import javax.inject.{ Inject, Singleton }
@@ -48,7 +48,7 @@ class TestController @Inject() (implicit
}
- override def testWithBlob(body: ByteArray, contentType: String): ContextRoute[BlobResponse] = Kleisli { rc =>
+ override def testWithBlob(body: Blob, contentType: String): ContextRoute[BlobResponse] = Kleisli { rc =>
EitherT.rightT[Future, ContextRouteError](BlobResponse(body, "image/png"))
}
diff --git a/smithy4playTest/app/controller/XmlController.scala b/smithy4playTest/app/controller/XmlController.scala
new file mode 100644
index 00000000..964ec18e
--- /dev/null
+++ b/smithy4playTest/app/controller/XmlController.scala
@@ -0,0 +1,33 @@
+package controller
+
+import cats.data.{ EitherT, Kleisli }
+import cats.implicits.catsSyntaxEitherId
+import de.innfactory.smithy4play.{ AutoRouting, ContextRoute, ContextRouteError }
+import play.api.mvc.ControllerComponents
+import testDefinitions.test.{ XmlControllerDef, XmlTestInputBody, XmlTestOutput, XmlTestWithInputAndOutputOutput }
+
+import javax.inject.{ Inject, Singleton }
+import scala.concurrent.{ ExecutionContext, Future }
+
+@Singleton
+@AutoRouting
+class XmlController @Inject() (implicit
+ cc: ControllerComponents,
+ executionContext: ExecutionContext
+) extends XmlControllerDef[ContextRoute] {
+
+ override def xmlTestWithInputAndOutput(
+ xmlTest: String,
+ body: XmlTestInputBody
+ ): ContextRoute[XmlTestWithInputAndOutputOutput] =
+ Kleisli { _ =>
+ EitherT(
+ Future(
+ XmlTestWithInputAndOutputOutput(
+ XmlTestOutput(body.serverzeit, body.requiredTest + xmlTest, body.requiredInt.map(i => i * i))
+ )
+ .asRight[ContextRouteError]
+ )
+ )
+ }
+}
diff --git a/smithy4playTest/app/controller/middlewares/ChangeStatusCodeMiddleware.scala b/smithy4playTest/app/controller/middlewares/ChangeStatusCodeMiddleware.scala
index f92daa0c..77b19788 100644
--- a/smithy4playTest/app/controller/middlewares/ChangeStatusCodeMiddleware.scala
+++ b/smithy4playTest/app/controller/middlewares/ChangeStatusCodeMiddleware.scala
@@ -1,7 +1,9 @@
package controller.middlewares
import de.innfactory.smithy4play.middleware.MiddlewareBase
-import de.innfactory.smithy4play.{ EndpointResult, RouteResult, RoutingContext, Status }
+import de.innfactory.smithy4play.{ RouteResult, RoutingContext }
+import smithy4s.Blob
+import smithy4s.http.HttpResponse
import testDefinitions.test.ChangeStatusCode
import javax.inject.Inject
@@ -14,11 +16,11 @@ class ChangeStatusCodeMiddleware @Inject() (implicit executionContext: Execution
override protected def logic(
r: RoutingContext,
- next: RoutingContext => RouteResult[EndpointResult]
- ): RouteResult[EndpointResult] = {
+ next: RoutingContext => RouteResult[HttpResponse[Blob]]
+ ): RouteResult[HttpResponse[Blob]] = {
val res = next(r)
res.map { r =>
- r.copy(status = Status(r.status.headers, 269))
+ r.copy(statusCode = 269)
}
}
diff --git a/smithy4playTest/app/controller/middlewares/DisableAbleMiddleware.scala b/smithy4playTest/app/controller/middlewares/DisableAbleMiddleware.scala
index a7591e27..ed6340a0 100644
--- a/smithy4playTest/app/controller/middlewares/DisableAbleMiddleware.scala
+++ b/smithy4playTest/app/controller/middlewares/DisableAbleMiddleware.scala
@@ -1,7 +1,9 @@
package controller.middlewares
import de.innfactory.smithy4play.middleware.MiddlewareBase
-import de.innfactory.smithy4play.{ EndpointResult, RouteResult, RoutingContext }
+import de.innfactory.smithy4play.{ RouteResult, RoutingContext }
+import smithy4s.Blob
+import smithy4s.http.HttpResponse
import testDefinitions.test.DisableTestMiddleware
import javax.inject.{ Inject, Singleton }
@@ -15,14 +17,14 @@ class DisableAbleMiddleware @Inject() (implicit executionContext: ExecutionConte
override protected def logic(
r: RoutingContext,
- next: RoutingContext => RouteResult[EndpointResult]
- ): RouteResult[EndpointResult] = {
+ next: RoutingContext => RouteResult[HttpResponse[Blob]]
+ ): RouteResult[HttpResponse[Blob]] = {
logger.info("[DisableAbleMiddleware.logic1]")
val r1 = r.copy(attributes = r.attributes + ("Not" -> "Disabled"))
val res = next(r1)
logger.info("[DisableAbleMiddleware.logic2]")
res.map { r =>
- logger.info(s"[DisableAbleMiddleware.logic3] ${r.status.headers.toString()}")
+ logger.info(s"[DisableAbleMiddleware.logic3] ${r.headers.toString()}")
r
}
}
diff --git a/smithy4playTest/app/controller/middlewares/TestMiddlewareImpl.scala b/smithy4playTest/app/controller/middlewares/TestMiddlewareImpl.scala
index a9d9917e..bcd62b10 100644
--- a/smithy4playTest/app/controller/middlewares/TestMiddlewareImpl.scala
+++ b/smithy4playTest/app/controller/middlewares/TestMiddlewareImpl.scala
@@ -1,7 +1,9 @@
package controller.middlewares
import de.innfactory.smithy4play.middleware.MiddlewareBase
-import de.innfactory.smithy4play.{ EndpointResult, RouteResult, RoutingContext, Status }
+import de.innfactory.smithy4play.{ RouteResult, RoutingContext }
+import smithy4s.Blob
+import smithy4s.http.{ CaseInsensitive, HttpResponse }
import javax.inject.Inject
import scala.concurrent.ExecutionContext
@@ -10,15 +12,15 @@ class TestMiddlewareImpl @Inject() (implicit executionContext: ExecutionContext)
override protected def logic(
r: RoutingContext,
- next: RoutingContext => RouteResult[EndpointResult]
- ): RouteResult[EndpointResult] = {
+ next: RoutingContext => RouteResult[HttpResponse[Blob]]
+ ): RouteResult[HttpResponse[Blob]] = {
logger.info("[TestMiddleware.logic1]")
val r1 = r.copy(attributes = r.attributes + ("Test" -> "Test"))
val res = next(r1)
logger.info("[TestMiddleware.logic2]")
res.map { r =>
logger.info("[TestMiddleware.logic3]")
- r.addHeaders(Map("EndpointResultTest" -> "Test123"))
+ r.addHeaders(Map(CaseInsensitive("EndpointResultTest") -> Seq("Test123")))
}
}
diff --git a/smithy4playTest/app/controller/models/TestError.scala b/smithy4playTest/app/controller/models/TestError.scala
index d4c454ec..6c7ee587 100644
--- a/smithy4playTest/app/controller/models/TestError.scala
+++ b/smithy4playTest/app/controller/models/TestError.scala
@@ -1,7 +1,7 @@
package controller.models
import de.innfactory.smithy4play.{ ContextRouteError, Status }
-import play.api.libs.json.{ JsValue, Json }
+import play.api.libs.json.{ JsValue, Json, OFormat }
case class TestError(
message: String,
@@ -16,5 +16,5 @@ case class TestError(
}
object TestError {
- implicit val format = Json.format[TestError]
+ implicit val format: OFormat[TestError] = Json.format[TestError]
}
diff --git a/smithy4playTest/test/TestControllerTest.scala b/smithy4playTest/test/TestControllerTest.scala
index bd857297..7bdb8c9b 100644
--- a/smithy4playTest/test/TestControllerTest.scala
+++ b/smithy4playTest/test/TestControllerTest.scala
@@ -1,57 +1,32 @@
import controller.models.TestError
-import de.innfactory.smithy4play.CodecUtils
import de.innfactory.smithy4play.client.GenericAPIClient.EnhancedGenericAPIClient
-import de.innfactory.smithy4play.client.{ RequestClient, SmithyClientResponse }
import de.innfactory.smithy4play.client.SmithyPlayTestUtils._
import de.innfactory.smithy4play.compliancetests.ComplianceClient
-import models.TestJson
-import org.scalatestplus.play.{ BaseOneAppPerSuite, FakeApplicationFactory, PlaySpec }
+import models.NodeImplicits.NodeEnhancer
+import models.{ TestBase, TestJson }
+import org.scalatest.time.SpanSugar.convertIntToGrainOfTime
import play.api.Application
-import play.api.Play.materializer
import play.api.inject.guice.GuiceApplicationBuilder
import play.api.libs.json.{ Json, OWrites }
-import play.api.mvc.{ AnyContentAsEmpty, Result }
+import play.api.mvc.Result
import play.api.test.FakeRequest
import play.api.test.Helpers._
-import testDefinitions.test.{ SimpleTestResponse, TestControllerServiceGen, TestRequestBody }
-import smithy4s.ByteArray
+import smithy4s.Blob
+import smithy4s.http.CaseInsensitive
+import testDefinitions.test.{
+ SimpleTestResponse,
+ TestControllerServiceGen,
+ TestRequestBody,
+ TestResponseBody,
+ TestWithOutputResponse
+}
import java.io.File
import java.nio.file.Files
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
-import scala.concurrent.duration.DurationInt
-
-class TestControllerTest extends PlaySpec with BaseOneAppPerSuite with FakeApplicationFactory {
-
- implicit object FakeRequestClient extends RequestClient {
- override def send(
- method: String,
- path: String,
- headers: Map[String, Seq[String]],
- body: Option[Array[Byte]]
- ): Future[SmithyClientResponse] = {
- val baseRequest: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(method, path)
- .withHeaders(headers.toList.flatMap(headers => headers._2.map(v => (headers._1, v))): _*)
- val res =
- if (body.isDefined) route(app, baseRequest.withBody(body.get)).get
- else
- route(
- app,
- baseRequest
- ).get
-
- for {
- result <- res
- headers = result.header.headers.map(v => (v._1, Seq(v._2)))
- body <- result.body.consumeData.map(_.toArrayUnsafe())
- bodyConsumed = if (result.body.isKnownEmpty) None else Some(body)
- contentType = result.body.contentType
- headersWithContentType =
- if (contentType.isDefined) headers + ("Content-Type" -> Seq(contentType.get)) else headers
- } yield SmithyClientResponse(bodyConsumed, headersWithContentType, result.header.status)
- }
- }
+
+class TestControllerTest extends TestBase {
val genericClient = TestControllerServiceGen.withClientAndHeaders(FakeRequestClient, None, List(269))
@@ -86,7 +61,7 @@ class TestControllerTest extends PlaySpec with BaseOneAppPerSuite with FakeAppli
val body = TestRequestBody("thisIsARequestBody")
val result = genericClient.testWithOutput(pathParam, testQuery, testHeader, body).awaitRight
- val responseBody = result.body.get
+ val responseBody = result.body
result.statusCode mustBe 200
responseBody.body.testQuery mustBe testQuery
responseBody.body.pathParam mustBe pathParam
@@ -94,7 +69,7 @@ class TestControllerTest extends PlaySpec with BaseOneAppPerSuite with FakeAppli
responseBody.body.testHeader mustBe testHeader
}
- "route to Test Endpoint but should return error because required header is not set" in {
+ "route to Test Endpoint with Query Parameter, Path Parameter and Body with fake request" in {
val pathParam = "thisIsAPathParam"
val testQuery = "thisIsATestQuery"
val testHeader = "thisIsATestHeader"
@@ -104,29 +79,74 @@ class TestControllerTest extends PlaySpec with BaseOneAppPerSuite with FakeAppli
route(
app,
FakeRequest("POST", s"/test/$pathParam?testQuery=$testQuery")
- .withHeaders(("Wrong-Header", testHeader))
+ .withHeaders(("Test-Header", testHeader))
.withBody(Json.toJson(body))
).get
- status(future) mustBe 500
+ implicit val formatBody = Json.format[TestResponseBody]
+ val responseBody = contentAsJson(future).as[TestResponseBody]
+ status(future) mustBe 200
+ responseBody.testQuery mustBe testQuery
+ responseBody.pathParam mustBe pathParam
+ responseBody.bodyMessage mustBe body.message
+ responseBody.testHeader mustBe testHeader
}
- "route to Query Endpoint but should return error because query is not set" in {
+ "route to Test Endpoint with Query Parameter, Path Parameter and Body with fake request and xml protocol" in {
+ val pathParam = "thisIsAPathParam"
+ val testQuery = "thisIsATestQuery"
+ val testHeader = "thisIsATestHeader"
+ val testBody = "thisIsARequestBody"
+ val xml = {testBody}
+ val future: Future[Result] =
+ route(
+ app,
+ FakeRequest("POST", s"/test/$pathParam?testQuery=$testQuery")
+ .withHeaders(("Test-Header", testHeader))
+ .withXmlBody(xml)
+ ).get
+ status(future) mustBe 200
+ val xmlRes = scala.xml.XML.loadString(contentAsString(future))
+ xmlRes.normalize mustBe
+ {testHeader}
+ {pathParam}
+ {testQuery}
+ {testBody}
+ .normalize
+ }
+
+ "route to Test Endpoint but should return error because required header is not set" in {
+ val pathParam = "thisIsAPathParam"
val testQuery = "thisIsATestQuery"
+ val testHeader = "thisIsATestHeader"
+ val body = TestRequestBody("thisIsARequestBody")
implicit val format: OWrites[TestRequestBody] = Json.writes[TestRequestBody]
val future: Future[Result] =
+ route(
+ app,
+ FakeRequest("POST", s"/test/$pathParam?testQuery=$testQuery")
+ .withHeaders(("Wrong-Header", testHeader))
+ .withBody(Json.toJson(body))
+ ).get
+
+ status(future) mustBe 400
+ }
+
+ "route to Query Endpoint but should return error because query is not set" in {
+ val testQuery = "thisIsATestQuery"
+ val future: Future[Result] =
route(
app,
FakeRequest("GET", s"/query?WrongQuery=$testQuery")
).get
- status(future) mustBe 500
+ status(future) mustBe 400
}
"route to Health Endpoint" in {
val result = genericClient.health().awaitRight
- result.headers.contains("endpointresulttest") mustBe true
+ result.headers.contains(CaseInsensitive("endpointresulttest")) mustBe true
result.statusCode mustBe 200
}
@@ -140,11 +160,11 @@ class TestControllerTest extends PlaySpec with BaseOneAppPerSuite with FakeAppli
"route to Blob Endpoint" in {
val path = getClass.getResource("/testPicture.png").getPath
val file = new File(path)
- val pngAsBytes = ByteArray(Files.readAllBytes(file.toPath))
- val result = genericClient.testWithBlob(pngAsBytes, "image/png").awaitRight
+ val pngAsBytes = Blob(Files.readAllBytes(file.toPath))
+ val result = genericClient.testWithBlob(pngAsBytes, "image/png").awaitRight(global, 5.hours)
result.statusCode mustBe 200
- pngAsBytes mustBe result.body.get.body
+ pngAsBytes mustBe result.body.body
}
"route to Auth Test" in {
@@ -161,17 +181,18 @@ class TestControllerTest extends PlaySpec with BaseOneAppPerSuite with FakeAppli
"manual writing json" in {
- val writtenData = CodecUtils.writeEntityToJsonBytes(SimpleTestResponse(Some("Test")), SimpleTestResponse.schema)
+ val writtenData = smithy4s.json.Json.writeBlob(SimpleTestResponse(Some("Test")))
- val writtenJson = Json.parse(writtenData).as[TestJson]
+ val writtenJson = Json.parse(writtenData.toArray).as[TestJson]
- val readData = CodecUtils.readFromJsonBytes(
- Json.toBytes(Json.toJson(TestJson(Some("Test")))),
- SimpleTestResponse.schema
- )
+ val readData =
+ smithy4s.json.Json.read(Blob(Json.toBytes(Json.toJson(TestJson(Some("Test"))))))(SimpleTestResponse.schema)
writtenJson.message mustBe Some("Test")
- readData.get.message mustBe Some("Test")
+ readData match {
+ case Right(value) => value.message mustBe Some("Test")
+ case _ => fail("should parse")
+ }
}
}
}
diff --git a/smithy4playTest/test/XmlControllerTest.scala b/smithy4playTest/test/XmlControllerTest.scala
new file mode 100644
index 00000000..57b86ed2
--- /dev/null
+++ b/smithy4playTest/test/XmlControllerTest.scala
@@ -0,0 +1,144 @@
+import de.innfactory.smithy4play.CodecDecider
+import de.innfactory.smithy4play.client.GenericAPIClient.EnhancedGenericAPIClient
+import de.innfactory.smithy4play.client.SmithyPlayTestUtils._
+import models.NodeImplicits.NodeEnhancer
+import models.TestBase
+import play.api.Application
+import play.api.inject.guice.GuiceApplicationBuilder
+import play.api.libs.json.{ JsValue, Json, OFormat }
+import play.api.test.FakeRequest
+import play.api.test.Helpers._
+import smithy4s.Blob
+import smithy4s.http.CaseInsensitive
+import testDefinitions.test.{ XmlTestInputBody, XmlControllerDefGen, XmlTestOutput }
+
+import scala.xml._
+import scala.concurrent.ExecutionContext.Implicits.global
+import scala.xml.{ Elem, Node, PrettyPrinter }
+
+class XmlControllerTest extends TestBase {
+
+ val genericClient = XmlControllerDefGen.withClientAndHeaders(FakeRequestClient, None, List(269))
+
+ override def fakeApplication(): Application =
+ new GuiceApplicationBuilder().build()
+
+ "controller.XmlController" must {
+
+ "route to xml test endpoint" in {
+ val res = genericClient
+ .xmlTestWithInputAndOutput(
+ "Concat",
+ XmlTestInputBody("05.02.2024", "ThisGets", Some(10))
+ )
+ .awaitRight
+ res.body.body.requiredIntSquared mustBe Some(100)
+ res.headers.get(CaseInsensitive("content-type")) mustBe Some(List("application/xml"))
+ res.body.body.requiredTestStringConcat mustBe "ThisGetsConcat"
+ }
+
+ "route to xml test endpoint with external client" in {
+ val concatVal1 = "ConcatThis"
+ val concatVal2 = "Test2"
+ val squareTest = 3
+ val xml =
+
+ {concatVal1}
+ {squareTest}
+
+ val request = route(
+ app,
+ FakeRequest("POST", s"/xml/$concatVal2")
+ .withHeaders(("content-type", "application/xml"))
+ .withXmlBody(
+ xml
+ )
+ ).get
+ status(request) mustBe 200
+
+ val result = scala.xml.XML.loadString(contentAsString(request))
+ result.normalize mustBe
+
+ {concatVal1 + concatVal2}
+
+ {squareTest * squareTest}
+ .normalize
+ }
+
+ "route to xml test endpoint with external client and throw error because of missing attribute" in {
+ val xml =
+
+
+ val request = route(
+ app,
+ FakeRequest("POST", s"/xml/Test2")
+ .withHeaders(("content-type", "application/xml"))
+ .withXmlBody(
+ xml
+ )
+ ).get
+ status(request) mustBe 400
+ val result = scala.xml.XML.loadString(contentAsString(request))
+ result.normalize mustBe Expected a single node with text content (path: .XmlTestInputBody.requiredTest).normalize
+ }
+
+ "route to xml test endpoint with external client and set json header but send xml" in {
+ val xml =
+
+
+ val request = route(
+ app,
+ FakeRequest("POST", s"/xml/Test2")
+ .withHeaders(("content-type", "application/json"))
+ .withXmlBody(
+ xml
+ )
+ ).get
+ status(request) mustBe 400
+ val result = contentAsJson(request)
+ result.toString() mustBe
+ "{\"message\":\"Expected JSON object: (path: .)\",\"status\":{\"headers\":{},\"statusCode\":400}," +
+ "\"contentType\":\"application/json\"}"
+ }
+
+ "route to test endpoint with external client and set xml header but send " in {
+ implicit val format = Json.format[XmlTestInputBody]
+
+ val request = route(
+ app,
+ FakeRequest("POST", s"/xml/Test2")
+ .withHeaders(("content-type", "application/xml"))
+ .withJsonBody(
+ Json.toJson(XmlTestInputBody("05.02.2024", "ThisShouldNotWork", Some(10)))
+ )
+ ).get
+ status(request) mustBe 400
+ val result = scala.xml.XML.loadString(contentAsString(request))
+ result.normalize mustBe
+ {"Could not parse XML document: unexpected character '{' (path: .)"}.normalize
+ }
+
+ "route to test endpoint with external client and json protocol" in {
+ implicit val formatI: OFormat[XmlTestInputBody] = Json.format[XmlTestInputBody]
+ implicit val formatO: OFormat[XmlTestOutput] = Json.format[XmlTestOutput]
+ val concatVal2 = "Test2"
+ val concatVal1 = "ConcatThis"
+ val squareTest = Some(15)
+ val date = "05.02.2024"
+ val request = route(
+ app,
+ FakeRequest("POST", s"/xml/$concatVal2")
+ .withHeaders(("content-type", "application/json"))
+ .withJsonBody(
+ Json.toJson(XmlTestInputBody(date, concatVal1, squareTest))
+ )
+ ).get
+ status(request) mustBe 200
+ val result = contentAsJson(request).as[XmlTestOutput]
+ result.requiredTestStringConcat mustBe concatVal1 + concatVal2
+ result.requiredIntSquared mustBe squareTest.map(s => s * s)
+ result.serverzeit mustBe date
+ }
+
+ }
+}
diff --git a/smithy4playTest/test/models/NodeImplicits.scala b/smithy4playTest/test/models/NodeImplicits.scala
new file mode 100644
index 00000000..bc04148d
--- /dev/null
+++ b/smithy4playTest/test/models/NodeImplicits.scala
@@ -0,0 +1,25 @@
+package models
+
+import scala.xml.{ Elem, Node, Text }
+
+object NodeImplicits {
+
+ implicit class NodeEnhancer(xml: Elem) {
+ def normalize: String = {
+ def normalizeText(text: String): String = text.trim
+
+ def normalizeElem(node: Node): Node = node match {
+ case elem: Elem =>
+ val normalizedChildren = elem.child.map {
+ case t: Text => Text(normalizeText(t.text))
+ case other => normalizeElem(other)
+ }
+ elem.copy(child = normalizedChildren)
+ case other => other
+ }
+
+ val normalizedXml = normalizeElem(xml)
+ normalizedXml.mkString
+ }
+ }
+}
diff --git a/smithy4playTest/test/models/TestBase.scala b/smithy4playTest/test/models/TestBase.scala
new file mode 100644
index 00000000..949afc29
--- /dev/null
+++ b/smithy4playTest/test/models/TestBase.scala
@@ -0,0 +1,50 @@
+package models
+
+import de.innfactory.smithy4play.EndpointRequest
+import de.innfactory.smithy4play.client.RequestClient
+import org.scalatestplus.play.{BaseOneAppPerSuite, FakeApplicationFactory, PlaySpec}
+import play.api.mvc.AnyContentAsEmpty
+import play.api.test.FakeRequest
+import play.api.test.Helpers.{route, writeableOf_AnyContentAsEmpty}
+import smithy4s.Blob
+import smithy4s.http.{CaseInsensitive, HttpResponse}
+
+import scala.concurrent.Future
+import scala.concurrent.ExecutionContext.Implicits.global
+import play.api.Play.materializer
+
+
+trait TestBase extends PlaySpec with BaseOneAppPerSuite with FakeApplicationFactory {
+
+ implicit object FakeRequestClient extends RequestClient {
+ override def send(
+ method: String,
+ path: String,
+ headers: Map[CaseInsensitive, Seq[String]],
+ result: EndpointRequest
+ ): Future[HttpResponse[Blob]] = {
+ val baseRequest: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(method, path)
+ .withHeaders(headers.toList.flatMap(headers => headers._2.map(v => (headers._1.toString, v))): _*)
+ val res =
+ if (!result.body.isEmpty) route(app, baseRequest.withBody(result.body.toArray)).get
+ else
+ route(
+ app,
+ baseRequest
+ ).get
+
+ for {
+ result <- res
+ headers = result.header.headers.map(v => (CaseInsensitive(v._1), Seq(v._2)))
+ body <- result.body.consumeData.map(_.toArrayUnsafe())
+ bodyConsumed = if (result.body.isKnownEmpty) None else Some(body)
+ contentType = result.body.contentType
+ } yield HttpResponse(
+ result.header.status,
+ headers,
+ bodyConsumed.map(Blob(_)).getOrElse(Blob.empty)
+ ).withContentType(contentType.getOrElse("application/json"))
+ }
+ }
+
+}
diff --git a/smithy4playTest/testSpecs/TestController.smithy b/smithy4playTest/testSpecs/TestController.smithy
index e36d60f8..066ffd2c 100644
--- a/smithy4playTest/testSpecs/TestController.smithy
+++ b/smithy4playTest/testSpecs/TestController.smithy
@@ -8,7 +8,16 @@ use alloy#simpleRestJson
@simpleRestJson
service TestControllerService {
version: "0.0.1",
- operations: [Test, TestWithOutput, Health, TestWithBlob, TestWithQuery, TestThatReturnsError, TestAuth, TestWithOtherStatusCode]
+ operations: [
+ Test
+ TestWithOutput
+ Health
+ TestWithBlob
+ TestWithQuery
+ TestThatReturnsError
+ TestAuth
+ TestWithOtherStatusCode
+ ]
}
@auth([])
@@ -24,11 +33,13 @@ operation TestWithBlob {
input: BlobRequest,
output: BlobResponse
}
+
@auth([])
@readonly
@http(method: "GET", uri: "/error", code: 200)
operation TestThatReturnsError {
}
+
@auth([])
@readonly
@http(method: "GET", uri: "/query", code: 200)
diff --git a/smithy4playTest/testSpecs/XmlController.smithy b/smithy4playTest/testSpecs/XmlController.smithy
new file mode 100644
index 00000000..9daa33e2
--- /dev/null
+++ b/smithy4playTest/testSpecs/XmlController.smithy
@@ -0,0 +1,52 @@
+$version: "2"
+namespace testDefinitions.test
+
+use aws.protocols#restXml
+
+@restXml
+service XmlControllerDef {
+ version: "0.0.1",
+ operations: [
+ XmlTestWithInputAndOutput
+ ]
+}
+
+@http(method: "POST", uri: "/xml/{xmlTest}")
+operation XmlTestWithInputAndOutput {
+ input: XmlTestInput
+ output := {
+ @required
+ @httpPayload
+ body: XmlTestOutput
+ }
+}
+
+structure XmlTestInput {
+ @httpLabel
+ @required
+ xmlTest: String
+ @required
+ @httpPayload
+ body: XmlTestInputBody
+}
+
+
+structure XmlTestInputBody {
+ @xmlAttribute
+ @required
+ serverzeit: String
+ @required
+ requiredTest: String
+ requiredInt: Integer
+}
+
+
+structure XmlTestOutput {
+ @xmlAttribute
+ @required
+ serverzeit: String
+ @required
+ requiredTestStringConcat: String
+ requiredIntSquared: Integer
+}
+