diff options
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) } } |