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

org.http4s.UriTemplate.scala Maven / Gradle / Ivy

There is a newer version: 0.21.0-M1
Show newest version
package org.http4s

import java.net.URLEncoder

import org.http4s.Uri.{Authority, Host, IPv4, IPv6, RegName, Scheme}
import org.http4s.UriTemplate._

import scala.collection.mutable
import scala.collection.mutable.ArrayBuffer
import scala.util.{Failure, Success, Try}

/**
 * Simple representation of a URI Template that can be rendered as RFC6570
 * conform string.
 *
 * This model reflects only a subset of RFC6570.
 *
 * Level 1 and Level 2 are completely modeled and
 * Level 3 features are limited to:
 *  - Path segments, slash-prefixed
 *  - Form-style query, ampersand-separated
 *  - Fragment expansion
 */
case class UriTemplate(
  scheme: Option[Scheme] = None,
  authority: Option[Authority] = None,
  path: Path = Nil,
  query: UriTemplate.Query = Nil,
  fragment: Fragment = Nil) {

  /**
   * Replaces any expansion type that matches the given `name`. If no matching
   * `expansion` could be found the same instance will be returned.
   */
  def expandAny[T: QueryParamEncoder](name: String, value: T): UriTemplate =
    expandPath(name, value).expandQuery(name, value).expandFragment(name, value)

  /**
   * Replaces any expansion type in `fragment` that matches the given `name`.
   * If no matching `expansion` could be found the same instance will be
   * returned.
   */
  def expandFragment[T: QueryParamEncoder](name: String, value: T): UriTemplate = {
    if (fragment.isEmpty) this
    else copy(fragment = expandFragmentN(fragment, name, String.valueOf(value)))
  }

  /**
   * Replaces any expansion type in `path` that matches the given `name`. If no
   * matching `expansion` could be found the same instance will be returned.
   */
  def expandPath[T: QueryParamEncoder](name: String, values: List[T]): UriTemplate =
    copy(path = expandPathN(path, name, values.map(QueryParamEncoder[T].encode)))

  /**
   * Replaces any expansion type in `path` that matches the given `name`. If no
   * matching `expansion` could be found the same instance will be returned.
   */
  def expandPath[T: QueryParamEncoder](name: String, value: T): UriTemplate =
    copy(path = expandPathN(path, name, QueryParamEncoder[T].encode(value)::Nil))

  /**
   * Replaces any expansion type in `query` that matches the specified `name`.
   * If no matching `expansion` could be found the same instance will be
   * returned.
   */
  def expandQuery[T: QueryParamEncoder](name: String, values: List[T]): UriTemplate = {
    if (query.isEmpty) this
    else copy(query = expandQueryN(query, name, values.map(QueryParamEncoder[T].encode(_).value)))
  }

  /**
   * Replaces any expansion type in `query` that matches the specified `name`.
   * If no matching `expansion` could be found the same instance will be
   * returned.
   */
  def expandQuery(name: String): UriTemplate = expandQuery(name, List[String]())

  /**
   * Replaces any expansion type in `query` that matches the specified `name`.
   * If no matching `expansion` could be found the same instance will be
   * returned.
   */
  def expandQuery[T: QueryParamEncoder](name: String, values: T*): UriTemplate =
    expandQuery(name, values.toList)

  override lazy val toString =
    renderUriTemplate(this)

  /**
   * If no expansion is available an `Uri` will be created otherwise the
   * current instance of `UriTemplate` will be returned.
   */
  def toUriIfPossible: Try[Uri] =
    if (containsExpansions(this)) Failure(new IllegalStateException(s"all expansions must be resolved to be convertable: $this"))
    else Success(toUri(this))

}

object UriTemplate {

  type Path = List[PathDef]
  type Query = List[QueryDef]
  type Fragment = List[FragmentDef]

  protected val unreserved = (('a' to 'z') ++ ('A' to 'Z') ++ ('0' to '9') :+ '-' :+ '.' :+ '_' :+ '~').toSet

  //  protected val genDelims = ':' :: '/' :: '?' :: '#' :: '[' :: ']' :: '@' :: Nil
  //  protected val subDelims = '!' :: '$' :: '&' :: '\'' :: '(' :: ')' :: '*' :: '+' :: ',' :: ';' :: '=' :: Nil
  //  protected val reserved = genDelims ::: subDelims

  def isUnreserved(s: String) = s.forall(unreserved.contains)

  def isUnreservedOrEncoded(s: String): Boolean =
    URLEncoder.encode(s, "UTF-8").forall(c => unreserved.contains(c) || c == '%')

  protected def expandPathN(path: Path, name: String, values: List[QueryParameterValue]): Path = {
    val acc = new ArrayBuffer[PathDef]()
    def appendValues() = values foreach { v => acc.append(PathElm(v.value)) }
    path foreach {
      case p@PathElm(_) => acc.append(p)
      case p@VarExp(Seq(n)) =>
        if (n == name) appendValues()
        else acc.append(p)
      case p@VarExp(ns) =>
        if (ns.contains(name)) {
          appendValues()
          acc.append(VarExp(ns.filterNot(_ == name)))
        } else acc.append(p)
      case p@ReservedExp(Seq(n)) =>
        if (n == name) appendValues()
        else acc.append(p)
      case p@ReservedExp(ns) =>
        if (ns.contains(name)) {
          appendValues()
          acc.append(VarExp(ns.filterNot(_ == name)))
        } else acc.append(p)
      case p@PathExp(Seq(n)) =>
        if (n == name) appendValues()
        else acc.append(p)
      case p@PathExp(ns) =>
        if (ns.contains(name)) {
          appendValues()
          acc.append(PathExp(ns.filterNot(_ == name)))
        } else acc.append(p)
    }
    acc.toList
  }

  protected def expandQueryN(query: Query, name: String, values: List[String]): Query = {
    val acc = new ArrayBuffer[QueryDef]()
    query.foreach {
      case p@ParamElm(_, _) => acc.append(p)
      case p@ParamVarExp(r, List(n)) =>
        if (n == name) acc.append(ParamElm(r, values))
        else acc.append(p)
      case p@ParamVarExp(r, ns) =>
        if (ns.contains(name)) {
          acc.append(ParamElm(r, values))
          acc.append(ParamVarExp(r, ns.filterNot(_ == name)))
        } else acc.append(p)
      case p@ParamReservedExp(r, List(n)) =>
        if (n == name) acc.append(ParamElm(r, values))
        else acc.append(p)
      case p@ParamReservedExp(r, ns) =>
        if (ns.contains(name)) {
          acc.append(ParamElm(r, values))
          acc.append(ParamReservedExp(r, ns.filterNot(_ == name)))
        } else acc.append(p)
      case p@ParamExp(Seq(n)) =>
        if (n == name) acc.append(ParamElm(name, values))
        else acc.append(p)
      case p@ParamExp(ns) =>
        if (ns.contains(name)) {
          acc.append(ParamElm(name, values))
          acc.append(ParamExp(ns.filterNot(_ == name)))
        } else acc.append(p)
      case p@ParamContExp(Seq(n)) =>
        if (n == name) acc.append(ParamElm(name, values))
        else acc.append(p)
      case p@ParamContExp(ns) =>
        if (ns.contains(name)) {
          acc.append(ParamElm(name, values))
          acc.append(ParamContExp(ns.filterNot(_ == name)))
        } else acc.append(p)
    }
    acc.toList
  }

  protected def expandFragmentN(fragment: Fragment, name: String, value: String): Fragment = {
    val acc = new ArrayBuffer[FragmentDef]()
    fragment.foreach {
      case p@FragmentElm(_) => acc.append(p)
      case p@SimpleFragmentExp(n) => if (n == name) acc.append(FragmentElm(value)) else acc.append(p)
      case p@MultiFragmentExp(Seq(n)) => if (n == name) acc.append(FragmentElm(value)) else acc.append(p)
      case p@MultiFragmentExp(ns) =>
        if (ns.contains(name)) {
          acc.append(FragmentElm(value))
          acc.append(MultiFragmentExp(ns.filterNot(_ == name)))
        } else acc.append(p)
    }
    acc.toList
  }

  protected def renderAuthority(a: Authority): String = a match {
    case Authority(Some(u), h, None) => u + "@" + renderHost(h)
    case Authority(Some(u), h, Some(p)) => u + "@" + renderHost(h) + ":" + p
    case Authority(None, h, Some(p)) => renderHost(h) + ":" + p
    case Authority(_, h, _) => renderHost(h)
    case _ => ""
  }

  protected def renderHost(h: Host): String = h match {
    case RegName(n) => n.toString
    case IPv4(a) => a.toString
    case IPv6(a) => "[" + a.toString + "]"
    case _ => ""
  }

  protected def renderScheme(s: Scheme): String = s + ":"

  protected def renderSchemeAndAuthority(t: UriTemplate): String = t match {
    case UriTemplate(None, None, _, _, _) => ""
    case UriTemplate(Some(s), Some(a), _, _, _) => renderScheme(s) + "//" + renderAuthority(a)
    case UriTemplate(Some(s), None, _, _, _) => renderScheme(s)
    case UriTemplate(None, Some(a), _, _, _) => renderAuthority(a)
  }

  protected def renderQuery(ps: Query): String = {
    val parted = ps partition {
      case ParamElm(_, _) => false
      case ParamVarExp(_, _) => false
      case ParamReservedExp(_, _) => false
      case ParamExp(_) => true
      case ParamContExp(_) => true
    }
    val elements = new ArrayBuffer[String]()
    parted._2 foreach {
      case ParamElm(n, Nil) => elements.append(n)
      case ParamElm(n, List(v)) => elements.append(n + "=" + v)
      case ParamElm(n, vs) => vs.foreach(v => elements.append(n + "=" + v))
      case ParamVarExp(n, vs) => elements.append(n + "=" + "{" + vs.mkString(",") + "}")
      case ParamReservedExp(n, vs) => elements.append(n + "=" + "{+" + vs.mkString(",") + "}")
      case u => throw new IllegalStateException(s"type ${u.getClass.getName} not supported")
    }
    val exps = new ArrayBuffer[String]()
    def separator = if (elements.isEmpty && exps.isEmpty) "?" else "&"
    parted._1 foreach {
      case ParamExp(ns) => exps.append("{" + separator + ns.mkString(",") + "}")
      case ParamContExp(ns) => exps.append("{" + separator + ns.mkString(",") + "}")
      case u => throw new IllegalStateException(s"type ${u.getClass.getName} not supported")
    }
    if (elements.isEmpty) exps.mkString
    else "?" + elements.mkString("&") + exps.mkString
  }

  protected def renderFragment(f: Fragment): String = {
    val elements = new mutable.ArrayBuffer[String]()
    val expansions = new mutable.ArrayBuffer[String]()
    f map {
      case FragmentElm(v) => elements.append(v)
      case SimpleFragmentExp(n) => expansions.append(n)
      case MultiFragmentExp(ns) => expansions.append(ns.mkString(","))
    }
    if (elements.nonEmpty && expansions.nonEmpty)
      "#" + elements.mkString(",") + "{#" + expansions.mkString(",") + "}"
    else if (elements.nonEmpty)
      "#" + elements.mkString(",")
    else if (expansions.nonEmpty)
      "{#" + expansions.mkString(",") + "}"
    else
      "#"
  }

  protected def renderFragmentIdentifier(f: Fragment): String = {
    val elements = new mutable.ArrayBuffer[String]()
    f map {
      case FragmentElm(v) => elements.append(v)
      case SimpleFragmentExp(_) => throw new IllegalStateException("SimpleFragmentExp cannot be converted to a Uri")
      case MultiFragmentExp(_) => throw new IllegalStateException("MultiFragmentExp cannot be converted to a Uri")
    }
    if (elements.isEmpty) ""
    else elements.mkString(",")
  }

  protected def buildQuery(q: Query): org.http4s.Query = {
    val elements = Query.newBuilder
    q map {
      case ParamElm(n, Nil) => elements += ((n, None))
      case ParamElm(n, List(v)) => elements += ((n, Some(v)))
      case ParamElm(n, vs) => vs.foreach(v => elements += ((n, Some(v))))
      case u => throw new IllegalStateException(s"${u.getClass.getName} cannot be converted to a Uri")
    }

    elements.result()
  }

  protected def renderPath(p: Path): String = p match {
    case Nil => "/"
    case ps =>
      val elements = new ArrayBuffer[String]()
      ps foreach {
        case PathElm(n) => elements.append("/" + n)
        case VarExp(ns) => elements.append("{" + ns.mkString(",") + "}")
        case ReservedExp(ns) => elements.append("{+" + ns.mkString(",") + "}")
        case PathExp(ns) => elements.append("{/" + ns.mkString(",") + "}")
        case u => throw new IllegalStateException(s"type ${u.getClass.getName} not supported")
      }
      elements.mkString
  }

  protected def renderPathAndQueryAndFragment(t: UriTemplate): String = t match {
    case UriTemplate(_, _, Nil, Nil, Nil) => "/"
    case UriTemplate(_, _, Nil, Nil, f) => "/" + renderFragment(f)
    case UriTemplate(_, _, Nil, query, Nil) => "/" + renderQuery(query)
    case UriTemplate(_, _, Nil, query, f) => "/" + renderQuery(query) + renderFragment(f)
    case UriTemplate(_, _, path, Nil, Nil) => renderPath(path)
    case UriTemplate(_, _, path, query, Nil) => renderPath(path) + renderQuery(query)
    case UriTemplate(_, _, path, Nil, f) => renderPath(path) + renderFragment(f)
    case UriTemplate(_, _, path, query, f) => renderPath(path) + renderQuery(query) + renderFragment(f)

    case _ => ""
  }

  protected def renderUriTemplate(t: UriTemplate): String = t match {
    case UriTemplate(None, None, Nil, Nil, Nil) => "/"
    case UriTemplate(Some(s), Some(a), Nil, Nil, Nil) => renderSchemeAndAuthority(t)
    case UriTemplate(Some(s), Some(a), List(), Nil, Nil) => renderSchemeAndAuthority(t)
    case UriTemplate(scheme, authority, path, params, fragment) => renderSchemeAndAuthority(t) + renderPathAndQueryAndFragment(t)
    case _ => ""
  }

  protected def fragmentExp(f: FragmentDef): Boolean = f match {
    case FragmentElm(_) => false
    case SimpleFragmentExp(_) => true
    case MultiFragmentExp(_) => true
  }

  protected def pathExp(p: PathDef): Boolean = p match {
    case PathElm(n) => false
    case VarExp(ns) => true
    case ReservedExp(ns) => true
    case PathExp(ns) => true
  }

  protected def queryExp(q: QueryDef): Boolean = q match {
    case ParamElm(_, _) => false
    case ParamVarExp(_, _) => true
    case ParamReservedExp(_, _) => true
    case ParamExp(_) => true
    case ParamContExp(_) => true
  }

  protected def containsExpansions(t: UriTemplate): Boolean = t match {
    case UriTemplate(_, _, Nil, Nil, Nil) => false
    case UriTemplate(_, _, Nil, Nil, f)   => f exists fragmentExp
    case UriTemplate(_, _, Nil, q,   Nil) => q exists queryExp
    case UriTemplate(_, _, Nil, q,   f)   => (q exists queryExp) || (f exists fragmentExp)
    case UriTemplate(_, _, p,   Nil, Nil) => p exists pathExp
    case UriTemplate(_, _, p,   Nil, f)   => (p exists pathExp) || (f exists fragmentExp)
    case UriTemplate(_, _, p,   q,   Nil) => (p exists pathExp) || (q exists queryExp)
    case UriTemplate(_, _, p,   q,   f)   => (p exists pathExp) || (q exists queryExp) || (f exists fragmentExp)

  }

  protected def toUri(t: UriTemplate): Uri = t match {
    case UriTemplate(s, a, Nil, Nil, Nil) => Uri(s, a)
    case UriTemplate(s, a, Nil, Nil, f)   => Uri(s, a, fragment = Some(renderFragmentIdentifier(f)))
    case UriTemplate(s, a, Nil, q,   Nil) => Uri(s, a, query = buildQuery(q))
    case UriTemplate(s, a, Nil, q,   f)   => Uri(s, a, query = buildQuery(q), fragment = Some(renderFragmentIdentifier(f)))
    case UriTemplate(s, a, p,   Nil, Nil) => Uri(s, a, renderPath(p))
    case UriTemplate(s, a, p,   q,   Nil) => Uri(s, a, renderPath(p), buildQuery(q))
    case UriTemplate(s, a, p,   Nil, f)   => Uri(s, a, renderPath(p), fragment = Some(renderFragmentIdentifier(f)))
    case UriTemplate(s, a, p,   q,   f)   => Uri(s, a, renderPath(p), buildQuery(q), Some(renderFragmentIdentifier(f)))
  }

  sealed trait PathDef

  /** Static path element */
  case class PathElm(value: String) extends PathDef

  sealed trait QueryDef

  sealed trait QueryExp extends QueryDef
  /** Static query parameter element */
  case class ParamElm(name: String, values: List[String]) extends QueryDef
  object ParamElm {
    def apply(name: String): ParamElm = new ParamElm(name, Nil)
    def apply(name: String, values: String*): ParamElm = new ParamElm(name, values.toList)
  }

  /**
   * Simple string expansion for query parameter
   */
  case class ParamVarExp(name: String, variables: List[String]) extends QueryDef {
    require(variables forall isUnreserved, "all variables must consist of unreserved characters")
  }
  object ParamVarExp {
    def apply(name: String): ParamVarExp = new ParamVarExp(name, Nil)
    def apply(name: String, variables: String*): ParamVarExp = new ParamVarExp(name, variables.toList)
  }

  /**
   * Reserved string expansion for query parameter
   */
  case class ParamReservedExp(name: String, variables: List[String]) extends QueryDef {
    require(variables forall isUnreserved, "all variables must consist of unreserved characters")
  }
  object ParamReservedExp {
    def apply(name: String): ParamReservedExp = new ParamReservedExp(name, Nil)
    def apply(name: String, variables: String*): ParamReservedExp = new ParamReservedExp(name, variables.toList)
  }

  /**
   * URI Templates are similar to a macro language with a fixed set of macro
   * definitions: the expression type determines the expansion process.
   *
   * The default expression type is simple string expansion (Level 1), wherein a
   * single named variable is replaced by its value as a string after
   * pct-encoding any characters not in the set of unreserved URI characters
   * (Section 1.5).
   *
   * Level 2 templates add the plus ("+") operator, for expansion of values that
   * are allowed to include reserved URI characters
   * (Section 1.5),
   * and the crosshatch ("#") operator for expansion of fragment identifiers.
   *
   * Level 3 templates allow multiple variables per expression, each
   * separated by a comma, and add more complex operators for dot-prefixed
   * labels, slash-prefixed path segments, semicolon-prefixed path
   * parameters, and the form-style construction of a query syntax
   * consisting of name=value pairs that are separated by an ampersand
   * character.
   */
  sealed trait ExpansionType

  sealed trait FragmentDef

  /** Static fragment element */
  case class FragmentElm(value: String) extends FragmentDef

  /**
   * Fragment expansion, crosshatch-prefixed
   * (Section 3.2.4)
   */
  case class SimpleFragmentExp(name: String) extends FragmentDef {
    require(name.nonEmpty, "at least one character must be set")
    require(isUnreserved(name), "name must consist of unreserved characters")
  }

  /**
   * Level 1 allows string expansion
   * (Section 3.2.2)
   *
   * Level 3 allows string expansion with multiple variables
   * (Section 3.2.2)
   */
  case class VarExp(names: List[String]) extends PathDef {
    require(names.nonEmpty, "at least one name must be set")
    require(names forall isUnreserved, "all names must consist of unreserved characters")
  }
  object VarExp {
    def apply(names: String*): VarExp = new VarExp(names.toList)
  }

  /**
   * Level 2 allows reserved string expansion
   * (Section 3.2.3)
   *
   * Level 3 allows reserved expansion with multiple variables
   * (Section 3.2.3)
   */
  case class ReservedExp(names: List[String]) extends PathDef {
    require(names.nonEmpty, "at least one name must be set")
    require(names forall isUnreserved, "all names must consist of unreserved characters")
  }
  object ReservedExp {
    def apply(names: String*): ReservedExp = new ReservedExp(names.toList)
  }

  /**
   * Fragment expansion with multiple variables, crosshatch-prefixed
   * (Section 3.2.4)
   */
  case class MultiFragmentExp(names: List[String]) extends FragmentDef {
    require(names.nonEmpty, "at least one name must be set")
    require(names forall isUnreserved, "all names must consist of unreserved characters")
  }
  object MultiFragmentExp {
    def apply(names: String*): MultiFragmentExp = new MultiFragmentExp(names.toList)
  }

  /**
   * Path segments, slash-prefixed
   * (Section 3.2.6)
   */
  case class PathExp(names: List[String]) extends PathDef {
    require(names.nonEmpty, "at least one name must be set")
    require(names forall isUnreserved, "all names must consist of unreserved characters")
  }
  object PathExp {
    def apply(names: String*): PathExp = new PathExp(names.toList)
  }

  /**
   * Form-style query, ampersand-separated
   * (Section 3.2.8)
   */
  case class ParamExp(names: List[String]) extends QueryExp {
    require(names.nonEmpty, "at least one name must be set")
    require(names forall isUnreservedOrEncoded, "all names must consist of unreserved characters or be encoded")
  }
  object ParamExp {
    def apply(names: String*): ParamExp = new ParamExp(names.toList)
  }

  /**
   * Form-style query continuation
   * (Section 3.2.9)
   */
  case class ParamContExp(names: List[String]) extends QueryExp {
    require(names.nonEmpty, "at least one name must be set")
    require(names forall isUnreserved, "all names must consist of unreserved characters")
  }
  object ParamContExp {
    def apply(names: String*): ParamContExp = new ParamContExp(names.toList)
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy