Skip to content

Commit

Permalink
Allow using streaming bodies in oneOf
Browse files Browse the repository at this point in the history
  • Loading branch information
adamw committed Apr 21, 2022
1 parent 3dfbc78 commit 4343ced
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 5 deletions.
13 changes: 9 additions & 4 deletions core/src/main/scala/sttp/tapir/EndpointIO.scala
Original file line number Diff line number Diff line change
Expand Up @@ -591,7 +591,7 @@ object EndpointIO {
}

/*
Streaming body is a special kind of input, as it influences the 4th type parameter of `Endpoint`. Other inputs
Streaming body is a special kind of input/output, as it influences the 4th type parameter of `Endpoint`. Other inputs
(`EndpointInput`s and `EndpointIO`s) aren't parametrised with the type of streams that they use (to make them simpler),
so we need to pass the streaming information directly between the streaming body input and the endpoint.
Expand All @@ -600,8 +600,8 @@ other inputs, and the `Endpoint.in(EndpointInput)` method can't be used to add a
overloaded variant `Endpoint.in(StreamBody)`, which takes into account the streaming type.
Internally, the streaming body is converted into a wrapper `EndpointIO`, which "forgets" about the streaming
information. The `EndpointIO.StreamBodyWrapper` should only be used internally, not by the end user: there's no
factory method in `Tapir` which would directly create an instance of it.
information. This can also be done by the end user with `.toEndpointIO`, if the body should be used e.g. in `oneOf`.
However, this decreases type safety, as the streaming requirement is lost.
BS == streams.BinaryStream, but we can't express this using dependent types here.
*/
Expand All @@ -617,7 +617,12 @@ case class StreamBodyIO[BS, T, S](
override private[tapir] type CF = CodecFormat
override private[tapir] def copyWith[U](c: Codec[BS, U, CodecFormat], i: Info[U]) = copy(codec = c, info = i)

private[tapir] def toEndpointIO: EndpointIO.StreamBodyWrapper[BS, T] = EndpointIO.StreamBodyWrapper(this)
/** Lift this streaming body into an [[EndpointIO]], so that it can be used as a regular endpoint input/output, "forgetting" the streaming
* requirement. This is useful when using the streaming body in [[Tapir.oneOf]] or [[Tapir.oneOfBody]], however at the expense of type
* safety: the fact that the endpoint can only be interpreted by an interpreter supporting the given stream type is lost; in case of a
* mismatch, a run-time error will occur.
*/
def toEndpointIO: EndpointIO.StreamBodyWrapper[BS, T] = EndpointIO.StreamBodyWrapper(this)

/** Add an example of a "deserialized" stream value. This should be given in an encoded form, e.g. in case of json - as a [[String]], as
* the stream body doesn't have access to the codec that will be later used for deserialization.
Expand Down
15 changes: 15 additions & 0 deletions doc/endpoint/oneof.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,21 @@ oneOfBody(
)
```

## oneOf and non-blocking streaming

[Streaming bodies](streaming.md) can't be used as normal inputs/outputs, as the streaming requirement needs to be
propagated to the `Endpoint` type. This way, we assure at compile-time that only interpreters supporting the given
streaming type can be used to interpret an endpoint.

However, this makes it impossible to use streaming bodies in `oneOf` (via `oneOfVariant`) and `oneOfBody`, as both
require normal input/outputs as parameters. To bypass this limitation, a `.toEndpointIO` method is available
on streaming bodies, which "lifts" them to an `EndpointIO` type, forgetting the streaming requirement. This decreases
type safety, as a run-time error might occur if an incompatible interpreter is used, however allows describing
endpoints, which require including streaming bodies in output variants.

Note that if the same streaming body description is used in all branches of a `oneOf`, this can be refactored into
a regular streaming body output + a varying set of output headers, expressed using `oneOf`.

## Next

Read on about [codecs](codecs.md).
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import sttp.client3._
import sttp.model.{Header, HeaderNames, MediaType}
import sttp.monad.MonadError
import sttp.tapir.tests.Test
import sttp.tapir.tests.Streaming.{in_stream_out_stream, in_stream_out_stream_with_content_length, out_custom_content_type_stream_body}
import sttp.tapir.tests.Streaming.{
in_stream_out_stream,
in_stream_out_stream_with_content_length,
in_string_stream_out_either_stream_string,
out_custom_content_type_stream_body
}

class ServerStreamingTests[F[_], S, OPTIONS, ROUTE](createServerTest: CreateServerTest[F, S, OPTIONS, ROUTE], streams: Streams[S])(implicit
m: MonadError[F]
Expand Down Expand Up @@ -60,6 +65,21 @@ class ServerStreamingTests[F[_], S, OPTIONS, ROUTE](createServerTest: CreateServ
r.body shouldBe Right(penPineapple)
r.contentType shouldBe Some(MediaType.ApplicationXml.toString())
}
},
testServer(in_string_stream_out_either_stream_string(streams)) {
case ("left", s) => pureResult((Left(s): Either[streams.BinaryStream, String]).asRight[Unit])
case _ => pureResult((Right("was not left"): Either[streams.BinaryStream, String]).asRight[Unit])
} { (backend, baseUri) =>
basicRequest
.post(uri"$baseUri?which=left")
.body(penPineapple)
.send(backend)
.map(_.body shouldBe Right(penPineapple)) >>
basicRequest
.post(uri"$baseUri?which=right")
.body(penPineapple)
.send(backend)
.map(_.body shouldBe Right("was not left"))
}
)
}
Expand Down
16 changes: 16 additions & 0 deletions tests/src/main/scala/sttp/tapir/tests/Streaming.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,20 @@ object Streaming {
.out(header[String](HeaderNames.ContentType))
.out(sb)
}

def in_string_stream_out_either_stream_string[S](
s: Streams[S]
): PublicEndpoint[(String, s.BinaryStream), Unit, Either[s.BinaryStream, String], S] = {
val sb = streamTextBody(s)(CodecFormat.TextPlain(), Some(StandardCharsets.UTF_8))

endpoint.post
.in(query[String]("which"))
.in(sb)
.out(
oneOf(
oneOfVariantClassMatcher(sb.toEndpointIO.map(Left(_))(_.value), classOf[Left[s.BinaryStream, String]]),
oneOfVariantClassMatcher(stringBody.map(Right(_))(_.value), classOf[Right[s.BinaryStream, String]])
)
)
}
}

0 comments on commit 4343ced

Please sign in to comment.