Skip to content

Commit

Permalink
fix: using correct content type to decode response
Browse files Browse the repository at this point in the history
  • Loading branch information
MoeQuadrat committed Apr 29, 2024
1 parent 18effc6 commit 9508f09
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import play.api.http.MimeTypes
import smithy4s.capability.instances.either._
import smithy4s.codecs.Writer.CachedCompiler
import smithy4s.codecs._
import smithy4s.http.{ HttpResponse, HttpRestSchema, Metadata, MetadataError }
import smithy4s.http.{HttpResponse, HttpRestSchema, Metadata, MetadataError}
import smithy4s.json.Json
import smithy4s.kinds.PolyFunction
import smithy4s.schema.CachedSchemaCompiler
import smithy4s.schema.{CachedSchemaCompiler, Schema}
import smithy4s.xml.Xml
import smithy4s.{ codecs, Blob }
import smithy4s.{Blob, PartialData, codecs}

case class CodecDecider(readerConfig: ReaderConfig) {

Expand Down Expand Up @@ -87,6 +87,13 @@ case class CodecDecider(readerConfig: ReaderConfig) {
_ => Right(())
)(eitherZipper)

def test(): Unit = {
val x = metadataEncoder.mapK(
httpRequestMetadataPipe
)

}

def httpMessageEncoder(
contentType: Seq[String]
): CachedCompiler[HttpResponse[Blob]] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,58 +14,63 @@ import javax.inject.Inject
import scala.concurrent.{ ExecutionContext, Future }

class SmithyPlayEndpoint[Alg[_[_, _, _, _, _]], F[_] <: ContextRoute[_], Op[_, _, _, _, _], I, E, O, SI, SO](
service: Service[Alg],
impl: FunctorInterpreter[Op, F],
middleware: Seq[MiddlewareBase],
endpoint: Endpoint[Op, I, E, O, SI, SO],
codecDecider: CodecDecider
)(implicit cc: ControllerComponents, ec: ExecutionContext)
extends AbstractController(cc) {
service: Service[Alg],
impl: FunctorInterpreter[Op, F],
middleware: Seq[MiddlewareBase],
endpoint: Endpoint[Op, I, E, O, SI, SO],
codecDecider: CodecDecider
)(implicit cc: ControllerComponents, ec: ExecutionContext)
extends AbstractController(cc) {

private val httpEndpoint: Either[HttpEndpoint.HttpEndpointError, HttpEndpoint[I]] = HttpEndpoint.cast(endpoint.schema)
private val serviceHints = service.hints
private val endpointHints = endpoint.hints
private val serviceContentType: String = serviceHints.toMimeType

private implicit val inputSchema: Schema[I] = endpoint.input
private implicit val outputSchema: Schema[O] = endpoint.output

def handler(v1: RequestHeader): Handler =
private implicit val inputSchema: Schema[I] = endpoint.input
private implicit val outputSchema: Schema[O] = endpoint.output
private val outputMetadataEncoder: Metadata.Encoder[O] =
Metadata.Encoder.fromSchema(outputSchema)
def handler(v1: RequestHeader): Handler =
httpEndpoint.map { httpEp =>
Action.async(parse.raw) { implicit request =>
if (request.body.size > 0 && request.body.asBytes().isEmpty) {
logger.error(
"received body size does not equal the parsed body size. \n" +
"This is probably due to the body being too large and thus play is unable to parse.\n" +
"Try setting play.http.parser.maxMemoryBuffer in application.conf"
)
}
Action.async(parse.raw) { implicit request =>
if (request.body.size > 0 && request.body.asBytes().isEmpty) {
logger.error(
"received body size does not equal the parsed body size. \n" +
"This is probably due to the body being too large and thus play is unable to parse.\n" +
"Try setting play.http.parser.maxMemoryBuffer in application.conf"
)
}

implicit val epContentType: ContentType = ContentType(request.contentType.getOrElse(serviceContentType))
val result = for {
pathParams <- getPathParams(v1, httpEp)
metadata = getMetadata(pathParams, v1)
input <- getInput(request, metadata)
endpointLogic = impl(endpoint.wrap(input))
.asInstanceOf[Kleisli[RouteResult, RoutingContext, O]]
.map(mapToEndpointResult(httpEp.code))
chainedMiddlewares = middleware.foldRight(endpointLogic)((a, b) => a.middleware(b.run))
res <-
chainedMiddlewares.run(RoutingContext.fromRequest(request, serviceHints, endpointHints, v1))
} yield res
result.value.map {
case Left(value) => handleFailure(value)
case Right(value) => handleSuccess(value)
implicit val epContentType: ContentType = ContentType(request.contentType.getOrElse(serviceContentType))
val result = for {
pathParams <- getPathParams(v1, httpEp)
metadata = getMetadata(pathParams, v1)
input <- getInput(request, metadata)
endpointLogic = impl(endpoint.wrap(input))
.asInstanceOf[Kleisli[RouteResult, RoutingContext, O]]
.map(mapToEndpointResult(httpEp.code))
chainedMiddlewares = middleware.foldRight(endpointLogic)((a, b) => a.middleware(b.run))
res <-
chainedMiddlewares.run(RoutingContext.fromRequest(request, serviceHints, endpointHints, v1))
} yield res
result.value.map {
case Left(value) => handleFailure(value)
case Right(value) => handleSuccess(value)
}
}
}
}
.getOrElse(Action(NotFound("404")))

private def mapToEndpointResult(
statusCode: Int
)(output: O)(implicit defaultContentType: ContentType): HttpResponse[Blob] =
statusCode: Int
)(output: O): HttpResponse[Blob] = {
val outputMetadata = outputMetadataEncoder.encode(output).headers.get(CaseInsensitive("content-type")) match {
case Some(value) => value
case None => Seq(serviceContentType)
}
codecDecider
.httpMessageEncoder(Seq(defaultContentType.value))
.httpMessageEncoder(outputMetadata)
.fromSchema(outputSchema)
.write(
HttpResponse(
Expand All @@ -75,11 +80,12 @@ class SmithyPlayEndpoint[Alg[_[_, _, _, _, _]], F[_] <: ContextRoute[_], Op[_, _
),
output
)
}

private def getPathParams(
v1: RequestHeader,
httpEp: HttpEndpoint[I]
)(implicit defaultContentType: ContentType): EitherT[Future, ContextRouteError, Map[String, String]] =
v1: RequestHeader,
httpEp: HttpEndpoint[I]
)(implicit defaultContentType: ContentType): EitherT[Future, ContextRouteError, Map[String, String]] =
EitherT(
Future(
matchRequestPath(v1, httpEp)
Expand All @@ -94,9 +100,9 @@ class SmithyPlayEndpoint[Alg[_[_, _, _, _, _]], F[_] <: ContextRoute[_], Op[_, _
)

private def getInput(
request: Request[RawBuffer],
metadata: Metadata
)(implicit defaultContentType: ContentType): EitherT[Future, ContextRouteError, I] =
request: Request[RawBuffer],
metadata: Metadata
)(implicit defaultContentType: ContentType): EitherT[Future, ContextRouteError, I] =
EitherT {
Future {
val codec = codecDecider.requestDecoder(Seq(defaultContentType.value))
Expand Down Expand Up @@ -138,13 +144,13 @@ class SmithyPlayEndpoint[Alg[_[_, _, _, _, _]], F[_] <: ContextRoute[_], Op[_, _
.withHeaders(error.status.headers.toList: _*)
.as(error.contentType)

private def handleSuccess(output: HttpResponse[Blob])(implicit defaultContentType: ContentType): Result = {
private def handleSuccess(output: HttpResponse[Blob]): Result = {
val status = Results.Status(output.statusCode)
val contentTypeKey = CaseInsensitive("content-type")
val outputHeadersWithoutContentType =
output.headers.-(contentTypeKey).toList.map(h => (h._1.toString, h._2.head))
val contentType =
output.headers.getOrElse(contentTypeKey, Seq(defaultContentType.value))
output.headers.getOrElse(contentTypeKey, Seq(serviceContentType))

if (!output.body.isEmpty) {
status(output.body.toArray)
Expand Down
6 changes: 5 additions & 1 deletion smithy4playTest/app/controller/TestController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import cats.data.{ EitherT, Kleisli }
import controller.models.TestError
import de.innfactory.smithy4play.{ AutoRouting, ContextRoute, ContextRouteError }
import play.api.mvc.ControllerComponents
import smithy4s.Blob
import smithy4s.{ Blob, Document }
import testDefinitions.test._

import javax.inject.{ Inject, Singleton }
Expand Down Expand Up @@ -67,4 +67,8 @@ class TestController @Inject() (implicit
override def testWithOtherStatusCode(): ContextRoute[Unit] = Kleisli { rc =>
EitherT.rightT[Future, ContextRouteError](())
}

override def testWithJsonInputAndBlobOutput(body: JsonInput): ContextRoute[BlobResponse] = Kleisli { rc =>
EitherT.rightT[Future, ContextRouteError](BlobResponse(Blob(body.message), "image/png"))
}
}
22 changes: 12 additions & 10 deletions smithy4playTest/test/TestControllerTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,17 @@ import de.innfactory.smithy4play.client.GenericAPIClient.EnhancedGenericAPIClien
import de.innfactory.smithy4play.client.SmithyPlayTestUtils._
import de.innfactory.smithy4play.compliancetests.ComplianceClient
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
import smithy4s.{Blob, Document}
import smithy4s.http.CaseInsensitive
import testDefinitions.test.{
SimpleTestResponse,
TestControllerServiceGen,
TestRequestBody,
TestResponseBody,
TestWithOutputResponse
}
import testDefinitions.test.{JsonInput, SimpleTestResponse, TestControllerServiceGen, TestRequestBody, TestResponseBody, TestWithOutputResponse}

import java.io.File
import java.nio.file.Files
Expand Down Expand Up @@ -167,6 +161,14 @@ class TestControllerTest extends TestBase {
pngAsBytes mustBe result.body.body
}

"route 123 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

Expand Down
18 changes: 18 additions & 0 deletions smithy4playTest/testSpecs/TestController.smithy
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ service TestControllerService {
Health
TestWithBlob
TestWithQuery
TestWithJsonInputAndBlobOutput
TestThatReturnsError
TestAuth
TestWithOtherStatusCode
Expand All @@ -27,6 +28,17 @@ service TestControllerService {
operation TestWithOtherStatusCode {
}

@auth([])
@http(method: "POST", uri: "/jsoninput/bloboutput", code: 200)
operation TestWithJsonInputAndBlobOutput {
input:= {
@httpPayload
@required
body: JsonInput
}
output: BlobResponse
}

@auth([])
@http(method: "POST", uri: "/blob", code: 200)
operation TestWithBlob {
Expand Down Expand Up @@ -134,6 +146,12 @@ structure TestResponseBody {
bodyMessage: String
}


structure JsonInput {
@required
message: String
}

@http(method: "GET", uri: "/auth", code: 200)
operation TestAuth {
}
Expand Down

0 comments on commit 9508f09

Please sign in to comment.