aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoradamw <adam@warski.org>2017-09-13 15:12:49 +0100
committeradamw <adam@warski.org>2017-09-13 15:12:49 +0100
commit82320c4e764edd493a826de26e6899ae4e2bf518 (patch)
treef57989f2b08839692d8ff46217916c4d6af378a2
parent1347386b75bf5a4784d20ba20d73eea6721b682f (diff)
downloadsttp-82320c4e764edd493a826de26e6899ae4e2bf518.tar.gz
sttp-82320c4e764edd493a826de26e6899ae4e2bf518.tar.bz2
sttp-82320c4e764edd493a826de26e6899ae4e2bf518.zip
By default, only escaping in queries the & and = reserved characters.
Other (allowed) are left intact. Adding an option to escape all to the URI API.
-rw-r--r--core/src/main/scala/com/softwaremill/sttp/Uri.scala91
-rw-r--r--core/src/test/scala/com/softwaremill/sttp/UriInterpolatorTests.scala4
-rw-r--r--core/src/test/scala/com/softwaremill/sttp/UriTests.scala23
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 {