From 8c7945ea87371d02906b84c45dc2d3b92f815306 Mon Sep 17 00:00:00 2001 From: adamw Date: Fri, 28 Jul 2017 09:43:06 +0200 Subject: Reverting in-progress changes --- .../scala/com/softwaremill/sttp/RequestT.scala | 2 +- .../src/main/scala/com/softwaremill/sttp/Uri.scala | 95 ----------- .../com/softwaremill/sttp/UriInterpolator.scala | 176 ++++++++++++--------- .../main/scala/com/softwaremill/sttp/package.scala | 2 +- .../softwaremill/sttp/UriInterpolatorTests.scala | 13 +- .../scala/com/softwaremill/sttp/UriTests.scala | 57 ------- 6 files changed, 105 insertions(+), 240 deletions(-) delete mode 100644 core/src/main/scala/com/softwaremill/sttp/Uri.scala delete mode 100644 core/src/test/scala/com/softwaremill/sttp/UriTests.scala 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) - } - } -} -- cgit v1.2.3