aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoradamw <adam@warski.org>2017-07-28 09:43:06 +0200
committeradamw <adam@warski.org>2017-07-28 09:43:06 +0200
commit8c7945ea87371d02906b84c45dc2d3b92f815306 (patch)
tree8cda5f1fd5afd9515eb535c415e33212fb5d5156
parent2b38d9b18620f9d4796f8b56f73e9b093051a1fa (diff)
downloadsttp-8c7945ea87371d02906b84c45dc2d3b92f815306.tar.gz
sttp-8c7945ea87371d02906b84c45dc2d3b92f815306.tar.bz2
sttp-8c7945ea87371d02906b84c45dc2d3b92f815306.zip
Reverting in-progress changes
-rw-r--r--core/src/main/scala/com/softwaremill/sttp/RequestT.scala2
-rw-r--r--core/src/main/scala/com/softwaremill/sttp/Uri.scala95
-rw-r--r--core/src/main/scala/com/softwaremill/sttp/UriInterpolator.scala176
-rw-r--r--core/src/main/scala/com/softwaremill/sttp/package.scala2
-rw-r--r--core/src/test/scala/com/softwaremill/sttp/UriInterpolatorTests.scala13
-rw-r--r--core/src/test/scala/com/softwaremill/sttp/UriTests.scala57
6 files changed, 105 insertions, 240 deletions
diff --git a/core/src/main/scala/com/softwaremill/sttp/RequestT.scala b/core/src/main/scala/com/softwaremill/sttp/RequestT.scala
index c582ea1..1aa8770 100644
--- a/core/src/main/scala/com/softwaremill/sttp/RequestT.scala
+++ b/core/src/main/scala/com/softwaremill/sttp/RequestT.scala
@@ -63,7 +63,7 @@ case class RequestT[U[_], T, +S](
this.copy(headers = current :+ (k -> v))
}
def headers(hs: Map[String, String]): RequestT[U, T, S] =
- headers(hs.toSeq: _*)
+ this.copy(headers = headers ++ hs.toSeq)
def headers(hs: (String, String)*): RequestT[U, T, S] =
this.copy(headers = headers ++ hs)
def cookie(nv: (String, String)): RequestT[U, T, S] = cookies(nv)
diff --git a/core/src/main/scala/com/softwaremill/sttp/Uri.scala b/core/src/main/scala/com/softwaremill/sttp/Uri.scala
deleted file mode 100644
index c19bbc1..0000000
--- a/core/src/main/scala/com/softwaremill/sttp/Uri.scala
+++ /dev/null
@@ -1,95 +0,0 @@
-package com.softwaremill.sttp
-
-import java.net.URLEncoder
-
-import com.softwaremill.sttp.QueryFragment.{KeyValue, Plain}
-
-import scala.collection.immutable.Seq
-
-case class Uri(scheme: String,
- host: String,
- port: Option[Int],
- path: Seq[String],
- queryFragments: Seq[QueryFragment],
- fragment: Option[String]) {
-
- def scheme(s: String): Uri = this.copy(scheme = s)
-
- def host(h: String): Uri = this.copy(host = h)
-
- def port(p: Int): Uri = this.copy(port = Some(p))
-
- def port(p: Option[Int]): Uri = this.copy(port = p)
-
- def path(p: String): Uri = {
- // removing the leading slash, as it is added during serialization anyway
- val pWithoutLeadingSlash = if (p.startsWith("/")) p.substring(1) else p
- val ps = pWithoutLeadingSlash.split("/", -1).toList
- this.copy(path = ps)
- }
-
- def path(p1: String, p2: String, ps: String*): Uri =
- this.copy(path = p1 :: p2 :: ps.toList)
-
- def path(ps: scala.collection.Seq[String]): Uri = this.copy(path = ps.toList)
-
- /**
- * Adds the given parameter to the query.
- */
- def param(k: String, v: String): Uri = params(k -> v)
-
- /**
- * Adds the given parameters to the query.
- */
- def params(ps: Map[String, String]): Uri = params(ps.toSeq: _*)
-
- /**
- * Adds the given parameters to the query.
- */
- def params(ps: (String, String)*): Uri = {
- this.copy(queryFragments = queryFragments ++ ps.map(KeyValue.tupled))
- }
-
- def paramsMap: Map[String, String] = paramsSeq.toMap
-
- def paramsSeq: Seq[(String, String)] = queryFragments.collect {
- case KeyValue(k, v) => k -> v
- }
-
- def fragment(f: String): Uri = this.copy(fragment = Some(f))
-
- def fragment(f: Option[String]): Uri = this.copy(fragment = f)
-
- override def toString: String = {
- val schemeS = encode(scheme)
- val hostS = encode(host)
- val portS = port.fold("")(":" + _)
- val pathPrefixS = if (path.isEmpty) "" else "/"
- val pathS = path.map(encode).mkString("/")
- val queryPrefixS = if (queryFragments.isEmpty) "" else "?"
- val queryS = queryFragments
- .map {
- case KeyValue(k, v) => encodeQuery(k) + "=" + encodeQuery(v)
- case Plain(v, encode) => if (encode) encodeQuery(v) else v
- }
- .mkString("&")
- val fragS = fragment.fold("")("#" + _)
- s"$schemeS://$hostS$portS$pathPrefixS$pathS$queryPrefixS$queryS$fragS"
- }
-
- private def encode(s: Any): String = {
- // space is encoded as a +, which is only valid in the query;
- // in other contexts, it must be percent-encoded; see
- // https://stackoverflow.com/questions/2678551/when-to-encode-space-to-plus-or-20
- URLEncoder.encode(String.valueOf(s), "UTF-8").replaceAll("\\+", "%20")
- }
-
- private def encodeQuery(s: Any): String =
- URLEncoder.encode(String.valueOf(s), "UTF-8")
-}
-
-sealed trait QueryFragment
-object QueryFragment {
- case class KeyValue(k: String, v: String) extends QueryFragment
- case class Plain(v: String, encode: Boolean) extends QueryFragment
-}
diff --git a/core/src/main/scala/com/softwaremill/sttp/UriInterpolator.scala b/core/src/main/scala/com/softwaremill/sttp/UriInterpolator.scala
index fc6815f..3e2ebcf 100644
--- a/core/src/main/scala/com/softwaremill/sttp/UriInterpolator.scala
+++ b/core/src/main/scala/com/softwaremill/sttp/UriInterpolator.scala
@@ -1,35 +1,33 @@
package com.softwaremill.sttp
-import java.net.URLDecoder
+import java.net.{URI, URLEncoder}
object UriInterpolator {
- private type Decode = String => String
-
- def interpolate(sc: StringContext, args: Any*): Uri = {
+ def interpolate(sc: StringContext, args: Any*): URI = {
val strings = sc.parts.iterator
val expressions = args.iterator
- var ub = UriBuilderStart.parseS(strings.next(), defaultDecode)
+ var ub = UriBuilderStart.parseS(strings.next())
while (strings.hasNext) {
ub = ub.parseE(expressions.next())
- ub = ub.parseS(strings.next(), defaultDecode)
+ ub = ub.parseS(strings.next())
}
- ub.build
+ new URI(ub.build)
}
sealed trait UriBuilder {
- def parseS(s: String, decode: Decode): UriBuilder
+ def parseS(s: String): UriBuilder
def parseE(e: Any): UriBuilder
- def build: Uri
+ def build: String
- protected def parseE_skipNone(e: Any): UriBuilder = e match {
- case s: String => parseS(s, identity)
+ protected def parseE_asEncodedS_skipNone(e: Any): UriBuilder = e match {
+ case s: String => parseS(encode(s))
case None => this
case null => this
case Some(x) => parseE(x)
- case x => parseS(x.toString, identity)
+ case x => parseS(encode(x.toString))
}
}
@@ -37,12 +35,12 @@ object UriInterpolator {
case class Scheme(v: String) extends UriBuilder {
- override def parseS(s: String, decode: Decode): UriBuilder = {
+ override def parseS(s: String): UriBuilder = {
val splitAtSchemeEnd = s.split("://", 2)
splitAtSchemeEnd match {
case Array(schemeFragment, rest) =>
- Authority(append(schemeFragment, decode))
- .parseS(rest, decode)
+ Authority(append(schemeFragment))
+ .parseS(rest)
case Array(x) =>
if (!x.matches("[a-zA-Z0-9+\\.\\-]*")) {
@@ -50,109 +48,122 @@ object UriInterpolator {
// there is no scheme assuming whatever we parsed so far is part of
// authority, and parsing the rest; see
// https://stackoverflow.com/questions/3641722/valid-characters-for-uri-schemes
- Authority(Scheme("http"), v).parseS(x, decode)
- } else append(x, decode)
+ Authority(Scheme(""), v).parseS(x)
+ } else append(x)
}
}
- override def parseE(e: Any): UriBuilder = parseE_skipNone(e)
+ override def parseE(e: Any): UriBuilder = {
+ def encodeIfNotInitialEndpoint(s: String) = {
+ // special case: when this is the first expression, contains a complete
+ // schema with :// and nothing is yet parsed not escaping the contents
+ if (v.isEmpty && s.contains("://")) s else encode(s)
+ }
+
+ e match {
+ case s: String => parseS(encodeIfNotInitialEndpoint(s))
+ case None => this
+ case null => this
+ case Some(x) => parseE(x)
+ case x => parseS(encodeIfNotInitialEndpoint(x.toString))
+ }
+ }
- private def append(x: String, decode: Decode): Scheme = Scheme(v + x)
+ private def append(x: String): Scheme = Scheme(v + x)
- override def build: Uri = Uri(v, "", None, Nil, Nil, None)
+ override def build: String = if (v.isEmpty) "" else v + "://"
}
case class Authority(s: Scheme, v: String = "") extends UriBuilder {
- override def parseS(s: String, decode: Decode): UriBuilder = {
+ override def parseS(s: String): UriBuilder = {
// authority is terminated by /, ?, # or end of string (there might be
// other /, ?, # later on e.g. in the query)
// see https://tools.ietf.org/html/rfc3986#section-3.2
s.split("[/\\?#]", 2) match {
case Array(authorityFragment, rest) =>
val splitOn = charAfterPrefix(authorityFragment, s)
- append(authorityFragment, decode).next(splitOn, rest, decode)
- case Array(x) => append(x, decode)
+ append(authorityFragment).next(splitOn, rest)
+ case Array(x) => append(x)
}
}
- private def next(splitOn: Char, rest: String, decode: Decode): UriBuilder =
+ private def next(splitOn: Char, rest: String): UriBuilder =
splitOn match {
case '/' =>
// prepending the leading slash as we want it preserved in the
// output, if present
- Path(this).parseS("/" + rest, decode)
- case '?' => Query(Path(this)).parseS(rest, decode)
- case '#' => Fragment(Query(Path(this))).parseS(rest, decode)
+ Path(this).parseS("/" + rest)
+ case '?' => Query(Path(this)).parseS(rest)
+ case '#' => Fragment(Query(Path(this))).parseS(rest)
}
override def parseE(e: Any): UriBuilder = e match {
case s: Seq[_] =>
- val newAuthority = s.map(_.toString).mkString(".")
+ val newAuthority = s.map(_.toString).map(encode(_)).mkString(".")
copy(v = v + newAuthority)
- case x => parseE_skipNone(x)
+ case x => parseE_asEncodedS_skipNone(x)
}
- override def build: Uri = {
+ override def build: String = {
var vv = v
// remove dangling "." which might occur due to optional authority
// fragments
while (vv.startsWith(".")) vv = vv.substring(1)
+ while (vv.endsWith(".")) vv = vv.substring(0, vv.length - 1)
- val builtS = s.build
- vv.split(":", 2) match {
- case Array(host, port) if port.matches("\\d+") =>
- builtS.copy(host = host, port = Some(port.toInt))
- case Array(x) => builtS.copy(host = x)
- }
+ s.build + vv
}
- private def append(x: String, decode: Decode): Authority =
- copy(v = v + decode(x))
+ private def append(x: String): Authority = copy(v = v + x)
}
case class Path(a: Authority, fs: Vector[String] = Vector.empty)
extends UriBuilder {
- override def parseS(s: String, decode: Decode): UriBuilder = {
+ override def parseS(s: String): UriBuilder = {
// path is terminated by ?, # or end of string (there might be other
// ?, # later on e.g. in the query)
// see https://tools.ietf.org/html/rfc3986#section-3.3
s.split("[\\?#]", 2) match {
case Array(pathFragments, rest) =>
val splitOn = charAfterPrefix(pathFragments, s)
- appendS(pathFragments, decode).next(splitOn, rest, decode)
- case Array(x) => appendS(x, decode)
+ appendS(pathFragments).next(splitOn, rest)
+ case Array(x) => appendS(x)
}
}
- private def next(splitOn: Char, rest: String, decode: Decode): UriBuilder =
+ private def next(splitOn: Char, rest: String): UriBuilder =
splitOn match {
- case '?' => Query(this).parseS(rest, decode)
- case '#' => Fragment(Query(this)).parseS(rest, decode)
+ case '?' => Query(this).parseS(rest)
+ case '#' => Fragment(Query(this)).parseS(rest)
}
override def parseE(e: Any): UriBuilder = e match {
case s: Seq[_] =>
- val newFragments = s.map(_.toString).map(Some(_))
+ val newFragments = s.map(_.toString).map(encode(_)).map(Some(_))
newFragments.foldLeft(this)(_.appendE(_))
- case s: String => appendE(Some(s))
+ case s: String => appendE(Some(encode(s)))
case None => appendE(None)
case null => appendE(None)
case Some(x) => parseE(x)
- case x => appendE(Some(x.toString))
+ case x => appendE(Some(encode(x.toString)))
}
- override def build: Uri = a.build.copy(path = fs)
+ override def build: String = {
+ // if there is a trailing /, the last path fragment will be empty
+ val v = if (fs.isEmpty) "" else "/" + fs.mkString("/")
+ a.build + v
+ }
- private def appendS(fragments: String, decode: Decode): Path = {
+ private def appendS(fragments: String): Path = {
if (fragments.isEmpty) this
else if (fragments.startsWith("/"))
// avoiding an initial empty path fragment which would cause
// initial // the build output
- copy(fs = fs ++ fragments.substring(1).split("/", -1).map(decode))
+ copy(fs = fs ++ fragments.substring(1).split("/", -1))
else
- copy(fs = fs ++ fragments.split("/", -1).map(decode))
+ copy(fs = fs ++ fragments.split("/", -1))
}
private def appendE(fragment: Option[String]): Path = fs.lastOption match {
@@ -179,23 +190,23 @@ object UriInterpolator {
import QueryFragment._
- override def parseS(s: String, decode: Decode): UriBuilder = {
+ override def parseS(s: String): UriBuilder = {
s.split("#", 2) match {
case Array(queryFragment, rest) =>
- Fragment(appendS(queryFragment, decode)).parseS(rest, decode)
+ Fragment(appendS(queryFragment)).parseS(rest)
- case Array(x) => appendS(x, decode)
+ case Array(x) => appendS(x)
}
}
override def parseE(e: Any): UriBuilder = e match {
case m: Map[_, _] => parseSeq(m.toSeq)
case s: Seq[_] => parseSeq(s)
- case s: String => appendE(Some(s))
+ case s: String => appendE(Some(encodeQuery(s)))
case None => appendE(None)
case null => appendE(None)
case Some(x) => parseE(x)
- case x => appendE(Some(x.toString))
+ case x => appendE(Some(encodeQuery(x.toString)))
}
private def parseSeq(s: Seq[_]): UriBuilder = {
@@ -208,39 +219,40 @@ object UriInterpolator {
}
val newFragments = flattenedS.map {
case ("", "") => Eq
- case (k, "") => K_Eq(k.toString)
- case ("", v) => Eq_V(v.toString)
- case (k, v) => K_Eq_V(k.toString, v.toString)
- case x => K(x.toString)
+ case (k, "") => K_Eq(encodeQuery(k))
+ case ("", v) => Eq_V(encodeQuery(v))
+ case (k, v) => K_Eq_V(encodeQuery(k), encodeQuery(v))
+ case x => K(encodeQuery(x))
}
copy(fs = fs ++ newFragments)
}
- override def build: Uri = {
- val QF = com.softwaremill.sttp.QueryFragment
+ override def build: String = {
val fragments = fs.flatMap {
case Empty => None
- case K_Eq_V(k, v) => Some(QF.KeyValue(k, v))
- case K_Eq(k) => Some(QF.KeyValue(k, ""))
- case K(k) => Some(QF.Plain(k, encode = false))
- case Eq => Some(QF.KeyValue("", ""))
- case Eq_V(v) => Some(QF.KeyValue("", v))
+ case K_Eq_V(k, v) => Some(s"$k=$v")
+ case K_Eq(k) => Some(s"$k=")
+ case K(k) => Some(s"$k")
+ case Eq => Some("=")
+ case Eq_V(v) => Some(s"=$v")
}
- p.build.copy(queryFragments = fragments)
+ val query = if (fragments.isEmpty) "" else "?" + fragments.mkString("&")
+
+ p.build + query
}
- private def appendS(queryFragment: String, decode: Decode): Query = {
+ private def appendS(queryFragment: String): Query = {
val newVs = queryFragment.split("&").map { nv =>
if (nv.isEmpty) Empty
else if (nv == "=") Eq
- else if (nv.startsWith("=")) Eq_V(decode(nv.substring(1)))
+ else if (nv.startsWith("=")) Eq_V(nv.substring(1))
else
nv.split("=", 2) match {
- case Array(n, "") => K_Eq(decode(n))
- case Array(n, v) => K_Eq_V(decode(n), decode(v))
- case Array(n) => K(decode(n))
+ case Array(n, "") => K_Eq(n)
+ case Array(n, v) => K_Eq_V(n, v)
+ case Array(n) => K(n)
}
}
@@ -294,16 +306,22 @@ object UriInterpolator {
}
case class Fragment(q: Query, v: String = "") extends UriBuilder {
- override def parseS(s: String, decode: Decode): UriBuilder =
- copy(v = v + decode(s))
+ override def parseS(s: String): UriBuilder = copy(v = v + s)
- override def parseE(e: Any): UriBuilder = parseE_skipNone(e)
+ override def parseE(e: Any): UriBuilder = parseE_asEncodedS_skipNone(e)
+
+ override def build: String = q.build + (if (v.isEmpty) "" else s"#$v")
+ }
- override def build: Uri =
- q.build.copy(fragment = if (v.isEmpty) None else Some(v))
+ private def encode(s: Any): String = {
+ // space is encoded as a +, which is only valid in the query;
+ // in other contexts, it must be percent-encoded; see
+ // https://stackoverflow.com/questions/2678551/when-to-encode-space-to-plus-or-20
+ URLEncoder.encode(String.valueOf(s), "UTF-8").replaceAll("\\+", "%20")
}
- private def defaultDecode(v: String): String = URLDecoder.decode(v, Utf8)
+ private def encodeQuery(s: Any): String =
+ URLEncoder.encode(String.valueOf(s), "UTF-8")
private def charAfterPrefix(prefix: String, whole: String): Char = {
val pl = prefix.length
diff --git a/core/src/main/scala/com/softwaremill/sttp/package.scala b/core/src/main/scala/com/softwaremill/sttp/package.scala
index c2c4136..5e7b13c 100644
--- a/core/src/main/scala/com/softwaremill/sttp/package.scala
+++ b/core/src/main/scala/com/softwaremill/sttp/package.scala
@@ -163,6 +163,6 @@ package object sttp {
// uri interpolator
implicit class UriContext(val sc: StringContext) extends AnyVal {
- def uri(args: Any*): Uri = UriInterpolator.interpolate(sc, args: _*)
+ def uri(args: Any*): URI = UriInterpolator.interpolate(sc, args: _*)
}
}
diff --git a/core/src/test/scala/com/softwaremill/sttp/UriInterpolatorTests.scala b/core/src/test/scala/com/softwaremill/sttp/UriInterpolatorTests.scala
index b0dbf53..85348e6 100644
--- a/core/src/test/scala/com/softwaremill/sttp/UriInterpolatorTests.scala
+++ b/core/src/test/scala/com/softwaremill/sttp/UriInterpolatorTests.scala
@@ -1,5 +1,7 @@
package com.softwaremill.sttp
+import java.net.URI
+
import org.scalatest.{FunSuite, Matchers}
class UriInterpolatorTests extends FunSuite with Matchers {
@@ -13,7 +15,7 @@ class UriInterpolatorTests extends FunSuite with Matchers {
val v4encoded = "f%2Fg"
val secure = true
- val testData: List[(String, List[(Uri, String)])] = List(
+ val testData: List[(String, List[(URI, String)])] = List(
"basic" -> List(
(uri"http://example.com", "http://example.com"),
(uri"http://example.com/", "http://example.com/"),
@@ -21,17 +23,14 @@ class UriInterpolatorTests extends FunSuite with Matchers {
(uri"http://example.com/a/b/c", "http://example.com/a/b/c"),
(uri"http://example.com/a/b/c/", "http://example.com/a/b/c/"),
(uri"http://example.com/a/b/c?x=y&h=j",
- "http://example.com/a/b/c?x=y&h=j"),
- (uri"http://example.com/a%20b?v%26v=v+v",
- "http://example.com/a%20b?v%26v=v+v"),
- (uri"http://example.com?x=y;p", "http://example.com?x=y;p")
+ "http://example.com/a/b/c?x=y&h=j")
),
"scheme" -> List(
(uri"http${if (secure) "s" else ""}://example.com",
s"https://example.com"),
(uri"${if (secure) "https" else "http"}://example.com",
s"https://example.com"),
- (uri"example.com?a=$v2", s"http://example.com?a=$v2queryEncoded")
+ (uri"example.com?a=$v2", s"example.com?a=$v2queryEncoded")
),
"authority" -> List(
(uri"http://$v1.com", s"http://$v1.com"),
@@ -116,7 +115,7 @@ class UriInterpolatorTests extends FunSuite with Matchers {
((interpolated, expected), i) <- testCases.zipWithIndex
} {
test(s"[$groupName] interpolate to $expected (${i + 1})") {
- interpolated.toString should be(expected)
+ interpolated should be(new URI(expected))
}
}
}
diff --git a/core/src/test/scala/com/softwaremill/sttp/UriTests.scala b/core/src/test/scala/com/softwaremill/sttp/UriTests.scala
deleted file mode 100644
index e82cc9c..0000000
--- a/core/src/test/scala/com/softwaremill/sttp/UriTests.scala
+++ /dev/null
@@ -1,57 +0,0 @@
-package com.softwaremill.sttp
-
-import org.scalatest.{FunSuite, Matchers}
-
-class UriTests extends FunSuite with Matchers {
-
- val QF = QueryFragment
-
- val wholeUriTestData = List(
- Uri("http", "example.com", None, Nil, Nil, None) -> "http://example.com",
- Uri("https",
- "sub.example.com",
- Some(8080),
- List("a", "b", "xyz"),
- List(QF.KeyValue("p1", "v1"), QF.KeyValue("p2", "v2")),
- Some("f")) ->
- "https://sub.example.com:8080/a/b/xyz?p1=v1&p2=v2#f",
- Uri("http",
- "example.com",
- None,
- List(""),
- List(QF.KeyValue("p", "v"), QF.KeyValue("p", "v")),
- None) -> "http://example.com/?p=v&p=v",
- Uri("http",
- "exa mple.com",
- None,
- List("a b", "z", "ą:ę"),
- List(QF.KeyValue("p:1", "v&v"), QF.KeyValue("p2", "v v")),
- None) ->
- "http://exa%20mple.com/a%20b/z/%C4%85%3A%C4%99?p%3A1=v%26v&p2=v+v"
- )
-
- for {
- (uri, expected) <- wholeUriTestData
- } {
- test(s"$uri should serialize to $expected") {
- uri.toString should be(expected)
- }
- }
-
- val testUri = Uri("http", "example.com", None, Nil, Nil, None)
-
- val pathTestData = List(
- "a/b/c" -> List("a", "b", "c"),
- "/a/b/c" -> List("a", "b", "c"),
- "/" -> List(""),
- "" -> List("")
- )
-
- for {
- (path, expected) <- pathTestData
- } {
- test(s"$path should parse as $expected") {
- testUri.path(path).path.toList should be(expected)
- }
- }
-}