From 462b383c3f673d5752e030da89ff262d32967910 Mon Sep 17 00:00:00 2001 From: patsta32 Date: Fri, 9 Aug 2024 13:23:19 +0200 Subject: [PATCH] rework tracing and instrumentation --- build.sbt | 4 +- project/plugins.sbt | 2 +- .../Smithy4PlaySingleton.scala | 30 +-- .../instrumentation/TestInstrumentation.java | 13 +- .../client/Smithy4PlayWsClient.scala | 22 +- .../smithy4play/client/SmithyPlayClient.scala | 22 +- .../client/SmithyPlayTestUtils.scala | 55 ++-- .../core/Smithy4PlayClientCompiler.scala | 22 +- .../core/Smithy4PlayClientEndpoint.scala | 49 ++++ .../smithy4play/client/package.scala | 90 ++++--- .../innfactory/smithy4play/codecs/Codec.scala | 19 +- .../smithy4play/codecs/CodecSupport.scala | 8 +- .../smithy4play/routing/Controller.scala | 2 +- .../routing/internal/Smithy4PlayRouter.scala | 25 +- .../internal/Smithy4PlayServerEndpoint.scala | 14 +- .../routing/internal/package.scala | 38 +-- .../smithy4play/routing/package.scala | 5 +- .../app/controller/TestController.scala | 39 +-- .../app/controller/TestRouter.scala | 24 ++ .../app/controller/XmlController.scala | 13 +- .../middlewares/AddHeaderMiddleware.scala | 33 +++ .../middlewares/ValidateAuthMiddleware.scala | 46 ++++ smithy4playTest/conf/routes | 4 +- smithy4playTest/test/TestControllerTest.scala | 208 ++++++++-------- smithy4playTest/test/XmlControllerTest.scala | 235 +++++++++--------- .../test/models/Smithy4PlayTestClient.scala | 46 ++-- smithy4playTest/test/models/TestBase.scala | 12 +- smithy4playTest/testSpecs/200TestSuite.smithy | 4 +- .../testSpecs/TestController.smithy | 52 +++- .../testSpecs/XmlController.smithy | 2 + 30 files changed, 647 insertions(+), 491 deletions(-) create mode 100644 smithy4play/src/main/scala/de/innfactory/smithy4play/client/core/Smithy4PlayClientEndpoint.scala create mode 100644 smithy4playTest/app/controller/TestRouter.scala create mode 100644 smithy4playTest/app/controller/middlewares/AddHeaderMiddleware.scala create mode 100644 smithy4playTest/app/controller/middlewares/ValidateAuthMiddleware.scala diff --git a/build.sbt b/build.sbt index 454d5c92..811b9feb 100644 --- a/build.sbt +++ b/build.sbt @@ -4,9 +4,9 @@ import sbt.Keys.cleanFiles ThisBuild / scalaVersion := Dependencies.scalaVersion scalaVersion := Dependencies.scalaVersion -val releaseVersion = sys.env.getOrElse("TAG", "2.0.108") +val releaseVersion = sys.env.getOrElse("TAG", "2.0.0-alpha.rc.4") addCommandAlias("packageSmithy4Play", "smithy4play/package") -addCommandAlias("publishSmithy4Play", "smithy4play/publish") +addCommandAlias("publishSmithy4Play", "smithy4play/publish;smithy4playInstrumentation/publish") addCommandAlias("publishLocalWithInstrumentation", "publishLocalSmithy4PlayInstrumentation;publishLocalSmithy4Play") addCommandAlias("publishLocalSmithy4PlayInstrumentation", "smithy4playInstrumentation/publishLocal") addCommandAlias("publishLocalSmithy4Play", "smithy4play/publishLocal") diff --git a/project/plugins.sbt b/project/plugins.sbt index 1bb9d9ae..9772b9ab 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -2,7 +2,7 @@ addSbtPlugin("com.codecommit" %% "sbt-github-packages" % "0.5.3") addSbtPlugin("org.scalameta" %% "sbt-scalafmt" % "2.5.2") -addSbtPlugin("org.playframework" %% "sbt-plugin" % "3.0.4") +addSbtPlugin("org.playframework" %% "sbt-plugin" % "3.0.5") addSbtPlugin("org.scoverage" %% "sbt-scoverage" % "2.0.12") addSbtPlugin("com.disneystreaming.smithy4s" %% "smithy4s-sbt-codegen" % "0.18.22") diff --git a/smithy4play-instrumentation/src/main/scala/de/innfactory/smithy4play/instrumentation/Smithy4PlaySingleton.scala b/smithy4play-instrumentation/src/main/scala/de/innfactory/smithy4play/instrumentation/Smithy4PlaySingleton.scala index 85a5cd9a..dbe0a058 100644 --- a/smithy4play-instrumentation/src/main/scala/de/innfactory/smithy4play/instrumentation/Smithy4PlaySingleton.scala +++ b/smithy4play-instrumentation/src/main/scala/de/innfactory/smithy4play/instrumentation/Smithy4PlaySingleton.scala @@ -12,33 +12,5 @@ object Smithy4PlaySingleton { private val SPAN_NAME = "play.request" private val spanKindExtractor = SpanKindExtractor.alwaysServer() - private val INSTRUMENTER = Instrumenter - .builder[Void, Void](GlobalOpenTelemetry.get, "io.opentelemetry.smtihyt4play", (s) => SPAN_NAME) - .setEnabled(true) - .buildInstrumenter(spanKindExtractor) - - .pipe { (v: Instrumenter[Void, Void]) => - println(v.toString) - println("shouldstart: " + v.shouldStart(Context.root(), null)) - v - } - - def test() = { - println("Smithy4PlaySingleton test") - } - - def shouldStart(context: Context): Boolean = { - try { - INSTRUMENTER.shouldStart(context, null) - } catch - case e: Exception => { - e.printStackTrace() - println(e.getMessage) - println(e.getCause) - false - } - - } - - def instrumenter() = INSTRUMENTER + } diff --git a/smithy4play-instrumentation/src/main/scala/de/innfactory/smithy4play/instrumentation/TestInstrumentation.java b/smithy4play-instrumentation/src/main/scala/de/innfactory/smithy4play/instrumentation/TestInstrumentation.java index f477b27e..09e480e3 100644 --- a/smithy4play-instrumentation/src/main/scala/de/innfactory/smithy4play/instrumentation/TestInstrumentation.java +++ b/smithy4play-instrumentation/src/main/scala/de/innfactory/smithy4play/instrumentation/TestInstrumentation.java @@ -6,7 +6,6 @@ import static net.bytebuddy.matcher.ElementMatchers.named; import static net.bytebuddy.matcher.ElementMatchers.returns; import static net.bytebuddy.matcher.ElementMatchers.takesArgument; -import static de.innfactory.smithy4play.instrumentation.Smithy4PlaySingleton.test; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.trace.SpanBuilder; @@ -54,17 +53,15 @@ public static void onEnter( @Advice.Local("otelContext") Context context, @Advice.Local("otelScope") Scope scope) { - System.out.println("TestInstrumentation ApplyAdvice onEnter " + test); + //System.out.println("TestInstrumentation ApplyAdvice onEnter " + test); // span.addEvent("ADVICE smithy4play " + test); - test(); Context parentContext = currentContext(); - Boolean shouldStart = Smithy4PlaySingleton.shouldStart(parentContext); - System.out.println("TestInstrumentation ApplyAdvice shouldStart " + shouldStart); + //System.out.println("TestInstrumentation ApplyAdvice shouldStart " + shouldStart); Span mySpan = GlobalOpenTelemetry.get().getTracer("smithy4play").spanBuilder("test span").startSpan(); - System.out.println("TestInstrumentation ApplyAdvice mySpan " + mySpan.getSpanContext().getSpanId()); + //System.out.println("TestInstrumentation ApplyAdvice mySpan " + mySpan.getSpanContext().getSpanId()); context = mySpan.storeInContext(parentContext); Scope myScope = mySpan.makeCurrent(); - System.out.println("TestInstrumentation ApplyAdvice should start"); + //System.out.println("TestInstrumentation ApplyAdvice should start"); scope = myScope; } @@ -91,7 +88,7 @@ public static void stopTraceOnResponse( throwable.printStackTrace(); } if (context != null) { - System.out.println("TestInstrumentation ApplyAdvice onExit update update Span name"); + // System.out.println("TestInstrumentation ApplyAdvice onExit update update Span name"); // Span.fromContext(context).updateName(testout); } } diff --git a/smithy4play/src/main/scala/de/innfactory/smithy4play/client/Smithy4PlayWsClient.scala b/smithy4play/src/main/scala/de/innfactory/smithy4play/client/Smithy4PlayWsClient.scala index 71bfbdc5..32885773 100644 --- a/smithy4play/src/main/scala/de/innfactory/smithy4play/client/Smithy4PlayWsClient.scala +++ b/smithy4play/src/main/scala/de/innfactory/smithy4play/client/Smithy4PlayWsClient.scala @@ -1,6 +1,6 @@ package de.innfactory.smithy4play.client -import cats.data.{ EitherT, Kleisli } +import cats.data.EitherT import play.api.libs.ws.{ writeableOf_ByteArray, WSClient, WSResponse } import smithy4s.client.UnaryLowLevelClient import smithy4s.http.{ CaseInsensitive, HttpRequest, HttpResponse } @@ -16,22 +16,22 @@ class Smithy4PlayWsClient[Alg[_[_, _, _, _, _]]]( requestIsSuccessful: (Hints, HttpResponse[Blob]) => Boolean = matchStatusCodeForResponse, explicitDefaultsEncoding: Boolean = true )(implicit ec: ExecutionContext, wsClient: WSClient) - extends UnaryLowLevelClient[RunnableClientRequest, HttpRequest[Blob], HttpResponse[Blob]] { + extends UnaryLowLevelClient[FinishedClientResponse, HttpRequest[Blob], HttpResponse[Blob]] { val underlyingClient = new SmithyPlayClient[Alg, Smithy4PlayWsClient[Alg]]( baseUri = baseUri, service = service, client = this, middleware = middleware, - requestIsSuccessful = (_, _) => true, + requestIsSuccessful = requestIsSuccessful, toSmithy4sClient = x => x ) - def transformer(): Alg[Kind1[ClientFinishedResponse]#toKind5] = + def transformer(): Alg[Kind1[RunnableClientResponse]#toKind5] = underlyingClient.service.algebra(underlyingClient.compiler) private def buildPath(req: HttpRequest[Blob]): String = - baseUri + req.uri.path.mkString("/") + baseUri + req.uri.path.mkString("/", "/", "") private def toHeaders(request: HttpRequest[Blob]): List[(String, String)] = request.headers.flatMap { case (insensitive, strings) => @@ -52,7 +52,7 @@ class Smithy4PlayWsClient[Alg[_[_, _, _, _, _]]]( override def run[Output]( request: HttpRequest[Blob] - )(responseCB: HttpResponse[Blob] => RunnableClientRequest[Output]): RunnableClientRequest[Output] = Kleisli { _ => + )(responseCB: HttpResponse[Blob] => FinishedClientResponse[Output]): FinishedClientResponse[Output] = { val clientResponse = wsClient .url(buildPath(request)) .withQueryStringParameters(toQueryParameters(request): _*) @@ -63,13 +63,7 @@ class Smithy4PlayWsClient[Alg[_[_, _, _, _, _]]]( val httpResponse = clientResponse.map(wsRequestToResponse) - println("run ws client") - - EitherT(httpResponse.flatMap { httpResponse => - println("httpresponse present and running internally") - val v = responseCB(httpResponse).run(() => httpResponse).value - v - }) + EitherT(httpResponse.flatMap(responseCB(_).value)) } } @@ -81,6 +75,6 @@ object Smithy4PlayWsClient { middleware: Endpoint.Middleware[Smithy4PlayWsClient[Alg]], requestIsSuccessful: (Hints, HttpResponse[Blob]) => Boolean = matchStatusCodeForResponse, explicitDefaultsEncoding: Boolean = true - )(implicit ec: ExecutionContext, wsClient: WSClient): Alg[Kind1[ClientFinishedResponse]#toKind5] = + )(implicit ec: ExecutionContext, wsClient: WSClient): Alg[Kind1[RunnableClientResponse]#toKind5] = new Smithy4PlayWsClient(baseUri, service, middleware, requestIsSuccessful, explicitDefaultsEncoding).transformer() } 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 51ae8cbb..40a5a858 100644 --- a/smithy4play/src/main/scala/de/innfactory/smithy4play/client/SmithyPlayClient.scala +++ b/smithy4play/src/main/scala/de/innfactory/smithy4play/client/SmithyPlayClient.scala @@ -1,17 +1,11 @@ package de.innfactory.smithy4play.client import cats.implicits.catsSyntaxApplicativeId -import com.github.plokhotnyuk.jsoniter_scala.core.ReaderConfig -import de.innfactory.smithy4play.client.RunnableClientRequest import de.innfactory.smithy4play.client.core.Smithy4PlayClientCompiler import de.innfactory.smithy4play.codecs.{ Codec, EndpointContentTypes } -import de.innfactory.smithy4play.routing.internal.toSmithy4sHttpUri -import smithy4s.capability.MonadThrowLike import smithy4s.{ Blob, Endpoint, Hints, Service } import smithy4s.http.{ - CaseInsensitive, HttpDiscriminator, - HttpEndpoint, HttpMethod, HttpRequest, HttpResponse, @@ -20,7 +14,7 @@ import smithy4s.http.{ HttpUriScheme, Metadata } -import smithy4s.client.{ UnaryClientCodecs, UnaryClientCompiler, UnaryClientEndpoint, UnaryLowLevelClient } +import smithy4s.client.UnaryLowLevelClient import smithy4s.interopcats.monadThrowShim import scala.concurrent.{ ExecutionContext, Future } @@ -30,7 +24,7 @@ class SmithyPlayClient[Alg[_[_, _, _, _, _]], Client]( val service: smithy4s.Service[Alg], client: Client, middleware: Endpoint.Middleware[Client], - toSmithy4sClient: Client => UnaryLowLevelClient[RunnableClientRequest, HttpRequest[Blob], HttpResponse[Blob]], + toSmithy4sClient: Client => UnaryLowLevelClient[FinishedClientResponse, HttpRequest[Blob], HttpResponse[Blob]], requestIsSuccessful: (Hints, HttpResponse[Blob]) => Boolean, explicitDefaultsEncoding: Boolean = true )(implicit executionContext: ExecutionContext) @@ -48,21 +42,21 @@ class SmithyPlayClient[Alg[_[_, _, _, _, _]], Client]( smithy4s.http.amazonErrorTypeHeader ) - val clientCodecBuilder: HttpUnaryClientCodecs.Builder[RunnableClientRequest, HttpRequest[Blob], HttpResponse[Blob]] = + val clientCodecBuilder: HttpUnaryClientCodecs.Builder[ClientResponse, HttpRequest[Blob], HttpResponse[Blob]] = HttpUnaryClientCodecs - .builder[RunnableClientRequest] - .withErrorDiscriminator(HttpDiscriminator.fromResponse(errorHeaders, _).pure[RunnableClientRequest]) + .builder[ClientResponse] + .withErrorDiscriminator(HttpDiscriminator.fromResponse(errorHeaders, _).pure[ClientResponse]) .withMetadataDecoders(Metadata.Decoder) .withMetadataEncoders( Metadata.Encoder.withExplicitDefaultsEncoding(explicitDefaultsEncoding) ) - .withBaseRequest(_ => baseRequest.pure[RunnableClientRequest]) + .withBaseRequest(_ => baseRequest.pure[ClientResponse]) val compiledClientCodec - : EndpointContentTypes => HttpUnaryClientCodecs.Builder[RunnableClientRequest, HttpRequest[Blob], HttpResponse[Blob]] = + : EndpointContentTypes => HttpUnaryClientCodecs.Builder[ClientResponse, HttpRequest[Blob], HttpResponse[Blob]] = buildClientCodecFromBase(clientCodecBuilder) - val compiler: service.FunctorEndpointCompiler[ClientFinishedResponse] = Smithy4PlayClientCompiler[Alg, Client]( + val compiler: service.FunctorEndpointCompiler[RunnableClientResponse] = Smithy4PlayClientCompiler[Alg, Client]( service = service, client = client, toSmithy4sClient = toSmithy4sClient, 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 abbbc589..4622f081 100644 --- a/smithy4play/src/main/scala/de/innfactory/smithy4play/client/SmithyPlayTestUtils.scala +++ b/smithy4play/src/main/scala/de/innfactory/smithy4play/client/SmithyPlayTestUtils.scala @@ -9,47 +9,50 @@ import scala.concurrent.{ Await, ExecutionContext } object SmithyPlayTestUtils { - implicit class EnhancedResponse[O](response: RunnableClientRequest[O]) { + implicit class EnhancedResponse[O](response: RunnableClientResponse[O]) { def awaitRight(implicit ec: ExecutionContext, timeout: Duration = 5.seconds ): HttpResponse[O] = { - val result = Await.result( - response.tapWith((ctx, o) => ctx.apply().copy(body = o)).run(null) - .bimap( - throwable => - logger.error( - s"Expected Right, got Left: ${throwable.toString} Error: ${throwable.underlying.getMessage}" - ), - res => res - ).value, - timeout - ).toOption.get - + val result = Await + .result( + response.run(identity) + .bimap( + throwable => + logger.error( + s"Expected Right, got Left: ${throwable.toString} ${throwable.getMessage} ${throwable.httpResponse.statusCode} Error: ${throwable.underlying.getMessage} ${throwable.underlying.getCause}" + ), + identity + ) + .value, + timeout + ) + .toOption + .get + result } - def awaitLeft(implicit ec: ExecutionContext, timeout: Duration = 5.seconds ): HttpResponse[Throwable] = { - val result = Await.result( - response.run(null) - .bimap( - throwable => throwable - , - res => logger.error( - s"Expected Left, got Right: ${res.toString}" + val result = Await + .result( + response.run(identity) + .bimap( + identity, + res => logger.error(s"Expected Left, got Right: ${res.toString}") ) - ).value, - timeout - ).swap.toOption.get + .value, + timeout + ) + .swap + .toOption + .get result.httpResponse.copy(body = result.underlying) } } - - } diff --git a/smithy4play/src/main/scala/de/innfactory/smithy4play/client/core/Smithy4PlayClientCompiler.scala b/smithy4play/src/main/scala/de/innfactory/smithy4play/client/core/Smithy4PlayClientCompiler.scala index 473ecd6c..6e17fd3c 100644 --- a/smithy4play/src/main/scala/de/innfactory/smithy4play/client/core/Smithy4PlayClientCompiler.scala +++ b/smithy4play/src/main/scala/de/innfactory/smithy4play/client/core/Smithy4PlayClientCompiler.scala @@ -1,13 +1,11 @@ package de.innfactory.smithy4play.client.core -import de.innfactory.smithy4play.client.{ClientFinishedResponse, OWrapper, RunnableClientRequest} +import de.innfactory.smithy4play.client.{ClientResponse, FinishedClientResponse, RunnableClientResponse} import de.innfactory.smithy4play.codecs.EndpointContentTypes import smithy4s.{Blob, Endpoint, Hints} import smithy4s.capability.MonadThrowLike -import smithy4s.client.{UnaryClientCodecs, UnaryClientEndpoint, UnaryLowLevelClient} +import smithy4s.client.UnaryLowLevelClient import smithy4s.http.{HttpRequest, HttpResponse, HttpUnaryClientCodecs} -import smithy4s.kinds.{Kind1, PolyFunction5} -import smithy4s.server.UnaryServerCodecs import scala.concurrent.ExecutionContext @@ -27,16 +25,16 @@ object Smithy4PlayClientCompiler { def apply[Alg[_[_, _, _, _, _]], Client]( service: smithy4s.Service[Alg], client: Client, - toSmithy4sClient: Client => UnaryLowLevelClient[RunnableClientRequest, HttpRequest[Blob], HttpResponse[Blob]], - codecs: EndpointContentTypes => HttpUnaryClientCodecs.Builder[RunnableClientRequest, HttpRequest[Blob], HttpResponse[Blob]], + toSmithy4sClient: Client => UnaryLowLevelClient[FinishedClientResponse, HttpRequest[Blob], HttpResponse[Blob]], + codecs: EndpointContentTypes => HttpUnaryClientCodecs.Builder[ClientResponse, HttpRequest[Blob], HttpResponse[Blob]], middleware: Endpoint.Middleware[Client], isSuccessful: (Hints, HttpResponse[Blob]) => Boolean - )(implicit F: MonadThrowLike[RunnableClientRequest], ec: ExecutionContext): service.FunctorEndpointCompiler[ClientFinishedResponse] = { + )(implicit F: MonadThrowLike[ClientResponse], ec: ExecutionContext): service.FunctorEndpointCompiler[RunnableClientResponse] = { - new service.FunctorEndpointCompiler[ClientFinishedResponse] { + new service.FunctorEndpointCompiler[RunnableClientResponse] { def apply[I, E, O, SI, SO]( endpoint: service.Endpoint[I, E, O, SI, SO] - ): I => ClientFinishedResponse[O] = { + ): I => RunnableClientResponse[O] = { val transformedClient = middleware.prepare(service)(endpoint).apply(client) @@ -46,15 +44,13 @@ object Smithy4PlayClientCompiler { val contentType = resolveContentType(endpoint.hints, service.hints, Seq.empty, None) val codec = codecs(contentType).build() - val clientEndpoint: I => RunnableClientRequest[O] = UnaryClientEndpoint( + val clientEndpoint: I => RunnableClientResponse[O] = Smithy4PlayClientEndpoint( adaptedClient, codec.apply(endpoint.schema), isSuccessful.curried(endpoint.hints) ) - clientEndpoint.andThen(q => { - q.tapWith((ctx, o) => ctx.apply().copy(body = o)) - }) + clientEndpoint } } } diff --git a/smithy4play/src/main/scala/de/innfactory/smithy4play/client/core/Smithy4PlayClientEndpoint.scala b/smithy4play/src/main/scala/de/innfactory/smithy4play/client/core/Smithy4PlayClientEndpoint.scala new file mode 100644 index 00000000..2428af5b --- /dev/null +++ b/smithy4play/src/main/scala/de/innfactory/smithy4play/client/core/Smithy4PlayClientEndpoint.scala @@ -0,0 +1,49 @@ +package de.innfactory.smithy4play.client.core + +import cats.data.Kleisli +import de.innfactory.smithy4play.client.{ ClientError, ClientResponse, FinishedClientResponse, RunnableClientResponse } +import de.innfactory.smithy4play.logger +import smithy4s.Blob +import smithy4s.capability.MonadThrowLike +import smithy4s.client.{ UnaryClientCodecs, UnaryLowLevelClient } +import smithy4s.http.{ HttpRequest, HttpResponse } + +import scala.concurrent.ExecutionContext + +object Smithy4PlayClientEndpoint { + + def apply[I, E, O, SI, SO]( + lowLevelClient: UnaryLowLevelClient[FinishedClientResponse, HttpRequest[Blob], HttpResponse[Blob]], + clientCodecs: UnaryClientCodecs[ClientResponse, HttpRequest[Blob], HttpResponse[Blob], I, E, O], + isSuccessful: HttpResponse[Blob] => Boolean + )(implicit F: MonadThrowLike[ClientResponse], ec: ExecutionContext): I => RunnableClientResponse[O] = { + + import clientCodecs._ + def inputToRequest(input: I): ClientResponse[HttpRequest[Blob]] = + inputEncoder(input) + + def mapToClientError(t: Throwable, response: HttpResponse[Blob]): ClientError = ClientError.create(t, response) + + def outputFromResponse(response: HttpResponse[Blob]): FinishedClientResponse[O] = + if (isSuccessful(response)) + outputDecoder(response).map(v => response.copy(body = v)).leftMap(mapToClientError.curried(_)(response)) + else { + val error: ClientResponse[O] = F.flatMap(errorDecoder(response))(F.raiseError[O]) + error.leftMap(mapToClientError.curried(_)(response)).map(v => response.copy(body = v)) + } + + (input: I) => + Kleisli { mapper => + inputToRequest(input) + .leftMap(t => + logger.error(s"Unhandled Error in ${this.getClass.getName}") + ClientError(t, HttpResponse(0, Map.empty, null)) + ) + .flatMapF { req => + lowLevelClient.run(mapper(req))(outputFromResponse).value + } + } + + } + +} diff --git a/smithy4play/src/main/scala/de/innfactory/smithy4play/client/package.scala b/smithy4play/src/main/scala/de/innfactory/smithy4play/client/package.scala index 66351fc4..34ff8ea9 100644 --- a/smithy4play/src/main/scala/de/innfactory/smithy4play/client/package.scala +++ b/smithy4play/src/main/scala/de/innfactory/smithy4play/client/package.scala @@ -1,76 +1,70 @@ package de.innfactory.smithy4play -import cats.{FlatMap, MonadThrow} -import cats.data.{EitherT, Kleisli} -import smithy4s.{Blob, Hints} -import smithy4s.http.HttpResponse +import cats.{ FlatMap, MonadThrow } +import cats.data.{ EitherT, Kleisli } +import smithy4s.{ Blob, Hints } +import smithy4s.http.{ HttpRequest, HttpResponse } -import scala.concurrent.{ExecutionContext, Future} +import scala.concurrent.{ ExecutionContext, Future } import cats.syntax.flatMap.given package object client { - - case class ClientError(underlying: Throwable, httpResponse: HttpResponse[Blob]) extends Throwable - - type ClientResponse[O] = EitherT[Future, ClientError, O] // Maybe handle all responses - type Context = () => HttpResponse[Blob] + + case class ClientError(underlying: Throwable, httpResponse: HttpResponse[Throwable]) extends Throwable + object ClientError { + def create(t: Throwable, httpResponse: HttpResponse[Blob]): ClientError = + ClientError.apply(t, httpResponse.copy(body = t)) + } + + type ClientResponse[O] = EitherT[Future, Throwable, O] // Maybe handle all responses + type FinishedClientResponse[O] = EitherT[Future, ClientError, HttpResponse[O]] // Maybe handle all responses + type ClientMiddleware = HttpRequest[Blob] => HttpRequest[Blob] + type RunnableClientResponse[O] = Kleisli[FinishedClientResponse, ClientMiddleware, O] + + type Context = () => HttpResponse[Blob] type RunnableClientRequest[O] = Kleisli[ClientResponse, Context, O] - type RunnableClientResponse[O] = Kleisli[ClientResponse, Nothing, O] - + type ClientFinishedResponse[O] = RunnableClientRequest[HttpResponse[O]] - + case class OWrapper[O](o: O, httpResponse: HttpResponse[Blob]) - + def matchStatusCodeForResponse(hints: Hints, httpResponse: HttpResponse[Blob]): Boolean = { val httpTag = hints.get(smithy.api.Http.tagInstance) - if (httpTag.isDefined) { - httpTag.get.code == httpResponse.statusCode - } else { - false - } + + val httpCode = httpTag.map(_.code).map(v => List(v)).getOrElse(List.empty) + val allowedStatusCodes = httpCode + + allowedStatusCodes.contains(httpResponse.statusCode) || + (httpResponse.statusCode >= 200 && httpResponse.statusCode < 300) } - -// implicit def flatMapClientResponse[O]: FlatMap[EitherT[Future, ClientError, HttpResponse[O]] = new FlatMap[EitherT[Future, ClientError, HttpResponse[O]]] { -// override def flatMap[A, B](fa: ClientResponse[A])(f: A => ClientResponse[B]): ClientResponse[B] = fa.flatMap(f) -// -// override def tailRecM[A, B](a: A)(f: A => ClientResponse[Either[A, B]]): ClientResponse[B] = ??? -// -// override def map[A, B](fa: ClientResponse[A])(f: A => B): ClientResponse[B] = fa.map(f) -// } - - implicit def monadThrowRunnableClientRequest(implicit ec: ExecutionContext): MonadThrow[RunnableClientRequest] = - new MonadThrow[RunnableClientRequest] { - - private def left[A](e: Throwable): RunnableClientRequest[A] = Kleisli { ctx => - EitherT.leftT(ClientError(e, ctx.apply())) - } - - private def right[A](a: A): RunnableClientRequest[A] = Kleisli { ctx => + + implicit def monadThrowRunnableClientRequest(implicit ec: ExecutionContext): MonadThrow[ClientResponse] = + new MonadThrow[ClientResponse] { + + private def left[A](e: Throwable): ClientResponse[A] = + EitherT.leftT(e) + + private def right[A](a: A): ClientResponse[A] = EitherT.rightT(a) - } - override def raiseError[A](e: Throwable): RunnableClientRequest[A] = left(e) + override def raiseError[A](e: Throwable): ClientResponse[A] = left(e) override def handleErrorWith[A]( - fa: RunnableClientRequest[A] - )(f: Throwable => RunnableClientRequest[A]): RunnableClientRequest[A] = Kleisli { ctx => - fa(ctx).biflatMap(v => f(v).run(ctx), o => EitherT.rightT(o)) - } + fa: ClientResponse[A] + )(f: Throwable => ClientResponse[A]): ClientResponse[A] = + fa.leftFlatMap(f(_)) - override def pure[A](x: A): RunnableClientRequest[A] = Kleisli { ctx => + override def pure[A](x: A): ClientResponse[A] = EitherT.rightT(x) - } - override def flatMap[A, B](fa: RunnableClientRequest[A])(f: A => RunnableClientRequest[B]): RunnableClientRequest[B] = + override def flatMap[A, B](fa: ClientResponse[A])(f: A => ClientResponse[B]): ClientResponse[B] = fa.flatMap(f) - override def tailRecM[A, B](a: A)(f: A => RunnableClientRequest[Either[A, B]]): RunnableClientRequest[B] = ??? + override def tailRecM[A, B](a: A)(f: A => ClientResponse[Either[A, B]]): ClientResponse[B] = ??? // f(a).flatMap { // case Left(value) => tailRecM(value)(f) // case Right(value) => Kleisli(ctx => EitherT.rightT(ctx().copy(body = value))) // } } - - } diff --git a/smithy4play/src/main/scala/de/innfactory/smithy4play/codecs/Codec.scala b/smithy4play/src/main/scala/de/innfactory/smithy4play/codecs/Codec.scala index 471b16e8..99d8b3da 100644 --- a/smithy4play/src/main/scala/de/innfactory/smithy4play/codecs/Codec.scala +++ b/smithy4play/src/main/scala/de/innfactory/smithy4play/codecs/Codec.scala @@ -1,7 +1,8 @@ package de.innfactory.smithy4play.codecs import com.github.plokhotnyuk.jsoniter_scala.core.{ReaderConfig, WriterConfig} -import de.innfactory.smithy4play.client.RunnableClientRequest +import de.innfactory.smithy4play.client.{ClientResponse, RunnableClientRequest} +import de.innfactory.smithy4play.routing.internal.RequestWrapped import de.innfactory.smithy4play.{ContentType, ContextRoute} import play.api.http.MimeTypes import play.api.mvc.{RawBuffer, Request, Result} @@ -14,19 +15,19 @@ import smithy4s.server.UnaryServerCodecs import smithy4s.xml.Xml trait Codec { - + final case class Encoders( errorEncoder: CachedSchemaCompiler[BlobEncoder], payloadEncoder: CachedSchemaCompiler[BlobEncoder] ) - def buildServerCodecFromBase(codecBuilder: HttpUnaryServerCodecs.Builder[ContextRoute, Request[RawBuffer], Result])( + def buildServerCodecFromBase(codecBuilder: HttpUnaryServerCodecs.Builder[ContextRoute, RequestWrapped, Result])( contentType: EndpointContentTypes - ): UnaryServerCodecs.Make[ContextRoute, Request[RawBuffer], Result] = buildServerCodec(contentType, codecBuilder) + ): UnaryServerCodecs.Make[ContextRoute, RequestWrapped, Result] = buildServerCodec(contentType, codecBuilder) def buildClientCodecFromBase( - codecBuilder: HttpUnaryClientCodecs.Builder[RunnableClientRequest, HttpRequest[Blob], HttpResponse[Blob]] - )(contentTypes: EndpointContentTypes): HttpUnaryClientCodecs.Builder[RunnableClientRequest, HttpRequest[Blob], HttpResponse[Blob]] = + codecBuilder: HttpUnaryClientCodecs.Builder[ClientResponse, HttpRequest[Blob], HttpResponse[Blob]] + )(contentTypes: EndpointContentTypes): HttpUnaryClientCodecs.Builder[ClientResponse, HttpRequest[Blob], HttpResponse[Blob]] = buildClientCodec(contentTypes, codecBuilder) private val hintMask = alloy.SimpleRestJson.protocol.hintMask @@ -78,8 +79,8 @@ trait Codec { private def buildServerCodec( contentType: EndpointContentTypes, - codecBuilder: HttpUnaryServerCodecs.Builder[ContextRoute, Request[RawBuffer], Result] - ): UnaryServerCodecs.Make[ContextRoute, Request[RawBuffer], Result] = + codecBuilder: HttpUnaryServerCodecs.Builder[ContextRoute, RequestWrapped, Result] + ): UnaryServerCodecs.Make[ContextRoute, RequestWrapped, Result] = codecBuilder .withResponseMediaType(contentType.output.value) .withSuccessBodyEncoders(encoder(contentType.output)) @@ -89,7 +90,7 @@ trait Codec { private def buildClientCodec[Request, Response]( contentType: EndpointContentTypes, - codecBuilder: HttpUnaryClientCodecs.Builder[RunnableClientRequest, Request, Response] + codecBuilder: HttpUnaryClientCodecs.Builder[ClientResponse, Request, Response] ) = codecBuilder .withRequestMediaType(contentType.input.value) diff --git a/smithy4play/src/main/scala/de/innfactory/smithy4play/codecs/CodecSupport.scala b/smithy4play/src/main/scala/de/innfactory/smithy4play/codecs/CodecSupport.scala index e88e6cf9..4c1171c9 100644 --- a/smithy4play/src/main/scala/de/innfactory/smithy4play/codecs/CodecSupport.scala +++ b/smithy4play/src/main/scala/de/innfactory/smithy4play/codecs/CodecSupport.scala @@ -31,6 +31,8 @@ object CodecSupport { contentTypeHeader: Option[String] ): EndpointContentTypes = { logger.debug(s"[CodecSupport] endpoint supports: $supportedContentTypes") + logger.debug(s"[CodecSupport] client accepts: $acceptedTypes") + logger.debug(s"[CodecSupport] contentTypeHeader: $contentTypeHeader") val generalContentTypes = supportedContentTypes.general @@ -41,9 +43,9 @@ object CodecSupport { val preferredError: String = supportedContentTypes.error .extractPreferredContentType(jsonContentType, generalContentTypes) - val accepted = acceptedTypes.find(v => supportedContentTypes.output.findContentType(v).isDefined) - val errorAccepted = acceptedTypes.find(v => supportedContentTypes.error.findContentType(v).isDefined) - val inputContentType = contentTypeHeader.flatMap(v => supportedContentTypes.input.findContentType(v)) + val accepted = acceptedTypes.find(v => supportedContentTypes.output.orElse(generalContentTypes).findContentType(v).isDefined) + val errorAccepted = acceptedTypes.find(v => supportedContentTypes.error.orElse(generalContentTypes).findContentType(v).isDefined) + val inputContentType = contentTypeHeader.flatMap(v => supportedContentTypes.input.orElse(generalContentTypes).findContentType(v)) val contentType = EndpointContentTypes( ContentType(inputContentType.getOrElse(preferredInput)), diff --git a/smithy4play/src/main/scala/de/innfactory/smithy4play/routing/Controller.scala b/smithy4play/src/main/scala/de/innfactory/smithy4play/routing/Controller.scala index 2606b516..70024750 100644 --- a/smithy4play/src/main/scala/de/innfactory/smithy4play/routing/Controller.scala +++ b/smithy4play/src/main/scala/de/innfactory/smithy4play/routing/Controller.scala @@ -17,7 +17,7 @@ trait Controller[Alg[_[_, _, _, _, _]]](implicit ec: ExecutionContext ) extends AutoRoutableController { self: FunctorAlgebra[Alg, ContextRoute] => - + private def transform( impl: FunctorAlgebra[Alg, ContextRoute] ): (Codec, Middleware) => InternalRoute = (codec, middleware) => diff --git a/smithy4play/src/main/scala/de/innfactory/smithy4play/routing/internal/Smithy4PlayRouter.scala b/smithy4play/src/main/scala/de/innfactory/smithy4play/routing/internal/Smithy4PlayRouter.scala index 6e511715..1c65ec44 100644 --- a/smithy4play/src/main/scala/de/innfactory/smithy4play/routing/internal/Smithy4PlayRouter.scala +++ b/smithy4play/src/main/scala/de/innfactory/smithy4play/routing/internal/Smithy4PlayRouter.scala @@ -1,7 +1,7 @@ package de.innfactory.smithy4play.routing.internal import cats.data.{ EitherT, Kleisli } -import de.innfactory.smithy4play.{ ContextRoute, RoutingResult } +import de.innfactory.smithy4play.{ logger, ContextRoute, RoutingResult } import de.innfactory.smithy4play.codecs.Codec import de.innfactory.smithy4play.routing.context.RoutingContextBase import de.innfactory.smithy4play.routing.middleware.Middleware @@ -33,7 +33,7 @@ class Smithy4PlayRouter[Alg[_[_, _, _, _, _]]]( private val baseResponse = HttpResponse(200, Map.empty, Blob.empty) private val errorHeaders = List(smithy4s.http.errorTypeHeader) - private val baseServerCodec: HttpUnaryServerCodecs.Builder[ContextRoute, Request[RawBuffer], Result] = + private val baseServerCodec: HttpUnaryServerCodecs.Builder[ContextRoute, RequestWrapped, Result] = HttpUnaryServerCodecs .builder[ContextRoute] .withErrorTypeHeaders(errorHeaders: _*) @@ -41,7 +41,7 @@ class Smithy4PlayRouter[Alg[_[_, _, _, _, _]]]( .withMetadataEncoders(Metadata.Encoder) .withBaseResponse(_ => Kleisli(ctx => EitherT.rightT[Future, Throwable](baseResponse))) .withWriteEmptyStructs(!_.isUnit) - .withRequestTransformation[Request[RawBuffer]](v => toSmithy4sHttpRequest(v)) + .withRequestTransformation[RequestWrapped](v => toSmithy4sHttpRequest(v)) .withResponseTransformation[Result](v => Kleisli(ctx => EitherT.rightT[Future, Throwable](handleSuccess(v)(ctx)))) private def handleSuccess(output: HttpResponse[Blob])(ctx: RoutingContextBase): Result = { @@ -56,11 +56,11 @@ class Smithy4PlayRouter[Alg[_[_, _, _, _, _]]]( status("").withHeaders(outputHeadersWithoutContentType: _*) } } - + private val compileServerCodec = codec.buildServerCodecFromBase(baseServerCodec) private val router = - PlayPartialFunctionRouter.partialFunction[Alg, ContextRoute, RequestHeader, Request[RawBuffer], Result](service)( + PlayPartialFunctionRouter.partialFunction[Alg, ContextRoute, RequestHeader, RequestWrapped, Result](service)( impl, compileServerCodec, middleware.resolveMiddleware, @@ -70,20 +70,25 @@ class Smithy4PlayRouter[Alg[_[_, _, _, _, _]]]( deconstructPath(requestHeader.path), requestHeader.secure, requestHeader.host, - requestHeader.queryString + requestHeader.queryString, + Map.empty ), - addDecodedPathParams = (r, v) => r + addDecodedPathParams = (r, v) => r.copy(r.req, v) ) private val handler = new PartialFunction[RequestHeader, Request[RawBuffer] => RoutingResult[Result]] { - override def isDefinedAt(x: RequestHeader): Boolean = router.isDefinedAt(x) + override def isDefinedAt(x: RequestHeader): Boolean = { + val isdefined = router.isDefinedAt(x) + if (!isdefined) logger.debug(s"[${this.getClass.getName}] router is not defined at ${isdefined} ${x}") + isdefined + } override def apply(v1: RequestHeader): Request[RawBuffer] => RoutingResult[Result] = { request => val ctx: RoutingContextBase = RoutingContextBase.fromRequest(request, service.hints, v1) - router.apply(v1)(request).run(ctx) + router.apply(v1)(RequestWrapped(request, Map.empty)).run(ctx) } } - def routes(): PartialFunction[RequestHeader, Request[RawBuffer]=> RoutingResult[Result]] = handler + def routes(): PartialFunction[RequestHeader, Request[RawBuffer] => RoutingResult[Result]] = handler } diff --git a/smithy4play/src/main/scala/de/innfactory/smithy4play/routing/internal/Smithy4PlayServerEndpoint.scala b/smithy4play/src/main/scala/de/innfactory/smithy4play/routing/internal/Smithy4PlayServerEndpoint.scala index 84f9198c..cc754c69 100644 --- a/smithy4play/src/main/scala/de/innfactory/smithy4play/routing/internal/Smithy4PlayServerEndpoint.scala +++ b/smithy4play/src/main/scala/de/innfactory/smithy4play/routing/internal/Smithy4PlayServerEndpoint.scala @@ -35,11 +35,13 @@ object Smithy4PlayServerEndpoint { middleware: (Request => F[Response]) => (Request => F[Response]) )(implicit F: MonadThrowLike[F]): Request => F[Response] = { - def errorResponse(throwable: Throwable): F[Response] = throwable match { - case endpoint.Error((_, e)) => - codecs.errorEncoder(e) - case e: Throwable => - codecs.throwableEncoder(e) + def errorResponse(throwable: Throwable): F[Response] = { + throwable match { + case endpoint.Error((_, e)) => + codecs.errorEncoder(e) + case e: Throwable => + codecs.throwableEncoder(e) + } } val base = { (req: Request) => @@ -50,7 +52,7 @@ object Smithy4PlayServerEndpoint { } } // applying middleware after handling error throwable - val withMiddleware = middleware(base.andThen(F.handleErrorWith(_)(errorResponse))) + val withMiddleware = middleware(base).andThen(F.handleErrorWith(_)(errorResponse)) withMiddleware } diff --git a/smithy4play/src/main/scala/de/innfactory/smithy4play/routing/internal/package.scala b/smithy4play/src/main/scala/de/innfactory/smithy4play/routing/internal/package.scala index 7620a96f..e5d76618 100644 --- a/smithy4play/src/main/scala/de/innfactory/smithy4play/routing/internal/package.scala +++ b/smithy4play/src/main/scala/de/innfactory/smithy4play/routing/internal/package.scala @@ -1,14 +1,17 @@ package de.innfactory.smithy4play.routing -import cats.data.{EitherT, Kleisli} -import de.innfactory.smithy4play.{ContextRoute, RoutingResult} -import play.api.mvc.{Headers, RawBuffer, Request, RequestHeader, Result} +import cats.data.{ EitherT, Kleisli } +import de.innfactory.smithy4play.{ ContextRoute, RoutingResult } +import play.api.mvc.{ Headers, RawBuffer, Request, RequestHeader, Result } import smithy4s.Blob -import smithy4s.http.{CaseInsensitive, HttpEndpoint, HttpMethod, HttpRequest, HttpUri, HttpUriScheme} +import smithy4s.http.{ CaseInsensitive, HttpEndpoint, HttpMethod, HttpRequest, HttpUri, HttpUriScheme, PathParams } import scala.concurrent.ExecutionContext package object internal { + + case class RequestWrapped(req: Request[RawBuffer], pathParams: PathParams) + private[routing] def getHeaders(headers: Headers): Map[CaseInsensitive, Seq[String]] = headers.headers.groupBy(_._1).map { case (k, v) => (CaseInsensitive(k), v.map(_._2)) @@ -30,14 +33,15 @@ package object internal { HttpMethod.fromStringOrDefault(method.toUpperCase) private[smithy4play] def toSmithy4sHttpRequest( - request: Request[RawBuffer] + request: RequestWrapped )(implicit ec: ExecutionContext): ContextRoute[HttpRequest[Blob]] = Kleisli { ctx => - val pathParams = deconstructPath(request.path) - val uri = toSmithy4sHttpUri(pathParams, request.secure, request.host, request.queryString) - val headers = getHeaders(request.headers) - val method = getSmithy4sHttpMethod(request.method) - val parsedBody = request.body.asBytes().map(b => Blob(b.toByteBuffer)).getOrElse(Blob.empty) + val pathParams = deconstructPath(request.req.path) + val uri = + toSmithy4sHttpUri(pathParams, request.req.secure, request.req.host, request.req.queryString, request.pathParams) + val headers = getHeaders(request.req.headers) + val method = getSmithy4sHttpMethod(request.req.method) + val parsedBody = request.req.body.asBytes().map(b => Blob(b.toByteBuffer)).getOrElse(Blob.empty) EitherT.rightT(HttpRequest(method, uri, headers, parsedBody)) } @@ -45,7 +49,8 @@ package object internal { path: IndexedSeq[String], secure: Boolean, host: String, - queryString: Map[String, Seq[String]] + queryString: Map[String, Seq[String]], + pathParams: PathParams ): HttpUri = { val uriScheme = if (secure) HttpUriScheme.Https else HttpUriScheme.Http HttpUri( @@ -54,19 +59,18 @@ package object internal { None, path, queryString, - None + if (pathParams.nonEmpty) Some(pathParams) else None ) } type InternalRoute = PartialFunction[RequestHeader, Request[RawBuffer] => RoutingResult[Result]] - + def acceptedContentTypesForRequestHeader(requestHeader: RequestHeader) = { val accepted: Seq[String] = requestHeader.acceptedTypes.map(range => range.mediaType + "/" + range.mediaSubType) accepted } - - def contentTypeForRequestHeader(requestHeader: RequestHeader) = { - requestHeader.contentType - } + + def contentTypeForRequestHeader(requestHeader: RequestHeader) = + requestHeader.contentType } diff --git a/smithy4play/src/main/scala/de/innfactory/smithy4play/routing/package.scala b/smithy4play/src/main/scala/de/innfactory/smithy4play/routing/package.scala index e16fb755..662c8c36 100644 --- a/smithy4play/src/main/scala/de/innfactory/smithy4play/routing/package.scala +++ b/smithy4play/src/main/scala/de/innfactory/smithy4play/routing/package.scala @@ -1,7 +1,8 @@ package de.innfactory.smithy4play -import play.api.mvc.{ RawBuffer, Request, Result } +import de.innfactory.smithy4play.routing.internal.RequestWrapped +import play.api.mvc.{RawBuffer, Request, Result} package object routing { - type PlayTransformation = Request[RawBuffer] => ContextRoute[Result] + type PlayTransformation = RequestWrapped => ContextRoute[Result] } diff --git a/smithy4playTest/app/controller/TestController.scala b/smithy4playTest/app/controller/TestController.scala index 5c5c5e74..3171167c 100644 --- a/smithy4playTest/app/controller/TestController.scala +++ b/smithy4playTest/app/controller/TestController.scala @@ -3,14 +3,13 @@ package controller import cats.data.{EitherT, Kleisli} import controller.models.TestError import de.innfactory.smithy4play.ContextRoute -import de.innfactory.smithy4play.client.Smithy4PlayWsClient import de.innfactory.smithy4play.routing.Controller import play.api.mvc.ControllerComponents import play.api.libs.ws.WSClient -import smithy4s.Endpoint.Middleware import smithy4s.Blob import testDefinitions.test.* -import TestControllerService.serviceInstance +import testDefinitions.test.TestControllerServiceGen.serviceInstance + import javax.inject.{Inject, Singleton} import scala.concurrent.{ExecutionContext, Future} @@ -22,10 +21,7 @@ class TestController @Inject() (implicit ) extends TestControllerService[ContextRoute] with Controller { override def test(): ContextRoute[SimpleTestResponse] = Kleisli { rc => - rc.attributes.get("Not") match { - case Some(_) => EitherT.rightT[Future, Throwable](SimpleTestResponse(Some("TestWithSimpleResponse"))) - case None => EitherT.leftT[Future, SimpleTestResponse](TestError("Not attribute is not defined")) - } + EitherT.rightT[Future, Throwable](SimpleTestResponse(Some("TestWithSimpleResponse"))) } override def testWithOutput( @@ -49,42 +45,29 @@ class TestController @Inject() (implicit } case None => EitherT.leftT[Future, Unit](TestError("Test attribute is not defined")) } - + EitherT.rightT[Future, Throwable](()) } override def testWithBlob(body: Blob, contentType: String): ContextRoute[BlobResponse] = Kleisli { rc => EitherT.rightT[Future, Throwable](BlobResponse(body, "image/png")) } - override def testWithQuery(testQuery: String): ContextRoute[Unit] = Kleisli { rc => - EitherT.rightT[Future, Throwable](()) + override def testWithQuery(testQuery: String, testQueryTwo: String, testQueryList: List[String]): ContextRoute[QueryResponse] = Kleisli { rc => + EitherT.rightT[Future, Throwable](QueryResponse(Some(testQueryList))) } override def testThatReturnsError(): ContextRoute[Unit] = Kleisli { rc => - EitherT.leftT[Future, Unit](TestError("this is supposed to fail")) + EitherT.leftT[Future, Unit](InternalServerError("this is supposed to fail")) } - val client = Smithy4PlayWsClient("http://0.0.0.0:9000/", TestControllerServiceGen.service, Middleware.noop) override def testAuth(): ContextRoute[Unit] = Kleisli { rc => - - - val result = client.testWithJsonInputAndBlobOutput(JsonInput.apply("My Message")).run(null) - println("test auth") - val v = result.value.map { - case Left(value) => println(value.getCause) - case Right(value) => { - println(value.statusCode) - println(value.headers) - println(value.body.contentType) - } - } - EitherT.right(v) + println("testAuth") + EitherT.leftT[Future, Unit](new Throwable("Error")) } - override def testWithOtherStatusCode(): ContextRoute[Unit] = Kleisli { rc => - EitherT.rightT[Future, Throwable](()) + override def testWithOtherStatusCode(): ContextRoute[TestWithOtherStatus] = Kleisli { rc => + EitherT.rightT[Future, Throwable](TestWithOtherStatus(269)) } - override def testWithJsonInputAndBlobOutput(body: JsonInput): ContextRoute[BlobResponse] = Kleisli { rc => EitherT.rightT[Future, Throwable](BlobResponse(Blob(body.message), "image/png")) } diff --git a/smithy4playTest/app/controller/TestRouter.scala b/smithy4playTest/app/controller/TestRouter.scala new file mode 100644 index 00000000..84e1193c --- /dev/null +++ b/smithy4playTest/app/controller/TestRouter.scala @@ -0,0 +1,24 @@ +package controller + +import com.typesafe.config.Config +import controller.middlewares.ValidateAuthMiddleware +import controller.middlewares.AddHeaderMiddleware +import de.innfactory.smithy4play.AutoRouter +import de.innfactory.smithy4play.routing.middleware.Smithy4PlayMiddleware +import play.api.Application +import play.api.mvc.ControllerComponents +import javax.inject.{ Inject, Singleton } +import scala.concurrent.ExecutionContext + +@Singleton +class TestRouter @Inject() (implicit + cc: ControllerComponents, + app: Application, + ec: ExecutionContext, + config: Config +) extends AutoRouter { + + override def smithy4PlayMiddleware: Seq[Smithy4PlayMiddleware] = + super.smithy4PlayMiddleware ++ Seq(ValidateAuthMiddleware(), AddHeaderMiddleware()) + +} diff --git a/smithy4playTest/app/controller/XmlController.scala b/smithy4playTest/app/controller/XmlController.scala index e67cf3f5..3b21e630 100644 --- a/smithy4playTest/app/controller/XmlController.scala +++ b/smithy4playTest/app/controller/XmlController.scala @@ -1,19 +1,20 @@ package controller -import cats.data.{ EitherT, Kleisli } +import cats.data.{EitherT, Kleisli} import cats.implicits.catsSyntaxEitherId import de.innfactory.smithy4play.ContextRoute +import de.innfactory.smithy4play.routing.Controller import play.api.mvc.ControllerComponents -import testDefinitions.test.{ XmlControllerDef, XmlTestInputBody, XmlTestOutput, XmlTestWithInputAndOutputOutput } - -import javax.inject.{ Inject, Singleton } -import scala.concurrent.{ ExecutionContext, Future } +import testDefinitions.test.{XmlControllerDef, XmlTestInputBody, XmlTestOutput, XmlTestWithInputAndOutputOutput} +import XmlControllerDef.serviceInstance +import javax.inject.{Inject, Singleton} +import scala.concurrent.{ExecutionContext, Future} @Singleton class XmlController @Inject() (implicit cc: ControllerComponents, executionContext: ExecutionContext -) extends XmlControllerDef[ContextRoute] { +) extends XmlControllerDef[ContextRoute] with Controller { override def xmlTestWithInputAndOutput( xmlTest: String, diff --git a/smithy4playTest/app/controller/middlewares/AddHeaderMiddleware.scala b/smithy4playTest/app/controller/middlewares/AddHeaderMiddleware.scala new file mode 100644 index 00000000..3ebd7d35 --- /dev/null +++ b/smithy4playTest/app/controller/middlewares/AddHeaderMiddleware.scala @@ -0,0 +1,33 @@ +package controller.middlewares + +import cats.data.EitherT +import de.innfactory.smithy4play +import de.innfactory.smithy4play.routing.context.{ RoutingContext, RoutingContextBase } +import de.innfactory.smithy4play.routing.middleware.{ Middleware, Smithy4PlayMiddleware } +import de.innfactory.smithy4play.{ ContextRoute, RoutingResult } +import play.api.mvc.Result +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 } + +@Singleton +class AddHeaderMiddleware @Inject() (implicit + executionContext: ExecutionContext +) extends Smithy4PlayMiddleware { + + override def skipMiddleware(r: RoutingContext): Boolean = { + false + } + + def logic( + r: RoutingContext, + next: RoutingContext => RoutingResult[Result] + )(implicit ec: ExecutionContext): RoutingResult[Result] = { + next(r).map(v => v.withHeaders(("endpointresulttest", "test"))) + } + +} diff --git a/smithy4playTest/app/controller/middlewares/ValidateAuthMiddleware.scala b/smithy4playTest/app/controller/middlewares/ValidateAuthMiddleware.scala new file mode 100644 index 00000000..bd2eab47 --- /dev/null +++ b/smithy4playTest/app/controller/middlewares/ValidateAuthMiddleware.scala @@ -0,0 +1,46 @@ +package controller.middlewares + +import cats.data.EitherT +import de.innfactory.smithy4play +import de.innfactory.smithy4play.routing.context.{ RoutingContext, RoutingContextBase } +import de.innfactory.smithy4play.routing.middleware.{ Middleware, Smithy4PlayMiddleware } +import de.innfactory.smithy4play.{ ContextRoute, RoutingResult } +import smithy.api +import smithy.api.{ Auth, HttpBearerAuth } +import smithy4s.Blob +import smithy4s.http.HttpResponse +import play.api.mvc.Result +import javax.inject.{ Inject, Singleton } +import scala.concurrent.{ ExecutionContext, Future } + +@Singleton +class ValidateAuthMiddleware @Inject() (implicit + executionContext: ExecutionContext +) extends Smithy4PlayMiddleware { + + override def skipMiddleware(r: RoutingContext): Boolean = { + 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.name == HttpBearerAuth.id.name) + } yield { + r.headers.contains("Authorization") + } + }.getOrElse(true) + + def logic( + r: RoutingContext, + next: RoutingContext => RoutingResult[Result] + )(implicit ec: ExecutionContext): RoutingResult[Result] = { + println("ValidateAuthMiddleware logic") + EitherT.leftT[Future, Result]( + testDefinitions.test.UnauthorizedError("Unauthorized") + ) + } + +} diff --git a/smithy4playTest/conf/routes b/smithy4playTest/conf/routes index 2152173a..3aab22f2 100644 --- a/smithy4playTest/conf/routes +++ b/smithy4playTest/conf/routes @@ -4,5 +4,5 @@ # ~~~~ # An example controller showing a sample home page -#-> /1 de.innfactory.smithy4play.AutoRouter --> / de.innfactory.smithy4play.AutoRouter + +-> / controller.TestRouter diff --git a/smithy4playTest/test/TestControllerTest.scala b/smithy4playTest/test/TestControllerTest.scala index 791e172c..cd359afd 100644 --- a/smithy4playTest/test/TestControllerTest.scala +++ b/smithy4playTest/test/TestControllerTest.scala @@ -1,23 +1,17 @@ import controller.models.TestError import models.NodeImplicits.NodeEnhancer -import models.{ TestBase, TestJson } +import models.{TestBase, TestJson} import org.scalatest.time.SpanSugar.convertIntToGrainOfTime import play.api.Application import play.api.inject.guice.GuiceApplicationBuilder -import play.api.libs.json.{ Json, OWrites } +import play.api.libs.json.{Json, OWrites} import play.api.mvc.Result import play.api.test.FakeRequest -import play.api.test.Helpers._ -import smithy4s.{ Blob, Document } +import play.api.test.Helpers.* +import smithy4s.{Blob, Document} import smithy4s.http.CaseInsensitive -import testDefinitions.test.{ - JsonInput, - SimpleTestResponse, - TestControllerServiceGen, - TestRequestBody, - TestResponseBody, - TestWithOutputResponse -} +import de.innfactory.smithy4play.client.SmithyPlayTestUtils.* +import testDefinitions.test.{ErrorError, InternalServerError, JsonInput, SimpleTestResponse, TestControllerServiceGen, TestRequestBody, TestResponseBody, TestWithOutputResponse} import java.io.File import java.nio.file.Files @@ -33,39 +27,36 @@ class TestControllerTest extends TestBase { "controller.TestController" must { -// "new autoTest test" in { -// new ComplianceClient(genericClient).tests().map { result => -// 200 mustBe result.receivedCode -// result.expectedBody mustBe result.receivedBody -// } -// } -// -// "autoTest 500" in { -// new ComplianceClient(genericClient).tests(Some("500")).map { result => -// result.expectedCode must not be result.receivedCode -// result.receivedError mustBe result.expectedError -// } -// } -// -// "route to Test Endpoint" in { -// val result = genericClient.test().awaitRight -// result.statusCode mustBe 200 -// } -// -// "route to Test Endpoint by SmithyTestClient with Query Parameter, Path Parameter and Body" in { -// val pathParam = "thisIsAPathParam" -// val testQuery = "thisIsATestQuery" -// val testHeader = "thisIsATestHeader" -// val body = TestRequestBody("thisIsARequestBody") -// val result = genericClient.testWithOutput(pathParam, testQuery, testHeader, body).awaitRight -// -// val responseBody = result.body -// result.statusCode mustBe 200 -// responseBody.body.testQuery mustBe testQuery -// responseBody.body.pathParam mustBe pathParam -// responseBody.body.bodyMessage mustBe body.message -// responseBody.body.testHeader mustBe testHeader -// } + "route to Test Endpoint" in { + val result = genericClient.test().awaitRight + result.statusCode mustBe 200 + } + + "test query List" in { + val queryList = List("one", "two", "three") + val result = genericClient.testWithQuery( + testQuery = "testQuery1", + testQueryTwo = "testQuery2", + testQueryList = queryList, + ).awaitRight + result.statusCode mustBe 200 + result.body.body.getOrElse(List.empty) mustBe queryList + } + + "route to Test Endpoint by SmithyTestClient with Query Parameter, Path Parameter and Body" in { + val pathParam = "thisIsAPathParam" + val testQuery = "thisIsATestQuery" + val testHeader = "thisIsATestHeader" + val body = TestRequestBody("thisIsARequestBody") + val result = genericClient.testWithOutput(pathParam, testQuery, testHeader, body).awaitRight + + val responseBody = result.body + result.statusCode mustBe 200 + responseBody.body.testQuery mustBe testQuery + responseBody.body.pathParam mustBe pathParam + responseBody.body.bodyMessage mustBe body.message + responseBody.body.testHeader mustBe testHeader + } "route to Test Endpoint with Query Parameter, Path Parameter and Body with fake request" in { val pathParam = "thisIsAPathParam" @@ -78,12 +69,14 @@ class TestControllerTest extends TestBase { app, FakeRequest("POST", s"/test/$pathParam?testQuery=$testQuery") .withHeaders(("Test-Header", testHeader)) + .withHeaders(("accept", "application/json")) + .withHeaders(("content-type", "application/json")) .withBody(Json.toJson(body)) ).get + status(future) mustBe 200 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 @@ -101,8 +94,12 @@ class TestControllerTest extends TestBase { app, FakeRequest("POST", s"/test/$pathParam?testQuery=$testQuery") .withHeaders(("Test-Header", testHeader)) + .withHeaders(("content-type", "application/xml")) + .withHeaders(("accept", "application/xml")) .withXmlBody(xml) ).get + + println(contentAsString(future)) status(future) mustBe 200 val xmlRes = scala.xml.XML.loadString(contentAsString(future)) xmlRes.normalize mustBe @@ -141,64 +138,65 @@ class TestControllerTest extends TestBase { status(future) mustBe 400 } -// "route to Health Endpoint" in { -// val result = genericClient.health().run(null) -// -// result.headers.contains(CaseInsensitive("endpointresulttest")) mustBe true -// result.statusCode mustBe 200 -// } -// -// "route to error Endpoint" in { -// val result = genericClient.testThatReturnsError().awaitLeft -// -// result.toErrorResponse[TestError].message must include("fail") -// result.statusCode mustBe 500 -// } -// -// "route to Blob Endpoint" in { -// val path = getClass.getResource("/testPicture.png").getPath -// val file = new File(path) -// 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.body -// } -// -// "route with json body to Blob Endpoint" in { -// val testString = "StringToBeParsedCorrectly" -// val result = genericClient.testWithJsonInputAndBlobOutput(JsonInput(testString)).awaitRight(global, 5.hours) -// -// result.statusCode mustBe 200 -// testString mustBe result.body.body.toUTF8String -// } -// -// "route to Auth Test" in { -// val result = genericClient.testAuth().awaitLeft -// -// result.statusCode mustBe 401 -// } -// -// "test with different status code" in { -// val result = genericClient.testWithOtherStatusCode().awaitRight -// -// result.statusCode mustBe 269 -// } -// -// "manual writing json" in { -// -// val writtenData = smithy4s.json.Json.writeBlob(SimpleTestResponse(Some("Test"))) -// -// val writtenJson = Json.parse(writtenData.toArray).as[TestJson] -// -// val readData = -// smithy4s.json.Json.read(Blob(Json.toBytes(Json.toJson(TestJson(Some("Test"))))))(SimpleTestResponse.schema) -// -// writtenJson.message mustBe Some("Test") -// readData match { -// case Right(value) => value.message mustBe Some("Test") -// case _ => fail("should parse") -// } -// } + "route to Health Endpoint" in { + val result = genericClient.health().awaitRight + + result.headers.contains(CaseInsensitive("endpointresulttest")) mustBe true + result.statusCode mustBe 200 + } + + "route to error Endpoint" in { + val result = genericClient.testThatReturnsError().awaitLeft + + result.body match + case e: InternalServerError => e.message must include("fail") + result.statusCode mustBe 500 + } + + "route to Blob Endpoint" in { + val path = getClass.getResource("/testPicture.png").getPath + val file = new File(path) + 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.body + } + + "route with json body to Blob Endpoint" in { + val testString = "StringToBeParsedCorrectly" + val result = genericClient.testWithJsonInputAndBlobOutput(JsonInput(testString)).awaitRight(global, 5.hours) + + result.statusCode mustBe 200 + testString mustBe result.body.body.toUTF8String + } + + "route to Auth Test" in { + val result = genericClient.testAuth().awaitLeft + println(result) + result.statusCode mustBe 401 + } + + "test with different status code" in { + val result = genericClient.testWithOtherStatusCode().awaitRight + + result.statusCode mustBe 269 + } + + "manual writing json" in { + + val writtenData = smithy4s.json.Json.writeBlob(SimpleTestResponse(Some("Test"))) + + val writtenJson = Json.parse(writtenData.toArray).as[TestJson] + + val readData = + smithy4s.json.Json.read(Blob(Json.toBytes(Json.toJson(TestJson(Some("Test"))))))(SimpleTestResponse.schema) + + writtenJson.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 index 32c3292c..f12bbd32 100644 --- a/smithy4playTest/test/XmlControllerTest.scala +++ b/smithy4playTest/test/XmlControllerTest.scala @@ -5,135 +5,132 @@ import play.api.Application import play.api.inject.guice.GuiceApplicationBuilder import play.api.libs.json.{Json, OFormat} import play.api.test.FakeRequest -import play.api.test.Helpers._ +import play.api.test.Helpers.* import smithy4s.http.CaseInsensitive import testDefinitions.test.{XmlControllerDefGen, XmlTestInputBody, XmlTestOutput} - +import de.innfactory.smithy4play.client.SmithyPlayTestUtils.* import scala.concurrent.ExecutionContext.Implicits.global class XmlControllerTest extends TestBase { -// val genericClient = XmlControllerDefGen.withClientAndHeaders(FakeRequestClient, None, List(269)) + val genericClient = client(XmlControllerDefGen.service) 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 + "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 + println(contentAsString(request)) // 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 -// } -// -// } + } + + "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")) + .withHeaders(("accept", "application/json")) + .withXmlBody( + xml + ) + ).get + status(request) mustBe 400 + val result = contentAsJson(request) + } + + "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 + } + + "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")) + .withHeaders(("accept", "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/Smithy4PlayTestClient.scala b/smithy4playTest/test/models/Smithy4PlayTestClient.scala index e0301289..55d04ae4 100644 --- a/smithy4playTest/test/models/Smithy4PlayTestClient.scala +++ b/smithy4playTest/test/models/Smithy4PlayTestClient.scala @@ -1,10 +1,14 @@ package models -import cats.data.{ EitherT, Kleisli } -import de.innfactory.smithy4play.client.{ matchStatusCodeForResponse, RunnableClientRequest, SmithyPlayClient } +import cats.data.EitherT +import de.innfactory.smithy4play.client.{ + matchStatusCodeForResponse, + FinishedClientResponse, + RunnableClientResponse, + SmithyPlayClient +} import org.apache.pekko.stream.Materializer import play.api.Application -import play.api.libs.ws.{ writeableOf_ByteArray, WSClient, WSResponse } import play.api.mvc.AnyContentAsEmpty import play.api.test.FakeRequest import play.api.test.Helpers.route @@ -12,7 +16,7 @@ import smithy4s.client.UnaryLowLevelClient import smithy4s.http.{ CaseInsensitive, HttpRequest, HttpResponse } import smithy4s.kinds.Kind1 import smithy4s.{ Blob, Endpoint, Hints } -import play.api.test.Helpers.{ route, writeableOf_AnyContentAsEmpty } +import play.api.test.Helpers.writeableOf_AnyContentAsEmpty import scala.concurrent.ExecutionContext @@ -22,36 +26,42 @@ class Smithy4PlayTestClient[Alg[_[_, _, _, _, _]]]( requestIsSuccessful: (Hints, HttpResponse[Blob]) => Boolean = matchStatusCodeForResponse, explicitDefaultsEncoding: Boolean = true )(implicit ec: ExecutionContext, app: Application, mat: Materializer) - extends UnaryLowLevelClient[RunnableClientRequest, HttpRequest[Blob], HttpResponse[Blob]] { + extends UnaryLowLevelClient[FinishedClientResponse, HttpRequest[Blob], HttpResponse[Blob]] { val underlyingClient = new SmithyPlayClient[Alg, Smithy4PlayTestClient[Alg]]( baseUri = "", service = service, client = this, middleware = middleware, - requestIsSuccessful = (_, _) => true, + requestIsSuccessful = requestIsSuccessful, toSmithy4sClient = x => x ) - def transformer(): Alg[Kind1[RunnableClientRequest]#toKind5] = + def transformer(): Alg[Kind1[RunnableClientResponse]#toKind5] = underlyingClient.service.algebra(underlyingClient.compiler) private def buildPath(req: HttpRequest[Blob]): String = - req.uri.path.mkString("/") + toQueryParameters(req).map(s => s._1 + "=" + s._2).mkString("?", "&", "") + req.uri.path.mkString("/","/", "") + toQuery(req) + + private def toQuery(req: HttpRequest[Blob]) = { + val queryList = toQueryParameters(req) + val queryListMapped = queryList.map(s => s._1 + "=" + s._2) + if(queryList.nonEmpty) queryList.mkString("?", "&", "") else "" + } private def toHeaders(request: HttpRequest[Blob]): List[(String, String)] = - request.headers.flatMap { case (insensitive, strings) => + request.headers.toList.flatMap { case (insensitive, strings) => strings.map(v => (insensitive.value, v)) - }.toList + } private def toQueryParameters(request: HttpRequest[Blob]): List[(String, String)] = - request.uri.queryParams.flatMap { case (key, value) => + request.uri.queryParams.toList.flatMap { case (key, value) => value.map(v => (key, v)) - }.toList + } override def run[Output]( request: HttpRequest[Blob] - )(responseCB: HttpResponse[Blob] => RunnableClientRequest[Output]): RunnableClientRequest[Output] = { + )(responseCB: HttpResponse[Blob] => FinishedClientResponse[Output]): FinishedClientResponse[Output] = { val baseRequest: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(method = request.method.showUppercase, buildPath(request)) @@ -76,11 +86,9 @@ class Smithy4PlayTestClient[Alg[_[_, _, _, _, _]]]( bodyConsumed.map(Blob(_)).getOrElse(Blob.empty) ).withContentType(contentType.getOrElse("application/json")) - Kleisli { _ => - EitherT { - httpResponse.flatMap { httpResponse => - responseCB(httpResponse).run(() => httpResponse).value - } + EitherT { + httpResponse.flatMap { httpResponse => + responseCB(httpResponse).value } } @@ -93,6 +101,6 @@ object Smithy4PlayTestClient { middleware: Endpoint.Middleware[Smithy4PlayTestClient[Alg]], requestIsSuccessful: (Hints, HttpResponse[Blob]) => Boolean = matchStatusCodeForResponse, explicitDefaultsEncoding: Boolean = true - )(implicit ec: ExecutionContext, app: Application, mat: Materializer): Alg[Kind1[RunnableClientRequest]#toKind5] = + )(implicit ec: ExecutionContext, app: Application, mat: Materializer): Alg[Kind1[RunnableClientResponse]#toKind5] = new Smithy4PlayTestClient(service, middleware, requestIsSuccessful, explicitDefaultsEncoding).transformer() } diff --git a/smithy4playTest/test/models/TestBase.scala b/smithy4playTest/test/models/TestBase.scala index 2f567b75..cdfd8ff7 100644 --- a/smithy4playTest/test/models/TestBase.scala +++ b/smithy4playTest/test/models/TestBase.scala @@ -1,13 +1,13 @@ package models import de.innfactory.smithy4play.EndpointRequest -import de.innfactory.smithy4play.client.{RunnableClientRequest, matchStatusCodeForResponse} -import org.scalatestplus.play.{BaseOneAppPerSuite, FakeApplicationFactory, PlaySpec} +import de.innfactory.smithy4play.client.{ matchStatusCodeForResponse, FinishedClientResponse, RunnableClientResponse } +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, Hints, Service} -import smithy4s.http.{CaseInsensitive, HttpResponse} +import play.api.test.Helpers.{ route, writeableOf_AnyContentAsEmpty } +import smithy4s.{ Blob, Hints, Service } +import smithy4s.http.{ CaseInsensitive, HttpResponse } import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global @@ -20,7 +20,7 @@ trait TestBase extends PlaySpec with BaseOneAppPerSuite with FakeApplicationFact def client[Alg[_[_, _, _, _, _]]]( service: Service[Alg], requestIsSuccessful: (Hints, HttpResponse[Blob]) => Boolean = matchStatusCodeForResponse - ): Alg[Kind1[RunnableClientRequest]#toKind5] = + ): Alg[Kind1[RunnableClientResponse]#toKind5] = Smithy4PlayTestClient(service = service, middleware = Middleware.noop, requestIsSuccessful = requestIsSuccessful) } diff --git a/smithy4playTest/testSpecs/200TestSuite.smithy b/smithy4playTest/testSpecs/200TestSuite.smithy index a6621262..1ee61546 100644 --- a/smithy4playTest/testSpecs/200TestSuite.smithy +++ b/smithy4playTest/testSpecs/200TestSuite.smithy @@ -13,7 +13,9 @@ apply TestWithQuery @httpRequestTests([ uri: "/query", protocol: simpleRestJson, params: { - "testQuery": "Hello there" + "testQuery": "Hello there", + "testQueryTwo": "Hello there", + "testQueryList": ["test1", "test2"] }, } ]) diff --git a/smithy4playTest/testSpecs/TestController.smithy b/smithy4playTest/testSpecs/TestController.smithy index 2de835da..b29024bb 100644 --- a/smithy4playTest/testSpecs/TestController.smithy +++ b/smithy4playTest/testSpecs/TestController.smithy @@ -2,12 +2,15 @@ $version: "2" namespace testDefinitions.test use alloy#simpleRestJson +use de.innfactory.smithy4play.meta#contentTypes +use smithy.test#StringList @testMiddleware @httpBearerAuth @simpleRestJson service TestControllerService { version: "0.0.1", + errors: [UnauthorizedError], operations: [ Test TestWithOutput @@ -26,6 +29,13 @@ service TestControllerService { @changeStatusCode @http(method: "GET", uri: "/other/status/code", code: 200) operation TestWithOtherStatusCode { + output: TestWithOtherStatus +} + +structure TestWithOtherStatus { + @required + @httpResponseCode + code: Integer = 200 } @auth([]) @@ -43,27 +53,64 @@ operation TestWithJsonInputAndBlobOutput { @http(method: "POST", uri: "/blob", code: 200) operation TestWithBlob { input: BlobRequest, - output: BlobResponse + output: BlobResponse, +} + +@error("client") +@httpError(426) +structure Error426 { + @required + message: String +} + +@error("client") +@httpError(401) +structure UnauthorizedError { + @required + message: String +} + +@error("server") +@httpError(500) +structure InternalServerError { + @required + message: String } @auth([]) @readonly @http(method: "GET", uri: "/error", code: 200) operation TestThatReturnsError { + errors: [InternalServerError] } @auth([]) @readonly @http(method: "GET", uri: "/query", code: 200) operation TestWithQuery { - input: QueryRequest + input: QueryRequest, + output: QueryResponse +} + +structure QueryResponse { + @httpPayload + body: StringQueryList } structure QueryRequest { @httpQuery("testQuery") @required testQuery: String + @httpQuery("testQueryTwo") + @required + testQueryTwo: String + @httpQuery("testQueryList") + @required + testQueryList: StringQueryList +} +list StringQueryList { + member: String } structure BlobRequest { @@ -99,6 +146,7 @@ operation Test { } @auth([]) +@contentTypes(general: ["application/xml", "application/json"]) @http(method: "POST", uri: "/test/{pathParam}", code: 200) operation TestWithOutput { input: TestRequestWithQueryAndPathParams, diff --git a/smithy4playTest/testSpecs/XmlController.smithy b/smithy4playTest/testSpecs/XmlController.smithy index 9daa33e2..3bdda28a 100644 --- a/smithy4playTest/testSpecs/XmlController.smithy +++ b/smithy4playTest/testSpecs/XmlController.smithy @@ -2,8 +2,10 @@ $version: "2" namespace testDefinitions.test use aws.protocols#restXml +use de.innfactory.smithy4play.meta#contentTypes @restXml +@contentTypes(general: ["application/xml", "application/json"]) service XmlControllerDef { version: "0.0.1", operations: [