aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md71
-rw-r--r--akka-http-handler/src/main/scala/com/softwaremill/sttp/akkahttp/AkkaHttpSttpHandler.scala33
-rw-r--r--akka-http-handler/src/main/scala/com/softwaremill/sttp/akkahttp/package.scala16
-rw-r--r--core/src/main/scala/com/softwaremill/sttp/HttpURLConnectionSttpHandler.scala13
-rw-r--r--core/src/main/scala/com/softwaremill/sttp/SttpHandler.scala4
-rw-r--r--core/src/main/scala/com/softwaremill/sttp/model/package.scala13
-rw-r--r--core/src/main/scala/com/softwaremill/sttp/package.scala152
-rw-r--r--tests/src/test/scala/com/softwaremill/sttp/BasicTests.scala44
-rw-r--r--tests/src/test/scala/com/softwaremill/sttp/IllTypedTests.scala8
-rw-r--r--tests/src/test/scala/com/softwaremill/sttp/StreamingTests.scala8
10 files changed, 193 insertions, 169 deletions
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