From 5aaac06c2d5ea122470ee7b27277ac0747e767d1 Mon Sep 17 00:00:00 2001 From: adamw Date: Wed, 19 Jul 2017 14:45:11 +0200 Subject: How the response should be handled is now part of the request definition --- README.md | 71 ++++++---- .../sttp/akkahttp/AkkaHttpSttpHandler.scala | 33 ++--- .../com/softwaremill/sttp/akkahttp/package.scala | 16 --- .../sttp/HttpURLConnectionSttpHandler.scala | 13 +- .../scala/com/softwaremill/sttp/SttpHandler.scala | 4 +- .../com/softwaremill/sttp/model/package.scala | 13 +- .../main/scala/com/softwaremill/sttp/package.scala | 152 +++++++++++---------- .../scala/com/softwaremill/sttp/BasicTests.scala | 44 +++--- .../com/softwaremill/sttp/IllTypedTests.scala | 8 +- .../com/softwaremill/sttp/StreamingTests.scala | 8 +- 10 files changed, 193 insertions(+), 169 deletions(-) delete mode 100644 akka-http-handler/src/main/scala/com/softwaremill/sttp/akkahttp/package.scala diff --git a/README.md b/README.md index 93ace05..8c0de51 100644 --- a/README.md +++ b/README.md @@ -11,16 +11,17 @@ import com.softwaremill.sttp._ val sort: Option[String] = None val query = "http language:scala" -// the `query` parameter is automatically url-encoded and `sort` removed +// the `query` parameter is automatically url-encoded +// `sort` is removed, as the value is not defined val request = sttp.get(uri"https://api.github.com/search/repositories?q=$query&sort=$sort") -// response body is read into a string, no need to remember to consume it later -val response = request.send(responseAsString("utf-8")) +implicit val handler = HttpURLConnectionSttpHandler +val response = request.send() // response.header(...): Option[String] println(response.header("Content-Length")) -// response.body: String as specified when sending the request +// response.body: by default read into a String println(response.body) ``` @@ -97,14 +98,14 @@ request: ```scala implicit val handler = HttpConnectionSttpHandler -val response: Response[String] = request.send(responseAsString) +val response: Response[String] = request.send() ``` -Note that when sending the request, we have to specify how to read the response -body. That way, you don't need to remember to consume it later, avoiding a -potential resource leak. Response bodies can be ignored (`ignoreResponseBody`), -read into a parameter sequence (`responseAsParams`) and more; some backends -also support request & response streaming. +By default the response body is read into a utf-8 string. How the response body +is handled is also part of the request description. The body can be ignore +(`.response(ignore)`), read into a sequence of parameters +(`.response(asParams)`) and more; some backends also support request & response +streaming. The default handler doesn't wrap the response into any container, but other asynchronous handlers might do so. The type parameter in the `Response[_]` @@ -210,7 +211,7 @@ import akka.util.ByteString val source: Source[ByteString, Any] = ... sttp - .body(source) + .streamBody(source) .post(uri"...") ``` @@ -218,6 +219,7 @@ To receive the response body as a stream: ```scala import com.softwaremill.sttp._ +import com.softwaremill.sttp.akkahttp._ import akka.stream.scaladsl.Source import akka.util.ByteString @@ -227,21 +229,39 @@ implicit val sttpHandler = new AkkaHttpSttpHandler(actorSystem) val response: Future[Response[Source[ByteString, Any]]] = sttp .post(uri"...") - .send(responseAsStream[Source[ByteString, Any]]) + .response(asStream[Source[ByteString, Any]]) + .send() ``` -## Request types +## Request type + +All request descriptions have type `RequestT[U, T, S]` (T as in Template). +If this looks a bit complex, don't worry, what the three type parameters stand +for is the only thing you'll hopefully have to remember when using the API! + +Going one-by-one: -All requests have type `RequestTemplate[U]`, where `U[_]` specifies if the -request method and URL are specified. There are two type aliases for the -request template that are used: +* `U[_]` specifies if the request method and URL are specified. Using the API, +this can be either `type Empty[X] = None`, meaning that the request has neither +a method nor an URI. Or, it can be `type Id[X] = X` (type-level identity), +meaning that the request has both a method and an URI specified. Only requests +with a specified URI & method can be sent. +* `T` specifies the type to which the response will be read. By default, this +is `String`. But it can also be e.g. `Array[Byte]` or `Unit`, if the response +should be ignored. Response body handling can be changed by calling the +`.response` method. With backends which support streaming, this can also be +a supported stream type. +* `S` specifies the stream type that this request uses. Most of the time this +will be `Nothing`, meaning that this request does not send a streaming body +or receive a streaming response. So most of the times you can just ignore +that parameter. But, if you are using a streaming backend and want to +send/receive a stream, the `.streamBody` or `response(asStream[S])` will change +the type parameter. -* `type Request = RequestTemplate[Id]`, where `type Id[X] = X` is the identity, -meaning that the request has both a method and an URI specified. Such a request -can be sent. -* `type PartialRequest = RequestTemplate[Empty]`, where `type Empty[X] = None`, -meaning that the request has neither a method nor an URI. Both of these fields -will be set to `None` (the `Option` subtype). Such a request cannot be sent. +There are two type aliases for the request template that are used: + +* `type Request[T, S] = RequestT[Id, T, S]`. A sendable request. +* `type PartialRequest[T, S] = RequestT[Empty, T, S]` ## Notes @@ -269,4 +289,9 @@ will be set to `None` (the `Option` subtype). Such a request cannot be sent. * [dispatch](http://dispatch.databinder.net/Dispatch.html) * [play ws](https://github.com/playframework/play-ws) * [fs2-http](https://github.com/Spinoco/fs2-http) -* [http4s](http://http4s.org/v0.17/client/) \ No newline at end of file +* [http4s](http://http4s.org/v0.17/client/) + +## Credits + +* [Tomasz SzymaƄski](https://github.com/szimano) +* [Adam Warski](https://github.com/adamw) \ No newline at end of file diff --git a/akka-http-handler/src/main/scala/com/softwaremill/sttp/akkahttp/AkkaHttpSttpHandler.scala b/akka-http-handler/src/main/scala/com/softwaremill/sttp/akkahttp/AkkaHttpSttpHandler.scala index 866aaad..62e066b 100644 --- a/akka-http-handler/src/main/scala/com/softwaremill/sttp/akkahttp/AkkaHttpSttpHandler.scala +++ b/akka-http-handler/src/main/scala/com/softwaremill/sttp/akkahttp/AkkaHttpSttpHandler.scala @@ -19,22 +19,23 @@ import scala.collection.immutable.Seq class AkkaHttpSttpHandler(actorSystem: ActorSystem) extends SttpHandler[Future, Source[ByteString, Any]] { + // the supported stream type + private type S = Source[ByteString, Any] + def this() = this(ActorSystem("sttp")) private implicit val as = actorSystem private implicit val materializer = ActorMaterializer() import as.dispatcher - override def send[T](r: Request, - responseAs: ResponseAs[T, Source[ByteString, Any]]) - : Future[Response[T]] = { + override def send[T](r: Request[T, S]): Future[Response[T]] = { requestToAkka(r) .map(setBodyOnAkka(r, r.body, _).get) .flatMap(Http().singleRequest(_)) .flatMap { hr => val code = hr.status.intValue() - bodyFromAkka(responseAs, hr).map( - Response(_, code, headersFromAkka(hr))) + bodyFromAkka(r.responseAs, hr) + .map(Response(_, code, headersFromAkka(hr))) } } @@ -51,7 +52,7 @@ class AkkaHttpSttpHandler(actorSystem: ActorSystem) case _ => HttpMethod.custom(m.m) } - private def bodyFromAkka[T](rr: ResponseAs[T, Source[ByteString, Any]], + private def bodyFromAkka[T](rr: ResponseAs[T, S], hr: HttpResponse): Future[T] = { def asByteArray = hr.entity.dataBytes @@ -88,7 +89,7 @@ class AkkaHttpSttpHandler(actorSystem: ActorSystem) ch :: (cl.toList ++ other) } - private def requestToAkka(r: Request): Future[HttpRequest] = { + private def requestToAkka(r: Request[_, S]): Future[HttpRequest] = { val ar = HttpRequest(uri = r.uri.toString, method = methodToAkka(r.method)) val parsed = r.headers.filterNot(isContentType).map(h => HttpHeader.parse(h._1, h._2)) @@ -106,11 +107,11 @@ class AkkaHttpSttpHandler(actorSystem: ActorSystem) } } - private def setBodyOnAkka(r: Request, - body: RequestBody, + private def setBodyOnAkka(r: Request[_, S], + body: RequestBody[S], ar: HttpRequest): Try[HttpRequest] = { getContentTypeOrOctetStream(r).map { ct => - def doSet(body: RequestBody): HttpRequest = body match { + def doSet(body: RequestBody[S]): HttpRequest = body match { case NoBody => ar case StringBody(b, encoding) => val ctWithEncoding = HttpCharsets @@ -124,21 +125,15 @@ class AkkaHttpSttpHandler(actorSystem: ActorSystem) ar.withEntity( HttpEntity(ct, StreamConverters.fromInputStream(() => b))) case PathBody(b) => ar.withEntity(ct, b) - case s @ SerializableBody(_, _) => doSetSerializable(s) + case StreamBody(s) => ar.withEntity(HttpEntity(ct, s)) + case SerializableBody(f, t) => doSet(f(t)) } - def doSetSerializable[T](body: SerializableBody[T]): HttpRequest = - body match { - case SerializableBody(SourceBodySerializer, t) => - ar.withEntity(HttpEntity(ct, t)) - case SerializableBody(f, t) => doSet(f(t)) - } - doSet(body) } } - private def getContentTypeOrOctetStream(r: Request): Try[ContentType] = { + private def getContentTypeOrOctetStream(r: Request[_, S]): Try[ContentType] = { r.headers .find(isContentType) .map(_._2) diff --git a/akka-http-handler/src/main/scala/com/softwaremill/sttp/akkahttp/package.scala b/akka-http-handler/src/main/scala/com/softwaremill/sttp/akkahttp/package.scala deleted file mode 100644 index 0357dd8..0000000 --- a/akka-http-handler/src/main/scala/com/softwaremill/sttp/akkahttp/package.scala +++ /dev/null @@ -1,16 +0,0 @@ -package com.softwaremill.sttp - -import akka.stream.scaladsl.Source -import akka.util.ByteString -import com.softwaremill.sttp.model.{BasicRequestBody, BodySerializer} - -package object akkahttp { - private[akkahttp] case object SourceBodySerializer - extends BodySerializer[Source[ByteString, Any]] { - def apply(t: Source[ByteString, Any]): BasicRequestBody = - throw new RuntimeException("Can only be used with akka-http handler") - } - - implicit val sourceBodySerializer: BodySerializer[Source[ByteString, Any]] = - SourceBodySerializer -} diff --git a/core/src/main/scala/com/softwaremill/sttp/HttpURLConnectionSttpHandler.scala b/core/src/main/scala/com/softwaremill/sttp/HttpURLConnectionSttpHandler.scala index 7fd7220..a42e26d 100644 --- a/core/src/main/scala/com/softwaremill/sttp/HttpURLConnectionSttpHandler.scala +++ b/core/src/main/scala/com/softwaremill/sttp/HttpURLConnectionSttpHandler.scala @@ -12,8 +12,7 @@ import scala.io.Source import scala.collection.JavaConverters._ object HttpURLConnectionSttpHandler extends SttpHandler[Id, Nothing] { - override def send[T](r: Request, - responseAs: ResponseAs[T, Nothing]): Response[T] = { + override def send[T](r: Request[T, Nothing]): Response[T] = { val c = r.uri.toURL.openConnection().asInstanceOf[HttpURLConnection] c.setRequestMethod(r.method.m) r.headers.foreach { case (k, v) => c.setRequestProperty(k, v) } @@ -22,14 +21,14 @@ object HttpURLConnectionSttpHandler extends SttpHandler[Id, Nothing] { try { val is = c.getInputStream - readResponse(c, is, responseAs) + readResponse(c, is, r.responseAs) } catch { case _: IOException if c.getResponseCode != -1 => - readResponse(c, c.getErrorStream, responseAs) + readResponse(c, c.getErrorStream, r.responseAs) } } - private def setBody(body: RequestBody, c: HttpURLConnection): Unit = { + private def setBody(body: RequestBody[Nothing], c: HttpURLConnection): Unit = { if (body != NoBody) c.setDoOutput(true) def copyStream(in: InputStream, out: OutputStream): Unit = { @@ -71,6 +70,10 @@ object HttpURLConnectionSttpHandler extends SttpHandler[Id, Nothing] { case SerializableBody(f, t) => setBody(f(t), c) + + case StreamBody(s) => + // we have an instance of nothing - everything's possible! + assert(2 + 2 == 5) } } diff --git a/core/src/main/scala/com/softwaremill/sttp/SttpHandler.scala b/core/src/main/scala/com/softwaremill/sttp/SttpHandler.scala index a0a039e..f0f1fc9 100644 --- a/core/src/main/scala/com/softwaremill/sttp/SttpHandler.scala +++ b/core/src/main/scala/com/softwaremill/sttp/SttpHandler.scala @@ -1,9 +1,7 @@ package com.softwaremill.sttp -import com.softwaremill.sttp.model.ResponseAs - import scala.language.higherKinds trait SttpHandler[R[_], -S] { - def send[T](request: Request, responseAs: ResponseAs[T, S]): R[Response[T]] + def send[T](request: Request[T, S]): R[Response[T]] } diff --git a/core/src/main/scala/com/softwaremill/sttp/model/package.scala b/core/src/main/scala/com/softwaremill/sttp/model/package.scala index bd8b2e3..6fb41b8 100644 --- a/core/src/main/scala/com/softwaremill/sttp/model/package.scala +++ b/core/src/main/scala/com/softwaremill/sttp/model/package.scala @@ -26,21 +26,22 @@ package object model { * Provide an implicit value of this type to serialize arbitrary classes into a request body. * Handlers might also provide special logic for serializer instances which they define (e.g. to handle streaming). */ - type BodySerializer[T] = T => BasicRequestBody + type BodySerializer[B] = B => BasicRequestBody - sealed trait RequestBody - case object NoBody extends RequestBody - // TODO: extract StreamBody, with request parametrized to match the stream type? + sealed trait RequestBody[+S] + case object NoBody extends RequestBody[Nothing] case class SerializableBody[T](f: BodySerializer[T], t: T) - extends RequestBody + extends RequestBody[Nothing] - sealed trait BasicRequestBody extends RequestBody + sealed trait BasicRequestBody extends RequestBody[Nothing] case class StringBody(s: String, encoding: String) extends BasicRequestBody case class ByteArrayBody(b: Array[Byte]) extends BasicRequestBody case class ByteBufferBody(b: ByteBuffer) extends BasicRequestBody case class InputStreamBody(b: InputStream) extends BasicRequestBody case class PathBody(f: Path) extends BasicRequestBody + case class StreamBody[S](s: S) extends RequestBody[S] + /** * @tparam T Target type as which the response will be read. * @tparam S If `T` is a stream, the type of the stream. Otherwise, `Nothing`. diff --git a/core/src/main/scala/com/softwaremill/sttp/package.scala b/core/src/main/scala/com/softwaremill/sttp/package.scala index 763b6c3..925cb3b 100644 --- a/core/src/main/scala/com/softwaremill/sttp/package.scala +++ b/core/src/main/scala/com/softwaremill/sttp/package.scala @@ -16,27 +16,26 @@ package object sttp { type Id[X] = X type Empty[X] = None.type - def ignoreResponse: ResponseAs[Unit, Nothing] = IgnoreResponse + def ignore: ResponseAs[Unit, Nothing] = IgnoreResponse /** * Uses `utf-8` encoding. */ - def responseAsString: ResponseAs[String, Nothing] = responseAsString(Utf8) - def responseAsString(encoding: String): ResponseAs[String, Nothing] = + def asString: ResponseAs[String, Nothing] = asString(Utf8) + def asString(encoding: String): ResponseAs[String, Nothing] = ResponseAsString(encoding) - def responseAsByteArray: ResponseAs[Array[Byte], Nothing] = + def asByteArray: ResponseAs[Array[Byte], Nothing] = ResponseAsByteArray /** * Uses `utf-8` encoding. */ - def responseAsParams: ResponseAs[Seq[(String, String)], Nothing] = - responseAsParams(Utf8) - def responseAsParams( - encoding: String): ResponseAs[Seq[(String, String)], Nothing] = + def asParams: ResponseAs[Seq[(String, String)], Nothing] = + asParams(Utf8) + def asParams(encoding: String): ResponseAs[Seq[(String, String)], Nothing] = ResponseAsParams(encoding) - def responseAsStream[S]: ResponseAs[S, S] = ResponseAsStream[S, S]() + def asStream[S]: ResponseAs[S, S] = ResponseAsStream[S, S]() /** * Use the factory methods `multiPart` to conveniently create instances of @@ -129,69 +128,72 @@ package object sttp { * request is aliased to `Request`: the method and uri are * specified, and the request can be sent. */ - case class RequestTemplate[U[_]]( + case class RequestT[U[_], T, +S]( method: U[Method], uri: U[URI], - body: RequestBody, - headers: Seq[(String, String)] + body: RequestBody[S], + headers: Seq[(String, String)], + responseAs: ResponseAs[T, S] ) { - def get(uri: URI): Request = this.copy[Id](uri = uri, method = Method.GET) - def head(uri: URI): Request = - this.copy[Id](uri = uri, method = Method.HEAD) - def post(uri: URI): Request = - this.copy[Id](uri = uri, method = Method.POST) - def put(uri: URI): Request = this.copy[Id](uri = uri, method = Method.PUT) - def delete(uri: URI): Request = - this.copy[Id](uri = uri, method = Method.DELETE) - def options(uri: URI): Request = - this.copy[Id](uri = uri, method = Method.OPTIONS) - def patch(uri: URI): Request = - this.copy[Id](uri = uri, method = Method.PATCH) - - def contentType(ct: String): RequestTemplate[U] = + def get(uri: URI): Request[T, S] = + this.copy[Id, T, S](uri = uri, method = Method.GET) + def head(uri: URI): Request[T, S] = + this.copy[Id, T, S](uri = uri, method = Method.HEAD) + def post(uri: URI): Request[T, S] = + this.copy[Id, T, S](uri = uri, method = Method.POST) + def put(uri: URI): Request[T, S] = + this.copy[Id, T, S](uri = uri, method = Method.PUT) + def delete(uri: URI): Request[T, S] = + this.copy[Id, T, S](uri = uri, method = Method.DELETE) + def options(uri: URI): Request[T, S] = + this.copy[Id, T, S](uri = uri, method = Method.OPTIONS) + def patch(uri: URI): Request[T, S] = + this.copy[Id, T, S](uri = uri, method = Method.PATCH) + + def contentType(ct: String): RequestT[U, T, S] = header(ContentTypeHeader, ct, replaceExisting = true) - def contentType(ct: String, encoding: String): RequestTemplate[U] = + def contentType(ct: String, encoding: String): RequestT[U, T, S] = header(ContentTypeHeader, contentTypeWithEncoding(ct, encoding), replaceExisting = true) def header(k: String, v: String, - replaceExisting: Boolean = false): RequestTemplate[U] = { + replaceExisting: Boolean = false): RequestT[U, T, S] = { val current = if (replaceExisting) headers.filterNot(_._1.equalsIgnoreCase(k)) else headers this.copy(headers = current :+ (k -> v)) } - def headers(hs: Map[String, String]): RequestTemplate[U] = + def headers(hs: Map[String, String]): RequestT[U, T, S] = this.copy(headers = headers ++ hs.toSeq) - def headers(hs: (String, String)*): RequestTemplate[U] = + def headers(hs: (String, String)*): RequestT[U, T, S] = this.copy(headers = headers ++ hs) - def cookie(nv: (String, String)): RequestTemplate[U] = cookies(nv) - def cookie(n: String, v: String): RequestTemplate[U] = cookies((n, v)) - def cookies(r: Response[_]): RequestTemplate[U] = + def cookie(nv: (String, String)): RequestT[U, T, S] = cookies(nv) + def cookie(n: String, v: String): RequestT[U, T, S] = cookies((n, v)) + def cookies(r: Response[_]): RequestT[U, T, S] = cookies(r.cookies.map(c => (c.name, c.value)): _*) - def cookies(cs: Seq[Cookie]): RequestTemplate[U] = + def cookies(cs: Seq[Cookie]): RequestT[U, T, S] = cookies(cs.map(c => (c.name, c.value)): _*) - def cookies(nvs: (String, String)*): RequestTemplate[U] = + def cookies(nvs: (String, String)*): RequestT[U, T, S] = header(CookieHeader, nvs.map(p => p._1 + "=" + p._2).mkString("; ")) - def auth: SpecifyAuthScheme[U] = - new SpecifyAuthScheme[U](AuthorizationHeader, this) - def proxyAuth: SpecifyAuthScheme[U] = - new SpecifyAuthScheme[U](ProxyAuthorizationHeader, this) + def auth: SpecifyAuthScheme[U, T, S] = + new SpecifyAuthScheme[U, T, S](AuthorizationHeader, this) + def proxyAuth: SpecifyAuthScheme[U, T, S] = + new SpecifyAuthScheme[U, T, S](ProxyAuthorizationHeader, this) /** * Uses the `utf-8` encoding. * If content type is not yet specified, will be set to `text/plain` * with `utf-8` encoding. */ - def body(b: String): RequestTemplate[U] = body(b, Utf8) + def body(b: String): RequestT[U, T, S] = body(b, Utf8) /** * If content type is not yet specified, will be set to `text/plain` * with the given encoding. */ - def body(b: String, encoding: String): RequestTemplate[U] = + def body(b: String, encoding: String): RequestT[U, T, S] = setContentTypeIfMissing( contentTypeWithEncoding(TextPlainContentType, encoding)) .copy(body = StringBody(b, encoding)) @@ -200,7 +202,7 @@ package object sttp { * If content type is not yet specified, will be set to * `application/octet-stream`. */ - def body(b: Array[Byte]): RequestTemplate[U] = + def body(b: Array[Byte]): RequestT[U, T, S] = setContentTypeIfMissing(ApplicationOctetStreamContentType).copy( body = ByteArrayBody(b)) @@ -208,7 +210,7 @@ package object sttp { * If content type is not yet specified, will be set to * `application/octet-stream`. */ - def body(b: ByteBuffer): RequestTemplate[U] = + def body(b: ByteBuffer): RequestT[U, T, S] = setContentTypeIfMissing(ApplicationOctetStreamContentType).copy( body = ByteBufferBody(b)) @@ -216,7 +218,7 @@ package object sttp { * If content type is not yet specified, will be set to * `application/octet-stream`. */ - def body(b: InputStream): RequestTemplate[U] = + def body(b: InputStream): RequestT[U, T, S] = setContentTypeIfMissing(ApplicationOctetStreamContentType).copy( body = InputStreamBody(b)) @@ -224,13 +226,13 @@ package object sttp { * If content type is not yet specified, will be set to * `application/octet-stream`. */ - def body(b: File): RequestTemplate[U] = body(b.toPath) + def body(b: File): RequestT[U, T, S] = body(b.toPath) /** * If content type is not yet specified, will be set to * `application/octet-stream`. */ - def body(b: Path): RequestTemplate[U] = + def body(b: Path): RequestT[U, T, S] = setContentTypeIfMissing(ApplicationOctetStreamContentType).copy( body = PathBody(b)) @@ -239,7 +241,7 @@ package object sttp { * If content type is not yet specified, will be set to * `application/x-www-form-urlencoded`. */ - def body(fs: Map[String, String]): RequestTemplate[U] = + def body(fs: Map[String, String]): RequestT[U, T, S] = formDataBody(fs.toList, Utf8) /** @@ -247,7 +249,7 @@ package object sttp { * If content type is not yet specified, will be set to * `application/x-www-form-urlencoded`. */ - def body(fs: Map[String, String], encoding: String): RequestTemplate[U] = + def body(fs: Map[String, String], encoding: String): RequestT[U, T, S] = formDataBody(fs.toList, encoding) /** @@ -255,7 +257,7 @@ package object sttp { * If content type is not yet specified, will be set to * `application/x-www-form-urlencoded`. */ - def body(fs: (String, String)*): RequestTemplate[U] = + def body(fs: (String, String)*): RequestT[U, T, S] = formDataBody(fs.toList, Utf8) /** @@ -263,21 +265,24 @@ package object sttp { * If content type is not yet specified, will be set to * `application/x-www-form-urlencoded`. */ - def body(fs: Seq[(String, String)], encoding: String): RequestTemplate[U] = + def body(fs: Seq[(String, String)], encoding: String): RequestT[U, T, S] = formDataBody(fs, encoding) /** * If content type is not yet specified, will be set to * `application/octet-stream`. */ - def body[T: BodySerializer](b: T): RequestTemplate[U] = + def body[B: BodySerializer](b: B): RequestT[U, T, S] = setContentTypeIfMissing(ApplicationOctetStreamContentType).copy( - body = SerializableBody(implicitly[BodySerializer[T]], b)) + body = SerializableBody(implicitly[BodySerializer[B]], b)) //def multipartData(parts: MultiPart*): RequestTemplate[U] = ??? + def streamBody[S2 >: S](b: S2): RequestT[U, T, S2] = + this.copy[U, T, S2](body = StreamBody(b)) + /** - * @param responseAs What's the target type to which the response body + * What's the target type to which the response body * should be read. Needs to be specified upfront * so that the response is always consumed and hence * there are no requirements on client code to consume @@ -285,20 +290,25 @@ package object sttp { * which need to fully consumed by the client if such * a response type is requested. */ - def send[R[_], S, T](responseAs: ResponseAs[T, S])( - implicit handler: SttpHandler[R, S], - isRequest: IsRequest[U]): R[Response[T]] = { - - handler.send(this, responseAs) + def response[T2, S2 >: S](ra: ResponseAs[T2, S2]): RequestT[U, T2, S2] = + this.copy(responseAs = ra) + + def send[R[_]]()(implicit handler: SttpHandler[R, S], + isIdInRequest: IsIdInRequest[U]): R[Response[T]] = { + // we could avoid the asInstanceOf by creating an artificial copy + // changing the method & url fields using `isIdInRequest`, but that + // would be only to satisfy the type checker, and a needless copy at + // runtime. + handler.send(this.asInstanceOf[RequestT[Id, T, S]]) } private def hasContentType: Boolean = headers.exists(_._1.toLowerCase.contains(ContentTypeHeader)) - private def setContentTypeIfMissing(ct: String): RequestTemplate[U] = + private def setContentTypeIfMissing(ct: String): RequestT[U, T, S] = if (hasContentType) this else contentType(ct) private def formDataBody(fs: Seq[(String, String)], - encoding: String): RequestTemplate[U] = { + encoding: String): RequestT[U, T, S] = { val b = fs .map( p => @@ -310,32 +320,30 @@ package object sttp { } } - class SpecifyAuthScheme[U[_]](hn: String, rt: RequestTemplate[U]) { - def basic(user: String, password: String): RequestTemplate[U] = { + class SpecifyAuthScheme[U[_], T, +S](hn: String, rt: RequestT[U, T, S]) { + def basic(user: String, password: String): RequestT[U, T, S] = { val c = new String( Base64.getEncoder.encode(s"$user:$password".getBytes(Utf8)), Utf8) rt.header(hn, s"Basic $c") } - def bearer(token: String): RequestTemplate[U] = + def bearer(token: String): RequestT[U, T, S] = rt.header(hn, s"Bearer $token") } - object RequestTemplate { - val empty: RequestTemplate[Empty] = - RequestTemplate[Empty](None, None, NoBody, Vector()) + object RequestT { + val empty: RequestT[Empty, String, Nothing] = + RequestT[Empty, String, Nothing](None, None, NoBody, Vector(), asString) } - type PartialRequest = RequestTemplate[Empty] - type Request = RequestTemplate[Id] + type PartialRequest[T, +S] = RequestT[Empty, T, S] + type Request[T, +S] = RequestT[Id, T, S] @implicitNotFound( "This is a partial request, the method & url are not specified. Use " + ".get(...), .post(...) etc. to obtain a non-partial request.") - private type IsRequest[U[_]] = RequestTemplate[U] =:= Request - - val sttp: RequestTemplate[Empty] = RequestTemplate.empty + private type IsIdInRequest[U[_]] = U[Unit] =:= Id[Unit] private[sttp] val ContentTypeHeader = "Content-Type" private[sttp] val ContentLengthHeader = "Content-Length" @@ -350,6 +358,8 @@ package object sttp { private val TextPlainContentType = "text/plain" private val MultipartFormDataContentType = "multipart/form-data" + val sttp: RequestT[Empty, String, Nothing] = RequestT.empty + private def contentTypeWithEncoding(ct: String, enc: String) = s"$ct; charset=$enc" diff --git a/tests/src/test/scala/com/softwaremill/sttp/BasicTests.scala b/tests/src/test/scala/com/softwaremill/sttp/BasicTests.scala index f5a1796..621968b 100644 --- a/tests/src/test/scala/com/softwaremill/sttp/BasicTests.scala +++ b/tests/src/test/scala/com/softwaremill/sttp/BasicTests.scala @@ -121,6 +121,8 @@ class BasicTests val testBodyBytes = testBody.getBytes("UTF-8") val expectedPostEchoResponse = "POST /echo this is the body" + val sttpIgnore = com.softwaremill.sttp.ignore + parseResponseTests() parameterTests() bodyTests() @@ -131,13 +133,13 @@ class BasicTests def parseResponseTests(): Unit = { name should "parse response as string" in { - val response = postEcho.body(testBody).send(responseAsString).force() + val response = postEcho.body(testBody).send().force() response.body should be(expectedPostEchoResponse) } name should "parse response as a byte array" in { val response = - postEcho.body(testBody).send(responseAsByteArray).force() + postEcho.body(testBody).response(asByteArray).send().force() val fc = new String(response.body, "UTF-8") fc should be(expectedPostEchoResponse) } @@ -147,7 +149,8 @@ class BasicTests val response = sttp .post(uri"$endpoint/echo/form_params/as_params") .body(params: _*) - .send(responseAsParams) + .response(asParams) + .send() .force() response.body.toList should be(params) } @@ -157,7 +160,7 @@ class BasicTests name should "make a get request with parameters" in { val response = sttp .get(uri"$endpoint/echo?p2=v2&p1=v1") - .send(responseAsString) + .send() .force() response.body should be("GET /echo p1=v1 p2=v2") @@ -166,20 +169,20 @@ class BasicTests def bodyTests(): Unit = { name should "post a string" in { - val response = postEcho.body(testBody).send(responseAsString).force() + val response = postEcho.body(testBody).send().force() response.body should be(expectedPostEchoResponse) } name should "post a byte array" in { val response = - postEcho.body(testBodyBytes).send(responseAsString).force() + postEcho.body(testBodyBytes).send().force() response.body should be(expectedPostEchoResponse) } name should "post an input stream" in { val response = postEcho .body(new ByteArrayInputStream(testBodyBytes)) - .send(responseAsString) + .send() .force() response.body should be(expectedPostEchoResponse) } @@ -187,7 +190,7 @@ class BasicTests name should "post a byte buffer" in { val response = postEcho .body(ByteBuffer.wrap(testBodyBytes)) - .send(responseAsString) + .send() .force() response.body should be(expectedPostEchoResponse) } @@ -195,7 +198,7 @@ class BasicTests name should "post a file" in { val f = File.newTemporaryFile().write(testBody) try { - val response = postEcho.body(f.toJava).send(responseAsString).force() + val response = postEcho.body(f.toJava).send().force() response.body should be(expectedPostEchoResponse) } finally f.delete() } @@ -204,7 +207,7 @@ class BasicTests val f = File.newTemporaryFile().write(testBody) try { val response = - postEcho.body(f.toJava.toPath).send(responseAsString).force() + postEcho.body(f.toJava.toPath).send().force() response.body should be(expectedPostEchoResponse) } finally f.delete() } @@ -213,7 +216,7 @@ class BasicTests val response = sttp .post(uri"$endpoint/echo/form_params/as_string") .body("a" -> "b", "c" -> "d") - .send(responseAsString) + .send() .force() response.body should be("a=b c=d") } @@ -222,7 +225,7 @@ class BasicTests val response = sttp .post(uri"$endpoint/echo/form_params/as_string") .body("a=" -> "/b", "c:" -> "/d") - .send(responseAsString) + .send() .force() response.body should be("a==/b c:=/d") } @@ -232,7 +235,7 @@ class BasicTests val getHeaders = sttp.get(uri"$endpoint/set_headers") name should "read response headers" in { - val response = getHeaders.send(ignoreResponse).force() + val response = getHeaders.response(sttpIgnore).send().force() response.headers should have length (6) response.headers("Cache-Control").toSet should be( Set("no-cache", "max-age=1000")) @@ -248,7 +251,7 @@ class BasicTests val getHeaders = sttp.post(uri"$endpoint/set_headers") name should "return 405 when method not allowed" in { - val response = getHeaders.send(ignoreResponse).force() + val response = getHeaders.response(sttpIgnore).send().force() response.code should be(405) response.isClientError should be(true) } @@ -257,7 +260,11 @@ class BasicTests def cookiesTests(): Unit = { name should "read response cookies" in { val response = - sttp.get(uri"$endpoint/set_cookies").send(ignoreResponse).force() + sttp + .get(uri"$endpoint/set_cookies") + .response(sttpIgnore) + .send() + .force() response.cookies should have length (3) response.cookies.toSet should be( Set( @@ -274,7 +281,8 @@ class BasicTests name should "read response cookies with the expires attribute" in { val response = sttp .get(uri"$endpoint/set_cookies/with_expires") - .send(ignoreResponse) + .response(sttpIgnore) + .send() .force() response.cookies should have length (1) val c = response.cookies(0) @@ -296,7 +304,7 @@ class BasicTests name should "return a 401 when authorization fails" in { val req = secureBasic - val resp = req.send(responseAsString).force() + val resp = req.send().force() resp.code should be(401) resp.header("WWW-Authenticate") should be( Some("""Basic realm="test realm",charset=UTF-8""")) @@ -304,7 +312,7 @@ class BasicTests name should "perform basic authorization" in { val req = secureBasic.auth.basic("adam", "1234") - val resp = req.send(responseAsString).force() + val resp = req.send().force() resp.code should be(200) resp.body should be("Hello, adam!") } diff --git a/tests/src/test/scala/com/softwaremill/sttp/IllTypedTests.scala b/tests/src/test/scala/com/softwaremill/sttp/IllTypedTests.scala index caf6632..0d0c012 100644 --- a/tests/src/test/scala/com/softwaremill/sttp/IllTypedTests.scala +++ b/tests/src/test/scala/com/softwaremill/sttp/IllTypedTests.scala @@ -8,16 +8,16 @@ class IllTypedTests extends FlatSpec with Matchers { import akka.stream.scaladsl.Source import akka.util.ByteString import java.net.URI - implicit val sttpHandler = HttpConnectionSttpHandler - sttp.get(new URI("http://example.com")).send(responseAsStream[Source[ByteString, Any]]) + implicit val sttpHandler = HttpURLConnectionSttpHandler + sttp.get(new URI("http://example.com")).response(asStream[Source[ByteString, Any]]).send() """ shouldNot typeCheck } "compilation" should "fail when trying to send a request without giving an URL" in { """ import java.net.URI - implicit val sttpHandler = HttpConnectionSttpHandler - sttp.send(responseAsString) + implicit val sttpHandler = HttpURLConnectionSttpHandler + sttp.send() """ shouldNot typeCheck } } diff --git a/tests/src/test/scala/com/softwaremill/sttp/StreamingTests.scala b/tests/src/test/scala/com/softwaremill/sttp/StreamingTests.scala index ab77753..95a8bed 100644 --- a/tests/src/test/scala/com/softwaremill/sttp/StreamingTests.scala +++ b/tests/src/test/scala/com/softwaremill/sttp/StreamingTests.scala @@ -8,7 +8,6 @@ import com.softwaremill.sttp.akkahttp.AkkaHttpSttpHandler import com.typesafe.scalalogging.StrictLogging import org.scalatest.{BeforeAndAfterAll, FlatSpec, Matchers} import org.scalatest.concurrent.{IntegrationPatience, ScalaFutures} -import com.softwaremill.sttp.akkahttp._ class StreamingTests extends FlatSpec @@ -42,8 +41,8 @@ class StreamingTests "Akka HTTP" should "stream request body" in { val response = sttp .post(uri"$endpoint/echo") - .body(Source.single(ByteString(body))) - .send(responseAsString) + .streamBody(Source.single(ByteString(body))) + .send() .futureValue response.body should be(body) @@ -53,7 +52,8 @@ class StreamingTests val response = sttp .post(uri"$endpoint/echo") .body(body) - .send(responseAsStream[Source[ByteString, Any]]) + .response(asStream[Source[ByteString, Any]]) + .send() .futureValue val responseBody = response.body.runReduce(_ ++ _).futureValue.utf8String -- cgit v1.2.3