From 8e9676f940e5ac775ded83fa2308b31f4c9d9129 Mon Sep 17 00:00:00 2001 From: Moritz Lintterer Date: Tue, 24 Oct 2023 13:07:35 +0200 Subject: [PATCH 1/2] feat: adding feature to give additional status codes to client in case of an endpoint having multiple success codes --- .../smithy4play/client/GenericAPIClient.scala | 29 +++++++++++-------- .../smithy4play/client/SmithyPlayClient.scala | 13 +++++++-- .../client/SmithyPlayClientEndpoint.scala | 4 ++- smithy4playTest/test/TestControllerTest.scala | 12 ++++---- 4 files changed, 37 insertions(+), 21 deletions(-) diff --git a/smithy4play/src/main/scala/de/innfactory/smithy4play/client/GenericAPIClient.scala b/smithy4play/src/main/scala/de/innfactory/smithy4play/client/GenericAPIClient.scala index 0b74da55..ec8dfe25 100644 --- a/smithy4play/src/main/scala/de/innfactory/smithy4play/client/GenericAPIClient.scala +++ b/smithy4play/src/main/scala/de/innfactory/smithy4play/client/GenericAPIClient.scala @@ -8,10 +8,11 @@ import scala.concurrent.ExecutionContext private class GenericAPIClient[Alg[_[_, _, _, _, _]]]( service: Service[Alg], - client: RequestClient + client: RequestClient, + additionalSuccessCodes: List[Int] = List.empty )(implicit ec: ExecutionContext) { - private val smithyPlayClient = new SmithyPlayClient("/", service, client) + private val smithyPlayClient = new SmithyPlayClient("/", service, client, additionalSuccessCodes) /* Takes a service and creates a Transformation[Op, ClientRequest] */ private def transformer(): Alg[Kind1[RunnableClientRequest]#toKind5] = @@ -45,26 +46,30 @@ private class GenericAPIClient[Alg[_[_, _, _, _, _]]]( object GenericAPIClient { implicit class EnhancedGenericAPIClient[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _]](service: Service[Alg]) { + def withClientAndHeaders( client: RequestClient, additionalHeaders: Option[Map[String, Seq[String]]] - )(implicit ec: ExecutionContext) = apply(service, additionalHeaders, client) + )(implicit ec: ExecutionContext): Alg[Kind1[ClientResponse]#toKind5] = apply(service, client, additionalHeaders) def withClient( client: RequestClient - )(implicit ec: ExecutionContext) = apply(service, client) + )(implicit ec: ExecutionContext): Alg[Kind1[ClientResponse]#toKind5] = apply(service, client) + + def toGenericClient( + client: RequestClient, + additionalHeaders: Option[Map[String, Seq[String]]] = None, + additionalSuccessCodes: List[Int] = List.empty + )(implicit ec: ExecutionContext): Alg[Kind1[ClientResponse]#toKind5] = + apply(service, client, additionalHeaders, additionalSuccessCodes) } + def apply[Alg[_[_, _, _, _, _]]]( serviceI: Service[Alg], + client: RequestClient, additionalHeaders: Option[Map[String, Seq[String]]] = None, - client: RequestClient + additionalSuccessCodes: List[Int] = List.empty )(implicit ec: ExecutionContext): Alg[Kind1[ClientResponse]#toKind5] = - new GenericAPIClient(serviceI, client).transformer(additionalHeaders) - - def apply[Alg[_[_, _, _, _, _]]]( - serviceI: Service[Alg], - client: RequestClient - )(implicit ec: ExecutionContext): Alg[Kind1[RunnableClientRequest]#toKind5] = - new GenericAPIClient(serviceI, client).transformer() + new GenericAPIClient(serviceI, client, additionalSuccessCodes).transformer(additionalHeaders) } 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 9e5bf2f7..ecfae1b5 100644 --- a/smithy4play/src/main/scala/de/innfactory/smithy4play/client/SmithyPlayClient.scala +++ b/smithy4play/src/main/scala/de/innfactory/smithy4play/client/SmithyPlayClient.scala @@ -8,7 +8,8 @@ import scala.concurrent.ExecutionContext class SmithyPlayClient[Alg[_[_, _, _, _, _]], F[_]]( baseUri: String, val service: smithy4s.Service[Alg], - client: RequestClient + client: RequestClient, + additionalSuccessCodes: List[Int] = List.empty )(implicit executionContext: ExecutionContext) { def send[I, E, O, SI, SO]( @@ -20,7 +21,15 @@ class SmithyPlayClient[Alg[_[_, _, _, _, _]], F[_]]( HttpEndpoint .cast(endpoint) .map(httpEndpoint => - new SmithyPlayClientEndpoint(endpoint, baseUri, additionalHeaders, httpEndpoint, input, client).send() + new SmithyPlayClientEndpoint( + endpoint = endpoint, + baseUri = baseUri, + additionalHeaders = additionalHeaders, + additionalSuccessCodes = additionalSuccessCodes, + httpEndpoint = httpEndpoint, + input = input, + client = client + ).send() ) .toOption .get 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 a4ab517b..4af02b2b 100644 --- a/smithy4play/src/main/scala/de/innfactory/smithy4play/client/SmithyPlayClientEndpoint.scala +++ b/smithy4play/src/main/scala/de/innfactory/smithy4play/client/SmithyPlayClientEndpoint.scala @@ -11,6 +11,7 @@ private[smithy4play] class SmithyPlayClientEndpoint[Op[_, _, _, _, _], I, E, O, endpoint: Endpoint[Op, I, E, O, SI, SO], baseUri: String, additionalHeaders: Option[Map[String, Seq[String]]], + additionalSuccessCodes: List[Int] = List.empty, httpEndpoint: HttpEndpoint[I], input: I, client: RequestClient @@ -50,7 +51,8 @@ private[smithy4play] class SmithyPlayClientEndpoint[Op[_, _, _, _, _], I, E, O, for { res <- response metadata = Metadata(headers = res.headers.map(headers => (CaseInsensitive(headers._1), headers._2))) - output <- if (res.statusCode == expectedCode) handleSuccess(metadata, res, expectedCode) + output <- if ((additionalSuccessCodes :+ expectedCode).contains(res.statusCode)) + handleSuccess(metadata, res, expectedCode) else handleError(res, expectedCode) } yield output diff --git a/smithy4playTest/test/TestControllerTest.scala b/smithy4playTest/test/TestControllerTest.scala index 7ed5affd..d16886e8 100644 --- a/smithy4playTest/test/TestControllerTest.scala +++ b/smithy4playTest/test/TestControllerTest.scala @@ -1,19 +1,19 @@ import controller.models.TestError import de.innfactory.smithy4play.CodecUtils import de.innfactory.smithy4play.client.GenericAPIClient.EnhancedGenericAPIClient -import de.innfactory.smithy4play.client.{RequestClient, SmithyClientResponse} +import de.innfactory.smithy4play.client.{ RequestClient, SmithyClientResponse } import de.innfactory.smithy4play.client.SmithyPlayTestUtils._ import de.innfactory.smithy4play.compliancetests.ComplianceClient import models.TestJson -import org.scalatestplus.play.{BaseOneAppPerSuite, FakeApplicationFactory, PlaySpec} +import org.scalatestplus.play.{ BaseOneAppPerSuite, FakeApplicationFactory, PlaySpec } import play.api.Application import play.api.Play.materializer import play.api.inject.guice.GuiceApplicationBuilder -import play.api.libs.json.{Json, OWrites} -import play.api.mvc.{AnyContentAsEmpty, Result} +import play.api.libs.json.{ Json, OWrites } +import play.api.mvc.{ AnyContentAsEmpty, Result } import play.api.test.FakeRequest import play.api.test.Helpers._ -import testDefinitions.test.{SimpleTestResponse, TestControllerServiceGen, TestRequestBody} +import testDefinitions.test.{ SimpleTestResponse, TestControllerServiceGen, TestRequestBody } import smithy4s.ByteArray import java.io.File @@ -52,7 +52,7 @@ class TestControllerTest extends PlaySpec with BaseOneAppPerSuite with FakeAppli } } - val genericClient = TestControllerServiceGen.withClientAndHeaders(FakeRequestClient, None) + val genericClient = TestControllerServiceGen.toGenericClient(FakeRequestClient, None, List(200)) override def fakeApplication(): Application = new GuiceApplicationBuilder().build() From d936fc368c0b8f2cac82225f61b2108a80ec8593 Mon Sep 17 00:00:00 2001 From: Moritz Lintterer Date: Tue, 24 Oct 2023 15:02:08 +0200 Subject: [PATCH 2/2] tests: adding tests for alternative status codes, removing expected status code from response --- .../client/SmithyPlayClientEndpoint.scala | 20 +++++++-------- ...mithyPlayClientEndpointErrorResponse.scala | 3 +-- .../SmithyPlayClientEndpointResponse.scala | 3 +-- .../app/controller/TestController.scala | 4 +++ .../ChangeStatusCodeMiddleware.scala | 25 +++++++++++++++++++ .../middlewares/MiddlewareRegistry.scala | 6 +++-- .../middlewares/TestMiddlewareImpl.scala | 2 +- smithy4playTest/test/TestControllerTest.scala | 19 +++++++++----- smithy4playTest/testSpecs/Base.smithy | 5 ++++ .../testSpecs/TestController.smithy | 9 ++++++- 10 files changed, 72 insertions(+), 24 deletions(-) create mode 100644 smithy4playTest/app/controller/middlewares/ChangeStatusCodeMiddleware.scala 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 4af02b2b..06368ee1 100644 --- a/smithy4play/src/main/scala/de/innfactory/smithy4play/client/SmithyPlayClientEndpoint.scala +++ b/smithy4play/src/main/scala/de/innfactory/smithy4play/client/SmithyPlayClientEndpoint.scala @@ -52,11 +52,11 @@ private[smithy4play] class SmithyPlayClientEndpoint[Op[_, _, _, _, _], I, E, O, res <- response metadata = Metadata(headers = res.headers.map(headers => (CaseInsensitive(headers._1), headers._2))) output <- if ((additionalSuccessCodes :+ expectedCode).contains(res.statusCode)) - handleSuccess(metadata, res, expectedCode) - else handleError(res, expectedCode) + handleSuccess(metadata, res) + else handleError(res) } yield output - def handleSuccess(metadata: Metadata, response: SmithyClientResponse, expectedCode: Int) = { + def handleSuccess(metadata: Metadata, response: SmithyClientResponse) = { val headers = response.headers.map(x => (x._1.toLowerCase, x._2)) val output = outputMetadataDecoder.total match { case Some(totalDecoder) => @@ -67,23 +67,23 @@ private[smithy4play] class SmithyPlayClientEndpoint[Op[_, _, _, _, _], I, E, O, codecApi = CodecUtils.extractCodec(headers) bodyPartial <- codecApi.decodeFromByteArrayPartial(codecApi.compileCodec(outputSchema), response.body.get) - } yield metadataPartial.combine(bodyPartial) + output <- metadataPartial.combineCatch(bodyPartial) + } yield output } Future( - output.map(o => SmithyPlayClientEndpointResponse(Some(o), headers, response.statusCode, expectedCode)).left.map { + output.map(o => SmithyPlayClientEndpointResponse(Some(o), headers, response.statusCode)).left.map { case error: PayloadError => - SmithyPlayClientEndpointErrorResponse(error.expected.getBytes, response.statusCode, expectedCode) + SmithyPlayClientEndpointErrorResponse(error.expected.getBytes, response.statusCode) case error: MetadataError => - SmithyPlayClientEndpointErrorResponse(error.getMessage().getBytes(), response.statusCode, expectedCode) + SmithyPlayClientEndpointErrorResponse(error.getMessage().getBytes(), response.statusCode) } ) } - def handleError(response: SmithyClientResponse, expectedCode: Int) = Future( + def handleError(response: SmithyClientResponse) = Future( Left { SmithyPlayClientEndpointErrorResponse( response.body.getOrElse(Array.emptyByteArray), - response.statusCode, - expectedCode + response.statusCode ) } ) diff --git a/smithy4play/src/main/scala/de/innfactory/smithy4play/client/SmithyPlayClientEndpointErrorResponse.scala b/smithy4play/src/main/scala/de/innfactory/smithy4play/client/SmithyPlayClientEndpointErrorResponse.scala index 03322c2f..aca5e9f5 100644 --- a/smithy4play/src/main/scala/de/innfactory/smithy4play/client/SmithyPlayClientEndpointErrorResponse.scala +++ b/smithy4play/src/main/scala/de/innfactory/smithy4play/client/SmithyPlayClientEndpointErrorResponse.scala @@ -4,6 +4,5 @@ import de.innfactory.smithy4play.Showable case class SmithyPlayClientEndpointErrorResponse( error: Array[Byte], - statusCode: Int, - expectedStatusCode: Int + statusCode: Int ) extends Showable diff --git a/smithy4play/src/main/scala/de/innfactory/smithy4play/client/SmithyPlayClientEndpointResponse.scala b/smithy4play/src/main/scala/de/innfactory/smithy4play/client/SmithyPlayClientEndpointResponse.scala index 32ec6d25..1989a172 100644 --- a/smithy4play/src/main/scala/de/innfactory/smithy4play/client/SmithyPlayClientEndpointResponse.scala +++ b/smithy4play/src/main/scala/de/innfactory/smithy4play/client/SmithyPlayClientEndpointResponse.scala @@ -5,6 +5,5 @@ import de.innfactory.smithy4play.Showable case class SmithyPlayClientEndpointResponse[O]( body: Option[O], headers: Map[String, Seq[String]], - statusCode: Int, - expectedStatusCode: Int + statusCode: Int ) extends Showable diff --git a/smithy4playTest/app/controller/TestController.scala b/smithy4playTest/app/controller/TestController.scala index 6af9e764..ee60b420 100644 --- a/smithy4playTest/app/controller/TestController.scala +++ b/smithy4playTest/app/controller/TestController.scala @@ -63,4 +63,8 @@ class TestController @Inject() (implicit override def testAuth(): ContextRoute[Unit] = Kleisli { rc => EitherT.rightT[Future, ContextRouteError](()) } + + override def testWithOtherStatusCode(): ContextRoute[Unit] = Kleisli { rc => + EitherT.rightT[Future, ContextRouteError](()) + } } diff --git a/smithy4playTest/app/controller/middlewares/ChangeStatusCodeMiddleware.scala b/smithy4playTest/app/controller/middlewares/ChangeStatusCodeMiddleware.scala new file mode 100644 index 00000000..f92daa0c --- /dev/null +++ b/smithy4playTest/app/controller/middlewares/ChangeStatusCodeMiddleware.scala @@ -0,0 +1,25 @@ +package controller.middlewares + +import de.innfactory.smithy4play.middleware.MiddlewareBase +import de.innfactory.smithy4play.{ EndpointResult, RouteResult, RoutingContext, Status } +import testDefinitions.test.ChangeStatusCode + +import javax.inject.Inject +import scala.concurrent.ExecutionContext + +class ChangeStatusCodeMiddleware @Inject() (implicit executionContext: ExecutionContext) extends MiddlewareBase { + + override protected def skipMiddleware(r: RoutingContext): Boolean = + !r.endpointHints.has(ChangeStatusCode) + + override protected def logic( + r: RoutingContext, + next: RoutingContext => RouteResult[EndpointResult] + ): RouteResult[EndpointResult] = { + val res = next(r) + res.map { r => + r.copy(status = Status(r.status.headers, 269)) + } + } + +} diff --git a/smithy4playTest/app/controller/middlewares/MiddlewareRegistry.scala b/smithy4playTest/app/controller/middlewares/MiddlewareRegistry.scala index 9fb184c0..ef8ed0c2 100644 --- a/smithy4playTest/app/controller/middlewares/MiddlewareRegistry.scala +++ b/smithy4playTest/app/controller/middlewares/MiddlewareRegistry.scala @@ -7,7 +7,9 @@ import javax.inject.Inject class MiddlewareRegistry @Inject() ( disableAbleMiddleware: DisableAbleMiddleware, testMiddlewareImpl: TestMiddlewareImpl, - validateAuthMiddleware: ValidateAuthMiddleware + validateAuthMiddleware: ValidateAuthMiddleware, + changeStatusCodeMiddleware: ChangeStatusCodeMiddleware ) extends MiddlewareRegistryBase { - override val middlewares: Seq[MiddlewareBase] = Seq(disableAbleMiddleware, testMiddlewareImpl, validateAuthMiddleware) + override val middlewares: Seq[MiddlewareBase] = + Seq(disableAbleMiddleware, testMiddlewareImpl, validateAuthMiddleware, changeStatusCodeMiddleware) } diff --git a/smithy4playTest/app/controller/middlewares/TestMiddlewareImpl.scala b/smithy4playTest/app/controller/middlewares/TestMiddlewareImpl.scala index 2151b6c9..a9d9917e 100644 --- a/smithy4playTest/app/controller/middlewares/TestMiddlewareImpl.scala +++ b/smithy4playTest/app/controller/middlewares/TestMiddlewareImpl.scala @@ -1,7 +1,7 @@ package controller.middlewares import de.innfactory.smithy4play.middleware.MiddlewareBase -import de.innfactory.smithy4play.{ EndpointResult, RouteResult, RoutingContext } +import de.innfactory.smithy4play.{ EndpointResult, RouteResult, RoutingContext, Status } import javax.inject.Inject import scala.concurrent.ExecutionContext diff --git a/smithy4playTest/test/TestControllerTest.scala b/smithy4playTest/test/TestControllerTest.scala index d16886e8..1876ec17 100644 --- a/smithy4playTest/test/TestControllerTest.scala +++ b/smithy4playTest/test/TestControllerTest.scala @@ -20,6 +20,7 @@ import java.io.File import java.nio.file.Files import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future +import scala.concurrent.duration.DurationInt class TestControllerTest extends PlaySpec with BaseOneAppPerSuite with FakeApplicationFactory { @@ -52,7 +53,7 @@ class TestControllerTest extends PlaySpec with BaseOneAppPerSuite with FakeAppli } } - val genericClient = TestControllerServiceGen.toGenericClient(FakeRequestClient, None, List(200)) + val genericClient = TestControllerServiceGen.toGenericClient(FakeRequestClient, None, List(269)) override def fakeApplication(): Application = new GuiceApplicationBuilder().build() @@ -61,7 +62,7 @@ class TestControllerTest extends PlaySpec with BaseOneAppPerSuite with FakeAppli "new autoTest test" in { new ComplianceClient(genericClient).tests().map { result => - result.expectedCode mustBe result.receivedCode + 200 mustBe result.receivedCode result.expectedBody mustBe result.receivedBody } } @@ -75,7 +76,7 @@ class TestControllerTest extends PlaySpec with BaseOneAppPerSuite with FakeAppli "route to Test Endpoint" in { val result = genericClient.test().awaitRight - result.statusCode mustBe result.expectedStatusCode + result.statusCode mustBe 200 } "route to Test Endpoint by SmithyTestClient with Query Parameter, Path Parameter and Body" in { @@ -86,7 +87,7 @@ class TestControllerTest extends PlaySpec with BaseOneAppPerSuite with FakeAppli val result = genericClient.testWithOutput(pathParam, testQuery, testHeader, body).awaitRight val responseBody = result.body.get - result.statusCode mustBe result.expectedStatusCode + result.statusCode mustBe 200 responseBody.body.testQuery mustBe testQuery responseBody.body.pathParam mustBe pathParam responseBody.body.bodyMessage mustBe body.message @@ -126,7 +127,7 @@ class TestControllerTest extends PlaySpec with BaseOneAppPerSuite with FakeAppli val result = genericClient.health().awaitRight result.headers.contains("endpointresulttest") mustBe true - result.statusCode mustBe result.expectedStatusCode + result.statusCode mustBe 200 } "route to error Endpoint" in { @@ -142,7 +143,7 @@ class TestControllerTest extends PlaySpec with BaseOneAppPerSuite with FakeAppli val pngAsBytes = ByteArray(Files.readAllBytes(file.toPath)) val result = genericClient.testWithBlob(pngAsBytes, "image/png").awaitRight - result.statusCode mustBe result.expectedStatusCode + result.statusCode mustBe 200 pngAsBytes mustBe result.body.get.body } @@ -152,6 +153,12 @@ class TestControllerTest extends PlaySpec with BaseOneAppPerSuite with FakeAppli 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 = CodecUtils.writeEntityToJsonBytes(SimpleTestResponse(Some("Test")), SimpleTestResponse.schema) diff --git a/smithy4playTest/testSpecs/Base.smithy b/smithy4playTest/testSpecs/Base.smithy index e345b902..d7d4a024 100644 --- a/smithy4playTest/testSpecs/Base.smithy +++ b/smithy4playTest/testSpecs/Base.smithy @@ -13,6 +13,11 @@ structure testMiddleware {} breakingChanges: [{change: "remove"}] ) structure disableTestMiddleware {} +@trait( + selector: "operation", + breakingChanges: [{change: "remove"}] +) +structure changeStatusCode {} diff --git a/smithy4playTest/testSpecs/TestController.smithy b/smithy4playTest/testSpecs/TestController.smithy index 49aad292..e36d60f8 100644 --- a/smithy4playTest/testSpecs/TestController.smithy +++ b/smithy4playTest/testSpecs/TestController.smithy @@ -8,7 +8,14 @@ use alloy#simpleRestJson @simpleRestJson service TestControllerService { version: "0.0.1", - operations: [Test, TestWithOutput, Health, TestWithBlob, TestWithQuery, TestThatReturnsError, TestAuth] + operations: [Test, TestWithOutput, Health, TestWithBlob, TestWithQuery, TestThatReturnsError, TestAuth, TestWithOtherStatusCode] +} + +@auth([]) +@readonly +@changeStatusCode +@http(method: "GET", uri: "/other/status/code", code: 200) +operation TestWithOtherStatusCode { } @auth([])