Skip to content

Commit

Permalink
refactor FailedDecodeAttempt
Browse files Browse the repository at this point in the history
  • Loading branch information
GaryAghedo committed Aug 29, 2024
1 parent f3ca663 commit 66dc50a
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 83 deletions.
13 changes: 7 additions & 6 deletions modules/core/src/smithy4s/http/HttpResponse.scala
Original file line number Diff line number Diff line change
Expand Up @@ -247,11 +247,9 @@ object HttpResponse {
response.statusCode,
response.headers,
bodyBlob.toUTF8String,
Some(
FailedDecodeAttempt(
discriminator = discriminator,
contractError = error
)
FailedDecodeAttempt.DecodingFailure(
discriminator = discriminator,
contractError = error
)
)
)
Expand All @@ -263,7 +261,10 @@ object HttpResponse {
code = response.statusCode,
headers = response.headers,
body = bodyBlob.toUTF8String,
failedDecodeAttempt = None
failedDecodeAttempt =
FailedDecodeAttempt.UnrecognisedDiscriminator(
discriminator
)
)
)
}
Expand Down
41 changes: 27 additions & 14 deletions modules/core/src/smithy4s/http/RawErrorResponse.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,37 @@ case class RawErrorResponse(
code: Int,
headers: Map[CaseInsensitive, Seq[String]],
body: String,
failedDecodeAttempt: Option[FailedDecodeAttempt]
failedDecodeAttempt: FailedDecodeAttempt
) extends Throwable {
override def getMessage(): String = {
val baseMessage = s"status $code, headers: $headers, body:\n$body"
failedDecodeAttempt match {
case Some(attempt) =>
baseMessage +
s"""
|FailedDecodeAttempt:
| discriminator: ${attempt.discriminator}
| contractError: ${attempt.contractError.getMessage}
baseMessage +
s"""
|FailedDecodeAttempt:
| ${failedDecodeAttempt.getMessage}
""".stripMargin
case None => baseMessage
}
}

override def getCause: Throwable = failedDecodeAttempt
}

sealed trait FailedDecodeAttempt extends Throwable {
def discriminator: HttpDiscriminator
def getMessage: String
}

case class FailedDecodeAttempt(
discriminator: HttpDiscriminator,
contractError: HttpContractError
)
object FailedDecodeAttempt {
case class UnrecognisedDiscriminator(discriminator: HttpDiscriminator)
extends FailedDecodeAttempt {
override def getMessage: String =
s"Unrecognised descriminator: $discriminator"
}

case class DecodingFailure(
discriminator: HttpDiscriminator,
contractError: HttpContractError
) extends FailedDecodeAttempt {
override def getMessage: String =
s"Decoding failed for discriminator: $discriminator with error: ${contractError.getMessage}"
}
}
122 changes: 76 additions & 46 deletions modules/tests/src/smithy4s/tests/PizzaClientSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -114,61 +114,67 @@ abstract class PizzaClientSpec extends IOSuite {
GenericClientError("generic error message for 418")
)

clientTestForError(
"Handle error with a discriminator but can't be decoded",
clientAssertError[RawErrorResponse](
"Handle error with a discriminator but can't be decoded",
Response[IO](status = Status.NotFound)
.withEntity("malformed body")
.withHeaders(Header.Raw(CIString("X-Error-Type"), "NotFoundError")),
rawErrorResponse(
404,
Map("X-Error-Type" -> "NotFoundError"),
"malformed body",
Some(
FailedDecodeAttempt(
discriminator = HttpDiscriminator.NameOnly("NotFoundError"),
contractError = HttpPayloadError(
path = smithy4s.codecs.PayloadPath(List()),
expected = "object",
message =
"""Expected JSON object, offset: 0x00000000, buf:
|+----------+-------------------------------------------------+------------------+
|| | 0 1 2 3 4 5 6 7 8 9 a b c d e f | 0123456789abcdef |
|+----------+-------------------------------------------------+------------------+
|| 00000000 | 6d 61 6c 66 6f 72 6d 65 64 20 62 6f 64 79 | malformed body |
|+----------+-------------------------------------------------+------------------+""".stripMargin
)
)
)
)
error => {
error match {
case RawErrorResponse(
code,
headers,
body,
FailedDecodeAttempt.DecodingFailure(
discriminator,
HttpPayloadError(path, expected, _)
)
) =>
expect(code == 404) &&
expect(
headers == Map(
CaseInsensitive("X-Error-Type") -> List("NotFoundError")
)
) &&
expect(body == "malformed body") &&
expect(
discriminator == HttpDiscriminator.NameOnly("NotFoundError")
) &&
expect(path == smithy4s.codecs.PayloadPath(List())) &&
expect(expected == "object")
case _ => failure("Unexpected error type or values")
}
}
)

clientTestForError(
clientAssertError[RawErrorResponse](
"Handle malformed error response with no discriminator",
Response(status = Status.InternalServerError)
.withEntity("goodbye world"),
rawErrorResponse(
code = 500,
headers = Map(
"Content-Length" -> "13",
"Content-Type" -> "text/plain; charset=UTF-8"
),
body = "goodbye world",
failedDecodeAttempt = None
)
)
error => {
error match {
case RawErrorResponse(
code,
headers,
body,
FailedDecodeAttempt.UnrecognisedDiscriminator(discriminator)
) =>
expect(code == 500) &&
expect(
headers == Map(
CaseInsensitive("Content-Length") -> List("13"),
CaseInsensitive("Content-Type") -> List(
"text/plain; charset=UTF-8"
)
)
) &&
expect(body == "goodbye world") &&
expect(discriminator == HttpDiscriminator.StatusCode(500))
case _ => failure("Unexpected error type or values")
}

private def rawErrorResponse(
code: Int,
headers: Map[String, String],
body: String,
failedDecodeAttempt: Option[FailedDecodeAttempt]
): RawErrorResponse =
RawErrorResponse(
code,
headers.map { case (k, v) => CaseInsensitive(k) -> List(v) },
body,
failedDecodeAttempt
)
}
)

clientTest("Headers are case insensitive") { (client, backend, log) =>
for {
Expand Down Expand Up @@ -243,6 +249,30 @@ abstract class PizzaClientSpec extends IOSuite {
}
}

def clientAssertError[E](
name: String,
response: Response[IO],
assert: E => Expectations
)(implicit
loc: SourceLocation,
ct: scala.reflect.ClassTag[E]
) = {
clientTest(name) { (client, backend, log) =>
for {
_ <- backend.prepResponse(name, response)
maybeResult <- client.getMenu(name).attempt

} yield maybeResult match {
case Right(_) => failure("expected failure")
case Left(error: E) =>
assert(error)
case Left(error) =>
failure(s"Error of unexpected type: $error")
}
}

}

def clientTest(name: TestName)(
f: (
PizzaAdminService[IO],
Expand Down
29 changes: 12 additions & 17 deletions modules/tests/src/smithy4s/tests/PizzaSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import smithy4s.example.PizzaAdminService
import smithy4s.http.CaseInsensitive
import smithy4s.http.HttpContractError
import smithy4s.http.HttpDiscriminator
import smithy4s.http.FailedDecodeAttempt
import smithy4s.http.RawErrorResponse
import weaver._
import cats.Show
Expand Down Expand Up @@ -509,10 +510,18 @@ abstract class PizzaSpec
CaseInsensitive("Content-Length") -> List("14")
)
response match {
case Left(RawErrorResponse(code, headers, body, None)) =>
case Left(
RawErrorResponse(code, headers, body, failedDecodeAttempt)
) =>
expect(code == 500) &&
expect(headers == expectHeaders) &&
expect(body.contains("malformed body"))
expect(body.contains("malformed body")) &&
expect(
failedDecodeAttempt == FailedDecodeAttempt
.UnrecognisedDiscriminator(
HttpDiscriminator.StatusCode(500)
)
)
case _ =>
failure("Expected RawErrorResponse with status 500")
}
Expand Down Expand Up @@ -553,25 +562,14 @@ abstract class PizzaSpec
}

} yield {
val expectedHttpPayloadError = HttpPayloadError(
path = smithy4s.codecs.PayloadPath(List()),
expected = "object",
message =
"""Expected JSON object, offset: 0x00000000, buf:
|+----------+-------------------------------------------------+------------------+
|| | 0 1 2 3 4 5 6 7 8 9 a b c d e f | 0123456789abcdef |
|+----------+-------------------------------------------------+------------------+
|| 00000000 | 6d 61 6c 66 6f 72 6d 65 64 20 62 6f 64 79 | malformed body |
|+----------+-------------------------------------------------+------------------+""".stripMargin
)
val expectHeaders = Map(
CaseInsensitive("Content-Length") -> List("14"),
CaseInsensitive("Content-Type") -> List("text/plain"),
CaseInsensitive("X-Error-Type") -> List("GenericServerError")
)
response match {
case Left(
RawErrorResponse(code, headers, body, Some(failedDecodeAttempt))
RawErrorResponse(code, headers, body, failedDecodeAttempt)
) =>
expect(code == 500) &&
expect(headers == expectHeaders) &&
Expand All @@ -580,9 +578,6 @@ abstract class PizzaSpec
failedDecodeAttempt.discriminator == HttpDiscriminator.NameOnly(
"GenericServerError"
)
) &&
expect(
failedDecodeAttempt.contractError == expectedHttpPayloadError
)
case _ =>
failure("Expected RawErrorResponse with status 500")
Expand Down

0 comments on commit 66dc50a

Please sign in to comment.