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

io.lemonlabs.uri.Uri.scala Maven / Gradle / Ivy

The newest version!
package io.lemonlabs.uri

import java.util.Base64
import cats.{Eq, Order, Show}
import io.lemonlabs.uri.Path.SlashTermination
import io.lemonlabs.uri.config.UriConfig
import io.lemonlabs.uri.encoding.PercentEncoder
import io.lemonlabs.uri.parsing.{UriParser, UrlParser, UrnParser}
import io.lemonlabs.uri.redact.Redactor
import io.lemonlabs.uri.typesafe.{
  Fragment,
  PathPart,
  QueryKey,
  QueryKeyValue,
  QueryValue,
  TraversableParams,
  TraversablePathParts
}
import io.lemonlabs.uri.typesafe.TraversableParams.ops._
import io.lemonlabs.uri.typesafe.QueryKeyValue.ops._
import io.lemonlabs.uri.typesafe.QueryValue.ops._
import io.lemonlabs.uri.typesafe.QueryKey.ops._
import io.lemonlabs.uri.typesafe.PathPart.ops._
import io.lemonlabs.uri.typesafe.TraversablePathParts.ops._
import io.lemonlabs.uri.typesafe.Fragment.ops._

import java.util
import scala.util.Try

/** Represents a URI. See [[https://www.ietf.org/rfc/rfc3986 RFC 3986]]
  *
  * Can either be a URL or a URN
  *
  * URLs will be one of these forms:
  *
  *  -           Absolute: `http://example.com`
  *  -  Protocol Relative: `//example.com`
  *  -  Without Authority: `mailto:[email protected]`
  *  -      Root Relative: `/index.html?a=b`
  *  -  Rootless Relative: `index.html?a=b`
  *  -  Rootless Relative
  *    (with doc segment): `../index.html?a=b`
  *
  * URNs will be in the form `urn:example:example2`
  */
sealed trait Uri extends Product with Serializable {
  type Self <: Uri
  type SelfWithScheme <: Uri

  private[uri] def self: Self

  implicit def config: UriConfig

  def schemeOption: Option[String]
  def path: Path

  /** Copies this Uri but with the scheme set as the given value.
    *
    * @param scheme the new scheme to set
    * @return a new Uri with the specified scheme
    */
  def withScheme(scheme: String): SelfWithScheme

  /** Copies this Uri but with a new UriConfig
    *
    * @param config the new config to use
    * @return a new Uri with the specified config
    */
  def withConfig(config: UriConfig): Self

  def toUrl: Url
  def toUrn: Urn

  /** Converts to a `java.net.URI`
    *
    * This involves a `toString` and `URI.parse` because the specific `java.net.URI`
    * constructors do not deal properly with encoded elements
    *
    * @return a `java.net.URI` matching this `io.lemonlabs.uri.Uri`
    */
  def toJavaURI: java.net.URI =
    new java.net.URI(toStringWithConfig(config))

  /** Similar to `==` but ignores the ordering of any query string parameters
    */
  def equalsUnordered(other: Uri): Boolean

  /** Returns the path with no encoders taking place (e.g. non ASCII characters will not be percent encoded)
    * @return String containing the raw path for this Uri
    */
  def toStringRaw: String =
    toStringWithConfig(config.withNoEncoding)

  override def toString: String =
    toStringWithConfig(config)

  private[uri] def toStringWithConfig(config: UriConfig): String
}

object Uri {
  def apply(javaUri: java.net.URI): Uri =
    parse(javaUri.toASCIIString)

  def unapply(uri: Uri): Option[Path] =
    Some(uri.path)

  def parseTry(s: CharSequence)(implicit config: UriConfig = UriConfig.default): Try[Uri] =
    Try(s.toString).flatMap(UriParser.parseUri)

  def parseOption(s: CharSequence)(implicit config: UriConfig = UriConfig.default): Option[Uri] =
    parseTry(s).toOption

  def parse(s: CharSequence)(implicit config: UriConfig = UriConfig.default): Uri =
    parseTry(s).get

  implicit val eqUri: Eq[Uri] = Eq.fromUniversalEquals
  implicit val showUri: Show[Uri] = Show.fromToString
  implicit val orderUri: Order[Uri] = Order.by(_.toString())

  object unordered {
    implicit val eqUri: Eq[Uri] =
      (x: Uri, y: Uri) => x.equalsUnordered(y)
  }
}

/** Represents a URL, which will be one of these forms:
  *
  *  -           Absolute: `http://example.com`
  *  -  Protocol Relative: `//example.com`
  *  -  Without Authority: `mailto:[email protected]`
  *  -      Root Relative: `/index.html?a=b`
  *  -  Rootless Relative: `index.html?a=b`
  *  -  Rootless Relative
  *    (with doc segment): `../index.html?a=b`
  */
sealed trait Url extends Uri {
  type Self <: Url
  type SelfWithScheme <: UrlWithScheme
  type SelfWithAuthority <: UrlWithAuthority

  def authorityOption: Option[Authority]
  def hostOption: Option[Host]
  def port: Option[Int]

  def user: Option[String]
  def password: Option[String]

  def path: UrlPath
  def query: QueryString
  def fragment: Option[String]

  /** Returns the longest public suffix for the host in this URI. Examples include:
    *  `com`   for `www.example.com`
    *  `co.uk` for `www.example.co.uk`
    *
    * @return the longest public suffix for the host in this URI
    */
  def publicSuffix: Option[String]

  /** Returns all longest public suffixes for the host in this URI. Examples include:
    *  `com` for `www.example.com`
    *  `co.uk` and `uk` for `www.example.co.uk`
    *
    * @return all public suffixes for the host in this URI
    */
  def publicSuffixes: Vector[String]

  /** Returns the second largest subdomain for this URL's host.
    *
    * E.g. for http://a.b.c.example.com returns a.b.c
    *
    * Note: In the event there is only one subdomain (i.e. the host is the apex domain), this method returns `None`.
    * E.g. This method will return `None` for `http://example.com`.
    *
    * @return the second largest subdomain for this URL's host
    */
  def subdomain: Option[String]

  /** Returns all subdomains for this URL's host.
    * E.g. for http://a.b.c.example.com returns a, a.b, a.b.c and a.b.c.example
    * @return all subdomains for this URL's host
    */
  def subdomains: Vector[String]

  /** Returns the shortest subdomain for this URL's host.
    * E.g. for http://a.b.c.example.com returns a
    * @return the shortest subdomain for this URL's host
    */
  def shortestSubdomain: Option[String]

  /** Returns the longest subdomain for this URL's host.
    * E.g. for http://a.b.c.example.com returns a.b.c.example
    * @return the longest subdomain for this URL's host
    */
  def longestSubdomain: Option[String]

  /** Copies this Url but with the authority set as the given value.
    *
    * @param authority the authority to set
    * @return a new Url with the specified authority
    */
  def withAuthority(authority: Authority): SelfWithAuthority

  /** Copies this Url but with the host set as the given value.
    *
    * @param host the new host to set
    * @return a new Url with the specified host
    */
  def withHost(host: Host): SelfWithAuthority

  /** Copies this Url but with the host set as the given value.
    *
    * @param host the new host to set
    * @return a new Url with the specified host
    */
  def withHost(host: String): SelfWithAuthority =
    withHost(Host.parse(host))

  /** Copies this Url but with the fragment set as the given value.
    *
    * @param fragment the new fragment to set
    * @return a new Url with the specified fragment
    */
  def withFragment[T: Fragment](fragment: T): Self

  /** Copies this Url but with the path set as the given value.
    *
    * @param path the new path to set
    * @return a new Url with the specified path
    */
  def withPath(path: UrlPath): Self

  /** Copies this Url but with the path set as the given value.
    *
    * @param parts the parts that make up the new path
    * @return a new Url with the specified path
    */
  def withPathParts[P: TraversablePathParts](parts: P): Self =
    withPath(UrlPath(parts.toSeq))

  /** Copies this Url but with the query set as the given value.
    *
    * @param query the new QueryString to set
    * @return a new Url with the specified query
    */
  def withQueryString(query: QueryString): Self

  /** Replaces the all existing Query String parameters with a new set of query params
    */
  def withQueryString[T: TraversableParams](params: T): Self =
    withQueryString(QueryString.fromTraversable(params))

  /** Replaces the all existing Query String parameters with a new set of query params
    */
  def withQueryString[KV: QueryKeyValue](first: KV, second: KV, params: KV*): Self =
    withQueryString(QueryString.fromTraversable(Seq(first, second) ++ params))

  def addPathPart[P: PathPart](part: P): Self =
    withPath(path.addPart(part))

  def addPathParts[P: TraversablePathParts](parts: P): Self =
    withPath(path.addParts(parts))

  def addPathParts[P: PathPart](first: P, second: P, parts: P*): Self =
    withPath(path.addParts(first, second, parts: _*))

  /** Adds a new Query String parameter key-value pair.
    *
    * Pairs with values, such as `Some("value")`, represent query params with values, i.e `?param=value`
    *
    * By default, pairs without values, i.e `None`, represent query params without values, i.e `?param`
    * Using a `UriConfig(renderQuery = ExcludeNones)`, will cause pairs with `None` values not to be rendered
    *
    * @param a value that provides a name/value pair for the parameter. Can be a Tuple of any basic value
    *          types or a custom type if you provide a `QueryKeyValue` type-class
    * @return A new Url with the new Query String parameter
    */
  def addParam[A: QueryKeyValue](a: A): Self =
    withQueryString(query.addParam(a))

  /** Adds a new Query String parameter key-value pair.
    *
    * Pairs with values, such as `Some("value")`, represent query params with values, i.e `?param=value`
    *
    * By default, pairs without values, i.e `None`, represent query params without values, i.e `?param`
    * Using a `UriConfig(renderQuery = ExcludeNones)`, will cause pairs with `None` values not to be rendered
    *
    * @param k value that provides a name pair for the parameter. Can be a any basic value type or a
    *          custom type if you provide a `QueryKey` type-class
    * @param v value that provides a value for the parameter. Can be a any basic value type or a
    *          custom type if you provide a `QueryValue` type-class
    * @return A new Url with the new Query String parameter
    */
  def addParam[K: QueryKey, V: QueryValue](k: K, v: V): Self =
    withQueryString(query.addParam(k, v))

  /** Adds all the specified key-value pairs as parameters to the query
    *
    * @param params A list of key-value pairs to add as query parameters
    * @return A new Url with the new Query String parameters
    */
  def addParams[A: TraversableParams](params: A): Self =
    withQueryString(query.addParams(params))

  /** Adds all the specified key-value pairs as parameters to the query
    *
    * @param params A list of key-value pairs to add as query parameters
    * @return A new Url with the new Query String parameters
    */
  def addParams[KV: QueryKeyValue](first: KV, second: KV, params: KV*): Self =
    withQueryString(query.addParams(first, second, params: _*))

  /** Replaces the all existing Query String parameters with the specified key with a single Query String parameter
    * with the specified value.
    *
    * Pairs with values, such as `("param", Some("value"))`, represent query params with values, i.e `?param=value`
    *
    * By default, pairs without values, such as `("param", None)`, represent query params without values, i.e `?param`
    * Using a `UriConfig(renderQuery = ExcludeNones)`, will cause pairs with `None` values not to be rendered
    *
    * @param k Key for the Query String parameter(s) to replace
    * @param v value to replace with
    * @return A new Uri with the result of the replace
    */
  def replaceParams[K: QueryKey, V: QueryValue](k: K, v: V): Self =
    withQueryString(query.replaceAll(k, v))

  /** Removes all Query String parameters with the specified key
    * @param k Key for the Query String parameter(s) to remove
    * @return
    */
  def removeParams[K: QueryKey](k: K): Self =
    withQueryString(query.removeAll(k))

  /** Removes all Query String parameters with a name in the specified list
    * @param first Name of a Query String parameter to remove
    * @param second Name of another Query String parameter to remove
    * @param rest Name of more Query String parameter(s) to remove
    * @return
    */
  def removeParams[K: QueryKey](first: K, second: K, rest: K*): Self =
    withQueryString(query.removeAll(first, second, rest: _*))

  /** Removes all Query String parameters with a name in the specified list
    * @param k Names of Query String parameter(s) to remove
    * @return
    */
  def removeParams[K: QueryKey](k: Iterable[K]): Self =
    withQueryString(query.removeAll(k))

  /** Removes all Query String parameters
    * @return
    */
  def removeQueryString(): Self =
    withQueryString(QueryString.empty)

  /** Removes the user-info (both user and password) from this URL
    * @return This URL without the user-info
    */
  def removeUserInfo(): Self

  /** Removes any password from this URL's user-info
    * @return This URL without the password
    */
  def removePassword(): Self

  /** Transforms the Query String by applying the specified PartialFunction to each Query String Parameter
    *
    * Parameters not defined in the PartialFunction will be left as-is.
    *
    * @param f A function that returns a new Parameter when applied to each Parameter
    * @return
    */
  def mapQuery[KV: QueryKeyValue](f: PartialFunction[(String, Option[String]), KV]): Self =
    withQueryString(query.map(f))

  /** Transforms the Query String by applying the specified PartialFunction to each Query String Parameter
    *
    * Parameters not defined in the PartialFunction will be removed.
    *
    * @param f A function that returns a new Parameter when applied to each Parameter
    * @return
    */
  def collectQuery[KV: QueryKeyValue](f: PartialFunction[(String, Option[String]), KV]): Self =
    withQueryString(query.collect(f))

  /** Transforms the Query String by applying the specified Function to each Query String Parameter
    *
    * @param f A function that returns a collection of Parameters when applied to each parameter
    * @return
    */
  def flatMapQuery[A: TraversableParams](f: ((String, Option[String])) => A): Self =
    withQueryString(query.flatMap(f))

  /** Transforms the Query String by applying the specified Function to each Query String Parameter name
    *
    * @param f A function that returns a new Parameter name when applied to each Parameter name
    * @return
    */
  def mapQueryNames[K: QueryKey](f: String => K): Self =
    withQueryString(query.mapNames(f))

  /** Transforms the Query String by applying the specified Function to each Query String Parameter value
    *
    * @param f A function that returns a new Parameter value when applied to each Parameter value
    * @return
    */
  def mapQueryValues[V: QueryValue](f: String => V): Self =
    withQueryString(query.mapValues(f))

  /** Removes any Query String Parameters that return false when applied to the given Function
    *
    * @param f
    * @return
    */
  def filterQuery(f: ((String, Option[String])) => Boolean): Self =
    withQueryString(query.filter(f))

  /** Removes any Query String Parameters that return false when their name is applied to the given Function
    *
    * @param f
    * @return
    */
  def filterQueryNames(f: String => Boolean): Self =
    withQueryString(query.filterNames(f))

  /** Transforms this URL by applying the specified Function to the user if it exists
    * @return
    */
  def mapUser(f: String => String): Self

  /** Transforms this URL by applying the specified Function to the password if it exists
    * @return
    */
  def mapPassword(f: String => String): Self

  /** Returns the apex domain for this URL.
    *
    * The apex domain is constructed from the public suffix for this URL's host prepended with the
    * immediately preceding dot segment.
    *
    * Examples include:
    *  `example.com`   for `www.example.com`
    *  `example.co.uk` for `www.example.co.uk`
    *
    * @return the apex domain for this URL
    */
  def apexDomain: Option[String] =
    hostOption.flatMap(_.apexDomain)

  /** Removes any Query String Parameters that return false when their value is applied to the given Function
    *
    * @param f
    * @return
    */
  def filterQueryValues(f: String => Boolean): Self =
    withQueryString(query.filterValues(f))

  private[uri] def fragmentToString(c: UriConfig): String =
    fragment.map(f => "#" + c.fragmentEncoder.encode(f, c.charset)).getOrElse("")

  def toAbsoluteUrl: AbsoluteUrl =
    this match {
      case a: AbsoluteUrl => a
      case _ => throw new UriConversionException(getClass.getSimpleName + " cannot be converted to AbsoluteUrl")
    }

  def toRelativeUrl: RelativeUrl =
    this match {
      case r: RelativeUrl => r
      case _              => RelativeUrl(path, query, fragment)
    }

  def toProtocolRelativeUrl: ProtocolRelativeUrl =
    this match {
      case p: ProtocolRelativeUrl => p
      case a: AbsoluteUrl         => ProtocolRelativeUrl(a.authority, a.path, a.query, a.fragment)
      case _ => throw new UriConversionException(getClass.getSimpleName + " cannot be converted to ProtocolRelativeUrl")
    }

  def toUrl: Url = this
  def toUrn: Urn = throw new UriConversionException(getClass.getSimpleName + " cannot be converted to Urn")

  /** @return the URL as a String. If the URI has a domain name for a host, any unicode characters will be
    *         returned in ASCII Compatible Encoding (ACE), as defined by the ToASCII operation of
    *         RFC 3490.
    */
  def toStringPunycode: String =
    toStringWithConfig(config)

  protected def queryToString(config: UriConfig): String =
    query.toString(config) match {
      case "" => ""
      case s  => "?" + s
    }

  def toRedactedString(redactor: Redactor)(implicit conf: UriConfig = UriConfig.default): String =
    redactor.apply(this).toStringWithConfig(conf)

  /** Similar to `==` but ignores the ordering of any query string parameters
    */
  def equalsUnordered(other: Uri): Boolean = other match {
    case otherUrl: Url =>
      this.removeQueryString() == otherUrl.removeQueryString() && query.equalsUnordered(otherUrl.query)
    case _ =>
      false
  }

  /** @return this URL resolved with the given URL as the base according to section 5.2.2 Transform References of
    *         RFC 3986.
    */
  def resolve(base: UrlWithScheme, strict: Boolean = false): UrlWithScheme = {
    schemeOption match {
      case Some(scheme) if strict || scheme != base.scheme =>
        removeDotSegments.withScheme(scheme)
      case _ if authorityOption.isDefined =>
        removeDotSegments.withScheme(base.scheme)
      case _ =>
        val query = if (this.path == EmptyPath && this.query.isEmpty) base.query else this.query
        val path = this.path match {
          case EmptyPath =>
            base.path
          case _: AbsolutePath =>
            this.path.removeDotSegments
          // Section 5.2.3 clause 1
          case refPath: RootlessPath if base.authorityOption.isDefined && base.path.isEmpty =>
            AbsolutePath(refPath.parts).removeDotSegments
          // Section 5.2.3 clause 2
          case refPath: RootlessPath =>
            base.path.withParts(base.path.parts.dropRight(1) ++ refPath.parts).removeDotSegments
        }
        base.authorityOption
          .fold[Url](this)(withAuthority)
          .withQueryString(query)
          .withPath(path)
          .withScheme(base.scheme)
    }
  }

  private def removeDotSegments: Self = withPath(path.removeDotSegments)

  /** @return this URL with its case normalized according to section 6.2.2.1 Section Path Normalization of
    *         RFC 3986, optionally removing empty parts of the path
    *         and ensuring that it is or isn't terminated by a slash.
    */

  /** Normalizes this
    *
    * @param removeEmptyPathParts
    * @param slashTerminated
    * @param slashTerminatedEmptyPath
    * @return
    */
  def normalize(removeEmptyPathParts: Boolean = false,
                slashTermination: SlashTermination = SlashTermination.AddForEmptyPath
  ): Self

  def slashTerminated(slashTermination: SlashTermination = SlashTermination.AddForAll): Self =
    withPath(path.slashTerminated(slashTermination))
  def removeEmptyPathParts(): Self = withPath(path.removeEmptyParts)

}

object Url {
  def apply(scheme: String = null,
            user: String = null,
            password: String = null,
            host: String = null,
            port: Int = -1,
            path: String = "",
            query: QueryString = QueryString.empty,
            fragment: String = null
  )(implicit config: UriConfig = UriConfig.default): Url = {
    val urlPath = UrlPath.parse(path)
    val frag = Option(fragment)
    def authority = {
      val portOpt = if (port > 0) Some(port) else None
      val userInfo = Option(user).map(u => UserInfo(u, Option(password)))
      Authority(userInfo, Host.parse(host), portOpt)
    }

    (scheme, host) match {
      case (null, null) => RelativeUrl(urlPath, query, frag)
      case (_, null)    => UrlWithoutAuthority(scheme, urlPath, query, frag)
      case (null, _)    => ProtocolRelativeUrl(authority, urlPath.toAbsoluteOrEmpty, query, frag)
      case (_, _)       => AbsoluteUrl(scheme, authority, urlPath.toAbsoluteOrEmpty, query, frag)
    }
  }

  def unapply(url: Url): Option[(UrlPath, QueryString, Option[String])] =
    Some((url.path, url.query, url.fragment))

  def parse(s: CharSequence)(implicit config: UriConfig = UriConfig.default): Url =
    parseTry(s).get

  def parseOption(s: CharSequence)(implicit config: UriConfig = UriConfig.default): Option[Url] =
    parseTry(s).toOption

  def parseTry(s: CharSequence)(implicit config: UriConfig = UriConfig.default): Try[Url] =
    UrlParser.parseUrl(s.toString)

  implicit val eqUrl: Eq[Url] = Eq.fromUniversalEquals
  implicit val showUrl: Show[Url] = Show.fromToString
  implicit val orderUrl: Order[Url] = Order.by(_.toString())

  object unordered {
    implicit val eqUrl: Eq[Url] =
      (x: Url, y: Url) => x.equalsUnordered(y)
  }
}

/** Represents Relative URLs which do not contain an authority. Examples include:
  *
  *  -      Root Relative: `/index.html?a=b`
  *  -  Rootless Relative: `index.html?a=b`
  *  -  Rootless Relative
  *    (with dot segment): `../index.html?a=b`
  */
final case class RelativeUrl(path: UrlPath, query: QueryString, fragment: Option[String])(implicit
    val config: UriConfig = UriConfig.default
) extends Url {
  type Self = RelativeUrl
  type SelfWithAuthority = ProtocolRelativeUrl
  type SelfWithScheme = UrlWithoutAuthority

  def self: RelativeUrl = this

  def schemeOption: Option[String] = None

  def authorityOption: Option[Authority] = None
  def hostOption: Option[Host] = None
  def port: Option[Int] = None

  def user: Option[String] = None
  def password: Option[String] = None

  def publicSuffix: Option[String] = None
  def publicSuffixes: Vector[String] = Vector.empty
  def subdomain: Option[String] = None
  def subdomains: Vector[String] = Vector.empty
  def shortestSubdomain: Option[String] = None
  def longestSubdomain: Option[String] = None

  def withScheme(scheme: String): UrlWithoutAuthority =
    UrlWithoutAuthority(scheme, path, query, fragment)

  def withAuthority(authority: Authority): ProtocolRelativeUrl =
    ProtocolRelativeUrl(authority, path.toAbsoluteOrEmpty, query, fragment)

  def withHost(host: Host): ProtocolRelativeUrl =
    withAuthority(Authority(host))

  def withConfig(config: UriConfig): RelativeUrl =
    RelativeUrl(path, query, fragment)(config)

  /** Copies this Url but with the path set as the given value.
    *
    * @param path the new path to set
    * @return a new Url with the specified path
    */
  def withPath(path: UrlPath): RelativeUrl =
    copy(path = path)

  def withFragment[T: Fragment](fragment: T): RelativeUrl =
    copy(fragment = fragment.fragment)

  def withQueryString(query: QueryString): RelativeUrl =
    copy(query = query)

  private[uri] def toStringWithConfig(c: UriConfig): String =
    path.toStringWithConfig(c) + queryToString(c) + fragmentToString(c)

  def removeUserInfo(): RelativeUrl = this
  def removePassword(): RelativeUrl = this
  def mapUser(f: String => String): RelativeUrl = this
  def mapPassword(f: String => String): RelativeUrl = this

  def normalize(removeEmptyPathParts: Boolean = false,
                slashTermination: SlashTermination = SlashTermination.AddForEmptyPath
  ): Self =
    copy(path = path.normalize(removeEmptyPathParts, slashTermination))
}

object RelativeUrl {
  def empty: RelativeUrl =
    RelativeUrl(UrlPath.empty, QueryString.empty, None)

  def parse(s: CharSequence)(implicit config: UriConfig = UriConfig.default): RelativeUrl =
    parseTry(s).get

  def parseOption(s: CharSequence)(implicit config: UriConfig = UriConfig.default): Option[RelativeUrl] =
    parseTry(s).toOption

  def parseTry(s: CharSequence)(implicit config: UriConfig = UriConfig.default): Try[RelativeUrl] =
    UrlParser.parseRelativeUrl(s.toString)

  implicit val eqRelUrl: Eq[RelativeUrl] = Eq.fromUniversalEquals
  implicit val showRelUrl: Show[RelativeUrl] = Show.fromToString
  implicit val orderRelUrl: Order[RelativeUrl] = Order.by { url =>
    (url.path, url.query, url.fragment)
  }

  object unordered {
    implicit val eqRelUrl: Eq[RelativeUrl] =
      (x: RelativeUrl, y: RelativeUrl) => x.equalsUnordered(y)
  }
}

sealed trait UrlWithScheme extends Url {
  type Self <: UrlWithScheme
  type SelfWithScheme <: UrlWithScheme
  type SelfWithAuthority <: UrlWithAuthority with UrlWithScheme
  def scheme: String
}

/** Represents absolute URLs with an authority (i.e. URLs with a host), examples include:
  *
  *  -          Absolute URL: `http://example.com`
  *  - Protocol Relative URL: `//example.com`
  */
sealed trait UrlWithAuthority extends Url {
  type Self <: UrlWithAuthority
  type SelfWithScheme <: UrlWithAuthority with UrlWithScheme
  type SelfWithAuthority = Self

  def authority: Authority
  def authorityOption: Option[Authority] = Some(authority)

  def host: Host = authority.host
  def hostOption: Option[Host] = Some(host)

  def port: Option[Int] = authority.port
  def userInfo: Option[UserInfo] = authority.userInfo
  def user: Option[String] = authority.user
  def password: Option[String] = authority.password

  def withHost(host: Host): Self =
    withAuthority(authority.copy(host = host))

  /** Copies this Url but with the user set as the given value.
    *
    * @param user the new user to set
    * @return a new Url with the specified user
    */
  def withUser(user: String): Self = {
    withAuthority(authority.copy(userInfo = Some(UserInfo(user, password))))
  }

  /** Copies this Url but with the password set as the given value.
    *
    * @param password the new password to set
    * @return a new Url with the specified password
    */
  def withPassword(password: String): Self = {
    withAuthority(authority.copy(userInfo = Some(UserInfo(user.getOrElse(""), password))))
  }

  /** Copies this Url but with the port set as the given value.
    *
    * @param port the new port to set
    * @return a new Url with the specified port
    */
  def withPort(port: Int): Self =
    withAuthority(authority.copy(port = Some(port)))

  def withUserInfo(ui: Option[UserInfo]): Self =
    withAuthority(authority.copy(userInfo = ui))

  /** Removes any user from this URL
    *
    * @return This URL without the user
    */
  def removeUserInfo(): Self =
    withAuthority(authority.copy(userInfo = None))

  /** Removes any password from this URL
    *
    * @return This URL without the password
    */
  def removePassword(): Self =
    withUserInfo(userInfo.map(_.copy(password = None)))

  /** Transforms this URL by applying the specified Function to the user if it exists
    *
    * @return
    */
  override def mapUser(f: String => String): Self =
    user.fold(self) { u =>
      withUserInfo(userInfo.map(_.copy(user = f(u))))
    }

  /** Transforms this URL by applying the specified Function to the password if it exists
    *
    * @return
    */
  override def mapPassword(f: String => String): Self =
    password.fold(self) { p =>
      withUserInfo(userInfo.map(_.copy(password = Some(f(p)))))
    }

  /** Returns the longest public suffix for the host in this URI. Examples include:
    *  `com`   for `www.example.com`
    *  `co.uk` for `www.example.co.uk`
    *
    * @return the longest public suffix for the host in this URI
    */
  def publicSuffix: Option[String] =
    authority.publicSuffix

  /** Returns all longest public suffixes for the host in this URI. Examples include:
    *  `com` for `www.example.com`
    *  `co.uk` and `uk` for `www.example.co.uk`
    *
    * @return all public suffixes for the host in this URI
    */
  def publicSuffixes: Vector[String] =
    authority.publicSuffixes

  /** Returns the second largest subdomain for this URL's host.
    *
    * E.g. for http://a.b.c.example.com returns a.b.c
    *
    * Note: In the event there is only one subdomain (i.e. the host is the apex domain), this method returns `None`.
    * E.g. This method will return `None` for `http://example.com`.
    *
    * @return the second largest subdomain for this URL's host
    */
  def subdomain: Option[String] =
    authority.subdomain

  /** Returns all subdomains for this URL's host.
    * E.g. for http://a.b.c.example.com returns a, a.b, a.b.c and a.b.c.example
    * @return all subdomains for this URL's host
    */
  def subdomains: Vector[String] =
    authority.subdomains

  /** Returns the shortest subdomain for this URL's host.
    * E.g. for http://a.b.c.example.com returns a
    * @return the shortest subdomain for this URL's host
    */
  def shortestSubdomain: Option[String] =
    authority.shortestSubdomain

  /** Returns the longest subdomain for this URL's host.
    * E.g. for http://a.b.c.example.com returns a.b.c.example
    * @return the longest subdomain for this URL's host
    */
  def longestSubdomain: Option[String] =
    authority.longestSubdomain

  /** @return the URL as a String. If the URI has a domain name for a host, any unicode characters will be
    *         returned in ASCII Compatible Encoding (ACE), as defined by the ToASCII operation of
    *         RFC 3490.
    */
  override def toStringPunycode: String =
    toStringWithConfig(config, _.toStringPunycode)

  private[uri] def toStringWithConfig(c: UriConfig): String =
    toStringWithConfig(c, _.toString())

  private[uri] def toStringWithConfig(c: UriConfig, hostToString: Host => String): String
}

object UrlWithAuthority {
  def unapply(url: UrlWithAuthority): Option[(Authority, UrlPath, QueryString, Option[String])] =
    Some((url.authority, url.path, url.query, url.fragment))

  def parse(s: CharSequence)(implicit config: UriConfig = UriConfig.default): UrlWithAuthority =
    parseTry(s).get

  def parseOption(s: CharSequence)(implicit config: UriConfig = UriConfig.default): Option[UrlWithAuthority] =
    parseTry(s).toOption

  def parseTry(s: CharSequence)(implicit config: UriConfig = UriConfig.default): Try[UrlWithAuthority] =
    UrlParser.parseUrlWithAuthority(s.toString)

  implicit val eqUrlWithAuthority: Eq[UrlWithAuthority] = Eq.fromUniversalEquals
  implicit val showUrlWithAuthority: Show[UrlWithAuthority] = Show.fromToString
  implicit val orderUrlWithAuthority: Order[UrlWithAuthority] = Order.by(_.toString())

  object unordered {
    implicit val eqUrlWithAuthority: Eq[UrlWithAuthority] =
      (x: UrlWithAuthority, y: UrlWithAuthority) => x.equalsUnordered(y)
  }
}

/** Represents protocol relative URLs, for example: `//example.com`
  */
final case class ProtocolRelativeUrl(authority: Authority,
                                     path: AbsoluteOrEmptyPath,
                                     query: QueryString,
                                     fragment: Option[String]
)(implicit val config: UriConfig = UriConfig.default)
    extends UrlWithAuthority {
  type Self = ProtocolRelativeUrl
  type SelfWithScheme = AbsoluteUrl

  def self: ProtocolRelativeUrl = this

  def schemeOption: Option[String] = None

  def withScheme(scheme: String): AbsoluteUrl =
    AbsoluteUrl(scheme, authority, path, query, fragment)

  def withAuthority(authority: Authority): ProtocolRelativeUrl =
    copy(authority = authority)

  def withFragment[T: Fragment](fragment: T): ProtocolRelativeUrl =
    copy(fragment = fragment.fragment)

  def withConfig(config: UriConfig): ProtocolRelativeUrl =
    ProtocolRelativeUrl(authority, path, query, fragment)(config)

  /** Copies this Url but with the path set as the given value.
    *
    * If the specified path is non empty *and* doesn't have a leading slash, one will be added, as per RFC 3986:
    * When authority is present, the path must either be empty or begin with a slash ("/") character.
    *
    * @param path the new path to set
    * @return a new Url with the specified path
    */
  def withPath(path: UrlPath): ProtocolRelativeUrl =
    copy(path = path.toAbsoluteOrEmpty)

  def withQueryString(query: QueryString): ProtocolRelativeUrl =
    copy(query = query)

  private[uri] def toStringWithConfig(c: UriConfig, hostToString: Host => String): String =
    "//" + authority.toString(c, hostToString) + path.toStringWithConfig(c) + queryToString(c) + fragmentToString(c)

  def normalize(removeEmptyPathParts: Boolean = false,
                slashTermination: SlashTermination = SlashTermination.AddForEmptyPath
  ): Self = {
    copy(
      authority = authority.normalize(None),
      path = path.normalize(removeEmptyPathParts, slashTermination).toAbsoluteOrEmpty
    )
  }
}

object ProtocolRelativeUrl {
  def parse(s: CharSequence)(implicit config: UriConfig = UriConfig.default): ProtocolRelativeUrl =
    parseTry(s).get

  def parseOption(s: CharSequence)(implicit config: UriConfig = UriConfig.default): Option[ProtocolRelativeUrl] =
    parseTry(s).toOption

  def parseTry(s: CharSequence)(implicit config: UriConfig = UriConfig.default): Try[ProtocolRelativeUrl] =
    UrlParser.parseProtocolRelativeUrl(s.toString)

  implicit val eqProtocolRelUrl: Eq[ProtocolRelativeUrl] = Eq.fromUniversalEquals
  implicit val showProtocolRelUrl: Show[ProtocolRelativeUrl] = Show.fromToString
  implicit val orderProtocolRelUrl: Order[ProtocolRelativeUrl] = Order.by { url =>
    (url.authority, url.path, url.query, url.fragment)
  }

  object unordered {
    implicit val eqProtocolRelUrl: Eq[ProtocolRelativeUrl] =
      (x: ProtocolRelativeUrl, y: ProtocolRelativeUrl) => x.equalsUnordered(y)
  }
}

/** Represents absolute URLs, for example: `http://example.com`
  */
final case class AbsoluteUrl(scheme: String,
                             authority: Authority,
                             path: AbsoluteOrEmptyPath,
                             query: QueryString,
                             fragment: Option[String]
)(implicit val config: UriConfig = UriConfig.default)
    extends UrlWithAuthority
    with UrlWithScheme {
  type Self = AbsoluteUrl
  type SelfWithScheme = AbsoluteUrl

  def self: AbsoluteUrl = this

  def schemeOption: Option[String] = Some(scheme)

  def withScheme(scheme: String): AbsoluteUrl =
    AbsoluteUrl(scheme, authority, path, query, fragment)

  def withAuthority(authority: Authority): AbsoluteUrl =
    copy(authority = authority)

  def withFragment[T: Fragment](fragment: T): AbsoluteUrl =
    copy(fragment = fragment.fragment)

  def withConfig(config: UriConfig): AbsoluteUrl =
    AbsoluteUrl(scheme, authority, path, query, fragment)(config)

  /** Copies this Url but with the path set as the given value.
    *
    * If the specified path is non empty *and* doesn't have a leading slash, one will be added, as per RFC 3986:
    * When authority is present, the path must either be empty or begin with a slash ("/") character.
    *
    * @param path the new path to set
    * @return a new Url with the specified path
    */
  def withPath(path: UrlPath): AbsoluteUrl =
    copy(path = path.toAbsoluteOrEmpty)

  def withQueryString(query: QueryString): AbsoluteUrl =
    copy(query = query)

  private[uri] def toStringWithConfig(c: UriConfig, hostToString: Host => String): String =
    scheme + "://" + authority.toString(c, hostToString) + path.toStringWithConfig(c) + queryToString(
      c
    ) + fragmentToString(c)

  def normalize(removeEmptyPathParts: Boolean = false,
                slashTermination: SlashTermination = SlashTermination.AddForEmptyPath
  ): Self = {
    val scheme = this.scheme.toLowerCase
    copy(
      scheme = scheme,
      authority = authority.normalize(Some(scheme)),
      path = path.normalize(removeEmptyPathParts, slashTermination).toAbsoluteOrEmpty
    )
  }
}

object AbsoluteUrl {
  def parse(s: CharSequence)(implicit config: UriConfig = UriConfig.default): AbsoluteUrl =
    parseTry(s).get

  def parseOption(s: CharSequence)(implicit config: UriConfig = UriConfig.default): Option[AbsoluteUrl] =
    parseTry(s).toOption

  def parseTry(s: CharSequence)(implicit config: UriConfig = UriConfig.default): Try[AbsoluteUrl] =
    UrlParser.parseAbsoluteUrl(s.toString)

  implicit val eqAbsUrl: Eq[AbsoluteUrl] = Eq.fromUniversalEquals
  implicit val showAbsUrl: Show[AbsoluteUrl] = Show.fromToString
  implicit val orderAbsUrl: Order[AbsoluteUrl] = Order.by { url =>
    (url.scheme, url.authority, url.path, url.query, url.fragment)
  }

  object unordered {
    implicit val eqAbsUrl: Eq[AbsoluteUrl] =
      (x: AbsoluteUrl, y: AbsoluteUrl) => x.equalsUnordered(y)
  }
}

/** Represents URLs that do not have an authority, for example:
  * `mailto:[email protected]` and `data:text/plain;charset=UTF-8;page=21,the%20data:1234,5678`
  */
sealed trait UrlWithoutAuthority extends Url with UrlWithScheme {
  type Self <: UrlWithoutAuthority
  type SelfWithScheme <: UrlWithoutAuthority
  type SelfWithAuthority = AbsoluteUrl

  def scheme: String

  def authorityOption: Option[Authority] = None
  def hostOption: Option[Host] = None
  def port: Option[Int] = None
  def user: Option[String] = None
  def password: Option[String] = None

  def publicSuffix: Option[String] = None
  def publicSuffixes: Vector[String] = Vector.empty
  def subdomain: Option[String] = None
  def subdomains: Vector[String] = Vector.empty
  def shortestSubdomain: Option[String] = None
  def longestSubdomain: Option[String] = None

  def removeUserInfo(): Self = self
  def removePassword(): Self = self
  def mapUser(f: String => String): Self = self
  def mapPassword(f: String => String): Self = self
}

object UrlWithoutAuthority {
  def apply(scheme: String, path: UrlPath, query: QueryString, fragment: Option[String])(implicit
      config: UriConfig = UriConfig.default
  ): UrlWithoutAuthority =
    SimpleUrlWithoutAuthority(scheme, path, query, fragment)(config)

  def unapply(url: UrlWithoutAuthority): Option[(String, UrlPath, QueryString, Option[String])] =
    Some((url.scheme, url.path, url.query, url.fragment))

  def parse(s: CharSequence)(implicit config: UriConfig = UriConfig.default): UrlWithoutAuthority =
    parseTry(s).get

  def parseOption(s: CharSequence)(implicit config: UriConfig = UriConfig.default): Option[UrlWithoutAuthority] =
    parseTry(s).toOption

  def parseTry(s: CharSequence)(implicit config: UriConfig = UriConfig.default): Try[UrlWithoutAuthority] =
    UrlParser.parseUrlWithoutAuthority(s.toString)

  implicit val eqUrlWithoutAuthority: Eq[UrlWithoutAuthority] = Eq.fromUniversalEquals
  implicit val showUrlWithoutAuthority: Show[UrlWithoutAuthority] = Show.fromToString
  implicit val orderUrlWithoutAuthority: Order[UrlWithoutAuthority] = Order.by { url =>
    (url.scheme, url.path, url.query, url.fragment)
  }

  object unordered {
    implicit val eqUrlWithoutAuthority: Eq[UrlWithoutAuthority] =
      (x: UrlWithoutAuthority, y: UrlWithoutAuthority) => x.equalsUnordered(y)
  }
}

/** Represents URLs that do not have an authority, for example: `mailto:[email protected]`
  */
final case class SimpleUrlWithoutAuthority(scheme: String, path: UrlPath, query: QueryString, fragment: Option[String])(
    implicit val config: UriConfig = UriConfig.default
) extends UrlWithoutAuthority {
  type Self = SimpleUrlWithoutAuthority
  type SelfWithScheme = SimpleUrlWithoutAuthority

  def self: SimpleUrlWithoutAuthority = this

  def schemeOption: Option[String] = Some(scheme)

  def withScheme(scheme: String): SimpleUrlWithoutAuthority =
    copy(scheme = scheme)

  /** Copies this Url but with the host set as the given value.
    *
    * @param host the new host to set
    * @return a new Url with the specified host
    */
  def withHost(host: Host): AbsoluteUrl =
    AbsoluteUrl(scheme, Authority(host), path.toAbsoluteOrEmpty, query, fragment)

  /** Copies this Url but with the path set as the given value.
    *
    * @param path the new path to set
    * @return a new Url with the specified path
    */
  def withPath(path: UrlPath): SimpleUrlWithoutAuthority =
    copy(path = path)

  /** Copies this Url but with the port set as the given value.
    *
    * @param port the new port to set
    * @return a new Url with the specified port
    */
  def withPort(port: Int): AbsoluteUrl =
    AbsoluteUrl(scheme, Authority(host = "", port), path.toAbsoluteOrEmpty, query, fragment)

  def withAuthority(authority: Authority): AbsoluteUrl =
    AbsoluteUrl(scheme, authority, path.toAbsoluteOrEmpty, query, fragment)

  def withFragment[T: Fragment](fragment: T): SimpleUrlWithoutAuthority =
    copy(fragment = fragment.fragment)

  def withConfig(config: UriConfig): SimpleUrlWithoutAuthority =
    SimpleUrlWithoutAuthority(scheme, path, query, fragment)(config)

  def withQueryString(query: QueryString): SimpleUrlWithoutAuthority =
    copy(query = query)

  private[uri] def toStringWithConfig(c: UriConfig): String =
    scheme + ":" + path.toStringWithConfig(c) + queryToString(c) + fragmentToString(c)

  def normalize(removeEmptyPathParts: Boolean = false,
                slashTermination: SlashTermination = SlashTermination.AddForEmptyPath
  ): Self = {
    copy(
      scheme = scheme.toLowerCase,
      path = path.normalize(removeEmptyPathParts, slashTermination)
    )
  }
}

object SimpleUrlWithoutAuthority {
  def parse(s: CharSequence)(implicit config: UriConfig = UriConfig.default): SimpleUrlWithoutAuthority =
    parseTry(s).get

  def parseOption(s: CharSequence)(implicit config: UriConfig = UriConfig.default): Option[SimpleUrlWithoutAuthority] =
    parseTry(s).toOption

  def parseTry(s: CharSequence)(implicit config: UriConfig = UriConfig.default): Try[SimpleUrlWithoutAuthority] =
    UrlParser.parseSimpleUrlWithoutAuthority(s.toString)

  implicit val eqSimpleUrlWithoutAuthority: Eq[SimpleUrlWithoutAuthority] = Eq.fromUniversalEquals
  implicit val showSimpleUrlWithoutAuthority: Show[SimpleUrlWithoutAuthority] = Show.fromToString
  implicit val orderSimpleUrlWithoutAuthority: Order[SimpleUrlWithoutAuthority] = Order.by { url =>
    (url.scheme, url.path, url.query, url.fragment)
  }

  object unordered {
    implicit val eqSimpleUrlWithoutAuthority: Eq[SimpleUrlWithoutAuthority] =
      (x: SimpleUrlWithoutAuthority, y: SimpleUrlWithoutAuthority) => x.equalsUnordered(y)
  }
}

/** Represents URLs with the data scheme, for example: `data:text/plain;charset=UTF-8;page=21,the%20data:1234,5678`
  */
final case class DataUrl(mediaType: MediaType, base64: Boolean, data: Array[Byte])(implicit
    val config: UriConfig = UriConfig.default
) extends UrlWithoutAuthority {
  type Self = DataUrl
  type SelfWithScheme = UrlWithoutAuthority

  def self: DataUrl = this

  val scheme = "data"
  def schemeOption: Option[String] = Some(scheme)

  def query: QueryString = QueryString.empty
  def fragment: Option[String] = None
  def path: UrlPath = RootlessPath.fromParts(pathString(config.withNoEncoding))

  /** @return The data from this data URL using the charset provided by the URL's mediatype
    */
  def dataAsString: String =
    new String(data, mediaType.charset)

  protected def pathString(c: UriConfig): String = {
    val base64Str = if (base64) ";base64" else ""
    val dataStr =
      if (base64) {
        val b64Encoded = Base64.getEncoder.encodeToString(data)
        c.pathEncoder.encode(b64Encoded, mediaType.charset)
      } else {
        c.pathEncoder.encode(data, mediaType.charset)
      }
    mediaType.toString + base64Str + "," + dataStr
  }

  def withScheme(scheme: String): UrlWithoutAuthority =
    if (scheme.equalsIgnoreCase("data"))
      this
    else
      SimpleUrlWithoutAuthority(scheme, path, query, fragment)

  /** Copies this Url but with the host set as the given value.
    *
    * @param host the new host to set
    * @return a new Url with the specified host
    */
  def withHost(host: Host): AbsoluteUrl =
    AbsoluteUrl(scheme, Authority(host), path.toAbsoluteOrEmpty, query, fragment)

  /** Copies this Url but with the path set as the given value.
    *
    * @param path the new path to set
    * @return a new Url with the specified path
    */
  def withPath(path: UrlPath): DataUrl =
    DataUrl.parse(scheme + ":" + path.toString())

  /** Copies this Url but with the port set as the given value.
    *
    * @param port the new port to set
    * @return a new Url with the specified port
    */
  def withPort(port: Int): AbsoluteUrl =
    AbsoluteUrl(scheme, Authority(host = "", port), path.toAbsoluteOrEmpty, query, fragment)

  def withAuthority(authority: Authority): AbsoluteUrl =
    AbsoluteUrl(scheme, authority, path.toAbsoluteOrEmpty, query, fragment)

  def withFragment[T: Fragment](fragment: T): DataUrl =
    this

  def withQueryString(query: QueryString): DataUrl =
    this

  def withConfig(config: UriConfig): DataUrl =
    DataUrl(mediaType, base64, data)(config)

  private[uri] def toStringWithConfig(c: UriConfig): String =
    scheme + ":" + pathString(c)

  override def equals(obj: Any): Boolean = obj match {
    case other: DataUrl =>
      other.canEqual(this) &&
      mediaType == other.mediaType &&
      base64 == other.base64 &&
      util.Arrays.equals(data, other.data)
    case _ => false
  }

  override def hashCode(): Int =
    41 * (41 * (41 + mediaType.hashCode()) + base64.hashCode()) + util.Arrays.hashCode(data)

  /** For DataUrls this method is exactly the same as `==`
    */
  override def equalsUnordered(other: Uri): Boolean =
    this == other

  def normalize(removeEmptyPathParts: Boolean = false,
                slashTermination: SlashTermination = SlashTermination.AddForEmptyPath
  ): Self = this
}

object DataUrl {
  def fromBase64(mediaType: MediaType, data: String)(implicit config: UriConfig = UriConfig.default): DataUrl = {
    val base64Data = config.pathDecoder.decodeBytes(data, mediaType.charset)
    DataUrl(mediaType, base64 = true, Base64.getDecoder.decode(base64Data))
  }

  def fromPercentEncoded(mediaType: MediaType, data: String)(implicit config: UriConfig = UriConfig.default): DataUrl =
    DataUrl(mediaType, base64 = false, config.pathDecoder.decodeBytes(data, mediaType.charset))

  def parse(s: CharSequence)(implicit config: UriConfig = UriConfig.default): DataUrl =
    parseTry(s).get

  def parseOption(s: CharSequence)(implicit config: UriConfig = UriConfig.default): Option[DataUrl] =
    parseTry(s).toOption

  def parseTry(s: CharSequence)(implicit config: UriConfig = UriConfig.default): Try[DataUrl] =
    UrlParser.parseDataUrl(s.toString)

  implicit val eqDataUrl: Eq[DataUrl] = Eq.fromUniversalEquals
  implicit val showDataUrl: Show[DataUrl] = Show.fromToString
  implicit val orderDataUrl: Order[DataUrl] = Order.by(_.toString())
}

/** Represents scp-like URLs, for example: `[email protected]:lemonlabsuk/scala-uri.git`
  *
  * From the `scp` manpage: [user@]host:[path]
  */
final case class ScpLikeUrl(override val user: Option[String], override val host: Host, path: UrlPath)(implicit
    val config: UriConfig = UriConfig.default
) extends UrlWithAuthority {
  type Self = ScpLikeUrl
  type SelfWithScheme = AbsoluteUrl
  def self: ScpLikeUrl = this

  override def userInfo: Option[UserInfo] = user.map(UserInfo.apply)
  def authority: Authority = Authority(userInfo, host, port = None)
  def query: QueryString = QueryString.empty
  def fragment: Option[String] = None

  /** Copies this Url but with the authority set as the given value.
    *
    * @param authority the authority host to set
    * @return a new Url with the specified authority
    */
  def withAuthority(authority: Authority): ScpLikeUrl = copy(host = authority.host)

  /** Copies this Url but with the fragment set as the given value.
    *
    * @param fragment the new fragment to set
    * @return a new Url with the specified fragment
    */
  def withFragment[T: Fragment](fragment: T): ScpLikeUrl = this

  /** Copies this Url but with the path set as the given value.
    *
    * @param path the new path to set
    * @return a new Url with the specified path
    */
  def withPath(path: UrlPath): ScpLikeUrl = copy(path = path)

  /** Copies this Url but with the query set as the given value.
    *
    * @param query the new QueryString to set
    * @return a new Url with the specified query
    */
  def withQueryString(query: QueryString): ScpLikeUrl = this

  def schemeOption: Option[String] = None

  /** Copies this Uri but with the scheme set as the given value.
    *
    * @param scheme the new scheme to set
    * @return a new Uri with the specified scheme
    */
  def withScheme(scheme: String): AbsoluteUrl =
    AbsoluteUrl(scheme, authority, path.toAbsoluteOrEmpty, QueryString.empty, None)

  def withConfig(config: UriConfig): ScpLikeUrl =
    ScpLikeUrl(user, host, path)(config)

  private[uri] def toStringWithConfig(c: UriConfig, hostToString: Host => String): String = {
    // Don't do percent encoding. Can't find any reference to it being
    user.fold("")(_ + "@") + hostToString(host) + ":" + path.toStringWithConfig(config.withNoEncoding)
  }

  /** For ScpLikeUrls this method is exactly the same as `==`
    */
  override def equalsUnordered(other: Uri): Boolean =
    this == other

  def normalize(removeEmptyPathParts: Boolean = false,
                slashTermination: SlashTermination = SlashTermination.AddForEmptyPath
  ): Self =
    copy(host = host.normalize, path = path.normalize(removeEmptyPathParts, slashTermination))
}

object ScpLikeUrl {
  def parse(s: CharSequence)(implicit config: UriConfig = UriConfig.default): ScpLikeUrl =
    parseTry(s).get

  def parseOption(s: CharSequence)(implicit config: UriConfig = UriConfig.default): Option[ScpLikeUrl] =
    parseTry(s).toOption

  def parseTry(s: CharSequence)(implicit config: UriConfig = UriConfig.default): Try[ScpLikeUrl] =
    UrlParser.parseScpLikeUrl(s.toString)

  implicit val eqScpLikeUrl: Eq[ScpLikeUrl] = Eq.fromUniversalEquals
  implicit val showScpLikeUrl: Show[ScpLikeUrl] = Show.fromToString
  implicit val orderScpLikeUrl: Order[ScpLikeUrl] = Order.by(_.toString())
}

/** Represents a URN. See [[https://www.ietf.org/rfc/rfc2141 RFC 2141]]
  * and [[https://tools.ietf.org/html/rfc8141 RFC 8141]]
  *
  * URNs will be in the form `urn:nid:nss`
  */
final case class Urn(path: UrnPath)(implicit val config: UriConfig = UriConfig.default) extends Uri {
  type Self = Urn
  type SelfWithScheme = UrlWithoutAuthority

  def self: Self = this

  def scheme = "urn"
  def schemeOption = Some(scheme)

  def nss: String = path.nss
  def nid: String = path.nid

  /** Converts this URN into a URL with the given scheme.
    * The NID and NSS will be made path segments of the URL.
    *
    * @param scheme the new scheme to set
    * @return a new Uri with the specified scheme
    */
  def withScheme(scheme: String): UrlWithoutAuthority =
    UrlWithoutAuthority(scheme, path.toUrlPath.toRootless, QueryString.empty, fragment = None)

  def toUrl: Url = throw new UriConversionException("Urn cannot be converted to Url")
  def toUrn: Urn = this

  def withConfig(config: UriConfig): Urn =
    Urn(path)(config)

  private[uri] def toStringWithConfig(c: UriConfig): String =
    scheme + ":" + path.toStringWithConfig(c)

  /** For URNs this method is exactly the same as `==`
    */
  def equalsUnordered(other: Uri): Boolean =
    this == other
}

object Urn {
  def apply(nid: String, nss: String): Urn =
    new Urn(UrnPath(nid, nss))

  def parse(s: CharSequence)(implicit config: UriConfig = UriConfig.default): Urn =
    parseTry(s).get

  def parseOption(s: CharSequence)(implicit config: UriConfig = UriConfig.default): Option[Urn] =
    parseTry(s).toOption

  def parseTry(s: CharSequence)(implicit config: UriConfig = UriConfig.default): Try[Urn] =
    UrnParser.parseUrn(s.toString)

  implicit val eqUrn: Eq[Urn] = Eq.fromUniversalEquals
  implicit val showUrn: Show[Urn] = Show.fromToString
  implicit val orderUrn: Order[Urn] = Order.by(_.path)
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy