diff options
author | adamw <adam@warski.org> | 2017-07-28 09:34:35 +0200 |
---|---|---|
committer | adamw <adam@warski.org> | 2017-07-28 09:34:35 +0200 |
commit | 2b38d9b18620f9d4796f8b56f73e9b093051a1fa (patch) | |
tree | 0863d93df58d26a9e68fe16feec6602b7b1f8023 /core | |
parent | b645204f43c18db8e3ffc80c6f83bf44badb3e88 (diff) | |
download | sttp-2b38d9b18620f9d4796f8b56f73e9b093051a1fa.tar.gz sttp-2b38d9b18620f9d4796f8b56f73e9b093051a1fa.tar.bz2 sttp-2b38d9b18620f9d4796f8b56f73e9b093051a1fa.zip |
Updating scalafmt plugin
Diffstat (limited to 'core')
6 files changed, 240 insertions, 105 deletions
diff --git a/core/src/main/scala/com/softwaremill/sttp/RequestT.scala b/core/src/main/scala/com/softwaremill/sttp/RequestT.scala index 1aa8770..c582ea1 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] = - this.copy(headers = headers ++ hs.toSeq) + 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 new file mode 100644 index 0000000..c19bbc1 --- /dev/null +++ b/core/src/main/scala/com/softwaremill/sttp/Uri.scala @@ -0,0 +1,95 @@ +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 3e2ebcf..fc6815f 100644 --- a/core/src/main/scala/com/softwaremill/sttp/UriInterpolator.scala +++ b/core/src/main/scala/com/softwaremill/sttp/UriInterpolator.scala @@ -1,33 +1,35 @@ package com.softwaremill.sttp -import java.net.{URI, URLEncoder} +import java.net.URLDecoder object UriInterpolator { - def interpolate(sc: StringContext, args: Any*): URI = { + private type Decode = String => String + + def interpolate(sc: StringContext, args: Any*): Uri = { val strings = sc.parts.iterator val expressions = args.iterator - var ub = UriBuilderStart.parseS(strings.next()) + var ub = UriBuilderStart.parseS(strings.next(), defaultDecode) while (strings.hasNext) { ub = ub.parseE(expressions.next()) - ub = ub.parseS(strings.next()) + ub = ub.parseS(strings.next(), defaultDecode) } - new URI(ub.build) + ub.build } sealed trait UriBuilder { - def parseS(s: String): UriBuilder + def parseS(s: String, decode: Decode): UriBuilder def parseE(e: Any): UriBuilder - def build: String + def build: Uri - protected def parseE_asEncodedS_skipNone(e: Any): UriBuilder = e match { - case s: String => parseS(encode(s)) + protected def parseE_skipNone(e: Any): UriBuilder = e match { + case s: String => parseS(s, identity) case None => this case null => this case Some(x) => parseE(x) - case x => parseS(encode(x.toString)) + case x => parseS(x.toString, identity) } } @@ -35,12 +37,12 @@ object UriInterpolator { case class Scheme(v: String) extends UriBuilder { - override def parseS(s: String): UriBuilder = { + override def parseS(s: String, decode: Decode): UriBuilder = { val splitAtSchemeEnd = s.split("://", 2) splitAtSchemeEnd match { case Array(schemeFragment, rest) => - Authority(append(schemeFragment)) - .parseS(rest) + Authority(append(schemeFragment, decode)) + .parseS(rest, decode) case Array(x) => if (!x.matches("[a-zA-Z0-9+\\.\\-]*")) { @@ -48,122 +50,109 @@ 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(""), v).parseS(x) - } else append(x) + Authority(Scheme("http"), v).parseS(x, decode) + } else append(x, decode) } } - 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)) - } - } + override def parseE(e: Any): UriBuilder = parseE_skipNone(e) - private def append(x: String): Scheme = Scheme(v + x) + private def append(x: String, decode: Decode): Scheme = Scheme(v + x) - override def build: String = if (v.isEmpty) "" else v + "://" + override def build: Uri = Uri(v, "", None, Nil, Nil, None) } case class Authority(s: Scheme, v: String = "") extends UriBuilder { - override def parseS(s: String): UriBuilder = { + override def parseS(s: String, decode: Decode): 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).next(splitOn, rest) - case Array(x) => append(x) + append(authorityFragment, decode).next(splitOn, rest, decode) + case Array(x) => append(x, decode) } } - private def next(splitOn: Char, rest: String): UriBuilder = + private def next(splitOn: Char, rest: String, decode: Decode): UriBuilder = splitOn match { case '/' => // prepending the leading slash as we want it preserved in the // output, if present - Path(this).parseS("/" + rest) - case '?' => Query(Path(this)).parseS(rest) - case '#' => Fragment(Query(Path(this))).parseS(rest) + Path(this).parseS("/" + rest, decode) + case '?' => Query(Path(this)).parseS(rest, decode) + case '#' => Fragment(Query(Path(this))).parseS(rest, decode) } override def parseE(e: Any): UriBuilder = e match { case s: Seq[_] => - val newAuthority = s.map(_.toString).map(encode(_)).mkString(".") + val newAuthority = s.map(_.toString).mkString(".") copy(v = v + newAuthority) - case x => parseE_asEncodedS_skipNone(x) + case x => parseE_skipNone(x) } - override def build: String = { + override def build: Uri = { 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) - s.build + vv + 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) + } } - private def append(x: String): Authority = copy(v = v + x) + private def append(x: String, decode: Decode): Authority = + copy(v = v + decode(x)) } case class Path(a: Authority, fs: Vector[String] = Vector.empty) extends UriBuilder { - override def parseS(s: String): UriBuilder = { + override def parseS(s: String, decode: Decode): 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).next(splitOn, rest) - case Array(x) => appendS(x) + appendS(pathFragments, decode).next(splitOn, rest, decode) + case Array(x) => appendS(x, decode) } } - private def next(splitOn: Char, rest: String): UriBuilder = + private def next(splitOn: Char, rest: String, decode: Decode): UriBuilder = splitOn match { - case '?' => Query(this).parseS(rest) - case '#' => Fragment(Query(this)).parseS(rest) + case '?' => Query(this).parseS(rest, decode) + case '#' => Fragment(Query(this)).parseS(rest, decode) } override def parseE(e: Any): UriBuilder = e match { case s: Seq[_] => - val newFragments = s.map(_.toString).map(encode(_)).map(Some(_)) + val newFragments = s.map(_.toString).map(Some(_)) newFragments.foldLeft(this)(_.appendE(_)) - case s: String => appendE(Some(encode(s))) + case s: String => appendE(Some(s)) case None => appendE(None) case null => appendE(None) case Some(x) => parseE(x) - case x => appendE(Some(encode(x.toString))) + case x => appendE(Some(x.toString)) } - 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 - } + override def build: Uri = a.build.copy(path = fs) - private def appendS(fragments: String): Path = { + private def appendS(fragments: String, decode: Decode): 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)) + copy(fs = fs ++ fragments.substring(1).split("/", -1).map(decode)) else - copy(fs = fs ++ fragments.split("/", -1)) + copy(fs = fs ++ fragments.split("/", -1).map(decode)) } private def appendE(fragment: Option[String]): Path = fs.lastOption match { @@ -190,23 +179,23 @@ object UriInterpolator { import QueryFragment._ - override def parseS(s: String): UriBuilder = { + override def parseS(s: String, decode: Decode): UriBuilder = { s.split("#", 2) match { case Array(queryFragment, rest) => - Fragment(appendS(queryFragment)).parseS(rest) + Fragment(appendS(queryFragment, decode)).parseS(rest, decode) - case Array(x) => appendS(x) + case Array(x) => appendS(x, decode) } } override def parseE(e: Any): UriBuilder = e match { case m: Map[_, _] => parseSeq(m.toSeq) case s: Seq[_] => parseSeq(s) - case s: String => appendE(Some(encodeQuery(s))) + case s: String => appendE(Some(s)) case None => appendE(None) case null => appendE(None) case Some(x) => parseE(x) - case x => appendE(Some(encodeQuery(x.toString))) + case x => appendE(Some(x.toString)) } private def parseSeq(s: Seq[_]): UriBuilder = { @@ -219,40 +208,39 @@ object UriInterpolator { } val newFragments = flattenedS.map { case ("", "") => Eq - 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)) + 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) } copy(fs = fs ++ newFragments) } - override def build: String = { + override def build: Uri = { + val QF = com.softwaremill.sttp.QueryFragment val fragments = fs.flatMap { case Empty => None - 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") + 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)) } - val query = if (fragments.isEmpty) "" else "?" + fragments.mkString("&") - - p.build + query + p.build.copy(queryFragments = fragments) } - private def appendS(queryFragment: String): Query = { + private def appendS(queryFragment: String, decode: Decode): Query = { val newVs = queryFragment.split("&").map { nv => if (nv.isEmpty) Empty else if (nv == "=") Eq - else if (nv.startsWith("=")) Eq_V(nv.substring(1)) + else if (nv.startsWith("=")) Eq_V(decode(nv.substring(1))) else nv.split("=", 2) match { - case Array(n, "") => K_Eq(n) - case Array(n, v) => K_Eq_V(n, v) - case Array(n) => K(n) + case Array(n, "") => K_Eq(decode(n)) + case Array(n, v) => K_Eq_V(decode(n), decode(v)) + case Array(n) => K(decode(n)) } } @@ -306,22 +294,16 @@ object UriInterpolator { } case class Fragment(q: Query, v: String = "") extends UriBuilder { - override def parseS(s: String): UriBuilder = copy(v = v + s) + override def parseS(s: String, decode: Decode): UriBuilder = + copy(v = v + decode(s)) - override def parseE(e: Any): UriBuilder = parseE_asEncodedS_skipNone(e) - - override def build: String = q.build + (if (v.isEmpty) "" else s"#$v") - } + override def parseE(e: Any): UriBuilder = parseE_skipNone(e) - 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") + override def build: Uri = + q.build.copy(fragment = if (v.isEmpty) None else Some(v)) } - private def encodeQuery(s: Any): String = - URLEncoder.encode(String.valueOf(s), "UTF-8") + private def defaultDecode(v: String): String = URLDecoder.decode(v, Utf8) 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 5e7b13c..c2c4136 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 85348e6..b0dbf53 100644 --- a/core/src/test/scala/com/softwaremill/sttp/UriInterpolatorTests.scala +++ b/core/src/test/scala/com/softwaremill/sttp/UriInterpolatorTests.scala @@ -1,7 +1,5 @@ package com.softwaremill.sttp -import java.net.URI - import org.scalatest.{FunSuite, Matchers} class UriInterpolatorTests extends FunSuite with Matchers { @@ -15,7 +13,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/"), @@ -23,14 +21,17 @@ 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") + "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") ), "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"example.com?a=$v2queryEncoded") + (uri"example.com?a=$v2", s"http://example.com?a=$v2queryEncoded") ), "authority" -> List( (uri"http://$v1.com", s"http://$v1.com"), @@ -115,7 +116,7 @@ class UriInterpolatorTests extends FunSuite with Matchers { ((interpolated, expected), i) <- testCases.zipWithIndex } { test(s"[$groupName] interpolate to $expected (${i + 1})") { - interpolated should be(new URI(expected)) + interpolated.toString should be(expected) } } } diff --git a/core/src/test/scala/com/softwaremill/sttp/UriTests.scala b/core/src/test/scala/com/softwaremill/sttp/UriTests.scala new file mode 100644 index 0000000..e82cc9c --- /dev/null +++ b/core/src/test/scala/com/softwaremill/sttp/UriTests.scala @@ -0,0 +1,57 @@ +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) + } + } +} |