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 --- .../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 +++++++++++---------- 4 files changed, 97 insertions(+), 85 deletions(-) (limited to 'core/src/main/scala') 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" -- cgit v1.2.3