diff --git a/smithy4play/src/main/scala/de/innfactory/smithy4play/SmithyPlayEndpoint.scala b/smithy4play/src/main/scala/de/innfactory/smithy4play/SmithyPlayEndpoint.scala index 6a8faf1c..e5ad58de 100644 --- a/smithy4play/src/main/scala/de/innfactory/smithy4play/SmithyPlayEndpoint.scala +++ b/smithy4play/src/main/scala/de/innfactory/smithy4play/SmithyPlayEndpoint.scala @@ -6,11 +6,21 @@ import cats.implicits.toBifunctorOps import de.innfactory.smithy4play import de.innfactory.smithy4play.middleware.MiddlewareBase import play.api.mvc._ -import smithy4s.codecs.{ PayloadError, StringAndBlobCodecs } -import smithy4s.http.{ HttpEndpoint, HttpResponse, Metadata, PathParams } -import smithy4s.json.Json +import smithy4s.codecs.{ + BlobDecoder, + BlobEncoder, + PayloadDecoder, + PayloadEncoder, + PayloadError, + StringAndBlobCodecs, + Writer +} +import smithy4s.http.{ HttpEndpoint, HttpRequest, HttpResponse, HttpRestSchema, Metadata, PathParams } +import smithy4s.json.{ Json, JsonPayloadCodecCompiler } +import smithy4s.json.Json.payloadCodecs import smithy4s.kinds.FunctorInterpreter import smithy4s.schema.{ CachedSchemaCompiler, Schema } +import smithy4s.xml.Xml.encoders import smithy4s.xml.{ Xml, XmlDecodeError } import smithy4s.{ Blob, Endpoint, Service } @@ -43,6 +53,15 @@ class SmithyPlayEndpoint[Alg[_[_, _, _, _, _]], F[_] <: ContextRoute[_], Op[ private val outputMetadataEncoder: Metadata.Encoder[O] = Metadata.Encoder.fromSchema(outputSchema) + private val jsonPayloadCodecs: JsonPayloadCodecCompiler = + Json.payloadCodecs + private val jsonWriters: CachedSchemaCompiler[PayloadEncoder] = jsonPayloadCodecs.encoders + private val jsonDecoders: CachedSchemaCompiler[PayloadDecoder] = jsonPayloadCodecs.decoders + private val xmlEncoder: BlobEncoder.Compiler = Xml.encoders + private val xmlDecoder = Xml.decoders.createCache() + private val stringAndBlobEncoder = StringAndBlobCodecs.decoders + private val stringAndBlobDecoder = StringAndBlobCodecs.encoders + def handler(v1: RequestHeader): Handler = httpEndpoint.map { httpEp => Action.async(parse.raw) { implicit request => @@ -112,21 +131,22 @@ class SmithyPlayEndpoint[Alg[_[_, _, _, _, _]], F[_] <: ContextRoute[_], Op[ EitherT( Future { val contentType = request.contentType.getOrElse("application/json") - val x = inputMetadataDecoder.decode(metadata) - val codecs = contentType match { - case "application/json" => (blob: Blob) => Json.read(blob)(inputSchema) - case "application/xml" => (blob: Blob) => Xml.read(blob)(inputSchema) - case _ => - (blob: Blob) => - StringAndBlobCodecs.decoders - .fromSchema(inputSchema) - .get - .decode(blob) - } - codecs(Blob(request.body.asBytes().getOrElse(ByteString.empty).toByteBuffer)).leftMap { - case error: PayloadError => Smithy4PlayError(error.expected, smithy4play.Status(Map.empty, 500)) - case error: XmlDecodeError => Smithy4PlayError(error.getMessage(), smithy4play.Status(Map.empty, 500)) + + val codecs = contentType match { + case "application/json" => (blob: Blob) => Some(payloadCodecs.decoders.fromSchema(inputSchema).decode(blob)) + case "application/xml" => (blob: Blob) => Some(Xml.decoders.fromSchema(inputSchema).decode(blob)) + case _ => (blob: Blob) => StringAndBlobCodecs.decoders.fromSchema(inputSchema).map(_.decode(blob)) } + + codecs(request.body.asBytes().map(b => Blob(b.toByteBuffer)).getOrElse(Blob.empty)) + .map(_.leftMap { error: PayloadError => + Smithy4PlayError(error.expected, smithy4play.Status(Map.empty, 500)) + }) + .getOrElse( + Left[ContextRouteError, I]( + Smithy4PlayError("No Codec for InputSchema found", smithy4play.Status(Map.empty, 500)) + ) + ) } ) diff --git a/smithy4play/src/main/scala/de/innfactory/smithy4play/SmithyPlayRouter.scala b/smithy4play/src/main/scala/de/innfactory/smithy4play/SmithyPlayRouter.scala index cab1b932..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 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 4887f107..c29c5e06 100644 --- a/smithy4play/src/main/scala/de/innfactory/smithy4play/client/RequestClient.scala +++ b/smithy4play/src/main/scala/de/innfactory/smithy4play/client/RequestClient.scala @@ -2,16 +2,15 @@ package de.innfactory.smithy4play.client import play.api.mvc.Headers import smithy4s.Blob +import smithy4s.http.HttpResponse import scala.concurrent.Future -case class SmithyClientResponse(body: Blob, headers: Map[String, Seq[String]], statusCode: Int) - trait RequestClient { def send( method: String, path: String, headers: Map[String, Seq[String]], body: Blob - ): Future[SmithyClientResponse] + ): 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 71b42e57..c3a1969d 100644 --- a/smithy4play/src/main/scala/de/innfactory/smithy4play/client/SmithyPlayClient.scala +++ b/smithy4play/src/main/scala/de/innfactory/smithy4play/client/SmithyPlayClient.scala @@ -2,6 +2,7 @@ package de.innfactory.smithy4play.client import cats.implicits.toBifunctorOps import de.innfactory.smithy4play.ClientResponse +import smithy4s.Blob import smithy4s.http.HttpEndpoint import scala.concurrent.{ ExecutionContext, Future } 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 04e8dd48..924f65a6 100644 --- a/smithy4play/src/main/scala/de/innfactory/smithy4play/client/SmithyPlayClientEndpoint.scala +++ b/smithy4play/src/main/scala/de/innfactory/smithy4play/client/SmithyPlayClientEndpoint.scala @@ -2,16 +2,16 @@ package de.innfactory package smithy4play package client import cats.implicits._ -import smithy4s.codecs.{PayloadError, StringAndBlobCodecs} -import smithy4s.http.{CaseInsensitive, HttpEndpoint, Metadata, MetadataError} +import smithy4s.codecs.{ PayloadError, StringAndBlobCodecs } +import smithy4s.http.{ CaseInsensitive, HttpEndpoint, HttpResponse, Metadata, MetadataError } import smithy4s.json.Json import smithy4s.json.Json.payloadCodecs import smithy4s.schema.CachedSchemaCompiler import smithy4s.xml.internals.XmlEncoder -import smithy4s.xml.{Xml, XmlDecodeError, XmlDocument} -import smithy4s.{Blob, Endpoint, Schema} +import smithy4s.xml.{ Xml, XmlDecodeError, XmlDocument } +import smithy4s.{ Blob, Endpoint, Schema } -import scala.concurrent.{ExecutionContext, Future} +import scala.concurrent.{ ExecutionContext, Future } private[smithy4play] class SmithyPlayClientEndpoint[Op[_, _, _, _, _], I, E, O, SI, SO]( endpoint: Endpoint[Op, I, E, O, SI, SO], @@ -49,24 +49,19 @@ private[smithy4play] class SmithyPlayClientEndpoint[Op[_, _, _, _, _], I, E, O, decodeResponse(response, code) } - private def writeInputToBlob(input: I, contentType: Seq[String]): Blob = { - //TODO: Use Correct Encoders for Json and Xml and fix StringAndBlobCodecs - contentType match { - case Seq("application/json") => Json.writeBlob(input) - case Seq("application/xml") => Xml.write(input) + private def writeInputToBlob(input: I, contentType: Seq[String]): Blob = + // TODO: Use Correct Encoders for Json and Xml and fix StringAndBlobCodecs + (contentType match { + case Seq("application/json") => Some(payloadCodecs.encoders.fromSchema(inputSchema)) + case Seq("application/xml") => Some(Xml.encoders.fromSchema(inputSchema)) case _ => StringAndBlobCodecs.encoders .fromSchema(inputSchema) - .map(_.encode(input)) - .getOrElse({ - logger.info("sending empty blob") - Blob.empty - }) - } - } + + }).map(_.encode(input)).getOrElse(Blob.empty) private def decodeResponse( - response: Future[SmithyClientResponse], + response: Future[HttpResponse[Blob]], expectedCode: Int ): ClientResponse[O] = for { @@ -76,24 +71,26 @@ private[smithy4play] class SmithyPlayClientEndpoint[Op[_, _, _, _, _], I, E, O, else handleError(res) } yield output - def handleSuccess(response: SmithyClientResponse) = { - val headers = response.headers.map(x => (x._1.toLowerCase, x._2)) - val contentType = headers.getOrElse("content-type", Seq("application/json")) + def handleSuccess(response: HttpResponse[Blob]): ClientResponse[O] = { + val headers = response.headers.map(x => (x._1, x._2)) + val contentType = headers.getOrElse(CaseInsensitive("content-type"), Seq("application/json")) val codec = contentType match { - case "application/json" :: _ => (o: Blob) => Json.read(o)(outputSchema) - case "application/xml" :: _ => (o: Blob) => Xml.read(o)(outputSchema) - case _ => (o: Blob) => StringAndBlobCodecs.decoders.fromSchema(outputSchema).get.decode(o) + case "application/json" :: _ => Some(payloadCodecs.decoders.fromSchema(outputSchema)) + case "application/xml" :: _ => Some(Xml.decoders.fromSchema(outputSchema)) + case _ => StringAndBlobCodecs.decoders.fromSchema(outputSchema) } Future( - codec(response.body).map(o => SmithyPlayClientEndpointResponse(Some(o), headers, response.statusCode)).leftMap { - case error: PayloadError => - SmithyPlayClientEndpointErrorResponse(error.expected.getBytes, response.statusCode) - case error: XmlDecodeError => - SmithyPlayClientEndpointErrorResponse(error.getMessage().getBytes(), response.statusCode) - } + codec + .map(_.decode(response.body)) + .map(o => + o.map(res => HttpResponse(response.statusCode, headers, res)).leftMap { error => + SmithyPlayClientEndpointErrorResponse(error.expected.getBytes, response.statusCode) + } + ) + .getOrElse(Left(SmithyPlayClientEndpointErrorResponse("No Decoder found".getBytes, response.statusCode))) ) } - private def handleError(response: SmithyClientResponse) = Future( + private def handleError(response: HttpResponse[Blob]): ClientResponse[O] = Future( Left { SmithyPlayClientEndpointErrorResponse( response.body.toArray, 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..9428209f 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 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 1a5234f9..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,7 +37,7 @@ 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] ) = { @@ -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/package.scala b/smithy4play/src/main/scala/de/innfactory/smithy4play/package.scala index 51d69f1d..d6034ea4 100644 --- a/smithy4play/src/main/scala/de/innfactory/smithy4play/package.scala +++ b/smithy4play/src/main/scala/de/innfactory/smithy4play/package.scala @@ -1,15 +1,15 @@ package de.innfactory -import cats.data.{EitherT, Kleisli} -import de.innfactory.smithy4play.client.{SmithyPlayClientEndpointErrorResponse, SmithyPlayClientEndpointResponse} +import cats.data.{ EitherT, Kleisli } +import de.innfactory.smithy4play.client.SmithyPlayClientEndpointErrorResponse import org.slf4j import play.api.Logger -import play.api.libs.json.{JsValue, Json} -import play.api.mvc.{Headers, RequestHeader} +import play.api.libs.json.{ JsValue, Json } +import play.api.mvc.{ Headers, RequestHeader } import smithy4s.Blob -import smithy4s.http.{CaseInsensitive, HttpEndpoint} +import smithy4s.http.{ CaseInsensitive, HttpEndpoint, HttpResponse } -import scala.annotation.{StaticAnnotation, compileTimeOnly} +import scala.annotation.{ compileTimeOnly, StaticAnnotation } import scala.concurrent.Future import scala.language.experimental.macros @@ -20,7 +20,7 @@ package object smithy4play { def toJson: JsValue } - type ClientResponse[O] = Future[Either[SmithyPlayClientEndpointErrorResponse, SmithyPlayClientEndpointResponse[O]]] + type ClientResponse[O] = Future[Either[SmithyPlayClientEndpointErrorResponse, HttpResponse[O]]] type RunnableClientRequest[O] = Kleisli[ClientResponse, Option[Map[String, Seq[String]]], O] type RouteResult[O] = EitherT[Future, ContextRouteError, O] type ContextRoute[O] = Kleisli[RouteResult, RoutingContext, O] diff --git a/smithy4playTest/app/controller/TestController.scala b/smithy4playTest/app/controller/TestController.scala index 089a63e3..20ec6951 100644 --- a/smithy4playTest/app/controller/TestController.scala +++ b/smithy4playTest/app/controller/TestController.scala @@ -1,14 +1,14 @@ package controller -import cats.data.{EitherT, Kleisli} +import cats.data.{ EitherT, Kleisli } import controller.models.TestError -import de.innfactory.smithy4play.{AutoRouting, ContextRoute, ContextRouteError} +import de.innfactory.smithy4play.{ AutoRouting, ContextRoute, ContextRouteError } import play.api.mvc.ControllerComponents import smithy4s.Blob import testDefinitions.test._ -import javax.inject.{Inject, Singleton} -import scala.concurrent.{ExecutionContext, Future} +import javax.inject.{ Inject, Singleton } +import scala.concurrent.{ ExecutionContext, Future } @Singleton @AutoRouting diff --git a/smithy4playTest/test/TestControllerTest.scala b/smithy4playTest/test/TestControllerTest.scala index ab572821..21628c38 100644 --- a/smithy4playTest/test/TestControllerTest.scala +++ b/smithy4playTest/test/TestControllerTest.scala @@ -1,7 +1,7 @@ import controller.models.TestError import de.innfactory.smithy4play.client.GenericAPIClient.EnhancedGenericAPIClient +import de.innfactory.smithy4play.client.RequestClient import de.innfactory.smithy4play.client.SmithyPlayTestUtils._ -import de.innfactory.smithy4play.client.{ RequestClient, SmithyClientResponse } import de.innfactory.smithy4play.compliancetests.ComplianceClient import models.TestJson import org.scalatestplus.play.{ BaseOneAppPerSuite, FakeApplicationFactory, PlaySpec } @@ -13,6 +13,7 @@ import play.api.mvc.{ AnyContentAsEmpty, Result } import play.api.test.FakeRequest import play.api.test.Helpers._ import smithy4s.Blob +import smithy4s.http.{ CaseInsensitive, HttpResponse } import testDefinitions.test.{ SimpleTestResponse, TestControllerServiceGen, TestRequestBody } import java.io.File @@ -28,7 +29,7 @@ class TestControllerTest extends PlaySpec with BaseOneAppPerSuite with FakeAppli path: String, headers: Map[String, Seq[String]], body: Blob - ): Future[SmithyClientResponse] = { + ): Future[HttpResponse[Blob]] = { val baseRequest: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(method, path) .withHeaders(headers.toList.flatMap(headers => headers._2.map(v => (headers._1, v))): _*) val res = @@ -40,18 +41,16 @@ class TestControllerTest extends PlaySpec with BaseOneAppPerSuite with FakeAppli ).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.map(Blob(_)).getOrElse(Blob.empty), - headersWithContentType, - result.header.status - ) + 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")) } } @@ -88,7 +87,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 @@ -128,7 +127,7 @@ class TestControllerTest extends PlaySpec with BaseOneAppPerSuite with FakeAppli "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 } @@ -146,7 +145,7 @@ class TestControllerTest extends PlaySpec with BaseOneAppPerSuite with FakeAppli val result = genericClient.testWithBlob(pngAsBytes, "image/png").awaitRight result.statusCode mustBe 200 - pngAsBytes mustBe result.body.get.body + pngAsBytes mustBe result.body.body } "route to Auth Test" in {