Skip to content

Commit

Permalink
client: fixing error handling in case of error
Browse files Browse the repository at this point in the history
  • Loading branch information
MoeQuadrat committed Sep 8, 2022
1 parent 6c82bf6 commit 44f3d46
Show file tree
Hide file tree
Showing 7 changed files with 79 additions and 46 deletions.
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import sbt.Keys.cleanFiles
val releaseVersion = sys.env.getOrElse("TAG", "0.2.3-3")
addCommandAlias("publishSmithy4Play", "smithy4play/publish")
addCommandAlias("publishLocalSmithy4Play", "smithy4play/publishLocal")

addCommandAlias("generateCoverage", "clean; coverage; test; coverageReport")
val token = sys.env.getOrElse("GITHUB_TOKEN", "")
val githubSettings = Seq(
githubOwner := "innFactory",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package de.innfactory.smithy4play.client

import smithy4s.Endpoint
import smithy4s.http.{ HttpEndpoint, PayloadError }
import de.innfactory.smithy4play.ClientResponse
import smithy4s.http.HttpEndpoint

import scala.concurrent.{ ExecutionContext, Future }
import scala.concurrent.ExecutionContext

class SmithyPlayClient[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _], F[_]](
baseUri: String,
Expand All @@ -12,13 +12,15 @@ class SmithyPlayClient[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _], F[_]](

def send[I, E, O, SI, SO](
op: Op[I, E, O, SI, SO],
authHeader: Option[String]
): Future[Either[SmithyPlayClientEndpointErrorResponse, SmithyPlayClientEndpointResponse[O]]] = {
additionalHeaders: Option[Map[String, Seq[String]]]
): ClientResponse[O] = {

val (input, endpoint) = service.endpoint(op)
HttpEndpoint
.cast(endpoint)
.map(httpEndpoint => new SmithyPlayClientEndpoint(endpoint, baseUri, authHeader, httpEndpoint, input).send())
.map(httpEndpoint =>
new SmithyPlayClientEndpoint(endpoint, baseUri, additionalHeaders, httpEndpoint, input).send()
)
.get
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,19 @@
package de.innfactory
package smithy4play
package client
import play.api.mvc.Headers
import smithy4s.http.json.codecs
import smithy4s.{ Endpoint, HintMask, Schema }
import smithy4s.http.{
BodyPartial,
CaseInsensitive,
CodecAPI,
HttpContractError,
HttpEndpoint,
Metadata,
MetadataError,
PayloadError
}
import smithy4s.http.{ CaseInsensitive, CodecAPI, HttpEndpoint, Metadata, MetadataError, PayloadError }
import cats.implicits._
import play.api.libs.json.Json
import smithy4s.internals.InputOutput

import scala.concurrent.{ ExecutionContext, Future }

private[smithy4play] class SmithyPlayClientEndpoint[Op[_, _, _, _, _], I, E, O, SI, SO](
endpoint: Endpoint[Op, I, E, O, SI, SO],
baseUri: String,
authHeader: Option[String],
additionalHeaders: Option[Map[String, Seq[String]]],
httpEndpoint: HttpEndpoint[I],
input: I
)(implicit executionContext: ExecutionContext, client: RequestClient) {
Expand All @@ -40,11 +32,11 @@ private[smithy4play] class SmithyPlayClientEndpoint[Op[_, _, _, _, _], I, E, O,
Metadata.TotalDecoder.fromSchema(inputSchema).isEmpty

def send(
): Future[Either[SmithyPlayClientEndpointErrorResponse, SmithyPlayClientEndpointResponse[O]]] = {
): ClientResponse[O] = {
val metadata = inputMetadataEncoder.encode(input)
val path = buildPath(metadata)
val headers = metadata.headers.map(x => (x._1.toString, x._2))
val headersWithAuth = if (authHeader.isDefined) headers + ("Authorization" -> Seq(authHeader.get)) else headers
val headersWithAuth = if (additionalHeaders.isDefined) headers.combine(additionalHeaders.get) else headers
val code = httpEndpoint.code
val codecApi: CodecAPI = extractCodec(headers)
val send = client.send(httpEndpoint.method.toString, path, headersWithAuth, _)
Expand All @@ -69,30 +61,42 @@ private[smithy4play] class SmithyPlayClientEndpoint[Op[_, _, _, _, _], I, E, O,
private def decodeResponse(
response: Future[SmithyClientResponse],
expectedCode: Int
): Future[Either[SmithyPlayClientEndpointErrorResponse, SmithyPlayClientEndpointResponse[O]]] =
): ClientResponse[O] =
for {
res <- response
codecApi = extractCodec(res.headers)
metadata = Metadata(headers = res.headers.map(headers => (CaseInsensitive(headers._1), headers._2)))
output <- Future(outputMetadataDecoder.total match {
case Some(totalDecoder) =>
totalDecoder.decode(metadata)
case None =>
for {
metadataPartial <- outputMetadataDecoder.decode(metadata)
bodyPartial <-
codecApi.decodeFromByteArrayPartial(codecApi.compileCodec(outputSchema), res.body.get)
} yield metadataPartial.combine(bodyPartial)
})
output <- if (res.statusCode == expectedCode) handleSuccess(metadata, res, expectedCode)
else handleError(res, expectedCode)
} yield output
.map(o => SmithyPlayClientEndpointResponse[O](res.body.map(_ => o), res.headers, res.statusCode, expectedCode))
.left
.map {
case e: PayloadError =>
SmithyPlayClientEndpointErrorResponse(e.expected, res.statusCode, expectedCode)

def handleSuccess(metadata: Metadata, response: SmithyClientResponse, expectedCode: Int) = {
val headers = response.headers
val output = outputMetadataDecoder.total match {
case Some(totalDecoder) =>
totalDecoder.decode(metadata)
case None =>
for {
metadataPartial <- outputMetadataDecoder.decode(metadata)
codecApi = extractCodec(headers)
bodyPartial <-
codecApi.decodeFromByteArrayPartial(codecApi.compileCodec(outputSchema), response.body.get)
} yield metadataPartial.combine(bodyPartial)
}
Future(
output.map(o => SmithyPlayClientEndpointResponse(Some(o), headers, response.statusCode, expectedCode)).left.map {
case error: PayloadError =>
SmithyPlayClientEndpointErrorResponse(error.expected, response.statusCode, expectedCode)
case error: MetadataError =>
SmithyPlayClientEndpointErrorResponse(error.getMessage(), res.statusCode, expectedCode)
SmithyPlayClientEndpointErrorResponse(error.getMessage(), response.statusCode, expectedCode)
}
)
}
def handleError(response: SmithyClientResponse, expectedCode: Int) = Future(
Left {
val errorMessage = Json.parse(response.body.getOrElse(Array.emptyByteArray)).toString()
SmithyPlayClientEndpointErrorResponse(errorMessage, response.statusCode, expectedCode)
}
)

def buildPath(metadata: Metadata): String =
baseUri + httpEndpoint.path(input).mkString("/") + metadata.queryFlattened
Expand Down
11 changes: 11 additions & 0 deletions smithy4playTest/app/controller/TestController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,15 @@ class TestController @Inject() (implicit
EitherT.rightT[Future, ContextRouteError](())
}

override def testThatReturnsError(): ContextRoute[Unit] = Kleisli { rc =>
EitherT.leftT[Future, Unit](new ContextRouteError {
override def message: String = "this is supposed to fail"

override def additionalInfoToLog: Option[String] = None

override def additionalInfoErrorCode: Option[String] = None

override def statusCode: Int = 500
})
}
}
15 changes: 9 additions & 6 deletions smithy4playTest/test/SmithyPlayTestClient.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ import testDefinitions.test.{

import scala.concurrent.ExecutionContext

class SmithyPlayTestClient(authHeader: Option[String], baseUri: String = "/")(implicit
class SmithyPlayTestClient(additionalHeaders: Option[Map[String, Seq[String]]], baseUri: String = "/")(implicit
ec: ExecutionContext,
client: RequestClient
) extends TestControllerService[ClientResponse] {

val smithyPlayClient = new SmithyPlayClient(baseUri, TestControllerService.service)

override def test(): ClientResponse[SimpleTestResponse] =
smithyPlayClient.send(TestControllerServiceGen.Test(), authHeader)
smithyPlayClient.send(TestControllerServiceGen.Test(), additionalHeaders)

override def testWithOutput(
pathParam: String,
Expand All @@ -32,15 +32,18 @@ class SmithyPlayTestClient(authHeader: Option[String], baseUri: String = "/")(im
body: TestRequestBody
): ClientResponse[TestWithOutputResponse] = smithyPlayClient.send(
TestControllerServiceGen.TestWithOutput(TestRequestWithQueryAndPathParams(pathParam, testQuery, testHeader, body)),
authHeader
additionalHeaders
)

override def health(): ClientResponse[Unit] = smithyPlayClient.send(TestControllerServiceGen.Health(), authHeader)
override def health(): ClientResponse[Unit] =
smithyPlayClient.send(TestControllerServiceGen.Health(), additionalHeaders)

override def testWithBlob(body: ByteArray, contentType: String): ClientResponse[BlobResponse] =
smithyPlayClient.send(TestControllerServiceGen.TestWithBlob(BlobRequest(body, contentType)), authHeader)
smithyPlayClient.send(TestControllerServiceGen.TestWithBlob(BlobRequest(body, contentType)), additionalHeaders)

override def testWithQuery(testQuery: String): ClientResponse[Unit] =
smithyPlayClient.send(TestControllerServiceGen.TestWithQuery(QueryRequest(testQuery)), authHeader)
smithyPlayClient.send(TestControllerServiceGen.TestWithQuery(QueryRequest(testQuery)), additionalHeaders)

override def testThatReturnsError(): ClientResponse[Unit] =
smithyPlayClient.send(TestControllerServiceGen.TestThatReturnsError(), additionalHeaders)
}
8 changes: 8 additions & 0 deletions smithy4playTest/test/TestControllerTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,14 @@ class TestControllerTest extends PlaySpec with BaseOneAppPerSuite with FakeAppli
result.statusCode mustBe result.expectedStatusCode
}

"route to error Endpoint" in {
val result = smithyTestTest.testThatReturnsError().awaitLeft

println(result)

result.statusCode mustBe 500
}

"route to Blob Endpoint" in {
val path = getClass.getResource("/testPicture.png").getPath
val file = new File(path)
Expand Down
7 changes: 6 additions & 1 deletion smithy4playTest/testSpecs/TestController.smithy
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use smithy4s.api#simpleRestJson
@simpleRestJson
service TestControllerService {
version: "0.0.1",
operations: [Test, TestWithOutput, Health, TestWithBlob, TestWithQuery]
operations: [Test, TestWithOutput, Health, TestWithBlob, TestWithQuery, TestThatReturnsError]
}

@http(method: "POST", uri: "/blob", code: 200)
Expand All @@ -16,6 +16,11 @@ operation TestWithBlob {
output: BlobResponse
}

@readonly
@http(method: "GET", uri: "/error", code: 200)
operation TestThatReturnsError {
}

@readonly
@http(method: "GET", uri: "/query", code: 200)
operation TestWithQuery {
Expand Down

0 comments on commit 44f3d46

Please sign in to comment.