aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoradamw <adam@warski.org>2017-06-29 22:12:32 +0200
committeradamw <adam@warski.org>2017-06-29 22:12:32 +0200
commit1bb7a630a3893978fe8be18fefe22ee25899f3ae (patch)
tree486a7eb395497e82db39c97a8b00828a4e179c11
parent034c40595f217ef1f11ca351666a03aa08976b81 (diff)
downloadsttp-1bb7a630a3893978fe8be18fefe22ee25899f3ae.tar.gz
sttp-1bb7a630a3893978fe8be18fefe22ee25899f3ae.tar.bz2
sttp-1bb7a630a3893978fe8be18fefe22ee25899f3ae.zip
Rework response as
-rw-r--r--README.md19
-rw-r--r--akka-http-handler/src/main/scala/com/softwaremill/sttp/akkahttp/AkkaHttpSttpHandler.scala51
-rw-r--r--akka-http-handler/src/main/scala/com/softwaremill/sttp/akkahttp/package.scala14
-rw-r--r--core/src/main/scala/com/softwaremill/sttp/HttpConnectionSttpHandler.scala34
-rw-r--r--core/src/main/scala/com/softwaremill/sttp/SttpHandler.scala9
-rw-r--r--core/src/main/scala/com/softwaremill/sttp/package.scala75
6 files changed, 119 insertions, 83 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..34eebed
--- /dev/null
+++ b/README.md
@@ -0,0 +1,19 @@
+# sttp
+
+The HTTP client for Scala that you always wanted
+
+## Goals of the project
+
+* provide a simple, discoverable, no-surprises, reasonably type-safe API for making HTTP requests
+* separate definition of a request from request execution
+* provide immutable, easily modifiable data structures for requests and responses
+* support both synchronous and asynchronous execution backends
+* provide support for backend-specific request/response streaming
+
+## Other Scala HTTP clients
+
+* [scalaj](https://github.com/scalaj/scalaj-http)
+* [akka-http client](http://doc.akka.io/docs/akka-http/current/scala/http/client-side/index.html)
+* [dispatch](http://dispatch.databinder.net/Dispatch.html)
+* [play ws](https://github.com/playframework/play-ws)
+* [fs2-http](https://github.com/Spinoco/fs2-http) \ 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 e167dd5..60d5686 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
@@ -7,7 +7,7 @@ import akka.http.scaladsl.model._
import akka.stream.ActorMaterializer
import akka.stream.scaladsl.Source
import akka.util.ByteString
-import com.softwaremill.sttp.{IgnoreResponseBody, Method, Request, Response, ResponseBodyReader, SttpStreamHandler}
+import com.softwaremill.sttp._
import scala.concurrent.Future
@@ -18,23 +18,28 @@ class AkkaHttpSttpHandler(actorSystem: ActorSystem) extends SttpStreamHandler[Fu
private implicit val materializer = ActorMaterializer()
import as.dispatcher
- override def send[T](r: Request, responseReader: ResponseBodyReader[T]): Future[Response[T]] = {
- requestToAkka(r).flatMap { ar =>
- Http().singleRequest(ar).flatMap { hr =>
- val code = hr.status.intValue()
- bodyFromAkkaResponse(responseReader, hr).map(Response(code, _))
- }
+ override def send[T](r: Request, responseAs: ResponseAs[T]): Future[Response[T]] = {
+ requestToAkka(r).flatMap(Http().singleRequest(_)).flatMap { hr =>
+ val code = hr.status.intValue()
+ bodyFromAkkaResponse(responseAs, hr).map(Response(code, _))
+ }
+ }
+
+ override def send[T](r: Request, responseAsStream: ResponseAsStream[Source[ByteString, Any]]): Future[Response[Source[ByteString, Any]]] = {
+ requestToAkka(r).flatMap(Http().singleRequest(_)).map { hr =>
+ val code = hr.status.intValue()
+ Response(code, hr.entity.dataBytes)
}
}
override def sendStream[T](r: Request, contentType: String, stream: Source[ByteString, Any],
- responseReader: ResponseBodyReader[T]): Future[Response[T]] = {
+ responseAs: ResponseAs[T]): Future[Response[T]] = {
for {
ar <- requestToAkka(r)
ct <- contentTypeToAkka(contentType)
hr <- Http().singleRequest(ar.withEntity(HttpEntity(ct, stream)))
- body <- bodyFromAkkaResponse(responseReader, hr)
+ body <- bodyFromAkkaResponse(responseAs, hr)
} yield Response(hr.status.intValue(), body)
}
@@ -44,19 +49,22 @@ class AkkaHttpSttpHandler(actorSystem: ActorSystem) extends SttpStreamHandler[Fu
case _ => HttpMethod.custom(m.m)
}
- private def bodyFromAkkaResponse[T](rr: ResponseBodyReader[T], hr: HttpResponse): Future[T] = rr match {
- case IgnoreResponseBody =>
- hr.discardEntityBytes()
- Future.successful(())
+ private def bodyFromAkkaResponse[T](rr: ResponseAs[T], hr: HttpResponse): Future[T] = {
+ def asByteArray = hr.entity.dataBytes
+ .runFold(ByteString(""))(_ ++ _)
+ .map(_.toArray[Byte])
- case AkkaStreamsSourceResponseBody =>
- Future.successful(hr.entity.dataBytes)
+ rr match {
+ case IgnoreResponse =>
+ hr.discardEntityBytes()
+ Future.successful(())
- case _ =>
- hr.entity.dataBytes
- .runFold(ByteString(""))(_ ++ _)
- .map(_.toArray[Byte])
- .map(rr.fromBytes)
+ case ResponseAsString(enc) =>
+ asByteArray.map(new String(_, enc))
+
+ case ResponseAsByteArray =>
+ asByteArray
+ }
}
private def requestToAkka(r: Request): Future[HttpRequest] = {
@@ -85,5 +93,4 @@ class AkkaHttpSttpHandler(actorSystem: ActorSystem) extends SttpStreamHandler[Fu
def close(): Future[Terminated] = {
actorSystem.terminate()
}
-}
-
+} \ No newline at end of file
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 3b9a1aa..0000000
--- a/akka-http-handler/src/main/scala/com/softwaremill/sttp/akkahttp/package.scala
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.softwaremill.sttp
-
-import java.io.InputStream
-
-import akka.stream.scaladsl.Source
-import akka.util.ByteString
-
-package object akkahttp {
- implicit object AkkaStreamsSourceResponseBody extends ResponseBodyReader[Source[ByteString, Any]] {
- override def fromInputStream(is: InputStream): Source[ByteString, Any] = ???
-
- override def fromBytes(bytes: Array[Byte]): Source[ByteString, Any] = ???
- }
-}
diff --git a/core/src/main/scala/com/softwaremill/sttp/HttpConnectionSttpHandler.scala b/core/src/main/scala/com/softwaremill/sttp/HttpConnectionSttpHandler.scala
index f2d5527..8144496 100644
--- a/core/src/main/scala/com/softwaremill/sttp/HttpConnectionSttpHandler.scala
+++ b/core/src/main/scala/com/softwaremill/sttp/HttpConnectionSttpHandler.scala
@@ -1,14 +1,15 @@
package com.softwaremill.sttp
-import java.io.{InputStream, OutputStream, OutputStreamWriter}
+import java.io.{ByteArrayOutputStream, InputStream, OutputStream, OutputStreamWriter}
import java.net.HttpURLConnection
import java.nio.channels.Channels
import java.nio.file.Files
import scala.annotation.tailrec
+import scala.io.Source
class HttpConnectionSttpHandler extends SttpHandler[Id] {
- override def send[T](r: Request, responseReader: ResponseBodyReader[T]): Response[T] = {
+ override def send[T](r: Request, responseAs: ResponseAs[T]): 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) }
@@ -16,7 +17,7 @@ class HttpConnectionSttpHandler extends SttpHandler[Id] {
setBody(r, c)
val status = c.getResponseCode
- Response(status, responseReader.fromInputStream(c.getInputStream))
+ Response(status, readResponse(c.getInputStream, responseAs))
}
private def setBody(r: Request, c: HttpURLConnection): Unit = {
@@ -64,4 +65,31 @@ class HttpConnectionSttpHandler extends SttpHandler[Id] {
Files.copy(b, c.getOutputStream)
}
}
+
+ private def readResponse[T](is: InputStream, responseAs: ResponseAs[T]): T = responseAs match {
+ case IgnoreResponse =>
+ @tailrec def consume(): Unit = if (is.read() != -1) consume()
+ consume()
+
+ case ResponseAsString(enc) =>
+ Source.fromInputStream(is, enc).mkString
+
+ case ResponseAsByteArray =>
+ val os = new ByteArrayOutputStream
+ var read = 0
+ val buf = new Array[Byte](1024)
+
+ @tailrec
+ def transfer(): Unit = {
+ read = is.read(buf, 0, buf.length)
+ if (read != -1) {
+ os.write(buf, 0, read)
+ transfer()
+ }
+ }
+
+ transfer()
+
+ os.toByteArray
+ }
}
diff --git a/core/src/main/scala/com/softwaremill/sttp/SttpHandler.scala b/core/src/main/scala/com/softwaremill/sttp/SttpHandler.scala
index a3b7685..f86467b 100644
--- a/core/src/main/scala/com/softwaremill/sttp/SttpHandler.scala
+++ b/core/src/main/scala/com/softwaremill/sttp/SttpHandler.scala
@@ -1,9 +1,12 @@
package com.softwaremill.sttp
+import scala.language.higherKinds
+
trait SttpHandler[R[_]] {
- def send[T](request: Request, responseReader: ResponseBodyReader[T]): R[Response[T]]
+ def send[T](request: Request, responseAs: ResponseAs[T]): R[Response[T]]
}
-trait SttpStreamHandler[R[_], -S] extends SttpHandler[R] {
- def sendStream[T](request: Request, contentType: String, stream: S, responseReader: ResponseBodyReader[T]): R[Response[T]]
+trait SttpStreamHandler[R[_], S] extends SttpHandler[R] {
+ def send[T](request: Request, responseAsStream: ResponseAsStream[S]): R[Response[S]]
+ def sendStream[T](request: Request, contentType: String, stream: S, responseReader: ResponseAs[T]): R[Response[T]]
} \ No newline at end of file
diff --git a/core/src/main/scala/com/softwaremill/sttp/package.scala b/core/src/main/scala/com/softwaremill/sttp/package.scala
index 459f13a..bb9c873 100644
--- a/core/src/main/scala/com/softwaremill/sttp/package.scala
+++ b/core/src/main/scala/com/softwaremill/sttp/package.scala
@@ -1,13 +1,11 @@
package com.softwaremill
-import java.io.File
-import java.io.InputStream
-import java.nio.file.Path
-import java.nio.ByteBuffer
+import java.io.{File, InputStream}
import java.net.URI
+import java.nio.ByteBuffer
+import java.nio.file.Path
-import scala.annotation.{implicitNotFound, tailrec}
-import scala.io.Source
+import scala.annotation.implicitNotFound
import scala.language.higherKinds
package object sttp {
@@ -30,7 +28,7 @@ package object sttp {
- stream responses (sendStreamAndReceive?) / strict responses
- make sure response is consumed - only fire request when we know what to do with response?
- - reuse connections / connectio pooling - in handler
+ - reuse connections / connection pooling - in handler
- handler restriction? AnyHandler <: Handler Restriction
@@ -70,45 +68,34 @@ package object sttp {
val PATCH = Method("PATCH")
}
- trait ResponseBodyReader[T] {
- def fromInputStream(is: InputStream): T
- def fromBytes(bytes: Array[Byte]): T
- }
- object IgnoreResponseBody extends ResponseBodyReader[Unit] {
- override def fromInputStream(is: InputStream): Unit = {
- @tailrec def consume(): Unit = if (is.read() != -1) consume()
- consume()
- }
- override def fromBytes(bytes: Array[Byte]): Unit = {}
- }
- implicit object StringResponseBody extends ResponseBodyReader[String] {
- override def fromInputStream(is: InputStream): String = {
- Source.fromInputStream(is, "UTF-8").mkString
- }
- override def fromBytes(bytes: Array[Byte]): String = {
- new String(bytes, "UTF-8")
- }
- }
+ sealed trait ResponseAs[T]
+ object IgnoreResponse extends ResponseAs[Unit]
+ case class ResponseAsString(encoding: String) extends ResponseAs[String]
+ object ResponseAsByteArray extends ResponseAs[Array[Byte]]
+
+ case class ResponseAsStream[-S]()
- def responseAs[T](implicit r: ResponseBodyReader[T]): ResponseBodyReader[T] = r
- def ignoreResponseBody: ResponseBodyReader[Unit] = IgnoreResponseBody
+ def ignoreResponse: ResponseAs[Unit] = IgnoreResponse
+ def responseAsString(encoding: String): ResponseAs[String] = ResponseAsString(encoding)
+ def responseAsByteArray: ResponseAs[Array[Byte]] = ResponseAsByteArray
+ def responseAsStream[S]: ResponseAsStream[S] = ResponseAsStream[S]()
sealed trait RequestBody
- sealed trait SimpleRequestBody
+ sealed trait BasicRequestBody extends RequestBody
case object NoBody extends RequestBody
- case class StringBody(s: String) extends RequestBody with SimpleRequestBody
- case class ByteArrayBody(b: Array[Byte]) extends RequestBody with SimpleRequestBody
- case class ByteBufferBody(b: ByteBuffer) extends RequestBody with SimpleRequestBody
- case class InputStreamBody(b: InputStream) extends RequestBody with SimpleRequestBody
- case class InputStreamSupplierBody(b: () => InputStream) extends RequestBody with SimpleRequestBody
- case class FileBody(f: File) extends RequestBody with SimpleRequestBody
- case class PathBody(f: Path) extends RequestBody with SimpleRequestBody
+ case class StringBody(s: 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 InputStreamSupplierBody(b: () => InputStream) extends BasicRequestBody
+ case class FileBody(f: File) extends BasicRequestBody
+ case class PathBody(f: Path) extends BasicRequestBody
/**
* Use the factory methods `multiPart` to conveniently create instances of this class. A part can be then
* further customised using `fileName`, `contentType` and `header` methods.
*/
- case class MultiPart(name: String, data: RequestBody with SimpleRequestBody, fileName: Option[String] = None,
+ case class MultiPart(name: String, data: BasicRequestBody, fileName: Option[String] = None,
contentType: Option[String] = None, additionalHeaders: Map[String, String] = Map()) {
def fileName(v: String): MultiPart = copy(fileName = Some(v))
def contentType(v: String): MultiPart = copy(contentType = Some(v))
@@ -155,16 +142,22 @@ package object sttp {
def multipartData(parts: MultiPart*): RequestTemplate[U] = ???
- def send[R[_], T](responseReader: ResponseBodyReader[T])(
+ def send[R[_], T](responseAs: ResponseAs[T])(
implicit handler: SttpHandler[R], isRequest: IsRequest[U]): R[Response[T]] = {
- handler.send(this, responseReader)
+ handler.send(this, responseAs)
+ }
+
+ def send[R[_], S](responseAs: ResponseAsStream[S])(
+ implicit handler: SttpStreamHandler[R, S], isRequest: IsRequest[U]): R[Response[S]] = {
+
+ handler.send(this, responseAs)
}
- def sendStream[R[_], S, T](contentType: String, stream: S, responseReader: ResponseBodyReader[T])(
+ def sendStream[R[_], S, T](contentType: String, stream: S, responseAs: ResponseAs[T])(
implicit handler: SttpStreamHandler[R, S], isRequest: IsRequest[U]): R[Response[T]] = {
- handler.sendStream(this, contentType, stream, responseReader)
+ handler.sendStream(this, contentType, stream, responseAs)
}
}