sttp.model.Uri.scala Maven / Gradle / Ivy
package sttp.model
import java.net.URI
import sttp.model.Uri.QuerySegment.{KeyValue, Plain, Value}
import sttp.model.Uri.{FragmentSegment, HostSegment, PathSegment, QuerySegment, Segment, UserInfo}
import sttp.model.internal.{Rfc3986, UriCompatibility, Validate}
import sttp.model.internal.Validate._
import scala.annotation.tailrec
import scala.collection.immutable.Seq
import scala.util.{Failure, Success, Try}
import sttp.model.internal.Rfc3986.encode
/** A [[https://en.wikipedia.org/wiki/Uniform_Resource_Identifier URI]].
* All components (scheme, host, query, ...) are stored decoded, and
* become encoded upon serialization (using [[toString]]).
*
* Instances can be created using the uri interpolator: `uri"..."` (see [[UriInterpolator]]), or the factory methods
* on the [[Uri]] companion object.
*
* @param querySegments Either key-value pairs, single values, or plain
* query segments. 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 segments - if
* required, they need to be added manually as part of the plain query
* segment.
*/
case class Uri(
scheme: String,
userInfo: Option[UserInfo],
hostSegment: Segment,
port: Option[Int],
pathSegments: Seq[Segment],
querySegments: Seq[QuerySegment],
fragmentSegment: Option[Segment]
) {
/** Replace the scheme. Does not validate the new scheme value. */
def scheme(s: String): Uri = this.copy(scheme = s)
def userInfo(username: String): Uri =
this.copy(userInfo = Some(UserInfo(username, None)))
def userInfo(username: String, password: String): Uri =
this.copy(userInfo = Some(UserInfo(username, Some(password))))
/** Replace the host. Does not validate the new host value if it's nonempty. */
def host(h: String): Uri = hostSegment(HostSegment(h))
/** Replace the host. Does not validate the new host value if it's nonempty. */
def hostSegment(s: Segment): Uri = this.copy(hostSegment = s)
def host: String = hostSegment.v
//
def port(p: Int): Uri = this.copy(port = Some(p))
def port(p: Option[Int]): Uri = this.copy(port = p)
//
/** Replace path with the given single-segment path. */
@deprecated(message = "Use addPath, withPath or withWholePath", since = "1.2.0")
def path(p: String): Uri = withWholePath(p)
/** Replace path with the given path segments. */
@deprecated(message = "Use addPath, withPath or withWholePath", since = "1.2.0")
def path(p1: String, p2: String, ps: String*): Uri = withPath(p1 :: p2 :: ps.toList)
/** Replace path with the given path segments. */
@deprecated(message = "Use addPath, withPath or withWholePath", since = "1.2.0")
def path(ps: scala.collection.Seq[String]): Uri = withPath(ps)
/** Replace path with the given path segment. */
@deprecated(message = "Use addPath, withPath or withWholePath", since = "1.2.0")
def pathSegment(s: Segment): Uri = withPathSegment(s)
/** Replace path with the given path segment. */
@deprecated(message = "Use addPath, withPath or withWholePath", since = "1.2.0")
def pathSegments(s1: Segment, s2: Segment, ss: Segment*): Uri = withPathSegments(s1, s2, ss: _*)
/** Replace path with the given path segments. */
@deprecated(message = "Use addPath, withPath or withWholePath", since = "1.2.0")
def pathSegments(ss: scala.collection.Seq[Segment]): Uri = withPathSegments(ss.toList)
def addPath(p: String): Uri = addPath(List(p))
def addPath(p: String, ps: String*): Uri = addPath(p :: ps.toList)
def addPath(ps: scala.collection.Seq[String]): Uri = addPathSegments(ps.toList.map(PathSegment(_)))
def addPathSegment(s: Segment): Uri = addPathSegments(List(s))
def addPathSegments(s1: Segment, s2: Segment, ss: Segment*): Uri = addPathSegments(s1 :: s2 :: ss.toList)
def addPathSegments(ss: scala.collection.Seq[Segment]): Uri = this.copy(pathSegments = pathSegments ++ ss.toList)
/** Replace the whole path with the given one. Leading `/` will be removed, if present, and the path will be
* split into segments on `/`.
*/
def withWholePath(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
withPath(ps)
}
def withPath(p: String, ps: String*): Uri = withPath(p :: ps.toList)
def withPath(ps: scala.collection.Seq[String]): Uri = withPathSegments(ps.toList.map(PathSegment(_)))
def withPathSegment(s: Segment): Uri = withPathSegments(List(s))
def withPathSegments(s1: Segment, s2: Segment, ss: Segment*): Uri = withPathSegments(s1 :: s2 :: ss.toList)
def withPathSegments(ss: scala.collection.Seq[Segment]): Uri = this.copy(pathSegments = ss.toList)
def path: Seq[String] = pathSegments.map(_.v)
//
/** Adds the given parameter to the query. */
@deprecated(message = "Use addParam or withParam", since = "1.2.0")
def param(k: String, v: String): Uri = addParam(k, v)
/** Adds the given parameter with an optional value to the query if it is present. */
@deprecated(message = "Use addParam or withParam", since = "1.2.0")
def param(k: String, v: Option[String]): Uri = addParam(k, v)
/** Adds the given parameters to the query. */
@deprecated(message = "Use addParam or withParam", since = "1.2.0")
def params(ps: Map[String, String]): Uri = addParams(ps)
/** Adds the given parameters to the query. */
@deprecated(message = "Use addParam or withParam", since = "1.2.0")
def params(mqp: QueryParams): Uri = addParams(mqp)
/** Adds the given parameters to the query. */
@deprecated(message = "Use addParam or withParam", since = "1.2.0")
def params(ps: (String, String)*): Uri = addParams(ps: _*)
def addParam(k: String, v: String): Uri = addParams(k -> v)
def addParam(k: String, v: Option[String]): Uri = v.map(addParam(k, _)).getOrElse(this)
def addParams(ps: Map[String, String]): Uri = addParams(ps.toSeq: _*)
def addParams(mqp: QueryParams): Uri = {
this.copy(querySegments = querySegments ++ QuerySegment.fromQueryParams(mqp))
}
def addParams(ps: (String, String)*): Uri = {
this.copy(querySegments = querySegments ++ ps.map { case (k, v) =>
KeyValue(k, v)
})
}
/** Replace query with the given single parameter. */
def withParam(k: String, v: String): Uri = withParams(k -> v)
/** Replace query with the given single optional parameter. */
def withParam(k: String, v: Option[String]): Uri = v.map(withParam(k, _)).getOrElse(this)
/** Replace query with the given parameters. */
def withParams(ps: Map[String, String]): Uri = withParams(ps.toSeq: _*)
/** Replace query with the given parameters. */
def withParams(mqp: QueryParams): Uri = this.copy(querySegments = QuerySegment.fromQueryParams(mqp).toList)
/** Replace query with the given parameters. */
def withParams(ps: (String, String)*): Uri = this.copy(querySegments = ps.map { case (k, v) =>
KeyValue(k, v)
}.toList)
def paramsMap: Map[String, String] = paramsSeq.toMap
def params: QueryParams = QueryParams.fromSeq(paramsSeq)
def paramsSeq: Seq[(String, String)] =
querySegments.collect { case KeyValue(k, v, _, _) =>
k -> v
}
/** Adds the given query segment.
*/
def querySegment(qf: QuerySegment): Uri =
this.copy(querySegments = querySegments :+ qf)
//
/** Replace the fragment.
*/
def fragment(f: String): Uri = fragment(Some(f))
/** Replace the fragment.
*/
def fragment(f: Option[String]): Uri = fragmentSegment(f.map(FragmentSegment(_)))
/** Replace the fragment.
*/
def fragmentSegment(s: Option[Segment]): Uri = this.copy(fragmentSegment = s)
def fragment: Option[String] = fragmentSegment.map(_.v)
//
def toJavaUri: URI = new URI(toString())
override def toString: String = {
def encodeUserInfo(ui: UserInfo): String =
encode(Rfc3986.UserInfo)(ui.username) + ui.password.fold("")(":" + encode(Rfc3986.UserInfo)(_))
@tailrec
def encodeQuerySegments(qss: List[QuerySegment], previousWasPlain: Boolean, sb: StringBuilder): String =
qss match {
case Nil => sb.toString()
case Plain(v, enc) :: t =>
encodeQuerySegments(t, previousWasPlain = true, sb.append(enc(v)))
case Value(v, enc) :: t =>
if (!previousWasPlain) sb.append("&")
sb.append(enc(v))
encodeQuerySegments(t, previousWasPlain = false, sb)
case KeyValue(k, v, kEnc, vEnc) :: t =>
if (!previousWasPlain) sb.append("&")
sb.append(kEnc(k)).append("=").append(vEnc(v))
encodeQuerySegments(t, previousWasPlain = false, sb)
}
val schemeS = encode(Rfc3986.Scheme)(scheme)
val userInfoS = userInfo.fold("")(encodeUserInfo(_) + "@")
val hostS = hostSegment.encoded
val portS = port.fold("")(":" + _)
val pathPrefixS = if (pathSegments.isEmpty) "" else "/"
val pathS = pathSegments.map(_.encoded).mkString("/")
val queryPrefixS = if (querySegments.isEmpty) "" else "?"
val queryS = encodeQuerySegments(querySegments.toList, previousWasPlain = true, new StringBuilder())
// https://stackoverflow.com/questions/2053132/is-a-colon-safe-for-friendly-url-use/2053640#2053640
val fragS = fragmentSegment.fold("")(s => "#" + s.encoded)
s"$schemeS://$userInfoS$hostS$portS$pathPrefixS$pathS$queryPrefixS$queryS$fragS"
}
}
/** For a general description of the behavior of `apply`, `parse`, `safeApply` and `unsafeApply` methods, see [[sttp.model]].
*
* The `safeApply` methods return a validation error if the scheme contains illegal characters or if the host is empty.
*/
object Uri extends UriInterpolator {
private val AllowedSchemeCharacters = "[a-zA-Z][a-zA-Z0-9+-.]*".r
private def validateHost(h: String): Option[String] = if (h.isEmpty) Some("Host cannot be empty") else None
private def validateScheme(s: String) =
if (AllowedSchemeCharacters.unapplySeq(s).isEmpty)
Some("Scheme can only contain alphanumeric characters, +, - and .")
else None
//
def safeApply(host: String): Either[String, Uri] =
safeApply("http", None, HostSegment(host), None, Vector.empty, Vector.empty, None)
def safeApply(host: String, port: Int): Either[String, Uri] =
safeApply("http", None, HostSegment(host), Some(port), Vector.empty, Vector.empty, None)
def safeApply(host: String, port: Int, path: Seq[String]): Either[String, Uri] =
safeApply("http", None, HostSegment(host), Some(port), path.map(PathSegment(_)), Vector.empty, None)
def safeApply(scheme: String, host: String): Either[String, Uri] =
safeApply(scheme, None, HostSegment(host), None, Vector.empty, Vector.empty, None)
def safeApply(scheme: String, host: String, port: Int): Either[String, Uri] =
safeApply(scheme, None, HostSegment(host), Some(port), Vector.empty, Vector.empty, None)
def safeApply(scheme: String, host: String, port: Int, path: Seq[String]): Either[String, Uri] =
safeApply(scheme, None, HostSegment(host), Some(port), path.map(PathSegment(_)), Vector.empty, None)
def safeApply(scheme: String, host: String, path: Seq[String]): Either[String, Uri] =
safeApply(scheme, None, HostSegment(host), None, path.map(PathSegment(_)), Vector.empty, None)
def safeApply(scheme: String, host: String, path: Seq[String], fragment: Option[String]): Either[String, Uri] =
safeApply(
scheme,
None,
HostSegment(host),
None,
path.map(PathSegment(_)),
Vector.empty,
fragment.map(FragmentSegment(_))
)
def safeApply(
scheme: String,
userInfo: Option[UserInfo],
host: String,
port: Option[Int],
path: Seq[String],
querySegments: Seq[QuerySegment],
fragment: Option[String]
): Either[String, Uri] =
safeApply(
scheme,
userInfo,
HostSegment(host),
port,
path.map(PathSegment(_)),
querySegments,
fragment.map(FragmentSegment(_))
)
def safeApply(
scheme: String,
userInfo: Option[UserInfo],
hostSegment: Segment,
port: Option[Int],
pathSegments: Seq[Segment],
querySegments: Seq[QuerySegment],
fragmentSegment: Option[Segment]
): Either[String, Uri] =
Validate.all(validateScheme(scheme), validateHost(hostSegment.v))(
apply(scheme, userInfo, hostSegment, port, pathSegments, querySegments, fragmentSegment)
)
//
def unsafeApply(host: String): Uri =
unsafeApply("http", None, HostSegment(host), None, Vector.empty, Vector.empty, None)
def unsafeApply(host: String, port: Int): Uri =
unsafeApply("http", None, HostSegment(host), Some(port), Vector.empty, Vector.empty, None)
def unsafeApply(host: String, port: Int, path: Seq[String]): Uri =
unsafeApply("http", None, HostSegment(host), Some(port), path.map(PathSegment(_)), Vector.empty, None)
def unsafeApply(scheme: String, host: String): Uri =
unsafeApply(scheme, None, HostSegment(host), None, Vector.empty, Vector.empty, None)
def unsafeApply(scheme: String, host: String, port: Int): Uri =
unsafeApply(scheme, None, HostSegment(host), Some(port), Vector.empty, Vector.empty, None)
def unsafeApply(scheme: String, host: String, port: Int, path: Seq[String]): Uri =
unsafeApply(scheme, None, HostSegment(host), Some(port), path.map(PathSegment(_)), Vector.empty, None)
def unsafeApply(scheme: String, host: String, path: Seq[String]): Uri =
unsafeApply(scheme, None, HostSegment(host), None, path.map(PathSegment(_)), Vector.empty, None)
def unsafeApply(scheme: String, host: String, path: Seq[String], fragment: Option[String]): Uri =
unsafeApply(
scheme,
None,
HostSegment(host),
None,
path.map(PathSegment(_)),
Vector.empty,
fragment.map(FragmentSegment(_))
)
def unsafeApply(
scheme: String,
userInfo: Option[UserInfo],
host: String,
port: Option[Int],
path: Seq[String],
querySegments: Seq[QuerySegment],
fragment: Option[String]
): Uri =
unsafeApply(
scheme,
userInfo,
HostSegment(host),
port,
path.map(PathSegment(_)),
querySegments,
fragment.map(FragmentSegment(_))
)
def unsafeApply(
scheme: String,
userInfo: Option[UserInfo],
hostSegment: Segment,
port: Option[Int],
pathSegments: Seq[Segment],
querySegments: Seq[QuerySegment],
fragmentSegment: Option[Segment]
): Uri =
safeApply(scheme, userInfo, hostSegment, port, pathSegments, querySegments, fragmentSegment).getOrThrow
//
def apply(host: String): Uri =
apply("http", None, HostSegment(host), None, Vector.empty, Vector.empty, None)
def apply(host: String, port: Int): Uri =
apply("http", None, HostSegment(host), Some(port), Vector.empty, Vector.empty, None)
def apply(host: String, port: Int, path: Seq[String]): Uri =
apply("http", None, HostSegment(host), Some(port), path.map(PathSegment(_)), Vector.empty, None)
def apply(scheme: String, host: String): Uri =
apply(scheme, None, HostSegment(host), None, Vector.empty, Vector.empty, None)
def apply(scheme: String, host: String, port: Int): Uri =
apply(scheme, None, HostSegment(host), Some(port), Vector.empty, Vector.empty, None)
def apply(scheme: String, host: String, port: Int, path: Seq[String]): Uri =
apply(scheme, None, HostSegment(host), Some(port), path.map(PathSegment(_)), Vector.empty, None)
def apply(scheme: String, host: String, path: Seq[String]): Uri =
apply(scheme, None, HostSegment(host), None, path.map(PathSegment(_)), Vector.empty, None)
def apply(scheme: String, host: String, path: Seq[String], fragment: Option[String]): Uri =
apply(
scheme,
None,
HostSegment(host),
None,
path.map(PathSegment(_)),
Vector.empty,
fragment.map(FragmentSegment(_))
)
def apply(
scheme: String,
userInfo: Option[UserInfo],
host: String,
port: Option[Int],
path: Seq[String],
querySegments: Seq[QuerySegment],
fragment: Option[String]
): Uri =
apply(
scheme,
userInfo,
HostSegment(host),
port,
path.map(PathSegment(_)),
querySegments,
fragment.map(FragmentSegment(_))
)
//
def apply(javaUri: URI): Uri = uri"${javaUri.toString}"
def parse(uri: String): Either[String, Uri] =
Try(uri"$uri") match {
case Success(u) => Right(u)
case Failure(e: Exception) => Left(e.getMessage)
case Failure(t: Throwable) => throw t
}
def unsafeParse(uri: String): Uri = uri"$uri"
case class Segment(v: String, encoding: Encoding) {
def encoded: String = encoding(v)
}
object HostSegment {
def apply(v: String): Segment = Segment(v, HostEncoding.Standard)
}
object PathSegment {
def apply(v: String): Segment = Segment(v, PathSegmentEncoding.Standard)
}
object FragmentSegment {
def apply(v: String): Segment = Segment(v, FragmentEncoding.Standard)
}
sealed trait QuerySegment
object QuerySegment {
/** @param keyEncoding See [[Plain.encoding]]
* @param valueEncoding See [[Plain.encoding]]
*/
case class KeyValue(
k: String,
v: String,
keyEncoding: Encoding = QuerySegmentEncoding.Standard,
valueEncoding: Encoding = QuerySegmentEncoding.Standard
) extends QuerySegment
/** A query fragment which contains only the value, without a key.
*/
case class Value(v: String, relaxedEncoding: Encoding = QuerySegmentEncoding.Standard) extends QuerySegment
/** 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.
*
* @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]]
* [[https://stackoverflow.com/questions/2366260/whats-valid-and-whats-not-in-a-uri-query]]
*/
case class Plain(v: String, encoding: Encoding = QuerySegmentEncoding.Standard) extends QuerySegment
private[model] def fromQueryParams(mqp: QueryParams): Iterable[QuerySegment] = {
mqp.toMultiSeq.flatMap { case (k, vs) =>
vs match {
case Seq() => List(Value(k))
case s => s.map(v => KeyValue(k, v))
}
}
}
}
type Encoding = String => String
object HostEncoding {
// TODO
private val IpV6Pattern = "[0-9a-fA-F:]+".r
val Standard: Encoding = {
case s @ IpV6Pattern() if s.count(_ == ':') >= 2 => s"[$s]"
case s => UriCompatibility.encodeDNSHost(s)
}
}
object PathSegmentEncoding {
val Standard: Encoding = encode(Rfc3986.PathSegment)
}
object QuerySegmentEncoding {
/** Encodes all reserved characters using [[java.net.URLEncoder.encode()]].
*/
val All: Encoding = UriCompatibility.encodeQuery(_, "UTF-8")
/** Encodes only the `&` and `=` reserved characters, which are usually
* used to separate query parameter names and values.
*/
val Standard: Encoding = encode(Rfc3986.QueryNoStandardDelims, spaceAsPlus = true, encodePlus = true)
/** Doesn't encode any of the reserved characters, leaving intact all
* characters allowed in the query string as defined by RFC3986.
*/
val Relaxed: Encoding = encode(Rfc3986.Query, spaceAsPlus = true)
/** Doesn't encode any of the reserved characters, leaving intact all
* characters allowed in the query string as defined by RFC3986 as well
* as the characters `[` and `]`. These brackets aren't legal in the
* query part of the URI, but some servers use them unencoded. See
* https://stackoverflow.com/questions/11490326/is-array-syntax-using-square-brackets-in-url-query-strings-valid
* for discussion.
*/
val RelaxedWithBrackets: Encoding = encode(Rfc3986.QueryWithBrackets, spaceAsPlus = true)
}
object FragmentEncoding {
val Standard: Encoding = encode(Rfc3986.Fragment)
}
case class UserInfo(username: String, password: Option[String])
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy