1
package hammock
2

3
import atto._
4
import Atto._
5
import cats._
6
import cats.implicits._
7
import Uri._
8
import cats.data.NonEmptyList
9
import Function.const
10

11

12
/**
13
  * Represents a [[HttpRequest]] URI.
14
  *
15
  * You have several different options for constructing [[Uri]]:
16
  *
17
  * {{{
18
  * scala> val uri1 = uri"http://google.com"
19
  * uri1: hammock.Uri = Uri(Some(http),None,google.com,Map(),None)
20
  *
21
  * scala> val uri2 = Uri(None, None, "path", Map(), None)
22
  * uri2: hammock.Uri = Uri(None,None,path,Map(),None)
23
  *
24
  * scala> val uri3 = Uri.fromString("http://google.com")
25
  * uri3: Either[String,hammock.Uri] = Right(Uri(Some(http),None,google.com,Map(),None))
26
  * }}}
27
  *
28
  * @param scheme    scheme of the uri. For example https
29
  * @param authority authority of the uri. For example: user:pass@google.com:443
30
  * @param path      path of the uri. For example /books/234
31
  * @param query     query string of the uri. For example ?page=3&utm_source=campaign
32
  * @param fragment  fragment of the uri. For example #header1
33
  */
34
case class Uri(
35
                scheme: Option[Scheme] = None,
36
                authority: Option[Authority] = None,
37
                path: String = "",
38
                query: Map[String, String] = Map(),
39
                fragment: Option[Fragment] = None) {
40

41
  /** Append a string to the path of the [[Uri]]
42
    */
43
  def /(str: String): Uri = {
44 1
    copy(path = s"$path/$str")
45
  }
46

47
  /**
48
    * Append query parameter to [[query]]
49
    *
50
    * @param key   - parameter name
51
    * @param value - the value
52
    * @return updated [[Uri]]
53
    **/
54 1
  def param(key: String, value: String): Uri = copy(query = this.query + (key -> value))
55

56
  /**
57
    * Appends multiple query parameters to [[query]]
58
    *
59
    * @param ps - parameters
60
    * @return updated [[Uri]]
61
    **/
62
  def params(ps: (String, String)*): Uri = ps match {
63
    case Seq() => this
64 1
    case _     => ps.foldLeft(this) { case (uri, (k, v)) => uri.copy(query = uri.query + (k -> v)) }
65
  }
66

67
  /**
68
    * Produces the same result as [[params]]
69
    * but provides syntax as you are writing URI query in browser
70
    * Usage example:
71
    * {{{
72
    *   uri"example.com" ? (("a" -> "b") & ("c" -> "d") & ("e" -> "f"))
73
    * }}}
74
    *
75
    * @param ps - parameters
76
    * @return updated [[Uri]]
77
    **/
78 1
  def ?(ps: NonEmptyList[(String, String)]): Uri = params(ps.toList: _*)
79
}
80

81
object Uri {
82

83
  sealed trait Host
84

85
  object Host {
86
    case class IPv4(a: Int, b: Int, c: Int, d: Int) extends Host
87
    object IPv4 {
88
      def parse: Parser[Host] = for {
89 1
        a <- ubyte <~ char('.')
90 1
        b <- ubyte <~ char('.')
91 1
        c <- ubyte <~ char('.')
92 1
        d <- ubyte
93 1
      } yield IPv4(a, b, c, d)
94
    }
95

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

98
    object IPv6 {
99
      def parse: Parser[Host] = for {
100 1
        a <- IPv6Group.parse <~ char(':')
101 1
        b <- IPv6Group.parse <~ char(':')
102 1
        c <- IPv6Group.parse <~ char(':')
103 1
        d <- IPv6Group.parse <~ char(':')
104 1
        m <- moreGroups
105 1
      } yield IPv6(a,b,c,d,m._1,m._2,m._3,m._4)
106

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

110
      private def fourMoreGroups: Parser[(IPv6Group, IPv6Group, IPv6Group, IPv6Group)] = for {
111 1
        e <- IPv6Group.parse <~ char(':')
112 1
        f <- IPv6Group.parse <~ char(':')
113 1
        g <- IPv6Group.parse <~ char(':')
114 1
        h <- IPv6Group.parse
115 1
      } yield (e, f, g, h)
116

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

120
    case class IPv6Group(value: Short)
121

122
    object IPv6Group {
123 1
      val empty = IPv6Group(0)
124

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

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

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

138
    case class Other(repr: String) extends Host
139

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

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

152

153 1
    implicit val showHost: Show[Host] = new Show[Host] {
154
      def show(host: Host): String = host match {
155 1
        case Host.IPv4(a,b,c,d) => s"$a.$b.$c.$d"
156
        case Host.IPv6(a,b,c,d,e,f,g,h) =>
157
          val reprLastGroups: String =
158
            if (
159 1
              e.value.isEmpty &&
160 0
              f.value.isEmpty &&
161 1
              g.value.isEmpty &&
162 0
              h.value.isEmpty) ":" // just append another colon
163
            else
164 1
              e.show ++ ":" ++
165 1
              f.show ++ ":" ++
166 1
              g.show ++ ":" ++
167 1
              h.show
168

169 1
        "[" ++ a.show ++ ":" ++ b.show ++ ":" ++ c.show ++ ":" ++ d.show ++ ":" ++ reprLastGroups ++ "]"
170 1
      case Host.Localhost                    => "localhost"
171
      case Host.Other(repr)                  => repr
172
    }
173
  }
174

175 1
    implicit val eqHost: Eq[Host] = Eq.fromUniversalEquals
176

177 1
    def parse: Parser[Host] = IPv4.parse |
178 1
      squareBrackets(IPv6.parse) |
179 1
      Localhost.parse |
180 1
      Other.parse
181
  }
182

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

185
  object Authority {
186
    def parse: Parser[Authority] = for {
187 1
      user <- opt(userParser)
188 1
      host <- Host.parse
189 1
      port <- opt(char(':') ~> long)
190 1
    } yield Authority(user, host, port)
191

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

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

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

206
  type Scheme = String
207
  type Fragment = String
208

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

217 1
      u.scheme.fold("")(_ ++ "://") ++ u.authority.fold("")(_.show) ++ u.path ++ queryString ++ u.fragment.fold("")(
218 1
        "#" ++ _)
219
    }
220
  }
221

222 1
  implicit val eqUri: Eq[Uri] = Eq.fromUniversalEquals
223

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

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

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

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

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

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

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

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

Read our documentation on viewing source code .

Loading