diff options
3 files changed, 82 insertions, 36 deletions
diff --git a/core/src/main/scala/com/softwaremill/sttp/Uri.scala b/core/src/main/scala/com/softwaremill/sttp/Uri.scala index ef94dd9..9945179 100644 --- a/core/src/main/scala/com/softwaremill/sttp/Uri.scala +++ b/core/src/main/scala/com/softwaremill/sttp/Uri.scala @@ -2,8 +2,8 @@ package com.softwaremill.sttp import java.net.URLEncoder -import Uri.{QueryFragment, UserInfo} -import Uri.QueryFragment.{KeyValue, Value, Plain} +import Uri.{QueryFragment, QueryFragmentEncoding, UserInfo} +import Uri.QueryFragment.{KeyValue, Plain, Value} import scala.annotation.tailrec import scala.collection.immutable.Seq @@ -124,14 +124,21 @@ case class Uri(scheme: String, val queryS = encodeQueryFragments(queryFragments.toList, previousWasPlain = true, new StringBuilder()) - val fragS = fragment.fold("")("#" + encodeFragment(_)) + + // https://stackoverflow.com/questions/2053132/is-a-colon-safe-for-friendly-url-use/2053640#2053640 + val fragS = fragment.fold("")("#" + encode(Rfc3986.Fragment)(_)) + s"$schemeS://$userInfoS$hostS$portS$pathPrefixS$pathS$queryPrefixS$queryS$fragS" } - private def encodeQuery(s: String, relaxed: Boolean): String = - if (relaxed) encodeQueryRelaxed(s) - else - URLEncoder.encode(String.valueOf(s), "UTF-8") + private def encodeQuery(s: String, e: QueryFragmentEncoding): String = + e match { + case QueryFragmentEncoding.All => URLEncoder.encode(s, "UTF-8") + case QueryFragmentEncoding.Standard => + encode(Rfc3986.QueryNoStandardDelims, spaceAsPlus = true)(s) + case QueryFragmentEncoding.Relaxed => + encode(Rfc3986.Query, spaceAsPlus = true)(s) + } private object Rfc3986 { val AlphaNum: Set[Char] = @@ -147,21 +154,21 @@ case class Uri(scheme: String, val PathSegment: Set[Char] = PChar val Query: Set[Char] = PChar ++ Set('/', '?') val Fragment: Set[Char] = Query - } - - // https://stackoverflow.com/questions/2322764/what-characters-must-be-escaped-in-an-http-query-string - private def encodeQueryRelaxed(s: String): String = - encode(Rfc3986.Query)(s) - // https://stackoverflow.com/questions/2053132/is-a-colon-safe-for-friendly-url-use/2053640#2053640 - private def encodeFragment(s: String): String = - encode(Rfc3986.Fragment)(s) + val QueryNoStandardDelims: Set[Char] = Query -- Set('&', '=') + } - private def encode(allowedCharacters: Set[Char])(s: String): String = { + /** + * @param spaceAsPlus In the query, space is encoded as a `+`. In other + * contexts, it should be %-encoded as `%20`. + */ + private def encode(allowedCharacters: Set[Char], + spaceAsPlus: Boolean = false)(s: String): String = { val sb = new StringBuilder() // based on https://gist.github.com/teigen/5865923 for (c <- s) { if (allowedCharacters(c)) sb.append(c) + else if (c == ' ' && spaceAsPlus) sb.append('+') else { for (b <- c.toString.getBytes("UTF-8")) { sb.append("%") @@ -191,19 +198,22 @@ object Uri { object QueryFragment { /** - * @param keyRelaxedEncoding See [[Plain.relaxedEncoding]] - * @param valueRelaxedEncoding See [[Plain.relaxedEncoding]] + * @param keyEncoding See [[Plain.encoding]] + * @param valueEncoding See [[Plain.encoding]] */ - case class KeyValue(k: String, - v: String, - keyRelaxedEncoding: Boolean = false, - valueRelaxedEncoding: Boolean = false) + case class KeyValue( + k: String, + v: String, + keyEncoding: QueryFragmentEncoding = QueryFragmentEncoding.Standard, + valueEncoding: QueryFragmentEncoding = QueryFragmentEncoding.Standard) extends QueryFragment /** * A query fragment which contains only the value, without a key. */ - case class Value(v: String, relaxedEncoding: Boolean = false) + case class Value(v: String, + relaxedEncoding: QueryFragmentEncoding = + QueryFragmentEncoding.Standard) extends QueryFragment /** @@ -211,17 +221,42 @@ object Uri { * preceding or following separators. Allows constructing query strings * which are not (only) &-separated key-value pairs. * - * @param relaxedEncoding Should characters, which are allowed in the - * query string, but normally escaped be left unchanged. These characters - * are: + * @param encoding Should reserved characters (in the RFC3986 sense), + * which are allowed in the query string, but can be also escaped be + * left unchanged. These characters are: * {{{ * /?:@-._~!$&()*+,;= * }}} - * See: [[https://stackoverflow.com/questions/2322764/what-characters-must-be-escaped-in-an-http-query-string]] + * See: + * [[https://stackoverflow.com/questions/2322764/what-characters-must-be-escaped-in-an-http-query-string]] + * [[https://stackoverflow.com/questions/2366260/whats-valid-and-whats-not-in-a-uri-query]] */ - case class Plain(v: String, relaxedEncoding: Boolean = false) + case class Plain(v: String, + encoding: QueryFragmentEncoding = + QueryFragmentEncoding.Standard) extends QueryFragment } + sealed trait QueryFragmentEncoding + object QueryFragmentEncoding { + + /** + * Encodes all reserved characters using [[java.net.URLEncoder.encode()]]. + */ + case object All extends QueryFragmentEncoding + + /** + * Encodes only the `&` and `=` reserved characters, which are usually + * used to separate query parameter names and values. + */ + case object Standard extends QueryFragmentEncoding + + /** + * Doesn't encode any of the reserved characters, leaving intact all + * characters allow in the query string as defined by RFC3986. + */ + case object Relaxed extends QueryFragmentEncoding + } + case class UserInfo(username: String, password: Option[String]) } diff --git a/core/src/test/scala/com/softwaremill/sttp/UriInterpolatorTests.scala b/core/src/test/scala/com/softwaremill/sttp/UriInterpolatorTests.scala index e9554df..9fc2d3a 100644 --- a/core/src/test/scala/com/softwaremill/sttp/UriInterpolatorTests.scala +++ b/core/src/test/scala/com/softwaremill/sttp/UriInterpolatorTests.scala @@ -8,7 +8,7 @@ class UriInterpolatorTests extends FunSuite with Matchers { val v2queryEncoded = "a+c" val v2encoded = "a%20c" val v3 = "a?=&c" - val v3encoded = "a%3F%3D%26c" + val v3encoded = "a?%3D%26c" val v4 = "f/g" val v4encoded = "f%2Fg" val v5 = "a:b" @@ -114,7 +114,7 @@ class UriInterpolatorTests extends FunSuite with Matchers { ), "everything" -> List( (uri"${"http"}://$v1.$v2.com/$v1/$v2?$v1=$v2&$v3=$v4#$v1", - s"http://$v1.$v2encoded.com/$v1/$v2encoded?$v1=$v2queryEncoded&$v3encoded=$v4encoded#$v1") + s"http://$v1.$v2encoded.com/$v1/$v2encoded?$v1=$v2queryEncoded&$v3encoded=$v4#$v1") ), "embed whole url" -> List( (uri"${"http://example.com:123/a"}/b/c", "http://example.com:123/a/b/c"), diff --git a/core/src/test/scala/com/softwaremill/sttp/UriTests.scala b/core/src/test/scala/com/softwaremill/sttp/UriTests.scala index f421413..341533a 100644 --- a/core/src/test/scala/com/softwaremill/sttp/UriTests.scala +++ b/core/src/test/scala/com/softwaremill/sttp/UriTests.scala @@ -1,6 +1,10 @@ package com.softwaremill.sttp -import com.softwaremill.sttp.Uri.{QueryFragment, UserInfo} +import com.softwaremill.sttp.Uri.{ + QueryFragment, + QueryFragmentEncoding, + UserInfo +} import org.scalatest.{FunSuite, Matchers} class UriTests extends FunSuite with Matchers { @@ -31,7 +35,7 @@ class UriTests extends FunSuite with Matchers { 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:%C4%99?p%3A1=v%26v&p2=v+v", + "http://exa%20mple.com/a%20b/z/%C4%85:%C4%99?p:1=v%26v&p2=v+v", Uri("http", Some(UserInfo("us&e/r", Some("pa ss"))), "example.com", @@ -82,10 +86,17 @@ class UriTests extends FunSuite with Matchers { QF.KeyValue("k3", "v3"), QF.KeyValue("k4", "v4")) -> "k1=v1&k2=v2-abc-k3=v3&k4=v4", List(QF.KeyValue("k1", "v1"), QF.Plain("&abc&"), QF.KeyValue("k2", "v2")) -> "k1=v1%26abc%26k2=v2", - List(QF.KeyValue("k1", "v1"), QF.Plain("&abc&", relaxedEncoding = true)) -> "k1=v1&abc&", - List(QF.KeyValue("k1?", "v1?", keyRelaxedEncoding = true)) -> "k1?=v1%3F", - List(QF.KeyValue("k1?", "v1?", valueRelaxedEncoding = true)) -> "k1%3F=v1?", - List(QF.Plain("ą/ę&+;?", relaxedEncoding = true)) -> "%C4%85/%C4%99&+;?" + List( + QF.KeyValue("k1", "v1"), + QF.Plain("&abc&", encoding = QueryFragmentEncoding.Relaxed)) -> "k1=v1&abc&", + List(QF.KeyValue("k1&", "v1&", keyEncoding = QueryFragmentEncoding.Relaxed)) -> "k1&=v1%26", + List(QF.KeyValue( + "k1&", + "v1&", + valueEncoding = QueryFragmentEncoding.Relaxed)) -> "k1%26=v1&", + List(QF.Plain("ą/ę&+;?", encoding = QueryFragmentEncoding.Relaxed)) -> "%C4%85/%C4%99&+;?", + List(QF.KeyValue("k", "v1,v2", valueEncoding = QueryFragmentEncoding.All)) -> "k=v1%2Cv2", + List(QF.KeyValue("k", "v1,v2")) -> "k=v1,v2" ) for { |