diff options
3 files changed, 392 insertions, 285 deletions
diff --git a/core/src/main/scala/com/softwaremill/sttp/Uri.scala b/core/src/main/scala/com/softwaremill/sttp/Uri.scala index 1d4b0c6..f66793a 100644 --- a/core/src/main/scala/com/softwaremill/sttp/Uri.scala +++ b/core/src/main/scala/com/softwaremill/sttp/Uri.scala @@ -3,17 +3,22 @@ package com.softwaremill.sttp import java.net.URLEncoder import Uri.{QueryFragment, UserInfo} -import Uri.QueryFragment.{KeyValue, Plain} +import Uri.QueryFragment.{KeyValue, Value, Plain} import scala.annotation.tailrec import scala.collection.immutable.Seq /** - * @param queryFragments Either key-value pairs, or plain query fragments. - * Key value pairs will be serialized as `k=v`, and blocks of key-value - * pairs will be combined using `&`. Note that no `&` or other separators - * are added around plain query fragments - if required, they need to be - * added manually as part of the plain query fragment. + * A [[https://en.wikipedia.org/wiki/Uniform_Resource_Identifier URI]]. + * All components (scheme, host, query, ...) are stored unencoded, and + * become encoded upon serialization (using [[toString]]). + * + * @param queryFragments Either key-value pairs, single values, or plain + * query fragments. Key value pairs will be serialized as `k=v`, and blocks + * of key-value pairs/single values will be combined using `&`. Note that no + * `&` or other separators are added around plain query fragments - if + * required, they need to be added manually as part of the plain query + * fragment. */ case class Uri(scheme: String, userInfo: Option[UserInfo], @@ -23,19 +28,6 @@ case class Uri(scheme: String, queryFragments: Seq[QueryFragment], fragment: Option[String]) { - def this(host: String) = - this("http", None, host, None, Vector.empty, Vector.empty, None) - def this(host: String, port: Int) = - this("http", None, host, Some(port), Vector.empty, Vector.empty, None) - def this(host: String, port: Int, path: Seq[String]) = - this("http", None, host, Some(port), path, Vector.empty, None) - def this(scheme: String, host: String) = - this(scheme, None, host, None, Vector.empty, Vector.empty, None) - def this(scheme: String, host: String, port: Int) = - this(scheme, None, host, Some(port), Vector.empty, Vector.empty, None) - def this(scheme: String, host: String, port: Int, path: Seq[String]) = - this(scheme, None, host, Some(port), path, Vector.empty, None) - def scheme(s: String): Uri = this.copy(scheme = s) def userInfo(username: String): Uri = @@ -100,19 +92,24 @@ case class Uri(scheme: String, @tailrec def encodeQueryFragments(qfs: List[QueryFragment], - previousWasKV: Boolean, + previousWasPlain: Boolean, sb: StringBuilder): String = qfs match { case Nil => sb.toString() case Plain(v, re) :: t => encodeQueryFragments(t, - previousWasKV = false, + previousWasPlain = true, sb.append(encodeQuery(v, re))) + case Value(v, re) :: t => + if (!previousWasPlain) sb.append("&") + sb.append(encodeQuery(v, re)) + encodeQueryFragments(t, previousWasPlain = false, sb) + case KeyValue(k, v, reK, reV) :: t => - if (previousWasKV) sb.append("&") + if (!previousWasPlain) sb.append("&") sb.append(encodeQuery(k, reK)).append("=").append(encodeQuery(v, reV)) - encodeQueryFragments(t, previousWasKV = true, sb) + encodeQueryFragments(t, previousWasPlain = false, sb) } val schemeS = encode(scheme) @@ -124,7 +121,7 @@ case class Uri(scheme: String, val queryPrefixS = if (queryFragments.isEmpty) "" else "?" val queryS = encodeQueryFragments(queryFragments.toList, - previousWasKV = false, + previousWasPlain = true, new StringBuilder()) val fragS = fragment.fold("")("#" + _) s"$schemeS://$userInfoS$hostS$portS$pathPrefixS$pathS$queryPrefixS$queryS$fragS" @@ -167,6 +164,19 @@ case class Uri(scheme: String, } object Uri { + def apply(host: String): Uri = + Uri("http", None, host, None, Vector.empty, Vector.empty, None) + def apply(host: String, port: Int): Uri = + Uri("http", None, host, Some(port), Vector.empty, Vector.empty, None) + def apply(host: String, port: Int, path: Seq[String]): Uri = + Uri("http", None, host, Some(port), path, Vector.empty, None) + def apply(scheme: String, host: String): Uri = + Uri(scheme, None, host, None, Vector.empty, Vector.empty, None) + def apply(scheme: String, host: String, port: Int): Uri = + Uri(scheme, None, host, Some(port), Vector.empty, Vector.empty, None) + def apply(scheme: String, host: String, port: Int, path: Seq[String]): Uri = + Uri(scheme, None, host, Some(port), path, Vector.empty, None) + sealed trait QueryFragment object QueryFragment { @@ -181,6 +191,12 @@ object Uri { extends QueryFragment /** + * A query fragment which contains only the value, without a key. + */ + case class Value(v: String, relaxedEncoding: Boolean = false) + extends QueryFragment + + /** * A query fragment which will be inserted into the query, without and * preceding or following separators. Allows constructing query strings * which are not (only) &-separated key-value pairs. diff --git a/core/src/main/scala/com/softwaremill/sttp/UriInterpolator.scala b/core/src/main/scala/com/softwaremill/sttp/UriInterpolator.scala index eec0b20..e90551f 100644 --- a/core/src/main/scala/com/softwaremill/sttp/UriInterpolator.scala +++ b/core/src/main/scala/com/softwaremill/sttp/UriInterpolator.scala @@ -1,5 +1,7 @@ package com.softwaremill.sttp +import java.net.URLDecoder + import scala.annotation.tailrec object UriInterpolator { @@ -7,340 +9,421 @@ object UriInterpolator { def interpolate(sc: StringContext, args: Any*): Uri = { val strings = sc.parts.iterator val expressions = args.iterator - var ub = UriBuilderStart.parseS(strings.next()) + + var (tokenizer, tokens) = SchemeTokenizer.tokenize(strings.next()) while (strings.hasNext) { - ub = ub.parseE(expressions.next()) - ub = ub.parseS(strings.next()) + // TODO + val exp = expressions.next() + if (tokens == Vector(StringToken("")) && exp.toString.contains("://")) { + val (nextTokenizer, nextTokens) = tokenizer.tokenize(exp.toString) + val (nextTokenizer2, nextTokens2) = + nextTokenizer.tokenize(strings.next()) + + val nextTokens3 = nextTokens2 match { + case StringToken("") +: tail => tail + case x => x + } + + tokenizer = nextTokenizer2 + tokens = tokens ++ nextTokens ++ nextTokens3 + } else { + tokens = tokens :+ ExpressionToken(exp) + + val (nextTokenizer, nextTokens) = tokenizer.tokenize(strings.next()) + tokenizer = nextTokenizer + tokens = tokens ++ nextTokens + } + } - ub.build - } + val builders = List( + SchemeBuilder, + UserInfoBuilder, + HostPortBuilder, + PathBuilder, + QueryBuilder, + FragmentBuilder + ) + + val startingUri = Uri("") + + val (uri, leftTokens) = + builders.foldLeft((startingUri, removeEmptyTokensAroundExp(tokens))) { + case ((u, t), builder) => + builder.fromTokens(u, t) + } - sealed trait UriBuilder { - def parseS(s: String): UriBuilder - def parseE(e: Any): UriBuilder - def build: Uri - - protected def parseE_skipNone(e: Any): UriBuilder = e match { - case s: String => parseS(s) - case None => this - case null => this - case Some(x) => parseE(x) - case x => parseS(x.toString) + if (leftTokens.nonEmpty) { + throw new IllegalArgumentException( + s"Tokens left after building the uri: $leftTokens") } - } - val UriBuilderStart = Scheme("") + uri + } - case class Scheme(v: String) extends UriBuilder { + sealed trait Token + case class StringToken(s: String) extends Token + case class ExpressionToken(e: Any) extends Token + case object SchemeEnd extends Token + case object ColonInAuthority extends Token + case object AtInAuthority extends Token + case object DotInAuthority extends Token + case object PathStart extends Token + case object SlashInPath extends Token + case object QueryStart extends Token + case object AmpInQuery extends Token + case object EqInQuery extends Token + case object FragmentStart extends Token + + trait Tokenizer { + def tokenize(s: String): (Tokenizer, Vector[Token]) + } - override def parseS(s: String): UriBuilder = { - val splitAtSchemeEnd = s.split("://", 2) - splitAtSchemeEnd match { - case Array(schemeFragment, rest) => - Authority(append(schemeFragment)) - .parseS(rest) + object SchemeTokenizer extends Tokenizer { + override def tokenize(s: String): (Tokenizer, Vector[Token]) = { + s.split("://", 2) match { + case Array(scheme, rest) => + val (next, authorityTokens) = AuthorityTokenizer.tokenize(rest) + (next, Vector(StringToken(scheme), SchemeEnd) ++ authorityTokens) case Array(x) => if (!x.matches("[a-zA-Z0-9+\\.\\-]*")) { // anything else than the allowed characters in scheme suggest that - // there is no scheme assuming whatever we parsed so far is part of - // authority, and parsing the rest; see + // there is no scheme; tokenizing using the next tokenizer in chain // https://stackoverflow.com/questions/3641722/valid-characters-for-uri-schemes - Authority(Scheme("http"), v).parseS(x) - } else append(x) + AuthorityTokenizer.tokenize(x) + } else { + (this, Vector(StringToken(x))) + } } } + } - override def parseE(e: Any): UriBuilder = parseE_skipNone(e) + object AuthorityTokenizer extends Tokenizer { + override def tokenize(s: String): (Tokenizer, Vector[Token]) = + tokenizeTerminatedFragment( + s, + this, + Set('/', '?', '#'), + Map(':' -> ColonInAuthority, + '@' -> AtInAuthority, + '.' -> DotInAuthority) + ) + } - private def append(x: String): Scheme = Scheme(v + x) + object PathTokenizer extends Tokenizer { + override def tokenize(s: String): (Tokenizer, Vector[Token]) = + tokenizeTerminatedFragment( + s, + this, + Set('?', '#'), + Map('/' -> SlashInPath) + ) + } - override def build: Uri = Uri(v, None, "", None, Nil, Nil, None) + object QueryTokenizer extends Tokenizer { + override def tokenize(s: String): (Tokenizer, Vector[Token]) = + tokenizeTerminatedFragment( + s, + this, + Set('#'), + Map('&' -> AmpInQuery, '=' -> EqInQuery) + ) } - case class Authority(s: Scheme, v: String = "") extends 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).next(splitOn, rest) - case Array(x) => append(x) + object FragmentTokenizer extends Tokenizer { + override def tokenize(s: String): (Tokenizer, Vector[Token]) = + (this, Vector(StringToken(s))) + } + + private def tokenizeTerminatedFragment( + s: String, + current: Tokenizer, + terminators: Set[Char], + fragmentCharsToTokens: Map[Char, Token]): (Tokenizer, Vector[Token]) = { + def tokenizeFragment(f: String): Vector[Token] = { + splitPreserveSeparators(f, fragmentCharsToTokens.keySet).map { t => + t.headOption.flatMap(fragmentCharsToTokens.get) match { + case Some(token) => token + case None => StringToken(t) + } } } - 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) - case '?' => Query(Path(this)).parseS(rest) - case '#' => Fragment(Query(Path(this))).parseS(rest) - } + // first checking if the fragment doesn't end; e.g. the 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 + split(s, terminators) match { + case Right((fragment, separator, rest)) => + tokenizeAfterSeparator(tokenizeFragment(fragment), separator, rest) - override def parseE(e: Any): UriBuilder = e match { - case s: Seq[_] => - val newAuthority = s.map(_.toString).mkString(".") - copy(v = v + newAuthority) - case x => parseE_skipNone(x) + case Left(fragment) => + (current, tokenizeFragment(fragment)) } + } - override def build: Uri = { - var vv = v - // remove dangling "." which might occur due to optional authority - // fragments - while (vv.startsWith(".")) vv = vv.substring(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) - } + private def tokenizeAfterSeparator(beforeSeparatorTokens: Vector[Token], + separator: Char, + s: String): (Tokenizer, Vector[Token]) = { + val (next, separatorToken) = separatorTokenizerAndToken(separator) + val (nextNext, nextTokens) = next.tokenize(s) + (nextNext, beforeSeparatorTokens ++ Vector(separatorToken) ++ nextTokens) + } + + private def separatorTokenizerAndToken(separator: Char): (Tokenizer, Token) = + separator match { + case '/' => (PathTokenizer, PathStart) + case '?' => (QueryTokenizer, QueryStart) + case '#' => (FragmentTokenizer, FragmentStart) } - private def append(x: String): Authority = - copy(v = v + x) + sealed trait UriBuilder { + def fromTokens(u: Uri, t: Vector[Token]): (Uri, Vector[Token]) } - case class Path(a: Authority, fs: Vector[String] = Vector.empty) - extends 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).next(splitOn, rest) - case Array(x) => appendS(x) + case object SchemeBuilder extends UriBuilder { + override def fromTokens(u: Uri, t: Vector[Token]): (Uri, Vector[Token]) = { + splitV(t, Set[Token](SchemeEnd)) match { + case Left(tt) => (u.scheme("http"), tt) + case Right((schemeTokens, _, otherTokens)) => + val scheme = tokensToString(schemeTokens) + (u.scheme(scheme), otherTokens) } } + } - private def next(splitOn: Char, rest: String): UriBuilder = - splitOn match { - case '?' => Query(this).parseS(rest) - case '#' => Fragment(Query(this)).parseS(rest) + case object UserInfoBuilder extends UriBuilder { + override def fromTokens(u: Uri, t: Vector[Token]): (Uri, Vector[Token]) = { + splitV(t, Set[Token](AtInAuthority)) match { + case Left(tt) => (u, tt) + case Right((uiTokens, _, otherTokens)) => + (uiFromTokens(u, uiTokens), otherTokens) } + } - override def parseE(e: Any): UriBuilder = e match { - case s: Seq[_] => - val newFragments = s.map(_.toString).map(Some(_)) - newFragments.foldLeft(this)(_.appendE(_)) - case s: String => appendE(Some(s)) - case None => appendE(None) - case null => appendE(None) - case Some(x) => parseE(x) - case x => appendE(Some(x.toString)) + private def uiFromTokens(u: Uri, uiTokens: Vector[Token]): Uri = { + val uiTokensWithDots = uiTokens.map { + case DotInAuthority => StringToken(".") + case x => x + } + splitV(uiTokensWithDots, Set[Token](ColonInAuthority)) match { + case Left(tt) => uiFromTokens(u, tt, Vector.empty) + case Right((usernameTokens, _, passwordTokens)) => + uiFromTokens(u, usernameTokens, passwordTokens) + } } - override def build: Uri = a.build.copy(path = fs) + private def uiFromTokens(u: Uri, + usernameTokens: Vector[Token], + passwordTokens: Vector[Token]): Uri = { + (tokensToStringOpt(usernameTokens), tokensToStringOpt(passwordTokens)) match { + case (Some(un), Some(p)) => u.userInfo(un, p) + case (Some(un), None) => u.userInfo(un) + case (None, Some(p)) => u.userInfo("", p) + case (None, None) => u + } + } + } + + case object HostPortBuilder extends UriBuilder { + override def fromTokens(u: Uri, t: Vector[Token]): (Uri, Vector[Token]) = { + splitV(t, Set[Token](PathStart, QueryStart, FragmentStart)) match { + case Left(tt) => + (hostPortFromTokens(u, tt), Vector.empty) + case Right((hpTokens, sep, otherTokens)) => + (hostPortFromTokens(u, hpTokens), sep +: otherTokens) + } + } - 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)) - else - copy(fs = fs ++ fragments.split("/", -1)) + private def hostPortFromTokens(u: Uri, hpTokens: Vector[Token]): Uri = { + splitV(hpTokens, Set[Token](ColonInAuthority)) match { + case Left(tt) => hostFromTokens(u, tt) + case Right((hostTokens, _, portTokens)) => + portFromTokens(hostFromTokens(u, hostTokens), portTokens) + } } - private def appendE(fragment: Option[String]): Path = fs.lastOption match { - case Some("") => - // if the currently last path fragment is empty, replacing with the - // expression value: corresponds to an interpolation of [anything]/$v - copy(fs = fs.init ++ fragment) - case _ => copy(fs = fs ++ fragment) + private def hostFromTokens(u: Uri, tokens: Vector[Token]): Uri = { + val hostFragments = tokensToStringSeq(tokens) + u.host(hostFragments.mkString(".")) } - } - sealed trait QueryFragment - object QueryFragment { - case object Empty extends QueryFragment - case class K_Eq_V(k: String, v: String) extends QueryFragment - case class K_Eq(k: String) extends QueryFragment - case class K(k: String) extends QueryFragment - case object Eq extends QueryFragment - case class Eq_V(v: String) extends QueryFragment + private def portFromTokens(u: Uri, tokens: Vector[Token]): Uri = { + u.port(tokensToStringOpt(tokens).map(_.toInt)) + } } - case class Query(p: Path, fs: Vector[QueryFragment] = Vector.empty) - extends UriBuilder { + case object PathBuilder extends UriBuilder { + override def fromTokens(u: Uri, t: Vector[Token]): (Uri, Vector[Token]) = { + t match { + case PathStart +: tt => + splitV(tt, Set[Token](QueryStart, FragmentStart)) match { + case Left(ttt) => + (pathFromTokens(u, ttt), Vector.empty) + case Right((pathTokens, sep, otherTokens)) => + (pathFromTokens(u, pathTokens), sep +: otherTokens) + } - import QueryFragment._ + case _ => (u, t) + } + } - override def parseS(s: String): UriBuilder = { - s.split("#", 2) match { - case Array(queryFragment, rest) => - Fragment(appendS(queryFragment)).parseS(rest) + private def pathFromTokens(u: Uri, tokens: Vector[Token]): Uri = { + u.path(tokensToStringSeq(tokens)) + } + } - case Array(x) => appendS(x) + case object QueryBuilder extends UriBuilder { + import com.softwaremill.sttp.Uri.{QueryFragment => QF} + + override def fromTokens(u: Uri, t: Vector[Token]): (Uri, Vector[Token]) = { + t match { + case QueryStart +: tt => + splitV(tt, Set[Token](FragmentStart)) match { + case Left(ttt) => + (queryFromTokens(u, ttt), Vector.empty) + case Right((queryTokens, sep, otherTokens)) => + (queryFromTokens(u, queryTokens), sep +: otherTokens) + } + + case _ => (u, t) } } - override def parseE(e: Any): UriBuilder = e match { - case m: Map[_, _] => parseSeqE(m.toSeq) - case s: Seq[_] => parseSeqE(s) - case s: String => appendE(Some(s)) - case None => appendE(None) - case null => appendE(None) - case Some(x) => parseE(x) - case x => appendE(Some(x.toString)) + private def queryFromTokens(u: Uri, tokens: Vector[Token]): Uri = { + val qfs = + splitToGroups(tokens, AmpInQuery) + .flatMap(queryMappingsFromTokens) + + u.copy(queryFragments = qfs) } - private def parseSeqE(s: Seq[_]): UriBuilder = { - val flattenedS = s.flatMap { - case (_, None) => None - case (k, Some(v)) => Some((k, v)) - case None => None - case Some(k) => Some(k) - case x => Some(x) + private def queryMappingsFromTokens(tokens: Vector[Token]): Vector[QF] = { + def expressionPairToQueryFragment(ke: Any, ve: Any): Option[QF.KeyValue] = + for { + k <- anyToStringOpt(ke) + v <- anyToStringOpt(ve) + } yield QF.KeyValue(k, v) + + def seqToQueryFragments(s: Seq[_]): Vector[QF] = { + s.flatMap { + case (ke, ve) => expressionPairToQueryFragment(ke, ve) + case ve => anyToStringOpt(ve).map(QF.Value(_)) + }.toVector } - 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) + + splitV(tokens, Set[Token](EqInQuery)) match { + case Left(Vector(ExpressionToken(e: Map[_, _]))) => + seqToQueryFragments(e.toSeq) + case Left(Vector(ExpressionToken(e: Seq[_]))) => seqToQueryFragments(e) + case Left(t) => tokensToStringOpt(t).map(QF.Value(_)).toVector + case Right((leftEq, _, rightEq)) => + tokensToStringOpt(leftEq) match { + case Some(k) => + tokensToStringSeq(rightEq).map(QF.KeyValue(k, _)).toVector + + case None => + Vector.empty + } } - copy(fs = fs ++ newFragments) } + } - override def build: Uri = { - import com.softwaremill.sttp.Uri.{QueryFragment => QF} - - val plainSeparator = QF.Plain("&", relaxedEncoding = true) - var fragments: Vector[QF] = 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) => - // if we have a key-only entry, we encode it as a plain query - // fragment - Some(QF.Plain(k)) - case Eq => Some(QF.KeyValue("", "")) - case Eq_V(v) => Some(QF.KeyValue("", v)) - } + case object FragmentBuilder extends UriBuilder { + override def fromTokens(u: Uri, t: Vector[Token]): (Uri, Vector[Token]) = { + t match { + case FragmentStart +: tt => + (u.fragment(tokensToStringOpt(tt)), Vector.empty) - // when serialized, plain query fragments don't have & separators - // prepended/appended - hence, if we have parsed them here, they - // need to be added by hand. Adding an & separator between each pair - // of fragments where one of them is plain. For example: - // KV P P KV KV P KV - // becomes: - // KV S P S P S KV KV S P S KV - @tailrec - def addPlainSeparators(qfs: Vector[QF], - previousWasPlain: Boolean, - acc: Vector[QF], - isFirst: Boolean = false): Vector[QF] = qfs match { - case Vector() => acc - case (p: QF.Plain) +: tail if !isFirst => - addPlainSeparators(tail, - previousWasPlain = true, - acc :+ plainSeparator :+ p) - case (p: QF.Plain) +: tail => - addPlainSeparators(tail, previousWasPlain = true, acc :+ p) - case (kv: QF.KeyValue) +: tail if previousWasPlain => - addPlainSeparators(tail, - previousWasPlain = false, - acc :+ plainSeparator :+ kv) - case (kv: QF.KeyValue) +: tail => - addPlainSeparators(tail, previousWasPlain = false, acc :+ kv) + case _ => (u, t) } + } + } - fragments = addPlainSeparators(fragments, - previousWasPlain = false, - Vector(), - isFirst = true) + private def anyToString(a: Any): String = anyToStringOpt(a).getOrElse("") - p.build.copy(queryFragments = fragments) - } + private def anyToStringOpt(a: Any): Option[String] = a match { + case None => None + case null => None + case Some(x) => Some(x.toString) + case x => Some(x.toString) + } - private def appendS(queryFragment: String): Query = { - val newVs = queryFragment.split("&", -1).map { nv => - if (nv.isEmpty) Empty - else if (nv == "=") Eq - else if (nv.startsWith("=")) Eq_V(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) - } - } + private def tokensToStringSeq(t: Vector[Token]): Seq[String] = t.flatMap { + case ExpressionToken(s: Seq[_]) => s.flatMap(anyToStringOpt).toVector + case ExpressionToken(e) => anyToStringOpt(e).toVector + case StringToken(s) => Vector(decode(s)) + case _ => Vector.empty + } - // it's possible that the current-last and new-first query fragments - // are indeed two parts of a single fragment. Attempting to merge, - // if possible - val (currentInit, currentLastV) = fs.splitAt(fs.length - 1) - val (newHeadV, newTail) = newVs.splitAt(1) - - val mergedOpt = for { - currentLast <- currentLastV.headOption - newHead <- newHeadV.headOption - } yield { - currentInit ++ merge(currentLast, newHead) ++ newTail - } + private def tokensToStringOpt(t: Vector[Token]): Option[String] = t match { + case Vector() => None + case Vector(ExpressionToken(e)) => anyToStringOpt(e) + case _ => Some(tokensToString(t)) + } - val combinedVs = mergedOpt match { - case None => fs ++ newVs // either current or new fragments are empty - case Some(merged) => merged + private def tokensToString(t: Vector[Token]): String = + t.collect { + case StringToken(s) => decode(s) + case ExpressionToken(e) => anyToString(e) + } + .mkString("") + + // TODO + private def removeEmptyTokensAroundExp(tokens: Vector[Token]): Vector[Token] = { + def doRemove(t: Vector[Token], acc: Vector[Token]): Vector[Token] = + t match { + case StringToken("") +: (e: ExpressionToken) +: tail => + doRemove(e +: tail, acc) + case (e: ExpressionToken) +: StringToken("") +: tail => + doRemove(tail, acc :+ e) + case v +: tail => doRemove(tail, acc :+ v) + case Vector() => acc } - copy(fs = combinedVs) - } + doRemove(tokens, Vector.empty) + } - private def merge(last: QueryFragment, - first: QueryFragment): Vector[QueryFragment] = { - /* - Only some combinations of fragments are possible. Part of them are - already handled in `appendE` (specifically, any expressions of - the form k=$v). Here we have to handle: $k=$v and $k=v. - */ - (last, first) match { - case (K(k), Eq) => Vector(K_Eq(k)) // k + = => k= - case (K(k), Eq_V(v)) => - Vector(K_Eq_V(k, v)) // k + =v => k=v - case (x, y) => Vector(x, y) - } - } + private def splitV[T]( + v: Vector[T], + sep: Set[T]): Either[Vector[T], (Vector[T], T, Vector[T])] = { + val i = v.indexWhere(sep.contains) + if (i == -1) Left(v) else Right((v.take(i), v(i), v.drop(i + 1))) + } - private def appendE(vo: Option[String]): Query = { - fs.lastOption match { - case Some(K_Eq(k)) => - // k= + None -> remove parameter; k= + Some(v) -> k=v - vo match { - case None => copy(fs = fs.init) - case Some("") => this - case Some(v) => copy(fs = fs.init :+ K_Eq_V(k, v)) - } - case _ => - copy(fs = fs :+ vo.fold[QueryFragment](Empty)(K)) + private def split(s: String, + sep: Set[Char]): Either[String, (String, Char, String)] = { + val i = s.indexWhere(sep.contains) + if (i == -1) Left(s) + else Right((s.substring(0, i), s.charAt(i), s.substring(i + 1))) + } + + private def splitToGroups[T](v: Vector[T], sep: T): Vector[Vector[T]] = { + def doSplit(vv: Vector[T], acc: Vector[Vector[T]]): Vector[Vector[T]] = { + vv.indexOf(sep) match { + case -1 => acc :+ vv + case i => doSplit(vv.drop(i + 1), acc :+ vv.take(i)) } } - } - case class Fragment(q: Query, v: String = "") extends UriBuilder { - override def parseS(s: String): UriBuilder = - copy(v = v + s) + doSplit(v, Vector.empty) + } - override def parseE(e: Any): UriBuilder = parseE_skipNone(e) + private def splitPreserveSeparators(s: String, + sep: Set[Char]): Vector[String] = { + @tailrec + def doSplit(s: String, acc: Vector[String]): Vector[String] = { + split(s, sep) match { + case Left(x) => acc :+ x + case Right((before, separator, after)) => + doSplit(after, acc ++ Vector(before, separator.toString)) + } + } - override def build: Uri = - q.build.copy(fragment = if (v.isEmpty) None else Some(v)) + doSplit(s, Vector.empty) } - private def charAfterPrefix(prefix: String, whole: String): Char = { - val pl = prefix.length - whole.substring(pl, pl + 1).charAt(0) - } + private def decode(s: String): String = URLDecoder.decode(s, Utf8) } diff --git a/core/src/test/scala/com/softwaremill/sttp/UriInterpolatorTests.scala b/core/src/test/scala/com/softwaremill/sttp/UriInterpolatorTests.scala index 9f0f081..4424d70 100644 --- a/core/src/test/scala/com/softwaremill/sttp/UriInterpolatorTests.scala +++ b/core/src/test/scala/com/softwaremill/sttp/UriInterpolatorTests.scala @@ -11,6 +11,8 @@ class UriInterpolatorTests extends FunSuite with Matchers { val v3encoded = "a%3F%3D%26c" val v4 = "f/g" val v4encoded = "f%2Fg" + val v5 = "a:b" + val v5encoded = "a%3Ab" val secure = true val testData: List[(String, List[(Uri, String)])] = List( @@ -30,6 +32,12 @@ class UriInterpolatorTests extends FunSuite with Matchers { s"https://example.com"), (uri"example.com?a=$v2", s"http://example.com?a=$v2queryEncoded") ), + "user info" -> List( + (uri"http://user:pass@example.com", s"http://user:pass@example.com"), + (uri"http://$v2@example.com", s"http://$v2encoded@example.com"), + (uri"http://$v5@example.com", s"http://$v5encoded@example.com"), + (uri"http://$v1:$v2@example.com", s"http://$v1:$v2encoded@example.com") + ), "authority" -> List( (uri"http://$v1.com", s"http://$v1.com"), (uri"http://$v2.com", s"http://$v2encoded.com"), |