aboutsummaryrefslogtreecommitdiff
path: root/core
diff options
context:
space:
mode:
authoradamw <adam@warski.org>2017-06-29 17:24:46 +0200
committeradamw <adam@warski.org>2017-06-29 17:24:46 +0200
commit034c40595f217ef1f11ca351666a03aa08976b81 (patch)
tree50ce45e9213120f7e6f74620c6b1eb4b3ffee86b /core
downloadsttp-034c40595f217ef1f11ca351666a03aa08976b81.tar.gz
sttp-034c40595f217ef1f11ca351666a03aa08976b81.tar.bz2
sttp-034c40595f217ef1f11ca351666a03aa08976b81.zip
Initital draft
Diffstat (limited to 'core')
-rw-r--r--core/src/main/scala/com/softwaremill/sttp/HttpConnectionSttpHandler.scala67
-rw-r--r--core/src/main/scala/com/softwaremill/sttp/Response.scala3
-rw-r--r--core/src/main/scala/com/softwaremill/sttp/SttpHandler.scala9
-rw-r--r--core/src/main/scala/com/softwaremill/sttp/UriInterpolator.scala32
-rw-r--r--core/src/main/scala/com/softwaremill/sttp/package.scala182
5 files changed, 293 insertions, 0 deletions
diff --git a/core/src/main/scala/com/softwaremill/sttp/HttpConnectionSttpHandler.scala b/core/src/main/scala/com/softwaremill/sttp/HttpConnectionSttpHandler.scala
new file mode 100644
index 0000000..f2d5527
--- /dev/null
+++ b/core/src/main/scala/com/softwaremill/sttp/HttpConnectionSttpHandler.scala
@@ -0,0 +1,67 @@
+package com.softwaremill.sttp
+
+import java.io.{InputStream, OutputStream, OutputStreamWriter}
+import java.net.HttpURLConnection
+import java.nio.channels.Channels
+import java.nio.file.Files
+
+import scala.annotation.tailrec
+
+class HttpConnectionSttpHandler extends SttpHandler[Id] {
+ override def send[T](r: Request, responseReader: ResponseBodyReader[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) }
+ c.setDoInput(true)
+ setBody(r, c)
+
+ val status = c.getResponseCode
+ Response(status, responseReader.fromInputStream(c.getInputStream))
+ }
+
+ private def setBody(r: Request, c: HttpURLConnection): Unit = {
+ if (r.body != NoBody) c.setDoOutput(true)
+
+ def copyStream(in: InputStream, out: OutputStream): Unit = {
+ val buf = new Array[Byte](1024)
+
+ @tailrec
+ def doCopy(): Unit = {
+ val read = in.read(buf)
+ if (read != -1) {
+ out.write(buf, 0, read)
+ doCopy()
+ }
+ }
+
+ doCopy()
+ }
+
+ r.body match {
+ case NoBody => // skip
+
+ case StringBody(b) =>
+ val writer = new OutputStreamWriter(c.getOutputStream)
+ try writer.write(b) finally writer.close()
+
+ case ByteArrayBody(b) =>
+ c.getOutputStream.write(b)
+
+ case ByteBufferBody(b) =>
+ val channel = Channels.newChannel(c.getOutputStream)
+ try channel.write(b) finally channel.close()
+
+ case InputStreamBody(b) =>
+ copyStream(b, c.getOutputStream)
+
+ case InputStreamSupplierBody(b) =>
+ copyStream(b(), c.getOutputStream)
+
+ case FileBody(b) =>
+ Files.copy(b.toPath, c.getOutputStream)
+
+ case PathBody(b) =>
+ Files.copy(b, c.getOutputStream)
+ }
+ }
+}
diff --git a/core/src/main/scala/com/softwaremill/sttp/Response.scala b/core/src/main/scala/com/softwaremill/sttp/Response.scala
new file mode 100644
index 0000000..4b90259
--- /dev/null
+++ b/core/src/main/scala/com/softwaremill/sttp/Response.scala
@@ -0,0 +1,3 @@
+package com.softwaremill.sttp
+
+case class Response[T](status: Int, body: T)
diff --git a/core/src/main/scala/com/softwaremill/sttp/SttpHandler.scala b/core/src/main/scala/com/softwaremill/sttp/SttpHandler.scala
new file mode 100644
index 0000000..a3b7685
--- /dev/null
+++ b/core/src/main/scala/com/softwaremill/sttp/SttpHandler.scala
@@ -0,0 +1,9 @@
+package com.softwaremill.sttp
+
+trait SttpHandler[R[_]] {
+ def send[T](request: Request, responseReader: ResponseBodyReader[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]]
+} \ No newline at end of file
diff --git a/core/src/main/scala/com/softwaremill/sttp/UriInterpolator.scala b/core/src/main/scala/com/softwaremill/sttp/UriInterpolator.scala
new file mode 100644
index 0000000..abb2327
--- /dev/null
+++ b/core/src/main/scala/com/softwaremill/sttp/UriInterpolator.scala
@@ -0,0 +1,32 @@
+package com.softwaremill.sttp
+
+// from https://gist.github.com/teigen/5865923
+object UriInterpolator {
+
+ private val unreserved = {
+ val alphanum = (('a' to 'z') ++ ('A' to 'Z') ++ ('0' to '9')).toSet
+ val mark = Set('-', '_', '.', '!', '~', '*', '\'', '(', ')')
+ alphanum ++ mark
+ }
+
+ implicit class UriContext(val sc:StringContext) extends AnyVal {
+ def uri(args:String*) = {
+ val strings = sc.parts.iterator
+ val expressions = args.iterator
+ val sb = new StringBuffer(strings.next())
+
+ while(strings.hasNext){
+ for(c <- expressions.next()){
+ if(unreserved(c))
+ sb.append(c)
+ else for(b <- c.toString.getBytes("UTF-8")){
+ sb.append("%")
+ sb.append("%02X".format(b))
+ }
+ }
+ sb.append(strings.next())
+ }
+ sb.toString
+ }
+ }
+}
diff --git a/core/src/main/scala/com/softwaremill/sttp/package.scala b/core/src/main/scala/com/softwaremill/sttp/package.scala
new file mode 100644
index 0000000..459f13a
--- /dev/null
+++ b/core/src/main/scala/com/softwaremill/sttp/package.scala
@@ -0,0 +1,182 @@
+package com.softwaremill
+
+import java.io.File
+import java.io.InputStream
+import java.nio.file.Path
+import java.nio.ByteBuffer
+import java.net.URI
+
+import scala.annotation.{implicitNotFound, tailrec}
+import scala.io.Source
+import scala.language.higherKinds
+
+package object sttp {
+ /*
+
+ - set headers
+ - set cookies (set from response)
+ - partial request (no uri + method) / full request
+ - start with an empty partial request
+ - multi-part uploads
+ - body: bytes, input stream (?), task/future, stream (fs2/akka), form data, file
+ - auth
+ - access uri/method/headers/cookies/body spec
+ - proxy
+ - user agent, buffer size
+ - charset
+ - zipped encodings
+ - SSL - mutual? (client side)
+
+ - 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
+
+ - handler restriction? AnyHandler <: Handler Restriction
+
+ Options:
+ - timeouts (connection/read)
+ - follow redirect
+ - ignore SSL
+
+ //
+
+ We want to serialize to:
+ - string
+ - byte array
+ - input stream
+ - handler-specific stream of bytes/strings
+
+ post:
+ - data (bytes/is/string - but which encoding?)
+ - form data (kv pairs - application/x-www-form-urlencoded)
+ - multipart (files mixed with forms - multipart/form-data)
+
+ */
+
+ //
+
+ type Id[X] = X
+ type Empty[X] = None.type
+
+ case class Method(m: String) extends AnyVal
+ object Method {
+ val GET = Method("GET")
+ val HEAD = Method("HEAD")
+ val POST = Method("POST")
+ val PUT = Method("PUT")
+ val DELETE = Method("DELETE")
+ val OPTIONS = Method("OPTIONS")
+ 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")
+ }
+ }
+
+ def responseAs[T](implicit r: ResponseBodyReader[T]): ResponseBodyReader[T] = r
+ def ignoreResponseBody: ResponseBodyReader[Unit] = IgnoreResponseBody
+
+ sealed trait RequestBody
+ sealed trait SimpleRequestBody
+ 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
+
+ /**
+ * 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,
+ 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))
+ def header(k: String, v: String): MultiPart = copy(additionalHeaders = additionalHeaders + (k -> v))
+ }
+
+ def multiPart(name: String, data: String): MultiPart = MultiPart(name, StringBody(data))
+ def multiPart(name: String, data: Array[Byte]): MultiPart = MultiPart(name, ByteArrayBody(data))
+ def multiPart(name: String, data: ByteBuffer): MultiPart = MultiPart(name, ByteBufferBody(data))
+ def multiPart(name: String, data: InputStream): MultiPart = MultiPart(name, InputStreamBody(data))
+ def multiPart(name: String, data: () => InputStream): MultiPart = MultiPart(name, InputStreamSupplierBody(data))
+ // mandatory content type?
+ def multiPart(name: String, data: File): MultiPart = MultiPart(name, FileBody(data), fileName = Some(data.getName))
+ def multiPart(name: String, data: Path): MultiPart = MultiPart(name, PathBody(data), fileName = Some(data.getFileName.toString))
+
+ case class RequestTemplate[U[_]](
+ method: U[Method],
+ uri: U[URI],
+ body: RequestBody,
+ headers: Map[String, String]
+ ) {
+
+ 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 header(k: String, v: String): RequestTemplate[U] = this.copy(headers = headers + (k -> v))
+
+ def data(b: String): RequestTemplate[U] = this.copy(body = StringBody(b))
+ def data(b: Array[Byte]): RequestTemplate[U] = this.copy(body = ByteArrayBody(b))
+ def data(b: ByteBuffer): RequestTemplate[U] = this.copy(body = ByteBufferBody(b))
+ def data(b: InputStream): RequestTemplate[U] = this.copy(body = InputStreamBody(b))
+ def data(b: () => InputStream): RequestTemplate[U] = this.copy(body = InputStreamSupplierBody(b))
+ // mandatory content type?
+ def data(b: File): RequestTemplate[U] = this.copy(body = FileBody(b))
+ def data(b: Path): RequestTemplate[U] = this.copy(body = PathBody(b))
+
+ def formData(fs: Map[String, String]): RequestTemplate[U] = ???
+ def formData(fs: (String, String)*): RequestTemplate[U] = ???
+
+ def multipartData(parts: MultiPart*): RequestTemplate[U] = ???
+
+ def send[R[_], T](responseReader: ResponseBodyReader[T])(
+ implicit handler: SttpHandler[R], isRequest: IsRequest[U]): R[Response[T]] = {
+
+ handler.send(this, responseReader)
+ }
+
+ def sendStream[R[_], S, T](contentType: String, stream: S, responseReader: ResponseBodyReader[T])(
+ implicit handler: SttpStreamHandler[R, S], isRequest: IsRequest[U]): R[Response[T]] = {
+
+ handler.sendStream(this, contentType, stream, responseReader)
+ }
+ }
+
+ object RequestTemplate {
+ val empty: RequestTemplate[Empty] = RequestTemplate[Empty](None, None, NoBody, Map.empty)
+ }
+
+ type PartialRequest = RequestTemplate[Empty]
+ type Request = RequestTemplate[Id]
+
+ @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
+}