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

hammock.Uri.scala Maven / Gradle / Ivy

The newest version!
package hammock

import atto._
import Atto._
import cats._
import cats.implicits._
import Uri._
import cats.data.NonEmptyList
import Function.const


/**
  * Represents a [[HttpRequest]] URI.
  *
  * You have several different options for constructing [[Uri]]:
  *
  * {{{
  * scala> val uri1 = uri"http://google.com"
  * uri1: hammock.Uri = Uri(Some(http),None,google.com,Map(),None)
  *
  * scala> val uri2 = Uri(None, None, "path", Map(), None)
  * uri2: hammock.Uri = Uri(None,None,path,Map(),None)
  *
  * scala> val uri3 = Uri.fromString("http://google.com")
  * uri3: Either[String,hammock.Uri] = Right(Uri(Some(http),None,google.com,Map(),None))
  * }}}
  *
  * @param scheme    scheme of the uri. For example https
  * @param authority authority of the uri. For example: user:[email protected]:443
  * @param path      path of the uri. For example /books/234
  * @param query     query string of the uri. For example ?page=3&utm_source=campaign
  * @param fragment  fragment of the uri. For example #header1
  */
case class Uri(
                scheme: Option[Scheme] = None,
                authority: Option[Authority] = None,
                path: String = "",
                query: Map[String, String] = Map(),
                fragment: Option[Fragment] = None) {

  /** Append a string to the path of the [[Uri]]
    */
  def /(str: String): Uri = {
    copy(path = s"$path/$str")
  }

  /**
    * Append query parameter to [[query]]
    *
    * @param key   - parameter name
    * @param value - the value
    * @return updated [[Uri]]
    **/
  def param(key: String, value: String): Uri = copy(query = this.query + (key -> value))

  /**
    * Appends multiple query parameters to [[query]]
    *
    * @param ps - parameters
    * @return updated [[Uri]]
    **/
  def params(ps: (String, String)*): Uri = ps match {
    case Seq() => this
    case _     => ps.foldLeft(this) { case (uri, (k, v)) => uri.copy(query = uri.query + (k -> v)) }
  }

  /**
    * Produces the same result as [[params]]
    * but provides syntax as you are writing URI query in browser
    * Usage example:
    * {{{
    *   uri"example.com" ? (("a" -> "b") & ("c" -> "d") & ("e" -> "f"))
    * }}}
    *
    * @param ps - parameters
    * @return updated [[Uri]]
    **/
  def ?(ps: NonEmptyList[(String, String)]): Uri = params(ps.toList: _*)
}

object Uri {

  sealed trait Host

  object Host {
    case class IPv4(a: Int, b: Int, c: Int, d: Int) extends Host
    object IPv4 {
      def parse: Parser[Host] = for {
        a <- ubyte <~ char('.')
        b <- ubyte <~ char('.')
        c <- ubyte <~ char('.')
        d <- ubyte
      } yield IPv4(a, b, c, d)
    }

    case class IPv6(a: IPv6Group, b: IPv6Group, c: IPv6Group, d: IPv6Group, e: IPv6Group, f: IPv6Group, g: IPv6Group, h: IPv6Group) extends Host

    object IPv6 {
      def parse: Parser[Host] = for {
        a <- IPv6Group.parse <~ char(':')
        b <- IPv6Group.parse <~ char(':')
        c <- IPv6Group.parse <~ char(':')
        d <- IPv6Group.parse <~ char(':')
        m <- moreGroups
      } yield IPv6(a,b,c,d,m._1,m._2,m._3,m._4)

      private def noMoreGroups: Parser[(IPv6Group, IPv6Group, IPv6Group, IPv6Group)] = char(':')
        .map(const((IPv6Group.empty,IPv6Group.empty,IPv6Group.empty,IPv6Group.empty)))

      private def fourMoreGroups: Parser[(IPv6Group, IPv6Group, IPv6Group, IPv6Group)] = for {
        e <- IPv6Group.parse <~ char(':')
        f <- IPv6Group.parse <~ char(':')
        g <- IPv6Group.parse <~ char(':')
        h <- IPv6Group.parse
      } yield (e, f, g, h)

      private def moreGroups: Parser[(IPv6Group, IPv6Group, IPv6Group, IPv6Group)] = noMoreGroups | fourMoreGroups
    }

    case class IPv6Group(value: Short)

    object IPv6Group {
      val empty = IPv6Group(0)

      implicit val showIpv6Group: Show[IPv6Group] = new Show[IPv6Group] {
        def show(group: IPv6Group): String = "%04X" format group.value
      }

      def parse: Parser[IPv6Group] = (manyN(4, hexDigit) | manyN(2, hexDigit)).map { chars =>
        IPv6Group(java.lang.Integer.parseInt(chars.mkString, 16).toShort)
      }
    }

    case object Localhost extends Host {
      def parse: Parser[Host] = string("localhost").map(const(Localhost))
    }

    case class Other(repr: String) extends Host

    object Other {
      def parse: Parser[Host] = many1(noneOf(":/?")).map(chars => Other(chars.toList.mkString))
    }

    /**
      * Adapted from http://tpolecat.github.io/atto/docs/next-steps.html
      */
    private val ubyte: Parser[Int] = {
      int.filter(n => n >= 0 && n < 256) // ensure value is in [0 .. 256)
        .namedOpaque("UByte")           // give our parser a name
    }


    implicit val showHost: Show[Host] = new Show[Host] {
      def show(host: Host): String = host match {
        case Host.IPv4(a,b,c,d) => s"$a.$b.$c.$d"
        case Host.IPv6(a,b,c,d,e,f,g,h) =>
          val reprLastGroups: String =
            if (
              e.value.isEmpty &&
              f.value.isEmpty &&
              g.value.isEmpty &&
              h.value.isEmpty) ":" // just append another colon
            else
              e.show ++ ":" ++
              f.show ++ ":" ++
              g.show ++ ":" ++
              h.show

        "[" ++ a.show ++ ":" ++ b.show ++ ":" ++ c.show ++ ":" ++ d.show ++ ":" ++ reprLastGroups ++ "]"
      case Host.Localhost                    => "localhost"
      case Host.Other(repr)                  => repr
    }
  }

    implicit val eqHost: Eq[Host] = Eq.fromUniversalEquals

    def parse: Parser[Host] = IPv4.parse |
      squareBrackets(IPv6.parse) |
      Localhost.parse |
      Other.parse
  }

  final case class Authority(user: Option[String], host: Host, port: Option[Long])

  object Authority {
    def parse: Parser[Authority] = for {
      user <- opt(userParser)
      host <- Host.parse
      port <- opt(char(':') ~> long)
    } yield Authority(user, host, port)

    implicit val showAuthority: Show[Authority] = new Show[Authority] {
      def show(auth: Authority): String =
        auth.user.fold("")(_ ++ "@") ++ auth.host.show ++ auth.port.fold("")(p => s":${p.toString}")
    }

    implicit val eqAuthority: Eq[Authority] = new Eq[Authority] {
      def eqv(a: Authority, b: Authority): Boolean =
        a.user === b.user && a.host === b.host && a.port === b.port
    }

    private def userParser: Parser[String] =
      many(noneOf("@,/?&=")).map(_.mkString) <~ char('@')
  }

  type Scheme = String
  type Fragment = String

  implicit val showUri = new Show[Uri] {
    override def show(u: Uri): String = {
      val queryString = if (u.query.isEmpty) {
        ""
      } else {
        u.query.map(kv => s"${kv._1}=${kv._2}").mkString("?", "&", "")
      }

      u.scheme.fold("")(_ ++ "://") ++ u.authority.fold("")(_.show) ++ u.path ++ queryString ++ u.fragment.fold("")(
        "#" ++ _)
    }
  }

  implicit val eqUri: Eq[Uri] = Eq.fromUniversalEquals

  def queryParamParser: Parser[(String, String)] =
    (stringOf(notChar('=')) <~ char('=')) ~ takeWhile(x => x != '&' && x != '#')

  def queryParamsParser: Parser[Map[String, String]] = sepBy(queryParamParser, char('&')).map(_.toMap)

  def schemeParser: Parser[String] = takeWhile(_ != ':') <~ char(':') <~ opt(string("//"))

  def path: Parser[String] = char('/') ~> takeWhile(x => x != '?' && x != '#') map (p => "/" ++ p)

  def parser: Parser[Uri] =
    for {
      scheme      <- opt(schemeParser)
      authority   <- opt(Authority.parse)
      path        <- path | ok("")
      queryParams <- opt(char('?') ~> queryParamsParser)
      fragment <- opt(char('#') ~> stringOf(anyChar))
    } yield Uri(scheme, authority, path, queryParams.getOrElse(Map()), fragment)

  def fromString(str: String): Either[String, Uri] = (parser parseOnly str).either

  def unsafeParse(str: String): Uri = fromString(str).right.get

  def isValid(str: String): Boolean = fromString(str).isRight
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy