All Downloads are FREE. Search and download functionalities are using the official Maven repository.

sttp.model.Uri.scala Maven / Gradle / Ivy

The newest version!
package sttp.model

import java.net.URI
import sttp.model.Uri.QuerySegment.{KeyValue, Plain, Value}
import sttp.model.Uri.{
  Authority,
  Encoding,
  FragmentSegment,
  HostSegment,
  PathSegment,
  PathSegments,
  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

import scala.collection.mutable

/** A [[https://en.wikipedia.org/wiki/Uniform_Resource_Identifier URI]]. Can represent both relative and absolute URIs,
  * hence in terms of [[https://tools.ietf.org/html/rfc3986]], this is a URI reference.
  *
  * 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.
  *
  * The `apply`/`safeApply`/`unsafeApply` methods create absolute URIs and require a host. The `relative` methods
  * creates a relative URI, given path/query/fragment components.
  *
  * @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.
  *   Custom encoding logic can be provided when creating a segment.
  */
case class Uri(
    scheme: Option[String],
    authority: Option[Authority],
    pathSegments: PathSegments,
    querySegments: Seq[QuerySegment],
    fragmentSegment: Option[Segment]
) {

  /** Replace the scheme. Does not validate the new scheme value. */
  def scheme(s: String): Uri = this.copy(scheme = Some(s))

  /** Replace the scheme. Does not validate the new scheme value. */
  def scheme(s: Option[String]): Uri = this.copy(scheme = s)

  //

  /** Replace the user info with a username only. Adds an empty host if one is absent. */
  def userInfo(username: String): Uri = userInfo(Some(UserInfo(username, None)))

  /** Replace the user info with username/password combination. Adds an empty host if one is absent. */
  def userInfo(username: String, password: String): Uri = userInfo(Some(UserInfo(username, Some(password))))

  /** Replace the user info with username/password combination. Adds an empty host if one is absent, and user info is
    * defined.
    */
  def userInfo(ui: Option[UserInfo]): Uri = ui match {
    case Some(v) => this.copy(authority = Some(authority.getOrElse(Authority.Empty).userInfo(Some(v))))
    case None    => this.copy(authority = authority.map(_.userInfo(None)))
  }

  def userInfo: Option[UserInfo] = authority.flatMap(_.userInfo)

  /** 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 = hostSegment(Some(s))

  /** Replace the host. Does not validate the new host value if it's nonempty. */
  def hostSegment(s: Option[Segment]): Uri = this.copy(authority = authority match {
    case Some(a) => s.map(a.hostSegment(_))
    case None    => s.map(Authority(None, _, None))
  })

  def host: Option[String] = authority.map(_.hostSegment.v)

  /** Replace the port. Adds an empty host if one is absent. */
  def port(p: Int): Uri = port(Some(p))

  /** Replace the port. Adds an empty host if one is absent, and port is defined. */
  def port(p: Option[Int]): Uri = p match {
    case Some(v) => this.copy(authority = Some(authority.getOrElse(Authority.Empty).port(v)))
    case None    => this.copy(authority = authority.map(_.port(None)))
  }

  def port: Option[Int] = authority.flatMap(_.port)

  /** Replace the authority. */
  def authority(a: Authority): Uri = this.copy(authority = Some(a))

  /** Replace the authority. */
  def authority(a: Some[Authority]): Uri = this.copy(authority = a)

  //

  /** 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, 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 = copy(pathSegments = pathSegments.addSegments(ss))

  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 = copy(pathSegments = pathSegments.withSegments(ss))

  /** 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 path: Seq[String] = pathSegments.segments.map(_.v).toList

  //

  /** 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] = params.toMap

  def params: QueryParams = {
    val m = new mutable.LinkedHashMap[String, List[String]] // keeping parameter order
    querySegments.foreach {
      case KeyValue(k, v, _, _) => m.update(k, m.getOrElse(k, Nil) ++ List(v))
      case Value(v, _)          => m.update(v, m.getOrElse(v, Nil))
      case Plain(v, _)          => m.update(v, m.getOrElse(v, Nil))
    }
    QueryParams.fromMultiSeq(m.toSeq)
  }

  def paramsSeq: Seq[(String, String)] = params.toSeq.toList

  /** Adds the given query segment. */
  @deprecated(message = "Use addQuerySegment", since = "1.2.0")
  def querySegment(qs: QuerySegment): Uri = addQuerySegment(qs)

  def addQuerySegment(qs: QuerySegment): Uri = addQuerySegments(List(qs))
  def addQuerySegments(qs1: QuerySegment, qs2: QuerySegment, qss: QuerySegment*): Uri = addQuerySegments(
    qs1 :: qs2 :: qss.toList
  )
  def addQuerySegments(qss: scala.collection.Seq[QuerySegment]): Uri = this.copy(querySegments = querySegments ++ qss)

  //

  /** 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())

  def isAbsolute: Boolean = scheme.isDefined
  def isRelative: Boolean = !isAbsolute

  def resolve(other: Uri): Uri = Uri(toJavaUri.resolve(other.toJavaUri))

  //

  def hostSegmentEncoding(encoding: Encoding): Uri =
    copy(authority = authority.map(a => a.copy(hostSegment = a.hostSegment.encoding(encoding))))

  def pathSegmentsEncoding(encoding: Encoding): Uri = copy(pathSegments = pathSegments match {
    case Uri.EmptyPath              => Uri.EmptyPath
    case Uri.AbsolutePath(segments) => Uri.AbsolutePath(segments.map(_.encoding(encoding)))
    case Uri.RelativePath(segments) => Uri.RelativePath(segments.map(_.encoding(encoding)))
  })

  /** Replace encoding for query segments: applies to key-value, only-value and plain ones. */
  def querySegmentsEncoding(encoding: Encoding): Uri = copy(querySegments = querySegments.map {
    case KeyValue(k, v, _, _) => KeyValue(k, v, encoding, encoding)
    case Value(v, _)          => Value(v, encoding)
    case Plain(v, _)          => Plain(v, encoding)
  })

  /** Replace encoding for the value part of key-value query segments and for only-value ones. */
  def queryValueSegmentsEncoding(valueEncoding: Encoding): Uri = copy(querySegments = querySegments.map {
    case KeyValue(k, v, keyEncoding, _) => KeyValue(k, v, keyEncoding, valueEncoding)
    case Value(v, _)                    => Value(v, valueEncoding)
    case s                              => s
  })

  def fragmentSegmentEncoding(encoding: Encoding): Uri =
    copy(fragmentSegment = fragmentSegment.map(f => f.encoding(encoding)))

  override def toString: String = {
    @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 = scheme.map(s => encode(Rfc3986.Scheme)(s) + ":").getOrElse("")
    val authorityS = authority.fold("")(_.toString)
    val pathPrefixS = pathSegments match {
      case _ if authority.isEmpty && scheme.isDefined => ""
      case Uri.EmptyPath                              => ""
      case Uri.AbsolutePath(_)                        => "/"
      case Uri.RelativePath(_)                        => ""
    }
    val pathS = pathSegments.segments.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$authorityS$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(host: Option[String]): Option[String] =
    host.flatMap(h => if (h.isEmpty) Some("Host cannot be empty") else None)
  private def validateScheme(scheme: Option[String]) = scheme.flatMap { s =>
    if (AllowedSchemeCharacters.unapplySeq(s).isEmpty)
      Some("Scheme can only contain alphanumeric characters, +, - and .")
    else None
  }

  def safeApply(host: String): Either[String, Uri] =
    safeApply("http", Some(Authority(host)), Vector.empty, Vector.empty, None)
  def safeApply(host: String, port: Int): Either[String, Uri] =
    safeApply("http", Some(Authority(host, port)), Vector.empty, Vector.empty, None)
  def safeApply(host: String, port: Int, path: Seq[String]): Either[String, Uri] =
    safeApply("http", Some(Authority(host, port)), path.map(PathSegment(_)), Vector.empty, None)
  def safeApply(scheme: String, path: Seq[String]): Either[String, Uri] =
    safeApply(scheme, None, path.map(PathSegment(_)), Vector.empty, None)
  def safeApply(scheme: String, host: String): Either[String, Uri] =
    safeApply(scheme, Some(Authority(host)), Vector.empty, Vector.empty, None)
  def safeApply(scheme: String, host: String, port: Int): Either[String, Uri] =
    safeApply(scheme, Some(Authority(host, port)), Vector.empty, Vector.empty, None)
  def safeApply(scheme: String, host: String, port: Int, path: Seq[String]): Either[String, Uri] =
    safeApply(scheme, Some(Authority(host, port)), path.map(PathSegment(_)), Vector.empty, None)
  def safeApply(scheme: String, host: String, path: Seq[String]): Either[String, Uri] =
    safeApply(scheme, Some(Authority(host)), path.map(PathSegment(_)), Vector.empty, None)
  def safeApply(scheme: String, host: String, path: Seq[String], fragment: Option[String]): Either[String, Uri] =
    safeApply(
      scheme,
      Some(Authority(host)),
      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,
      Some(Authority(userInfo, HostSegment(host), port)),
      path.map(PathSegment(_)),
      querySegments,
      fragment.map(FragmentSegment(_))
    )
  def safeApply(
      scheme: String,
      authority: Option[Authority],
      pathSegments: Seq[Segment],
      querySegments: Seq[QuerySegment],
      fragmentSegment: Option[Segment]
  ): Either[String, Uri] =
    Validate.all(validateScheme(Some(scheme)), validateHost(authority.map(_.hostSegment.v)))(
      apply(
        Some(scheme),
        authority,
        PathSegments.absoluteOrEmpty(pathSegments),
        querySegments,
        fragmentSegment
      )
    )

  //

  def unsafeApply(host: String): Uri =
    unsafeApply("http", Some(Authority(host)), Vector.empty, Vector.empty, None)
  def unsafeApply(host: String, port: Int): Uri =
    unsafeApply("http", Some(Authority(host, port)), Vector.empty, Vector.empty, None)
  def unsafeApply(host: String, port: Int, path: Seq[String]): Uri =
    unsafeApply("http", Some(Authority(host, port)), path.map(PathSegment(_)), Vector.empty, None)
  def unsafeApply(scheme: String, path: Seq[String]): Uri =
    unsafeApply(scheme, None, path.map(PathSegment(_)), Vector.empty, None)
  def unsafeApply(scheme: String, host: String): Uri =
    unsafeApply(scheme, Some(Authority(host)), Vector.empty, Vector.empty, None)
  def unsafeApply(scheme: String, host: String, port: Int): Uri =
    unsafeApply(scheme, Some(Authority(host, port)), Vector.empty, Vector.empty, None)
  def unsafeApply(scheme: String, host: String, port: Int, path: Seq[String]): Uri =
    unsafeApply(scheme, Some(Authority(host, port)), path.map(PathSegment(_)), Vector.empty, None)
  def unsafeApply(scheme: String, host: String, path: Seq[String]): Uri =
    unsafeApply(scheme, Some(Authority(host)), path.map(PathSegment(_)), Vector.empty, None)
  def unsafeApply(scheme: String, host: String, path: Seq[String], fragment: Option[String]): Uri =
    unsafeApply(
      scheme,
      Some(Authority(host)),
      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,
      Some(Authority(userInfo, HostSegment(host), port)),
      path.map(PathSegment(_)),
      querySegments,
      fragment.map(FragmentSegment(_))
    )
  def unsafeApply(
      scheme: String,
      authority: Option[Authority],
      pathSegments: Seq[Segment],
      querySegments: Seq[QuerySegment],
      fragmentSegment: Option[Segment]
  ): Uri =
    safeApply(scheme, authority, pathSegments, querySegments, fragmentSegment).getOrThrow

  //

  def apply(host: String): Uri =
    apply(Some("http"), Some(Authority(host)), EmptyPath, Vector.empty, None)
  def apply(host: String, port: Int): Uri =
    apply(Some("http"), Some(Authority(host, port)), EmptyPath, Vector.empty, None)
  def apply(host: String, port: Int, path: Seq[String]): Uri =
    apply(Some("http"), Some(Authority(host, port)), PathSegments.absoluteOrEmptyS(path), Vector.empty, None)
  def apply(scheme: String, path: Seq[String]): Uri =
    apply(Some(scheme), None, PathSegments.absoluteOrEmptyS(path), Vector.empty, None)
  def apply(scheme: String, host: String): Uri =
    apply(Some(scheme), Some(Authority(host)), EmptyPath, Vector.empty, None)
  def apply(scheme: String, host: String, port: Int): Uri =
    apply(Some(scheme), Some(Authority(host, port)), EmptyPath, Vector.empty, None)
  def apply(scheme: String, host: String, port: Int, path: Seq[String]): Uri =
    apply(Some(scheme), Some(Authority(host, port)), PathSegments.absoluteOrEmptyS(path), Vector.empty, None)
  def apply(scheme: String, host: String, path: Seq[String]): Uri =
    apply(Some(scheme), Some(Authority(host)), PathSegments.absoluteOrEmptyS(path), Vector.empty, None)
  def apply(scheme: String, host: String, path: Seq[String], fragment: Option[String]): Uri =
    apply(
      Some(scheme),
      Some(Authority(host)),
      PathSegments.absoluteOrEmptyS(path),
      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(
      Some(scheme),
      Some(Authority(userInfo, HostSegment(host), port)),
      PathSegments.absoluteOrEmptyS(path),
      querySegments,
      fragment.map(FragmentSegment(_))
    )
  }
  def apply(
      scheme: String,
      authority: Option[Authority],
      path: Seq[Segment],
      querySegments: Seq[QuerySegment],
      fragment: Option[Segment]
  ): Uri = {
    apply(
      Some(scheme),
      authority,
      PathSegments.absoluteOrEmpty(path),
      querySegments,
      fragment
    )
  }

  //

  /** Create a relative URI with an absolute path. */
  def relative(path: Seq[String]): Uri = relative(path, Vector.empty, None)

  /** Create a relative URI with an absolute path. */
  def relative(path: Seq[String], fragment: Option[String]): Uri = relative(path, Vector.empty, fragment)

  /** Create a relative URI with an absolute path. */
  def relative(path: Seq[String], querySegments: Seq[QuerySegment], fragment: Option[String]): Uri =
    apply(None, None, PathSegments.absoluteOrEmptyS(path), querySegments, fragment.map(FragmentSegment(_)))

  /** Create a relative URI with a relative path. */
  def pathRelative(path: Seq[String]): Uri = pathRelative(path, Vector.empty, None)

  /** Create a relative URI with a relative path. */
  def pathRelative(path: Seq[String], fragment: Option[String]): Uri = pathRelative(path, Vector.empty, fragment)

  /** Create a relative URI with a relative path. */
  def pathRelative(path: Seq[String], querySegments: Seq[QuerySegment], fragment: Option[String]): Uri =
    apply(None, None, RelativePath(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 Authority(userInfo: Option[UserInfo], hostSegment: Segment, port: Option[Int]) {

    /** Replace the user info with a username only. */
    def userInfo(username: String): Authority = this.copy(userInfo = Some(UserInfo(username, None)))

    /** Replace the user info with username/password combination. */
    def userInfo(username: String, password: String): Authority =
      this.copy(userInfo = Some(UserInfo(username, Some(password))))

    /** Replace the user info. */
    def userInfo(ui: Option[UserInfo]): Authority = this.copy(userInfo = ui)

    /** Replace the host. Does not validate the new host value if it's nonempty. */
    def host(h: String): Authority = hostSegment(HostSegment(h))

    /** Replace the host. Does not validate the new host value if it's nonempty. */
    def hostSegment(s: Segment): Authority = this.copy(hostSegment = s)

    def host: String = hostSegment.v

    /** Replace the port. */
    def port(p: Int): Authority = port(Some(p))

    /** Replace the port. */
    def port(p: Option[Int]): Authority = this.copy(port = p)

    override def toString: String = {
      def encodeUserInfo(ui: UserInfo): String =
        encode(Rfc3986.UserInfo)(ui.username) + ui.password.fold("")(":" + encode(Rfc3986.UserInfo)(_))

      val userInfoS = userInfo.fold("")(encodeUserInfo(_) + "@")
      val hostS = hostSegment.encoded
      val portS = port.fold("")(":" + _)

      s"//$userInfoS$hostS$portS"
    }
  }
  object Authority {
    private[model] val Empty = Authority("")

    def safeApply(host: String): Either[String, Authority] =
      Validate.all(validateHost(Some(host)))(Authority(None, HostSegment(host), None))
    def safeApply(host: String, port: Int): Either[String, Authority] =
      Validate.all(validateHost(Some(host)))(Authority(None, HostSegment(host), Some(port)))
    def unsafeApply(host: String): Authority = safeApply(host).getOrThrow
    def unsafeApply(host: String, port: Int): Authority = safeApply(host, port).getOrThrow
    def apply(host: String): Authority = Authority(None, HostSegment(host), None)
    def apply(host: String, port: Int): Authority = Authority(None, HostSegment(host), Some(port))
  }

  //

  case class Segment(v: String, encoding: Encoding) {
    def encoded: String = encoding(v)
    def encoding(e: Encoding): Segment = copy(encoding = e)
  }

  object HostSegment {
    def apply(v: String): Segment = Segment(v, HostEncoding.Standard)
  }

  sealed trait PathSegments {
    def segments: collection.Seq[Segment]

    def add(p: String, ps: String*): PathSegments = add(p :: ps.toList)
    def add(ps: scala.collection.Seq[String]): PathSegments = addSegments(ps.toList.map(PathSegment(_)))
    def addSegment(s: Segment): PathSegments = addSegments(List(s))
    def addSegments(s1: Segment, s2: Segment, ss: Segment*): PathSegments = addSegments(s1 :: s2 :: ss.toList)
    def addSegments(ss: scala.collection.Seq[Segment]): PathSegments = {
      val base = if (segments.lastOption.exists(_.v.isEmpty)) segments.init else segments
      withSegments(base ++ ss.toList)
    }

    def withS(p: String, ps: String*): PathSegments = withS(p :: ps.toList)
    def withS(ps: scala.collection.Seq[String]): PathSegments = withSegments(ps.toList.map(PathSegment(_)))
    def withSegment(s: Segment): PathSegments = withSegments(List(s))
    def withSegments(s1: Segment, s2: Segment, ss: Segment*): PathSegments = withSegments(s1 :: s2 :: ss.toList)
    def withSegments(ss: scala.collection.Seq[Segment]): PathSegments
  }
  object PathSegments {
    def absoluteOrEmptyS(segments: Seq[String]): PathSegments = absoluteOrEmpty(segments.map(PathSegment(_)))
    def absoluteOrEmpty(segments: Seq[Segment]): PathSegments =
      if (segments.isEmpty) EmptyPath else AbsolutePath(segments)
  }
  case object EmptyPath extends PathSegments {
    override def withSegments(ss: collection.Seq[Segment]): PathSegments = AbsolutePath(ss.toList)
    override def segments: collection.Seq[Segment] = Nil
    override def toString: String = ""
  }
  case class AbsolutePath(segments: Seq[Segment]) extends PathSegments {
    override def withSegments(ss: scala.collection.Seq[Segment]): AbsolutePath = copy(segments = ss.toList)
    override def toString: String = "/" + segments.map(_.encoded).mkString("/")
  }
  case class RelativePath(segments: Seq[Segment]) extends PathSegments {
    override def withSegments(ss: scala.collection.Seq[Segment]): RelativePath = copy(segments = ss.toList)
    override def toString: String = segments.map(_.encoded).mkString("/")
  }

  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 {
      override def toString = s"KeyValue($k,$v,[keyEncoding],[valueEncoding])"
    }

    /** A query fragment which contains only the value, without a key. */
    case class Value(v: String, encoding: Encoding = QuerySegmentEncoding.StandardValue) extends QuerySegment {
      override def toString = s"Value($v,[encoding])"
    }

    /** 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
      *   How to encode the value, and which characters should be escaped. The RFC3986 standard defines that the query
      *   can include these special characters, without escaping:
      *   {{{
      * /?:@-._~!$&()*+,;=
      *   }}}
      *   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.StandardValue) extends QuerySegment {
      override def toString = s"Plain($v,[encoding])"
    }

    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 {
    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.Query -- Set('&', '='), spaceAsPlus = true, encodePlus = true)

    /** Encodes only the `&` reserved character, which is usually used to separate query parameter names and values. The
      * '=' sign is allowed in values.
      */
    val StandardValue: Encoding = encode(Rfc3986.Query -- Set('&'), 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 - 2024 Weber Informatics LLC | Privacy Policy