-
Notifications
You must be signed in to change notification settings - Fork 426
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
315 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
209 changes: 209 additions & 0 deletions
209
client/play-client/src/main/scala/sttp/tapir/client/play/EndpointToPlayClient.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,209 @@ | ||
package sttp.tapir.client.play | ||
|
||
import java.io.{File, InputStream} | ||
import java.nio.ByteBuffer | ||
import java.util.function.Supplier | ||
|
||
import akka.stream.scaladsl.Source | ||
import akka.util.ByteString | ||
import play.api.libs.ws.DefaultBodyWritables._ | ||
import play.api.libs.ws.{DefaultWSCookie, StandaloneWSClient, StandaloneWSRequest, StandaloneWSResponse} | ||
import play.shaded.ahc.org.asynchttpclient.request.body.multipart.{ByteArrayPart, FilePart, InputStreamPart, StringPart} | ||
import sttp.model.{HeaderNames, Method, Part} | ||
import sttp.tapir.Codec.PlainCodec | ||
import sttp.tapir.internal.{Params, ParamsAsAny, RichEndpointInput, SplitParams} | ||
import sttp.tapir.{Codec, CodecFormat, DecodeResult, Endpoint, EndpointIO, EndpointInput, Mapping, RawBodyType, RawPart} | ||
|
||
class EndpointToPlayClient() { | ||
|
||
def toPlayRequest[I, E, O, S](e: Endpoint[I, E, O, S], baseUri: String)(implicit ws: StandaloneWSClient): I => StandaloneWSRequest = { | ||
params => | ||
val req = | ||
setInputParams( | ||
e.input, | ||
ParamsAsAny(params), | ||
ws.url(baseUri) | ||
) | ||
|
||
req.withMethod(e.input.method.getOrElse(Method.GET).method) | ||
} | ||
|
||
def parsePlayResponse[I, E, O, S](e: Endpoint[I, E, O, S]): StandaloneWSResponse => DecodeResult[Either[E, O]] = { response => | ||
// TODO | ||
DecodeResult.Value(Right(response.body.asInstanceOf[O])) | ||
} | ||
|
||
def parsePlayResponseUnsafe[I, E, O, S](e: Endpoint[I, E, O, S]): StandaloneWSResponse => Either[E, O] = { response => | ||
getOrThrow(parsePlayResponse(e).apply(response)) | ||
} | ||
|
||
@scala.annotation.tailrec | ||
private def setInputParams[I]( | ||
input: EndpointInput[I], | ||
params: Params, | ||
req: StandaloneWSRequest | ||
): StandaloneWSRequest = { | ||
def value: I = params.asAny.asInstanceOf[I] | ||
input match { | ||
case EndpointInput.FixedMethod(m, _, _) => req.withMethod(m.method) | ||
case EndpointInput.FixedPath(p, _, _) => | ||
req.withUrl(req.url + "/" + p) | ||
case EndpointInput.PathCapture(_, codec, _) => | ||
val v = codec.asInstanceOf[PlainCodec[Any]].encode(value: Any) | ||
req.withUrl(req.url + "/" + v) | ||
case EndpointInput.PathsCapture(codec, _) => | ||
val ps = codec.encode(value) | ||
req.withUrl(req.url + ps.mkString("/", "/", "")) | ||
case EndpointInput.Query(name, codec, _) => | ||
val req2 = codec.encode(value).foldLeft(req) { case (r, v) => r.addQueryStringParameters(name -> v) } | ||
req2 | ||
case EndpointInput.Cookie(name, codec, _) => | ||
val req2 = codec.encode(value).foldLeft(req) { case (r, v) => r.addCookies(DefaultWSCookie(name, v)) } | ||
req2 | ||
case EndpointInput.QueryParams(codec, _) => | ||
val mqp = codec.encode(value) | ||
req.addQueryStringParameters(mqp.toSeq: _*) | ||
case EndpointIO.Empty(_, _) => req | ||
case EndpointIO.Body(bodyType, codec, _) => | ||
val req2 = setBody(value, bodyType, codec, req) | ||
req2 | ||
case sbw @ EndpointIO.StreamBodyWrapper(_) => | ||
val req2 = setStreamingBody(value, sbw.codec, req) | ||
req2 | ||
case EndpointIO.Header(name, codec, _) => | ||
val req2 = codec | ||
.encode(value) | ||
.foldLeft(req) { case (r, v) => r.addHttpHeaders(name -> v) } | ||
req2 | ||
case EndpointIO.Headers(codec, _) => | ||
val headers = codec.encode(value) | ||
val req2 = headers.foldLeft(req) { case (r, h) => r.addHttpHeaders(h.name -> h.value) } | ||
req2 | ||
case EndpointIO.FixedHeader(h, _, _) => | ||
val req2 = req.addHttpHeaders(h.name -> h.value) | ||
req2 | ||
case EndpointInput.ExtractFromRequest(_, _) => | ||
// ignoring | ||
req | ||
case a: EndpointInput.Auth[_] => setInputParams(a.input, params, req) | ||
case EndpointInput.Pair(left, right, _, split) => handleInputPair(left, right, params, split, req) | ||
case EndpointIO.Pair(left, right, _, split) => handleInputPair(left, right, params, split, req) | ||
case EndpointInput.MappedPair(wrapped, codec) => handleMapped(wrapped, codec.asInstanceOf[Mapping[Any, Any]], params, req) | ||
case EndpointIO.MappedPair(wrapped, codec) => handleMapped(wrapped, codec.asInstanceOf[Mapping[Any, Any]], params, req) | ||
} | ||
} | ||
|
||
def handleInputPair( | ||
left: EndpointInput[_], | ||
right: EndpointInput[_], | ||
params: Params, | ||
split: SplitParams, | ||
req: StandaloneWSRequest | ||
): StandaloneWSRequest = { | ||
val (leftParams, rightParams) = split(params) | ||
val req2 = setInputParams(left.asInstanceOf[EndpointInput[Any]], leftParams, req) | ||
setInputParams(right.asInstanceOf[EndpointInput[Any]], rightParams, req2) | ||
} | ||
|
||
private def handleMapped[II, T]( | ||
tuple: EndpointInput[II], | ||
codec: Mapping[T, II], | ||
params: Params, | ||
req: StandaloneWSRequest | ||
): StandaloneWSRequest = { | ||
setInputParams(tuple.asInstanceOf[EndpointInput[Any]], ParamsAsAny(codec.encode(params.asAny.asInstanceOf[II])), req) | ||
} | ||
|
||
// TODO move elsewhere | ||
type PlayPart = play.shaded.ahc.org.asynchttpclient.request.body.multipart.Part | ||
type PlayPartBase = play.shaded.ahc.org.asynchttpclient.request.body.multipart.PartBase | ||
|
||
private def setBody[R, T, CF <: CodecFormat]( | ||
v: T, | ||
bodyType: RawBodyType[R], | ||
codec: Codec[R, T, CF], | ||
req: StandaloneWSRequest | ||
): StandaloneWSRequest = { | ||
val encoded: R = codec.encode(v) | ||
// TODO can't we get rid of asInstanceOf ? | ||
val req2 = bodyType match { | ||
case RawBodyType.StringBody(_) => req.withBody(encoded.asInstanceOf[String]) // TODO: what about charset? | ||
case RawBodyType.ByteArrayBody => req.withBody(encoded.asInstanceOf[Array[Byte]]) | ||
case RawBodyType.ByteBufferBody => req.withBody(encoded.asInstanceOf[ByteBuffer]) | ||
case RawBodyType.InputStreamBody => | ||
// For some reason, Play comes with a Writeable for Supplier[InputStream] but not InputStream directly | ||
val inputStreamSupplier: Supplier[InputStream] = () => encoded.asInstanceOf[InputStream] | ||
req.withBody(inputStreamSupplier) | ||
case RawBodyType.FileBody => req.withBody(encoded.asInstanceOf[File]) | ||
case m: RawBodyType.MultipartBody => | ||
// | ||
//import play.api.libs.ws.WSBodyReadables._ | ||
val parts: Seq[PlayPart] = (encoded: Seq[RawPart]).flatMap { p => | ||
m.partType(p.name).map { partType => | ||
// name, body, content type, content length, file name | ||
val playPart = | ||
partToPlayPart(p.asInstanceOf[Part[Any]], partType.asInstanceOf[RawBodyType[Any]], p.contentType, p.contentLength, p.fileName) | ||
// headers; except content type set above | ||
p.headers | ||
.filterNot(_.is(HeaderNames.ContentType)) | ||
.foreach { header => | ||
playPart.addCustomHeader(header.name, header.value) | ||
} | ||
// TODO other disposition params, is there anything else than filename? filename is handled above | ||
//p.otherDispositionParams.foreach{ case (k, v) => () } | ||
} | ||
} | ||
|
||
// TODO we need a BodyWritable[Source[PlayPart, _]], maybe reuse a Writable from Play core?? | ||
// req.withBody(Source(parts.toList)) | ||
throw new RuntimeException("Not implemented yet") | ||
} | ||
|
||
// TODO | ||
//req2.contentType(codec.format.mediaType) | ||
|
||
req2 | ||
} | ||
|
||
private def setStreamingBody[R, T, CF <: CodecFormat](v: T, codec: Codec[R, T, CF], req: StandaloneWSRequest): StandaloneWSRequest = { | ||
val encoded: R = codec.encode(v) | ||
encoded match { | ||
case s: Source[ByteString, _] => req.withBody(s) | ||
case is: InputStream => | ||
// For some reason, Play comes with a Writeable for Supplier[InputStream] but not InputStream directly | ||
val inputStreamSupplier: Supplier[InputStream] = () => is | ||
req.withBody(inputStreamSupplier) | ||
case f: File => req.withBody(f) | ||
case _ => | ||
// TODO what about other types that might have a BodyWritable defined out there? | ||
throw new IllegalArgumentException("Streaming input other than Source, InputStream or File aren't supported") | ||
} | ||
} | ||
|
||
private def partToPlayPart[R]( | ||
p: Part[R], | ||
bodyType: RawBodyType[R], | ||
contentType: Option[String], | ||
contentLength: Option[Long], | ||
fileName: Option[String] | ||
): PlayPartBase = { | ||
// TODO can't we get rid of the asInstanceOf??? | ||
bodyType match { | ||
case RawBodyType.StringBody(charset) => new StringPart(p.name, p.body.asInstanceOf[String], contentType.orNull, charset) | ||
case RawBodyType.ByteArrayBody => new ByteArrayPart(p.name, p.body.asInstanceOf[Array[Byte]], contentType.orNull) | ||
case RawBodyType.ByteBufferBody => new ByteArrayPart(p.name, p.body.asInstanceOf[ByteBuffer].array(), contentType.orNull) | ||
case RawBodyType.InputStreamBody => | ||
new InputStreamPart(p.name, p.body.asInstanceOf[InputStream], fileName.orNull, contentLength.getOrElse(-1L), contentType.orNull) | ||
case RawBodyType.FileBody => new FilePart(p.name, p.body.asInstanceOf[File], contentType.orNull) | ||
case RawBodyType.MultipartBody(_, _) => throw new IllegalArgumentException("Nested multipart bodies aren't supported") | ||
} | ||
} | ||
|
||
private def getOrThrow[T](dr: DecodeResult[T]): T = | ||
dr match { | ||
case DecodeResult.Value(v) => v | ||
case DecodeResult.Error(_, e) => throw e | ||
case f => throw new IllegalArgumentException(s"Cannot decode: $f") | ||
} | ||
|
||
} |
35 changes: 35 additions & 0 deletions
35
client/play-client/src/main/scala/sttp/tapir/client/play/TapirPlayClient.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package sttp.tapir.client.play | ||
|
||
import play.api.libs.ws.{StandaloneWSClient, StandaloneWSRequest, StandaloneWSResponse} | ||
import sttp.tapir.{DecodeResult, Endpoint} | ||
|
||
trait TapirPlayClient { | ||
|
||
implicit class RichPlayClientEndpoint[I, E, O, S](e: Endpoint[I, E, O, S]) { | ||
|
||
/** | ||
* Interprets the endpoint as a client call, using the given `baseUri` as the starting point to create the target | ||
* uri. | ||
* | ||
* Returns a function which, when applied to the endpoint's input parameters (given as a tuple), will encode them | ||
* to appropriate request parameters: path, query, headers and body. The result is a `StandaloneWSRequest`, | ||
* which can be sent using the `execute()` method. | ||
*/ | ||
def toPlayRequest(baseUri: String)(implicit ws: StandaloneWSClient): I => StandaloneWSRequest = | ||
new EndpointToPlayClient().toPlayRequest(e, baseUri) | ||
|
||
/** | ||
* Uses the endpoint definition to parse a response. | ||
*/ | ||
def parsePlayResponseUnsafe: StandaloneWSResponse => Either[E, O] = | ||
new EndpointToPlayClient().parsePlayResponseUnsafe(e) | ||
|
||
/** | ||
* Uses the endpoint definition to parse a response. | ||
*/ | ||
def parsePlayResponse: StandaloneWSResponse => DecodeResult[Either[E, O]] = | ||
new EndpointToPlayClient().parsePlayResponse(e) | ||
|
||
} | ||
|
||
} |
3 changes: 3 additions & 0 deletions
3
client/play-client/src/main/scala/sttp/tapir/client/play/package.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
package sttp.tapir.client | ||
|
||
package object play extends TapirPlayClient |
56 changes: 56 additions & 0 deletions
56
client/play-client/src/test/scala/sttp/tapir/client/play/PlayClientTests.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
package sttp.tapir.client.play | ||
|
||
import java.nio.charset.StandardCharsets | ||
|
||
import akka.actor.ActorSystem | ||
import akka.stream.Materializer | ||
import akka.stream.scaladsl.Source | ||
import akka.util.ByteString | ||
import cats.effect.{ContextShift, IO} | ||
import play.api.libs.ws.StandaloneWSClient | ||
import play.api.libs.ws.ahc.StandaloneAhcWSClient | ||
import sttp.tapir.client.tests.ClientTests | ||
import sttp.tapir.{DecodeResult, Endpoint} | ||
|
||
import scala.concurrent.ExecutionContext.Implicits.global | ||
import scala.concurrent.duration.DurationInt | ||
import scala.concurrent.{Await, Future} | ||
|
||
class PlayClientTests extends ClientTests[Source[ByteString, _]] { | ||
|
||
implicit val materializer: Materializer = Materializer(ActorSystem("tests")) | ||
|
||
implicit val wsClient: StandaloneWSClient = StandaloneAhcWSClient() | ||
|
||
implicit val cs: ContextShift[IO] = cats.effect.IO.contextShift(global) | ||
|
||
override def mkStream(s: String): Source[ByteString, _] = Source(List(ByteString.fromString(s))) | ||
|
||
override def rmStream(s: Source[ByteString, _]): String = { | ||
val f = s.runFold("")((str, bs) => str + bs.decodeString(StandardCharsets.UTF_8)) | ||
Await.result(f, 10.seconds) | ||
} | ||
|
||
override def send[I, E, O, FN[_]](e: Endpoint[I, E, O, Source[ByteString, _]], port: Port, args: I): IO[Either[E, O]] = { | ||
def response: Future[Either[E, O]] = | ||
e.toPlayRequest("http://localhost:$port") | ||
.apply(args) | ||
.execute() | ||
.map(e.parsePlayResponseUnsafe) | ||
IO.fromFuture(IO(response)) | ||
} | ||
|
||
override def safeSend[I, E, O, FN[_]]( | ||
e: Endpoint[I, E, O, Source[ByteString, _]], | ||
port: Port, | ||
args: I | ||
): IO[DecodeResult[Either[E, O]]] = { | ||
def response: Future[DecodeResult[Either[E, O]]] = | ||
e.toPlayRequest("http://localhost:$port") | ||
.apply(args) | ||
.execute() | ||
.map(e.parsePlayResponse) | ||
IO.fromFuture(IO(response)) | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters