Skip to content

Commit

Permalink
Catch PayloadErrors in complete decoding in CodecAPI (#596)
Browse files Browse the repository at this point in the history
  • Loading branch information
kubukoz authored Nov 10, 2022
1 parent e7016d6 commit fb6c2e3
Show file tree
Hide file tree
Showing 6 changed files with 60 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ private[aws] class AwsUnaryEndpoint[F[_], Op[_, _, _, _, _], I, E, O, SI, SO](
metadataPartial <- metadataDecoder.decode(metadata).liftTo[F]
bodyPartial <-
codecAPI.decodeFromByteArrayPartial(codec, response.body).liftTo[F]
} yield metadataPartial.combine(bodyPartial)
decoded <- metadataPartial.combineCatch(bodyPartial).liftTo[F]
} yield decoded
}
}

Expand Down
8 changes: 6 additions & 2 deletions modules/core/src/smithy4s/http/CodecAPI.scala
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ trait CodecAPI {
codec: Codec[A],
bytes: Array[Byte]
): Either[PayloadError, A] =
decodeFromByteArrayPartial(codec, bytes).map(_.complete(MMap.empty))
decodeFromByteArrayPartial(codec, bytes).flatMap(
_.completeCatch(MMap.empty)
)

/**
* Decodes partial data from a byte buffer, returning a function that is able
Expand All @@ -94,7 +96,9 @@ trait CodecAPI {
codec: Codec[A],
bytes: ByteBuffer
): Either[PayloadError, A] =
decodeFromByteBufferPartial(codec, bytes).map(_.complete(MMap.empty))
decodeFromByteBufferPartial(codec, bytes).flatMap(
_.completeCatch(MMap.empty)
)

/**
* Writes data to a byte array. Field values bound
Expand Down
29 changes: 27 additions & 2 deletions modules/core/src/smithy4s/http/Partial.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,19 @@
package smithy4s.http

import scala.collection.{Map => MMap}
import scala.annotation.nowarn

final class MetadataPartial[A] private[smithy4s] (
private[smithy4s] val decoded: MMap[String, Any]
) {
@deprecated(
"0.16.8",
"This may throw a PayloadError. Use combineCatch instead."
)
final def combine(body: BodyPartial[A]): A = body.complete(decoded)

final def combineCatch(body: BodyPartial[A]): Either[PayloadError, A] =
body.completeCatch(decoded)
}
object MetadataPartial {
private[smithy4s] def apply[A](decoded: MMap[String, Any]) =
Expand All @@ -31,13 +39,30 @@ object MetadataPartial {
final class BodyPartial[A] private[smithy4s] (
private[smithy4s] val complete: MMap[String, Any] => A
) {
final def combine(metadata: MetadataPartial[A]): A = complete(
metadata.decoded
@deprecated(
"0.16.8",
"This may throw a PayloadError. Use combineCatch instead."
)
@nowarn("msg=method combine in class MetadataPartial is deprecated")
final def combine(metadata: MetadataPartial[A]): A =
metadata.combine(this)

final def combineCatch(
metadata: MetadataPartial[A]
): Either[PayloadError, A] =
metadata.combineCatch(this)

final def map[B](f: A => B): BodyPartial[B] = new BodyPartial(
complete andThen f
)

@nowarn("msg=method combine in class BodyPartial is deprecated")
private[smithy4s] def completeCatch(
m: MMap[String, Any]
): Either[PayloadError, A] = try Right(complete(m))
catch {
case p: PayloadError => Left(p)
}
}
object BodyPartial {
def apply[A](complete: MMap[String, Any] => A): BodyPartial[A] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,18 +183,18 @@ private[smithy4s] class SmithyHttp4sClientEndpointImpl[F[_], Op[_, _, _, _, _],
metadataDecoder: Metadata.PartialDecoder[T]
)(implicit
entityDecoder: EntityDecoder[F, BodyPartial[T]]
): F[Either[MetadataError, T]] = {
): F[Either[HttpContractError, T]] = {
val headers = getHeaders(response)
val metadata =
Metadata(headers = headers, statusCode = Some(response.status.code))
metadataDecoder.total match {
case Some(totalDecoder) =>
totalDecoder.decode(metadata).pure[F]
totalDecoder.decode(metadata).pure[F].widen
case None =>
for {
metadataPartial <- metadataDecoder.decode(metadata).pure[F]
bodyPartial <- response.as[BodyPartial[T]]
} yield metadataPartial.map(_.combine(bodyPartial))
} yield metadataPartial.flatMap(_.combineCatch(bodyPartial))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ private[smithy4s] class SmithyHttp4sServerEndpointImpl[F[_], Op[_, _, _, _, _],
for {
metadataPartial <- inputMetadataDecoder.decode(metadata).liftTo[F]
bodyPartial <- request.as[BodyPartial[I]]
} yield metadataPartial.combine(bodyPartial)
decoded <- metadataPartial.combineCatch(bodyPartial).liftTo[F]
} yield decoded
}
}
}
Expand Down
20 changes: 20 additions & 0 deletions modules/json/test/src/smithy4s/http/json/JsonCodecApiTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,24 @@ class JsonCodecApiTests extends FunSuite {

assertEquals(encodedString, """{"a":"test"}""")
}

test(
"struct codec with a required field should return a Left when the field is missing"
) {
val schemaWithRequiredField =
Schema
.struct[String]
.apply(
Schema.string
.required[String]("a", identity)
)(identity)

val capi = codecs(HintMask.empty)

val codec = capi.compileCodec(schemaWithRequiredField)

val decoded = capi.decodeFromByteArray(codec, """{}""".getBytes())

assert(decoded.isLeft)
}
}

0 comments on commit fb6c2e3

Please sign in to comment.