├── .gitignore
├── .jvmopts
├── .travis.yml
├── LICENSE
├── README.md
├── build.sbt
├── project
├── UpdatePublicSuffixTrie.scala
├── build.properties
├── build_dependencies.sbt
└── scoverage.sbt
└── src
├── main
├── resources
│ └── public_suffix_trie.json
└── scala
│ └── com
│ └── netaporter
│ └── uri
│ ├── Parameters.scala
│ ├── PathPart.scala
│ ├── QueryString.scala
│ ├── Uri.scala
│ ├── config
│ └── UriConfig.scala
│ ├── decoding
│ ├── NoopDecoder.scala
│ ├── PercentDecoder.scala
│ ├── PermissiveDecoder.scala
│ ├── UriDecodeException.scala
│ └── UriDecoder.scala
│ ├── dsl
│ ├── UriDsl.scala
│ └── package.scala
│ ├── encoding
│ ├── ChainedUriEncoder.scala
│ ├── EncodeCharAs.scala
│ ├── NoopEncoder.scala
│ ├── PercentEncoder.scala
│ ├── UriEncoder.scala
│ └── package.scala
│ ├── inet
│ ├── PublicSuffixes.scala
│ └── Trie.scala
│ └── parsing
│ ├── DefaultUriParser.scala
│ ├── MatrixParamSupport.scala
│ └── UriParser.scala
└── test
└── scala
└── com
└── netaporter
└── uri
├── ApplyTests.scala
├── DecodingTests.scala
├── DslTests.scala
├── EncodingTests.scala
├── GithubIssueTests.scala
├── ParsingTests.scala
├── ProtocolTests.scala
├── PublicSuffixTests.scala
├── ToUriTests.scala
├── TransformTests.scala
└── TypeTests.scala
/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | *.iml
3 | *.ipr
4 | *.iws
5 | .idea/
6 |
7 | .project
8 |
9 | # OS X
10 | Icon
11 | Thumbs.db
12 | .DS_Store
13 |
14 | .history
--------------------------------------------------------------------------------
/.jvmopts:
--------------------------------------------------------------------------------
1 | -Xss1m
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | ---
2 | language: scala
3 | #script: "sbt ++$TRAVIS_SCALA_VERSION coverage test"
4 | script: "sbt ++$TRAVIS_SCALA_VERSION test"
5 |
6 | jdk:
7 | - oraclejdk8
8 |
9 | scala:
10 | - 2.10.6
11 | - 2.11.8
12 | - 2.12.0
13 |
14 | sudo: false # Enable new travis container-based infrastructure
15 |
16 | #after_success:
17 | # - bash <(curl -s https://codecov.io/bash)
18 |
19 | notifications:
20 | webhooks:
21 | urls:
22 | - https://webhooks.gitter.im/e/a3d21b4ef5b88d789383
23 | on_success: change # options: [always|never|change] default: always
24 | on_failure: always # options: [always|never|change] default: always
25 | on_start: false # default: false
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | This software is licensed under the Apache 2 license, quoted below.
2 |
3 | Copyright (C) 2011-2012 Ian Forsey
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License"); you may not
6 | use this file except in compliance with the License. You may obtain a copy of
7 | the License at
8 |
9 | [http://www.apache.org/licenses/LICENSE-2.0]
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 | License for the specific language governing permissions and limitations under
15 | the License.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # scala-uri has moved
2 |
3 | **scala-uri** now lives at [lemonlabsuk/scala-uri](https://github.com/lemonlabsuk/scala-uri)
4 |
--------------------------------------------------------------------------------
/build.sbt:
--------------------------------------------------------------------------------
1 | //import scoverage.ScoverageSbtPlugin.ScoverageKeys._
2 |
3 | name := "scala-uri"
4 |
5 | organization := "com.netaporter"
6 |
7 | version := "0.4.16"
8 |
9 | scalaVersion := "2.12.0"
10 |
11 | crossScalaVersions := Seq("2.10.6", "2.11.8", "2.12.0")
12 |
13 | def coverageEnabled(scalaVersion: String) = scalaVersion match {
14 | case v if v startsWith "2.10" => false
15 | case v if v startsWith "2.12" => false
16 | case _ => true
17 | }
18 |
19 | lazy val updatePublicSuffixes = taskKey[Unit]("Updates the public suffix Trie at com.netaporter.uri.internet.PublicSuffixes")
20 |
21 | updatePublicSuffixes := UpdatePublicSuffixTrie.generate()
22 |
23 | //coverageOutputXML := coverageEnabled(scalaVersion.value)
24 | //
25 | //coverageOutputCobertua := coverageEnabled(scalaVersion.value)
26 | //
27 | //coverageHighlighting := coverageEnabled(scalaVersion.value)
28 |
29 | publishMavenStyle := true
30 |
31 | publishArtifact in Test := false
32 |
33 | pomIncludeRepository := { _ => false }
34 |
35 | resolvers += "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots"
36 |
37 | resolvers += "Sonatype OSS Releases" at "https://oss.sonatype.org/content/repositories/releases"
38 |
39 | scalacOptions := Seq("-unchecked", "-deprecation", "-encoding", "utf8", "-feature")
40 |
41 | libraryDependencies += "org.parboiled" %% "parboiled" % "2.1.3"
42 |
43 | libraryDependencies += "io.spray" %% "spray-json" % "1.3.2"
44 |
45 | libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.0" % "test"
46 |
47 | parallelExecution in Test := false
48 |
49 | publishTo <<= version { (v: String) =>
50 | val nexus = "https://oss.sonatype.org/"
51 | if (v.trim.endsWith("SNAPSHOT"))
52 | Some("snapshots" at nexus + "content/repositories/snapshots")
53 | else
54 | Some("releases" at nexus + "service/local/staging/deploy/maven2")
55 | }
56 |
57 | pomExtra := (
58 | https://github.com/net-a-porter/scala-uri
59 |
60 |
61 | Apache 2
62 | http://www.apache.org/licenses/LICENSE-2.0
63 | repo
64 |
65 |
66 |
67 | git@github.com:net-a-porter/scala-uri.git
68 | scm:git@github.com:net-a-porter/scala-uri.git
69 |
70 |
71 |
72 | theon
73 | Ian Forsey
74 | http://theon.github.io
75 |
76 | )
77 |
--------------------------------------------------------------------------------
/project/UpdatePublicSuffixTrie.scala:
--------------------------------------------------------------------------------
1 | import java.io.{PrintWriter, File}
2 |
3 | import scala.io.{Codec, Source}
4 |
5 | import spray.json.DefaultJsonProtocol._
6 | import spray.json._
7 |
8 | object UpdatePublicSuffixTrie {
9 | object Trie {
10 | def apply(prefix: List[Char]): Trie = prefix match {
11 | case Nil => Trie(Map.empty, wordEnd = true)
12 | case x :: xs => Trie(Map(x -> Trie(xs)), wordEnd = false)
13 | }
14 |
15 | def empty = Trie(Map.empty, wordEnd = false)
16 | }
17 | case class Trie(children: Map[Char, Trie], wordEnd: Boolean = false) {
18 |
19 | def +(kv: (Char, Trie)): Trie =
20 | this.copy(children = children + kv)
21 |
22 | def next(ch: Char): Option[Trie] =
23 | children.get(ch)
24 |
25 | def insert(value: List[Char]): Trie = value match {
26 | case Nil => copy(wordEnd = true)
27 | case x :: xs =>
28 | next(x) match {
29 | case None =>
30 | this + (x -> Trie(xs))
31 | case Some(child) =>
32 | this + (x -> child.insert(xs))
33 | }
34 |
35 | }
36 |
37 | override def toString(): String = {
38 | s"""Trie(
39 | Map(${children.map(kv => s"'${kv._1}' -> ${kv._2.toString()}").mkString(",")})
40 | ${if(wordEnd) ", wordEnd = true" else ""}
41 | )
42 | """
43 | }
44 |
45 | }
46 |
47 | def generate(): Unit = {
48 | implicit val enc = Codec.UTF8
49 |
50 | val suffixes = for {
51 | line <- Source.fromURL("https://publicsuffix.org/list/public_suffix_list.dat").getLines
52 | trimLine = line.trim
53 | if !trimLine.startsWith("//") && !trimLine.isEmpty
54 | } yield trimLine
55 |
56 | val trie = suffixes.foldLeft(Trie.empty) { (trieSoFar, suffix) =>
57 | trieSoFar insert suffix.reverse.toCharArray.toList
58 | }
59 |
60 | // Reduce JSON file size by using short names "c" and "e" for children and wordEnd
61 | implicit lazy val trieFmt: JsonFormat[Trie] = lazyFormat(jsonFormat(Trie.apply, "c", "e"))
62 |
63 | val p = new PrintWriter(new File("src/main/resources/public_suffix_trie.json"))
64 | p.println(trie.toJson.compactPrint)
65 | p.close()
66 | }
67 | }
--------------------------------------------------------------------------------
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=0.13.6
--------------------------------------------------------------------------------
/project/build_dependencies.sbt:
--------------------------------------------------------------------------------
1 | libraryDependencies += "io.spray" %% "spray-json" % "1.3.2"
--------------------------------------------------------------------------------
/project/scoverage.sbt:
--------------------------------------------------------------------------------
1 | //addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.2.0")
--------------------------------------------------------------------------------
/src/main/scala/com/netaporter/uri/Parameters.scala:
--------------------------------------------------------------------------------
1 | package com.netaporter.uri
2 |
3 | import com.netaporter.uri.encoding.UriEncoder
4 | import com.netaporter.uri.Parameters._
5 | import scala.Some
6 | import scala.collection.GenTraversableOnce
7 | import scala.collection.Seq
8 |
9 | /**
10 | * Trait use to represent a list of key value parameters, such as query string parameters and matrix parameters
11 | */
12 | trait Parameters {
13 | type Self <: Parameters
14 |
15 | def separator: String
16 |
17 | def params: ParamSeq
18 |
19 | def withParams(params: ParamSeq): Self
20 |
21 | lazy val paramMap = params.foldLeft(Map.empty[String, Seq[String]]) {
22 |
23 | case (m, (k, Some(v))) =>
24 | val values = m.getOrElse(k, Nil)
25 | m + (k -> (values :+ v))
26 |
27 | // For query parameters with no value (e.g. /blah?q), Put at explicit Nil into the Map
28 | // If there is already an entry in the Map from a previous parameter with the same name, maintain it
29 | case (m, (k, None)) =>
30 | val values = m.getOrElse(k, Nil)
31 | m + (k -> values)
32 | }
33 |
34 | /**
35 | * Adds a new parameter key-value pair.
36 | *
37 | * @return A new instance with the new parameter added
38 | */
39 | def addParam(k: String, v: String): Self = addParam(k, Some(v))
40 |
41 | /**
42 | * Adds a new parameter key-value pair. If the value for the parameter is None, then this
43 | * parameter will be rendered without an = sign (use Some("") if this is not what you want).
44 | *
45 | * @return A new instance with the new parameter added
46 | */
47 | def addParam(k: String, v: Option[String]): Self =
48 | withParams(params :+ (k -> v.map(_.toString)))
49 |
50 | /**
51 | * Adds a new parameter key with no value. If the value for the parameter is None, then this
52 | * parameter will not be rendered
53 | *
54 | * @return A new instance with the new parameter added
55 | */
56 | def addParam(k: String): Self = addParam(k, None: Option[String])
57 |
58 | def addParams(other: Parameters) =
59 | withParams(params ++ other.params)
60 |
61 | def addParams(kvs: ParamSeq) =
62 | withParams(params ++ kvs)
63 |
64 | def params(key: String): Seq[Option[String]] = params.collect {
65 | case (k, v) if k == key => v
66 | }
67 |
68 | def param(key: String) = params.collectFirst {
69 | case (k, Some(v)) if k == key => v
70 | }
71 |
72 | /**
73 | * Transforms each parameter by applying the specified Function
74 | *
75 | * @param f
76 | * @return
77 | */
78 | def mapParams(f: Param => Param) =
79 | withParams(params.map(f))
80 |
81 | /**
82 | * Transforms each parameter by applying the specified Function
83 | *
84 | * @param f A function that returns a collection of Parameters when applied to each parameter
85 | * @return
86 | */
87 | def flatMapParams(f: Param => GenTraversableOnce[Param]) =
88 | withParams(params.flatMap(f))
89 |
90 | /**
91 | * Transforms each parameter name by applying the specified Function
92 | *
93 | * @param f
94 | * @return
95 | */
96 | def mapParamNames(f: String => String) =
97 | withParams(params.map {
98 | case (n, v) => (f(n), v)
99 | })
100 |
101 | /**
102 | * Transforms each parameter value by applying the specified Function
103 | *
104 | * @param f
105 | * @return
106 | */
107 | def mapParamValues(f: String => String) =
108 | withParams(params.map {
109 | case (n, v) => (n, v map f)
110 | })
111 |
112 | /**
113 | * Filters out just the parameters for which the provided function holds true
114 | *
115 | * @param f
116 | * @return
117 | */
118 | def filterParams(f: Param => Boolean) =
119 | withParams(params.filter(f))
120 |
121 | /**
122 | * Filters out just the parameters for which the provided function holds true when applied to the parameter name
123 | *
124 | * @param f
125 | * @return
126 | */
127 | def filterParamsNames(f: String => Boolean) =
128 | withParams(params.filter {
129 | case (n, _) => f(n)
130 | })
131 |
132 | /**
133 | * Filters out just the parameters for which the provided function holds true when applied to the parameter value
134 | *
135 | * @param f
136 | * @return
137 | */
138 | def filterParamsValues(f: String => Boolean) =
139 | filterParamsOptions(ov => ov match {
140 | case Some(v) => f(v)
141 | case _ => false
142 | })
143 |
144 | /**
145 | * Filters out just the parameters for which the provided function holds true when applied to the parameter value
146 | *
147 | * @param f
148 | * @return
149 | */
150 | def filterParamsOptions(f: Option[String] => Boolean) =
151 | withParams(params.filter {
152 | case (_, v) => f(v)
153 | })
154 |
155 | /**
156 | * Replaces the all existing Query String parameters with the specified key with a single Query String parameter
157 | * with the specified value.
158 | *
159 | * @param k Key for the Query String parameter(s) to replace
160 | * @param vOpt value to replace with
161 | * @return A new QueryString with the result of the replace
162 | */
163 | def replaceAll(k: String, vOpt: Option[Any]): Self =
164 | withParams(params.filterNot(_._1 == k) :+ (k -> vOpt.map(_.toString)))
165 |
166 | /**
167 | * Removes all Query String parameters with the specified key
168 | * @param k Key for the Query String parameter(s) to remove
169 | * @return
170 | */
171 | def removeAll(k: String) =
172 | filterParamsNames(_ != k)
173 |
174 | def removeAll(a: Seq[String]) =
175 | filterParamsNames(!a.contains(_))
176 |
177 | def removeAll() =
178 | withParams(Seq.empty)
179 |
180 | def paramsToString(e: UriEncoder, charset: String) =
181 | params.map(kv => {
182 | val (k, ov) = kv
183 | ov match {
184 | case Some(v) => e.encode(k, charset) + "=" + e.encode(v, charset)
185 | case None => e.encode(k, charset)
186 | }
187 | }).mkString(separator)
188 | }
189 |
190 | object Parameters {
191 | type Param = (String, Option[String])
192 | type ParamSeq = Seq[Param]
193 | }
--------------------------------------------------------------------------------
/src/main/scala/com/netaporter/uri/PathPart.scala:
--------------------------------------------------------------------------------
1 | package com.netaporter.uri
2 |
3 | import com.netaporter.uri.config.UriConfig
4 | import Parameters._
5 |
6 | /**
7 | * Date: 28/08/2013
8 | * Time: 21:21
9 | */
10 | trait PathPart extends Any {
11 |
12 | type Self <: PathPart
13 |
14 | /**
15 | * The non-parameter part of this pathPart
16 | *
17 | * @return
18 | */
19 | def part: String
20 |
21 | /**
22 | * Adds a matrix parameter to the end of this path part
23 | *
24 | * @param kv
25 | */
26 | def addParam(kv: Param): PathPart
27 |
28 | def params: ParamSeq
29 |
30 | def partToString(c: UriConfig): String
31 |
32 | def map(f: String=>String): Self
33 | }
34 |
35 | case class StringPathPart(part: String) extends AnyVal with PathPart {
36 |
37 | type Self = StringPathPart
38 |
39 | def params = Vector.empty
40 |
41 | def addParam(kv: Param) =
42 | MatrixParams(part, Vector(kv))
43 |
44 | def partToString(c: UriConfig) =
45 | c.pathEncoder.encode(part, c.charset)
46 |
47 | def map(f: String=>String) =
48 | StringPathPart(f(part))
49 | }
50 |
51 | case class MatrixParams(part: String, params: ParamSeq) extends PathPart with Parameters {
52 |
53 | type Self = MatrixParams
54 |
55 | def separator = ";"
56 |
57 | def withParams(paramsIn: ParamSeq) =
58 | MatrixParams(part, paramsIn)
59 |
60 | def partToString(c: UriConfig) =
61 | c.pathEncoder.encode(part, c.charset) + ";" + paramsToString(c.pathEncoder, c.charset)
62 |
63 | def addParam(kv: Param) =
64 | copy(params = params :+ kv)
65 |
66 | def map(f: String=>String) =
67 | MatrixParams(f(part), params)
68 | }
69 |
70 | object PathPart {
71 | def apply(path: String, matrixParams: ParamSeq = Seq.empty) =
72 | if(matrixParams.isEmpty) new StringPathPart(path) else MatrixParams(path, matrixParams)
73 | }
--------------------------------------------------------------------------------
/src/main/scala/com/netaporter/uri/QueryString.scala:
--------------------------------------------------------------------------------
1 | package com.netaporter.uri
2 |
3 | import com.netaporter.uri.config.UriConfig
4 | import com.netaporter.uri.Parameters.{Param, ParamSeq}
5 |
6 | /**
7 | * Date: 28/08/2013
8 | * Time: 21:22
9 | */
10 | case class QueryString(params: ParamSeq) extends Parameters {
11 |
12 | type Self = QueryString
13 |
14 | def separator = "&"
15 |
16 | def withParams(paramsIn: ParamSeq) =
17 | QueryString(paramsIn)
18 |
19 | def queryToString(c: UriConfig) =
20 | if(params.isEmpty) ""
21 | else "?" + paramsToString(c.queryEncoder, c.charset)
22 | }
23 |
24 | object QueryString {
25 | // Cannot call this apply, as it conflicts with the case class constructor :(
26 | def create(params: Param*) =
27 | new QueryString(params)
28 | }
29 |
30 | object EmptyQueryString extends QueryString(Seq.empty)
--------------------------------------------------------------------------------
/src/main/scala/com/netaporter/uri/Uri.scala:
--------------------------------------------------------------------------------
1 | package com.netaporter.uri
2 |
3 | import com.netaporter.uri.inet.PublicSuffixes
4 | import com.netaporter.uri.parsing.UriParser
5 | import com.netaporter.uri.config.UriConfig
6 | import com.netaporter.uri.Parameters.Param
7 | import scala.collection.GenTraversableOnce
8 | import scala.collection.Seq
9 |
10 | /**
11 | * http://tools.ietf.org/html/rfc3986
12 | */
13 | case class Uri (
14 | scheme: Option[String],
15 | user: Option[String],
16 | password: Option[String],
17 | host: Option[String],
18 | port: Option[Int],
19 | pathParts: Seq[PathPart],
20 | query: QueryString,
21 | fragment: Option[String]
22 | ) {
23 |
24 | lazy val hostParts: Seq[String] =
25 | host.map(h => h.split('.').toVector).getOrElse(Vector.empty)
26 |
27 | def subdomain = hostParts.headOption
28 |
29 | def pathPartOption(name: String) =
30 | pathParts.find(_.part == name)
31 |
32 | def pathPart(name: String) =
33 | pathPartOption(name).head
34 |
35 | def matrixParams =
36 | pathParts.lastOption match {
37 | case Some(pathPart) =>
38 | pathPart match {
39 | case MatrixParams(_, p) => p
40 | case _ => Seq.empty
41 | }
42 | case None => Seq.empty
43 | }
44 |
45 | def addMatrixParam(pp: String, k: String, v: String) = copy (
46 | pathParts = pathParts.map {
47 | case p: PathPart if p.part == pp => p.addParam(k -> Some(v))
48 | case x => x
49 | }
50 | )
51 |
52 | def addMatrixParam(k: String, v: String) = copy (
53 | pathParts = pathParts.dropRight(1) :+ pathParts.last.addParam(k -> Some(v))
54 | )
55 |
56 | /**
57 | * Adds a new Query String parameter key-value pair. If the value for the Query String parmeter is None, then this
58 | * Query String parameter will not be rendered in calls to toString or toStringRaw
59 | * @param name name of the parameter
60 | * @param value value for the parameter
61 | * @return A new Uri with the new Query String parameter
62 | */
63 | def addParam(name: String, value: Any) = (name, value) match {
64 | case (_, None) => this
65 | case (n, Some(v)) => copy(query = query.addParam(n, Some(v.toString)))
66 | case (n, v) => copy(query = query.addParam(n, Some(v.toString)))
67 | }
68 |
69 | def addParams(kvs: Seq[(String, Any)]) = {
70 | val cleanKvs = kvs.filterNot(_._2 == None).map {
71 | case (k, Some(v)) => (k, Some(v.toString))
72 | case (k, v) => (k, Some(v.toString))
73 | }
74 | copy(query = query.addParams(cleanKvs))
75 | }
76 |
77 | def protocol = scheme
78 |
79 | /**
80 | * Copies this Uri but with the scheme set as the given value.
81 | *
82 | * @param scheme the new scheme to set
83 | * @return a new Uri with the specified scheme
84 | */
85 | def withScheme(scheme: String): Uri = copy(scheme = Option(scheme))
86 |
87 | /**
88 | * Copies this Uri but with the host set as the given value.
89 | *
90 | * @param host the new host to set
91 | * @return a new Uri with the specified host
92 | */
93 | def withHost(host: String): Uri = copy(host = Option(host))
94 |
95 | /**
96 | * Copies this Uri but with the user set as the given value.
97 | *
98 | * @param user the new user to set
99 | * @return a new Uri with the specified user
100 | */
101 | def withUser(user: String): Uri = copy(user = Option(user))
102 |
103 | /**
104 | * Copies this Uri but with the password set as the given value.
105 | *
106 | * @param password the new password to set
107 | * @return a new Uri with the specified password
108 | */
109 | def withPassword(password: String): Uri = copy(password = Option(password))
110 |
111 | /**
112 | * Copies this Uri but with the port set as the given value.
113 | *
114 | * @param port the new port to set
115 | * @return a new Uri with the specified port
116 | */
117 | def withPort(port: Int): Uri = copy(port = Option(port))
118 |
119 | /**
120 | * Copies this Uri but with the fragment set as the given value.
121 | *
122 | * @param fragment the new fragment to set
123 | * @return a new Uri with the specified fragment
124 | */
125 | def withFragment(fragment: String): Uri = copy(fragment = Option(fragment))
126 |
127 | /**
128 | * Returns the path with no encoders taking place (e.g. non ASCII characters will not be percent encoded)
129 | * @return String containing the raw path for this Uri
130 | */
131 | def pathRaw(implicit c: UriConfig = UriConfig.default) =
132 | path(c.withNoEncoding)
133 |
134 | /**
135 | * Returns the encoded path. By default non ASCII characters in the path are percent encoded.
136 | * @return String containing the path for this Uri
137 | */
138 | def path(implicit c: UriConfig = UriConfig.default) =
139 | if(pathParts.isEmpty) ""
140 | else "/" + pathParts.map(_.partToString(c)).mkString("/")
141 |
142 | def queryStringRaw(implicit c: UriConfig = UriConfig.default) =
143 | queryString(c.withNoEncoding)
144 |
145 | def queryString(implicit c: UriConfig = UriConfig.default) =
146 | query.queryToString(c)
147 |
148 | /**
149 | * Replaces the all existing Query String parameters with the specified key with a single Query String parameter
150 | * with the specified value. If the value passed in is None, then all Query String parameters with the specified key
151 | * are removed
152 | *
153 | * @param k Key for the Query String parameter(s) to replace
154 | * @param v value to replace with
155 | * @return A new Uri with the result of the replace
156 | */
157 | def replaceParams(k: String, v: Any) = {
158 | v match {
159 | case valueOpt: Option[_] =>
160 | copy(query = query.replaceAll(k, valueOpt))
161 | case _ =>
162 | copy(query = query.replaceAll(k, Some(v)))
163 | }
164 | }
165 |
166 | /**
167 | * Replaces the all existing Query String parameters with a new set of query params
168 | */
169 | def replaceAllParams(params: Param*) =
170 | copy(query = query.withParams(params))
171 |
172 | /**
173 | * Transforms the Query String by applying the specified Function to each Query String Parameter
174 | *
175 | * @param f A function that returns a new Parameter when applied to each Parameter
176 | * @return
177 | */
178 | def mapQuery(f: Param=>Param) =
179 | copy(query = query.mapParams(f))
180 |
181 | /**
182 | * Transforms the Query String by applying the specified Function to each Query String Parameter
183 | *
184 | * @param f A function that returns a collection of Parameters when applied to each parameter
185 | * @return
186 | */
187 | def flatMapQuery(f: Param=>GenTraversableOnce[Param]) =
188 | copy(query = query.flatMapParams(f))
189 |
190 | /**
191 | * Transforms the Query String by applying the specified Function to each Query String Parameter name
192 | *
193 | * @param f A function that returns a new Parameter name when applied to each Parameter name
194 | * @return
195 | */
196 | def mapQueryNames(f: String=>String) =
197 | copy(query = query.mapParamNames(f))
198 |
199 | /**
200 | * Transforms the Query String by applying the specified Function to each Query String Parameter value
201 | *
202 | * @param f A function that returns a new Parameter value when applied to each Parameter value
203 | * @return
204 | */
205 | def mapQueryValues(f: String=>String) =
206 | copy(query = query.mapParamValues(f))
207 |
208 | /**
209 | * Removes any Query String Parameters that return false when applied to the given Function
210 | *
211 | * @param f
212 | * @return
213 | */
214 | def filterQuery(f: Param=>Boolean) =
215 | copy(query = query.filterParams(f))
216 |
217 | /**
218 | * Removes any Query String Parameters that return false when their name is applied to the given Function
219 | *
220 | * @param f
221 | * @return
222 | */
223 | def filterQueryNames(f: String=>Boolean) =
224 | copy(query = query.filterParamsNames(f))
225 |
226 | /**
227 | * Removes any Query String Parameters that return false when their value is applied to the given Function
228 | *
229 | * @param f
230 | * @return
231 | */
232 | def filterQueryValues(f: String=>Boolean) =
233 | copy(query = query.filterParamsValues(f))
234 |
235 | /**
236 | * Removes all Query String parameters with the specified key
237 | * @param k Key for the Query String parameter(s) to remove
238 | * @return
239 | */
240 | def removeParams(k: String) = {
241 | copy(query = query.removeAll(k))
242 | }
243 |
244 | /**
245 | * Removes all Query String parameters with the specified key contained in the a (Array)
246 | * @param a an Array of Keys for the Query String parameter(s) to remove
247 | * @return
248 | */
249 | def removeParams(a: Seq[String]) = {
250 | copy(query = query.removeAll(a))
251 | }
252 |
253 | /**
254 | * Removes all Query String parameters
255 | * @return
256 | */
257 | def removeAllParams() = {
258 | copy(query = query.removeAll())
259 | }
260 |
261 | def publicSuffix: Option[String] = {
262 | for {
263 | h <- host
264 | longestMatch <- PublicSuffixes.trie.longestMatch(h.reverse)
265 | } yield longestMatch.reverse
266 | }
267 |
268 | def publicSuffixes: Seq[String] = {
269 | for {
270 | h <- host.toSeq
271 | m <- PublicSuffixes.trie.matches(h.reverse)
272 | } yield m.reverse
273 | }
274 |
275 | override def toString = toString(UriConfig.default)
276 |
277 | def toString(implicit c: UriConfig = UriConfig.default): String = {
278 | //If there is no scheme, we use scheme relative
279 | def userInfo = for {
280 | userStr <- user
281 | userStrEncoded = c.userInfoEncoder.encode(userStr, c.charset)
282 | passwordStrEncoded = password.map(p => ":" + c.userInfoEncoder.encode(p, c.charset)).getOrElse("")
283 | } yield userStrEncoded + passwordStrEncoded + "@"
284 |
285 | val hostStr = for {
286 | hostStr <- host
287 | schemeStr = scheme.map(_ + "://").getOrElse("//")
288 | userInfoStr = userInfo.getOrElse("")
289 | } yield schemeStr + userInfoStr + hostStr
290 |
291 | hostStr.getOrElse("") +
292 | port.map(":" + _).getOrElse("") +
293 | path(c) +
294 | queryString(c) +
295 | fragment.map(f => "#" + c.fragmentEncoder.encode(f, c.charset)).getOrElse("")
296 | }
297 |
298 | /**
299 | * Returns the string representation of this Uri with no encoders taking place
300 | * (e.g. non ASCII characters will not be percent encoded)
301 | * @return String containing this Uri in it's raw form
302 | */
303 | def toStringRaw(implicit config: UriConfig = UriConfig.default): String =
304 | toString(config.withNoEncoding)
305 |
306 | /**
307 | * Converts to a Java URI.
308 | * This involves a toString + URI.parse because the specific URI constructors do not deal properly with encoded elements
309 | * @return a URI matching this Uri
310 | */
311 | def toURI(implicit c: UriConfig = UriConfig.conservative) = new java.net.URI(toString())
312 | }
313 |
314 | object Uri {
315 |
316 | def parse(s: CharSequence)(implicit config: UriConfig = UriConfig.default): Uri =
317 | UriParser.parse(s.toString, config)
318 |
319 |
320 | def parseQuery(s: CharSequence)(implicit config: UriConfig = UriConfig.default): QueryString =
321 | UriParser.parseQuery(s.toString, config)
322 |
323 | def apply(scheme: String = null,
324 | user: String = null,
325 | password: String = null,
326 | host: String = null,
327 | port: Int = 0,
328 | pathParts: Seq[PathPart] = Seq.empty,
329 | query: QueryString = EmptyQueryString,
330 | fragment: String = null) = {
331 | new Uri(Option(scheme),
332 | Option(user),
333 | Option(password),
334 | Option(host),
335 | if(port > 0) Some(port) else None,
336 | pathParts,
337 | query,
338 | Option(fragment)
339 | )
340 | }
341 |
342 | def empty = apply()
343 |
344 | def apply(javaUri: java.net.URI): Uri = parse(javaUri.toASCIIString())
345 | }
346 |
--------------------------------------------------------------------------------
/src/main/scala/com/netaporter/uri/config/UriConfig.scala:
--------------------------------------------------------------------------------
1 | package com.netaporter.uri.config
2 |
3 | import com.netaporter.uri.encoding.{NoopEncoder, UriEncoder, PercentEncoder}
4 | import com.netaporter.uri.decoding.{PercentDecoder, UriDecoder}
5 | import PercentEncoder._
6 |
7 | /**
8 | * Date: 28/08/2013
9 | * Time: 21:31
10 | */
11 | case class UriConfig(userInfoEncoder: UriEncoder,
12 | pathEncoder: UriEncoder,
13 | queryEncoder: UriEncoder,
14 | fragmentEncoder: UriEncoder,
15 | userInfoDecoder: UriDecoder,
16 | pathDecoder: UriDecoder,
17 | queryDecoder: UriDecoder,
18 | fragmentDecoder: UriDecoder,
19 | matrixParams: Boolean,
20 | charset: String) {
21 |
22 | def withNoEncoding = copy(pathEncoder = NoopEncoder, queryEncoder = NoopEncoder, fragmentEncoder = NoopEncoder)
23 |
24 | }
25 |
26 | object UriConfig {
27 |
28 | val default = UriConfig(userInfoEncoder = PercentEncoder(USER_INFO_CHARS_TO_ENCODE),
29 | pathEncoder = PercentEncoder(PATH_CHARS_TO_ENCODE),
30 | queryEncoder = PercentEncoder(QUERY_CHARS_TO_ENCODE),
31 | fragmentEncoder = PercentEncoder(FRAGMENT_CHARS_TO_ENCODE),
32 | userInfoDecoder = PercentDecoder,
33 | pathDecoder = PercentDecoder,
34 | queryDecoder = PercentDecoder,
35 | fragmentDecoder = PercentDecoder,
36 | matrixParams = false,
37 | charset = "UTF-8")
38 |
39 |
40 | /**
41 | * Probably more than you need to percent encode. Wherever possible try to use a tighter Set of characters
42 | * to encode depending on your use case
43 | */
44 | val conservative = UriConfig(PercentEncoder(), PercentDecoder)
45 |
46 | def apply(encoder: UriEncoder = PercentEncoder(),
47 | decoder: UriDecoder = PercentDecoder,
48 | matrixParams: Boolean = false,
49 | charset: String = "UTF-8"): UriConfig =
50 | UriConfig(encoder, encoder, encoder, encoder, decoder, decoder, decoder, decoder, matrixParams, charset)
51 | }
52 |
--------------------------------------------------------------------------------
/src/main/scala/com/netaporter/uri/decoding/NoopDecoder.scala:
--------------------------------------------------------------------------------
1 | package com.netaporter.uri.decoding
2 |
3 | import com.netaporter.uri.Uri
4 |
5 | /**
6 | * Date: 28/08/2013
7 | * Time: 20:58
8 | */
9 | object NoopDecoder extends UriDecoder {
10 | def decode(s: String) = s
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/scala/com/netaporter/uri/decoding/PercentDecoder.scala:
--------------------------------------------------------------------------------
1 | package com.netaporter.uri.decoding
2 |
3 |
4 | /**
5 | * Date: 23/06/2013
6 | * Time: 20:38
7 | */
8 | object PercentDecoder extends UriDecoder {
9 |
10 | def decode(s: String) = try {
11 | val segments = s.split('%')
12 | val decodedSegments = segments.tail.flatMap(seg => {
13 | val percentByte = Integer.parseInt(seg.substring(0, 2), 16).toByte
14 | percentByte +: seg.substring(2).getBytes("UTF-8")
15 | })
16 | segments.head + new String(decodedSegments, "UTF-8")
17 | } catch {
18 | case e: NumberFormatException => throw new UriDecodeException("Encountered '%' followed by a non hex number. It looks like this URL isn't Percent Encoded. If so, look at using the NoopDecoder")
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/scala/com/netaporter/uri/decoding/PermissiveDecoder.scala:
--------------------------------------------------------------------------------
1 | package com.netaporter.uri.decoding
2 |
3 | import com.netaporter.uri.Uri
4 |
5 | class PermissiveDecoder(child: UriDecoder) extends UriDecoder {
6 | def decode(s: String) = {
7 | try {
8 | child.decode(s)
9 | } catch {
10 | case _: Throwable => s
11 | }
12 | }
13 | }
14 |
15 | object PermissivePercentDecoder extends PermissiveDecoder(PercentDecoder)
--------------------------------------------------------------------------------
/src/main/scala/com/netaporter/uri/decoding/UriDecodeException.scala:
--------------------------------------------------------------------------------
1 | package com.netaporter.uri.decoding
2 |
3 | /**
4 | * Date: 28/08/2013
5 | * Time: 21:01
6 | */
7 | class UriDecodeException(message: String) extends Exception(message)
8 |
--------------------------------------------------------------------------------
/src/main/scala/com/netaporter/uri/decoding/UriDecoder.scala:
--------------------------------------------------------------------------------
1 | package com.netaporter.uri.decoding
2 |
3 | import com.netaporter.uri.Parameters
4 | import Parameters._
5 | import com.netaporter.uri.Parameters
6 |
7 | /**
8 | * Date: 28/08/2013
9 | * Time: 21:01
10 | */
11 | trait UriDecoder {
12 | def decode(u: String): String
13 |
14 | def decodeTuple(kv: Param) =
15 | decode(kv._1) -> kv._2.map(decode(_))
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/scala/com/netaporter/uri/dsl/UriDsl.scala:
--------------------------------------------------------------------------------
1 | package com.netaporter.uri.dsl
2 |
3 | import com.netaporter.uri.{Uri, StringPathPart}
4 |
5 | /**
6 | * Value class to add DSL functionality to Uris
7 | *
8 | * @param uri
9 | */
10 | class UriDsl(val uri: Uri) extends AnyVal {
11 |
12 | /**
13 | * Adds a new Query String parameter key-value pair. If the value for the Query String parameter is None, then this
14 | * Query String parameter will not be rendered in calls to toString or toStringRaw
15 | * @param kv Tuple2 representing the query string parameter
16 | * @return A new Uri with the new Query String parameter
17 | */
18 | def ?(kv: (String, Any)) = uri.addParam(kv._1, kv._2)
19 |
20 | /**
21 | * Adds a trailing forward slash to the path and a new Query String parameter key-value pair.
22 | * If the value for the Query String parameter is None, then this Query String parameter will
23 | * not be rendered in calls to toString or toStringRaw
24 | * @param kv Tuple2 representing the query string parameter
25 | * @return A new Uri with the new Query String parameter
26 | */
27 | def /?(kv: (String, Any)) = /("").addParam(kv._1, kv._2)
28 |
29 | /**
30 | * Adds a new Query String parameter key-value pair. If the value for the Query String parameter is None, then this
31 | * Query String parameter will not be rendered in calls to toString or toStringRaw
32 | * @param kv Tuple2 representing the query string parameter
33 | * @return A new Uri with the new Query String parameter
34 | */
35 | def &(kv: (String, Any)) = uri.addParam(kv._1, kv._2)
36 |
37 |
38 | /**
39 | * Adds a fragment to the end of the uri
40 | * @param fragment String representing the fragment
41 | * @return A new Uri with this fragment
42 | */
43 | def `#`(fragment: String) = uri.withFragment(fragment)
44 |
45 | /**
46 | * Appends a path part to the path of this URI
47 | * @param pp The path part
48 | * @return A new Uri with this path part appended
49 | */
50 | def /(pp: String) = uri.copy(pathParts = uri.pathParts :+ StringPathPart(pp))
51 |
52 | /**
53 | * Operator precedence in Scala will mean that our DSL will not always be executed left to right.
54 | *
55 | * For the operators this DSL cares about, the order will be
56 | *
57 | * (all letters)
58 | * &
59 | * :
60 | * /
61 | * `#` ?
62 | *
63 | * (see Scala Reference - 6.12.3 Infix Operations: http://www.scala-lang.org/docu/files/ScalaReference.pdf)
64 | *
65 | * To handle cases where the right hard part of the DSL is executed first, we turn that into a Uri, and merge
66 | * it with the left had side. It is assumed the right hand Uri is generated from this DSL only to add path
67 | * parts, query parameters or to overwrite the fragment
68 | *
69 | * @param other A Uri generated by more DSL to the right of us
70 | * @return A Uri with the right hand DSL merged into us
71 | */
72 | private def merge(other: Uri) =
73 | uri.copy(
74 | pathParts = uri.pathParts ++ other.pathParts,
75 | query = uri.query.addParams(other.query),
76 | fragment = other.fragment.orElse(uri.fragment)
77 | )
78 |
79 | def /(other: Uri) = merge(other)
80 | def ?(other: Uri) = merge(other)
81 | def `#`(other: Uri) = merge(other)
82 | def &(other: Uri) = merge(other)
83 | }
84 |
--------------------------------------------------------------------------------
/src/main/scala/com/netaporter/uri/dsl/package.scala:
--------------------------------------------------------------------------------
1 | package com.netaporter.uri
2 |
3 | import com.netaporter.uri.encoding.{ChainedUriEncoder, UriEncoder}
4 | import com.netaporter.uri.config.UriConfig
5 |
6 | /**
7 | * Date: 23/08/2013
8 | * Time: 09:10
9 | */
10 | package object dsl {
11 |
12 | import scala.language.implicitConversions
13 |
14 | implicit def uriToUriOps(uri: Uri) = new UriDsl(uri)
15 |
16 | implicit def encoderToChainedEncoder(enc: UriEncoder) = ChainedUriEncoder(enc :: Nil)
17 |
18 | implicit def stringToUri(s: String)(implicit c: UriConfig = UriConfig.default) = Uri.parse(s)(c)
19 | implicit def stringToUriDsl(s: String)(implicit c: UriConfig = UriConfig.default) = new UriDsl(stringToUri(s)(c))
20 |
21 | implicit def queryParamToUriDsl(kv: (String, Any))(implicit c: UriConfig = UriConfig.default) = new UriDsl(Uri.empty.addParam(kv._1, kv._2))
22 |
23 | implicit def uriToString(uri: Uri)(implicit c: UriConfig = UriConfig.default): String = uri.toString(c)
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/scala/com/netaporter/uri/encoding/ChainedUriEncoder.scala:
--------------------------------------------------------------------------------
1 | package com.netaporter.uri.encoding
2 |
3 | /**
4 | * Date: 28/08/2013
5 | * Time: 21:07
6 | */
7 | case class ChainedUriEncoder(encoders: Seq[UriEncoder]) extends UriEncoder {
8 | def shouldEncode(ch: Char) = findFirstEncoder(ch).isDefined
9 | def encodeChar(ch: Char) = findFirstEncoder(ch).getOrElse(NoopEncoder).encodeChar(ch)
10 |
11 | def findFirstEncoder(ch: Char) = {
12 | encoders.find(_.shouldEncode(ch))
13 | }
14 |
15 | def +(encoder: UriEncoder) = copy(encoders = encoder +: encoders)
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/scala/com/netaporter/uri/encoding/EncodeCharAs.scala:
--------------------------------------------------------------------------------
1 | package com.netaporter.uri.encoding
2 |
3 | /**
4 | * Date: 28/08/2013
5 | * Time: 21:07
6 | */
7 | case class EncodeCharAs(ch: Char, as: String) extends UriEncoder {
8 | def shouldEncode(x: Char) = x == ch
9 | def encodeChar(x: Char) = as
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/scala/com/netaporter/uri/encoding/NoopEncoder.scala:
--------------------------------------------------------------------------------
1 | package com.netaporter.uri.encoding
2 |
3 | /**
4 | * Date: 28/08/2013
5 | * Time: 21:15
6 | */
7 | object NoopEncoder extends UriEncoder {
8 | def shouldEncode(ch: Char) = false
9 | def encodeChar(ch: Char) = ch.toString
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/scala/com/netaporter/uri/encoding/PercentEncoder.scala:
--------------------------------------------------------------------------------
1 | package com.netaporter.uri.encoding
2 |
3 | import PercentEncoder._
4 |
5 | case class PercentEncoder(charsToEncode: Set[Char] = DEFAULT_CHARS_TO_ENCODE) extends UriEncoder {
6 |
7 | def shouldEncode(ch: Char) = {
8 | !ascii(ch) || charsToEncode.contains(ch)
9 | }
10 |
11 | def encodeChar(ch: Char) = "%" + toHex(ch)
12 | def toHex(ch: Char) = "%04x".format(ch.toInt).substring(2).toUpperCase
13 |
14 | /**
15 | * Determines if this character is in the ASCII range (excluding control characters)
16 | */
17 | def ascii(ch: Char) = ch > 31 && ch < 127
18 |
19 | def --(chars: Char*) = new PercentEncoder(charsToEncode -- chars)
20 | def ++(chars: Char*) = new PercentEncoder(charsToEncode ++ chars)
21 | }
22 |
23 | object PercentEncoder {
24 |
25 | val USER_INFO_CHARS_TO_ENCODE = Set (
26 | ' ', '%', '<', '>', '[', ']', '#', '%', '{', '}', '^', '`', '|', '?', '@', ':', '/'
27 | )
28 |
29 | val PATH_CHARS_TO_ENCODE = Set (
30 | ' ', '%', '<', '>', '[', ']', '#', '%', '{', '}', '^', '`', '|', '?'
31 | )
32 |
33 | val QUERY_CHARS_TO_ENCODE = Set (
34 | ' ', '%', '<', '>', '[', ']', '#', '%', '{', '}', '^', '`', '|', '&', '\\', '+', '='
35 | )
36 |
37 | val FRAGMENT_CHARS_TO_ENCODE = Set('#')
38 |
39 |
40 | val GEN_DELIMS = Set(':', '/', '?', '#', '[', ']', '@')
41 | val SUB_DELIMS = Set('!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=')
42 | val RESERVED = GEN_DELIMS ++ SUB_DELIMS
43 |
44 | val EXCLUDED = Set('"') // RFC 2396 section 2.4.3
45 |
46 | /**
47 | * Probably more than you need to percent encode. Wherever possible try to use a tighter Set of characters
48 | * to encode depending on your use case
49 | */
50 | val DEFAULT_CHARS_TO_ENCODE = RESERVED ++ PATH_CHARS_TO_ENCODE ++ QUERY_CHARS_TO_ENCODE ++ EXCLUDED
51 | }
--------------------------------------------------------------------------------
/src/main/scala/com/netaporter/uri/encoding/UriEncoder.scala:
--------------------------------------------------------------------------------
1 | package com.netaporter.uri.encoding
2 |
3 | import scala.Array
4 |
5 | /**
6 | * Date: 28/08/2013
7 | * Time: 21:07
8 | */
9 | trait UriEncoder {
10 | def shouldEncode(ch: Char): Boolean
11 | def encodeChar(ch: Char): String
12 |
13 | def encode(s: String, charset: String) = {
14 | val chars = s.getBytes(charset).map(_.toChar)
15 |
16 | val encChars = chars.flatMap(ch => {
17 | if (shouldEncode(ch)) {
18 | encodeChar(ch).getBytes(charset)
19 | } else {
20 | Array(ch.toByte)
21 | }
22 | })
23 |
24 | new String(encChars, charset)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/scala/com/netaporter/uri/encoding/package.scala:
--------------------------------------------------------------------------------
1 | package com.netaporter.uri
2 |
3 | import com.netaporter.uri.config.UriConfig
4 |
5 | /**
6 | * Date: 28/08/2013
7 | * Time: 21:08
8 | */
9 | package object encoding {
10 | val percentEncode = PercentEncoder()
11 | def percentEncode(chars: Char*) = PercentEncoder(chars.toSet)
12 |
13 | def encodeCharAs(c: Char, as: String) = EncodeCharAs(c, as)
14 | val spaceAsPlus = EncodeCharAs(' ', "+")
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/scala/com/netaporter/uri/inet/PublicSuffixes.scala:
--------------------------------------------------------------------------------
1 | package com.netaporter.uri.inet
2 |
3 | import spray.json.DefaultJsonProtocol._
4 | import spray.json.{JsonFormat, _}
5 |
6 | import scala.io.Source
7 |
8 | object PublicSuffixes {
9 | lazy val trie = {
10 | implicit lazy val trieFmt: JsonFormat[Trie] = lazyFormat(jsonFormat(Trie, "c", "e"))
11 | val trieJson = Source.fromURL(getClass.getResource("/public_suffix_trie.json"), "UTF-8")
12 | val trie = trieJson.mkString.parseJson.convertTo[Trie]
13 | trieJson.close()
14 | trie
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/scala/com/netaporter/uri/inet/Trie.scala:
--------------------------------------------------------------------------------
1 | package com.netaporter.uri.inet
2 |
3 | import scala.annotation.tailrec
4 |
5 | case class Trie(children: Map[Char, Trie], wordEnd: Boolean = false) {
6 |
7 | def next(c: Char) =
8 | children.get(c)
9 |
10 | def matches(s: String): Seq[String] = {
11 | @tailrec def collectMatches(previous: String, stillToGo: List[Char], current: Trie, matches: Seq[String]): Seq[String] = stillToGo match {
12 | case Nil =>
13 | matches
14 | case x :: xs =>
15 | current.next(x) match {
16 | case None =>
17 | matches
18 | case Some(next) =>
19 | val newPrevious = previous + x
20 | //TODO: When Scala 2.10 support is dropped, change headOption == Some to headOption.contains
21 | val newMatches = if(next.wordEnd && xs.headOption == Some('.')) newPrevious +: matches else matches
22 | collectMatches(newPrevious, xs, next, newMatches)
23 | }
24 | }
25 | collectMatches("", s.toCharArray.toList, this, Seq.empty)
26 | }
27 |
28 | def longestMatch(s: String): Option[String] =
29 | matches(s).headOption
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/scala/com/netaporter/uri/parsing/DefaultUriParser.scala:
--------------------------------------------------------------------------------
1 | package com.netaporter.uri.parsing
2 |
3 | import org.parboiled2._
4 | import com.netaporter.uri._
5 | import com.netaporter.uri.config.UriConfig
6 | import Parameters._
7 |
8 | class DefaultUriParser(val input: ParserInput, conf: UriConfig) extends Parser with UriParser {
9 |
10 | def _scheme: Rule1[String] = rule {
11 | capture(CharPredicate.Alpha ~ zeroOrMore(CharPredicate.AlphaNum | anyOf("+-.")))
12 | }
13 |
14 | def _host_name: Rule1[String] = rule {
15 | capture(oneOrMore(!anyOf(":/?") ~ ANY))
16 | }
17 |
18 | def _userInfo: Rule1[UserInfo] = rule {
19 | capture(oneOrMore(!anyOf(":/?@") ~ ANY)) ~ optional(":" ~ optional(capture(oneOrMore(!anyOf("@") ~ ANY)))) ~ "@" ~> extractUserInfo
20 | }
21 |
22 | //TODO Try harder to make this a Rule1[Int] using ~> extractInt
23 | def _port: Rule1[String] = rule {
24 | ":" ~ capture(oneOrMore(CharPredicate.Digit))
25 | }
26 |
27 | def _authority: Rule1[Authority] = rule {
28 | ((optional(_userInfo) ~ _host_name ~ optional(_port)) | (push[Option[UserInfo]](None) ~ _host_name ~ optional(_port))) ~> extractAuthority
29 | }
30 |
31 | def _pathSegment: Rule1[PathPart] = rule {
32 | capture(zeroOrMore(!anyOf("/?#") ~ ANY)) ~> extractPathPart
33 | }
34 |
35 | /**
36 | * A sequence of path parts that MUST start with a slash
37 | */
38 | def _abs_path: Rule1[Vector[PathPart]] = rule {
39 | zeroOrMore("/" ~ _pathSegment) ~> extractPathParts
40 | }
41 |
42 | /**
43 | * A sequence of path parts optionally starting with a slash
44 | */
45 | def _rel_path: Rule1[Vector[PathPart]] = rule {
46 | optional("/") ~ zeroOrMore(_pathSegment).separatedBy("/") ~> extractPathParts
47 | }
48 |
49 | def _queryParam: Rule1[Param] = rule {
50 | capture(zeroOrMore(!anyOf("=") ~ ANY)) ~ "=" ~ capture(zeroOrMore(!anyOf("") ~ ANY)) ~> extractTuple
51 | }
52 |
53 | def _queryTok: Rule1[Param] = rule {
54 | capture(zeroOrMore(!anyOf("=") ~ ANY)) ~> extractTok
55 | }
56 |
57 | def _queryString: Rule1[QueryString] = rule {
58 | "?" ~ zeroOrMore(_queryParam | _queryTok).separatedBy("&") ~> extractQueryString
59 | }
60 |
61 | def _fragment: Rule1[String] = rule {
62 | "#" ~ capture(zeroOrMore(ANY)) ~> extractFragment
63 | }
64 |
65 | def _abs_uri: Rule1[Uri] = rule {
66 | _scheme ~ "://" ~ optional(_authority) ~ _abs_path ~ optional(_queryString) ~ optional(_fragment) ~> extractAbsUri
67 | }
68 |
69 | def _protocol_rel_uri: Rule1[Uri] = rule {
70 | "//" ~ optional(_authority) ~ _abs_path ~ optional(_queryString) ~ optional(_fragment) ~> extractProtocolRelUri
71 | }
72 |
73 | def _rel_uri: Rule1[Uri] = rule {
74 | _rel_path ~ optional(_queryString) ~ optional(_fragment) ~> extractRelUri
75 | }
76 |
77 | def _uri: Rule1[Uri] = rule {
78 | (_abs_uri | _protocol_rel_uri | _rel_uri) ~ EOI
79 | }
80 |
81 | val extractAbsUri = (scheme: String, authority: Option[Authority], pp: Seq[PathPart], qs: Option[QueryString], f: Option[String]) =>
82 | extractUri (
83 | scheme = Some(scheme),
84 | authority = authority,
85 | pathParts = pp,
86 | query = qs,
87 | fragment = f
88 | )
89 |
90 | val extractProtocolRelUri = (authority: Option[Authority], pp: Seq[PathPart], qs: Option[QueryString], f: Option[String]) =>
91 | extractUri (
92 | authority = authority,
93 | pathParts = pp,
94 | query = qs,
95 | fragment = f
96 | )
97 |
98 | val extractRelUri = (pp: Seq[PathPart], qs: Option[QueryString], f: Option[String]) =>
99 | extractUri (
100 | pathParts = pp,
101 | query = qs,
102 | fragment = f
103 | )
104 |
105 | def extractUri(scheme: Option[String] = None,
106 | authority: Option[Authority] = None,
107 | pathParts: Seq[PathPart],
108 | query: Option[QueryString],
109 | fragment: Option[String]) =
110 | new Uri(
111 | scheme = scheme,
112 | user = authority.flatMap(_.user),
113 | password = authority.flatMap(_.password),
114 | host = authority.map(_.host),
115 | port = authority.flatMap(_.port),
116 | pathParts = pathParts,
117 | query = query.getOrElse(EmptyQueryString),
118 | fragment = fragment
119 | )
120 |
121 | def pathDecoder = conf.pathDecoder
122 | def queryDecoder = conf.queryDecoder
123 | def fragmentDecoder = conf.fragmentDecoder
124 | }
--------------------------------------------------------------------------------
/src/main/scala/com/netaporter/uri/parsing/MatrixParamSupport.scala:
--------------------------------------------------------------------------------
1 | package com.netaporter.uri.parsing
2 |
3 | import org.parboiled2._
4 | import com.netaporter.uri.PathPart
5 | import com.netaporter.uri.Parameters._
6 |
7 | trait MatrixParamSupport {
8 | this: Parser with UriParser =>
9 |
10 | def _plainPathPart: Rule1[String] = rule {
11 | capture(zeroOrMore(!anyOf(";/?#") ~ ANY))
12 | }
13 |
14 | def _matrixParam: Rule1[Param] = rule {
15 | capture(zeroOrMore(!anyOf(";/=?#") ~ ANY)) ~ "=" ~ capture(zeroOrMore(!anyOf(";/=?#") ~ ANY)) ~> extractTuple
16 | }
17 |
18 | override def _pathSegment: Rule1[PathPart] = rule {
19 | _plainPathPart ~ zeroOrMore(";") ~
20 | zeroOrMore(_matrixParam).separatedBy(oneOrMore(";")) ~
21 | zeroOrMore(";") ~> extractPathPartWithMatrixParams
22 | }
23 |
24 | val extractPathPartWithMatrixParams = (pathPart: String, matrixParams: ParamSeq) => {
25 | val decodedPathPart = pathDecoder.decode(pathPart)
26 | val decodedMatrixParams = matrixParams.map(pathDecoder.decodeTuple)
27 | PathPart(decodedPathPart, decodedMatrixParams.toVector)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/scala/com/netaporter/uri/parsing/UriParser.scala:
--------------------------------------------------------------------------------
1 | package com.netaporter.uri.parsing
2 |
3 | import com.netaporter.uri.config.UriConfig
4 | import scala.util.Failure
5 | import org.parboiled2._
6 | import com.netaporter.uri.{StringPathPart, QueryString, PathPart}
7 | import com.netaporter.uri.decoding.UriDecoder
8 | import com.netaporter.uri.Parameters._
9 | import org.parboiled2.ParseError
10 | import scala.util.Success
11 | import scala.util.Failure
12 |
13 | trait UriParser {
14 |
15 | def pathDecoder: UriDecoder
16 | def queryDecoder: UriDecoder
17 | def fragmentDecoder: UriDecoder
18 |
19 | def _pathSegment: Rule1[PathPart]
20 |
21 | val extractInt = (num: String) =>
22 | num.toInt
23 |
24 | val extractUserInfo = (user: String, pass: Option[Option[String]]) =>
25 | UserInfo(pathDecoder.decode(user), pass.map(_.fold("")(pathDecoder.decode)))
26 |
27 | val extractAuthority = (userInfo: Option[UserInfo], host: String, port: Option[String]) =>
28 | Authority(userInfo.map(_.user), userInfo.flatMap(_.pass), host, port.map(_.toInt))
29 |
30 | val extractFragment = (x: String) =>
31 | fragmentDecoder.decode(x)
32 |
33 | val extractQueryString = (tuples: ParamSeq) =>
34 | QueryString(tuples.toVector.map(queryDecoder.decodeTuple))
35 |
36 | val extractPathPart = (pathPart: String) => {
37 | val decodedPathPart = pathDecoder.decode(pathPart)
38 | StringPathPart(decodedPathPart)
39 | }
40 |
41 | val extractPathParts = (pp: Seq[PathPart]) =>
42 | pp.toVector
43 |
44 | val extractTuple = (k: String, v: String) =>
45 | k -> Some(v)
46 |
47 | val extractTok = (k: String) => (k -> None):(String,Option[String])
48 |
49 | /**
50 | * Used to made parsing easier to follow
51 | */
52 | case class Authority(user: Option[String], password: Option[String], host: String, port: Option[Int])
53 | case class UserInfo(user: String, pass: Option[String])
54 | }
55 |
56 | object UriParser {
57 | def parse(s: String, config: UriConfig) = {
58 | val parser =
59 | if(config.matrixParams) new DefaultUriParser(s, config) with MatrixParamSupport
60 | else new DefaultUriParser(s, config)
61 |
62 | parser._uri.run() match {
63 | case Success(uri) =>
64 | uri
65 |
66 | case Failure(pe@ParseError(position, _, formatTraces)) =>
67 | throw new java.net.URISyntaxException(s, "Invalid URI could not be parsed. " + formatTraces, position.index)
68 |
69 | case Failure(e) =>
70 | throw e
71 | }
72 | }
73 |
74 | def parseQuery(s: String, config: UriConfig) = {
75 | val withQuestionMark = if(s.head == '?') s else "?" + s
76 | val parser = new DefaultUriParser(withQuestionMark, config)
77 |
78 | parser._queryString.run() match {
79 | case Success(queryString) =>
80 | queryString
81 |
82 | case Failure(pe@ParseError(position, _, formatTraces)) =>
83 | throw new java.net.URISyntaxException(s, "Invalid URI could not be parsed. " + formatTraces, position.index)
84 |
85 | case Failure(e) =>
86 | throw e
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/test/scala/com/netaporter/uri/ApplyTests.scala:
--------------------------------------------------------------------------------
1 | package com.netaporter.uri
2 |
3 | import org.scalatest.FlatSpec
4 | import org.scalatest.Matchers
5 |
6 | class ApplyTests extends FlatSpec with Matchers {
7 |
8 | "Uri apply method" should "accept String scheme, String host and path" in {
9 | val uri = Uri(scheme = "http", host = "theon.github.com", pathParts = Seq(StringPathPart("blah")))
10 | uri.protocol should equal(Some("http"))
11 | uri.host should equal(Some("theon.github.com"))
12 | uri.path should equal("/blah")
13 | uri.query should equal(EmptyQueryString)
14 | }
15 |
16 | "Uri apply method" should "accept String scheme, String host and QueryString" in {
17 | val qs = QueryString(Vector("testKey" -> Some("testVal")))
18 | val uri = Uri(scheme = "http", host = "theon.github.com", query = qs)
19 | uri.protocol should equal(Some("http"))
20 | uri.host should equal(Some("theon.github.com"))
21 | uri.query should equal(qs)
22 | }
23 |
24 | "Uri apply method" should "accept Option[String] scheme, String host and QueryString" in {
25 | val qs = QueryString(Vector("testKey" -> Some("testVal")))
26 | val uri = Uri(scheme = "http", host = "theon.github.com", query = qs)
27 | uri.scheme should equal(Some("http"))
28 | uri.host should equal(Some("theon.github.com"))
29 | uri.query should equal(qs)
30 | }
31 |
32 | "Uri apply method" should "accept QueryString" in {
33 | val qs = QueryString(Vector("testKey" -> Some("testVal")))
34 | val uri = Uri(query = qs)
35 | uri.protocol should equal(None)
36 | uri.host should equal(None)
37 | uri.query should equal(qs)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/test/scala/com/netaporter/uri/DecodingTests.scala:
--------------------------------------------------------------------------------
1 | package com.netaporter.uri
2 |
3 | import org.scalatest.{Matchers, FlatSpec}
4 | import com.netaporter.uri.decoding.{UriDecodeException, NoopDecoder}
5 | import com.netaporter.uri.config.UriConfig
6 |
7 | class DecodingTests extends FlatSpec with Matchers {
8 |
9 | "Reserved characters" should "be percent decoded during parsing" in {
10 | val uri = Uri.parse("http://theon.github.com/uris-in-scala.html?reserved=%3A%2F%3F%23%5B%5D%40%21%24%26%27%28%29%2A%2B%2C%3B%3D%7B%7D%5C%0A%0D")
11 | uri.toStringRaw() should equal ("http://theon.github.com/uris-in-scala.html?reserved=:/?#[]@!$&'()*+,;={}\\\n\r")
12 | }
13 |
14 | it should "be percent decoded in the fragment" in {
15 | val uri = Uri.parse("http://theon.github.com/uris-in-scala.html#%3A%2F%3F%23%5B%5D%40%21%24%26%27%28%29%2A%2B%2C%3B%3D%7B%7D%5C%0A%0D")
16 | uri.toStringRaw() should equal ("http://theon.github.com/uris-in-scala.html#:/?#[]@!$&'()*+,;={}\\\n\r")
17 | }
18 |
19 | "Percent decoding" should "be disabled when requested" in {
20 | implicit val c = UriConfig(decoder = NoopDecoder)
21 | val uri = Uri.parse("http://theon.github.com/uris-in-scala.html?reserved=%3A%2F%3F%23%5B%5D%40%21%24%26%27%28%29%2A%2B%2C%3B%3D%7B%7D%5C%0A%0D")
22 | uri.toStringRaw() should equal ("http://theon.github.com/uris-in-scala.html?reserved=%3A%2F%3F%23%5B%5D%40%21%24%26%27%28%29%2A%2B%2C%3B%3D%7B%7D%5C%0A%0D")
23 | }
24 |
25 | it should "decode 2-byte groups" in {
26 | val uri = Uri.parse("http://example.com/%C2%A2?cents_sign=%C2%A2")
27 | uri.toStringRaw should equal("http://example.com/¢?cents_sign=¢")
28 | }
29 |
30 | it should "decode 3-byte groups" in {
31 | val uri = Uri.parse("http://example.com/%E2%82%AC?euro_sign=%E2%82%AC")
32 | uri.toStringRaw should equal("http://example.com/€?euro_sign=€")
33 | }
34 |
35 | it should "decode 4-byte groups" in {
36 | val uri = Uri.parse("http://example.com/%F0%9F%82%A0?ace_of_spades=%F0%9F%82%A1")
37 | uri.toStringRaw should equal("http://example.com/\uD83C\uDCA0?ace_of_spades=\uD83C\uDCA1")
38 | }
39 |
40 | "Parsing an non percent encoded URL containing percents" should "throw UriDecodeException" in {
41 | intercept[UriDecodeException] {
42 | Uri.parse("http://lesswrong.com/index.php?query=abc%yum&john=hello")
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/test/scala/com/netaporter/uri/DslTests.scala:
--------------------------------------------------------------------------------
1 | package com.netaporter.uri
2 |
3 | import org.scalatest.{Matchers, FlatSpec}
4 |
5 | class DslTests extends FlatSpec with Matchers {
6 |
7 | import dsl._
8 |
9 | "A simple absolute URI" should "render correctly" in {
10 | val uri: Uri = "http://theon.github.com/uris-in-scala.html"
11 | uri.toString should equal ("http://theon.github.com/uris-in-scala.html")
12 | }
13 |
14 | "A simple relative URI" should "render correctly" in {
15 | val uri: Uri = "/uris-in-scala.html"
16 | uri.toString should equal ("/uris-in-scala.html")
17 | }
18 |
19 | "An absolute URI with querystring parameters" should "render correctly" in {
20 | val uri = "http://theon.github.com/uris-in-scala.html" ? ("testOne" -> "1") & ("testTwo" -> "2")
21 | uri.toString should equal ("http://theon.github.com/uris-in-scala.html?testOne=1&testTwo=2")
22 | }
23 |
24 | "A relative URI with querystring parameters" should "render correctly" in {
25 | val uri = "/uris-in-scala.html" ? ("testOne" -> "1") & ("testTwo" -> "2")
26 | uri.toString should equal ("/uris-in-scala.html?testOne=1&testTwo=2")
27 | }
28 |
29 | "Multiple querystring parameters with the same name" should "render correctly" in {
30 | val uri = "/uris-in-scala.html" ? ("testOne" -> "1") & ("testOne" -> "2")
31 | uri.toString should equal ("/uris-in-scala.html?testOne=1&testOne=2")
32 | }
33 |
34 | "Replace param method" should "replace single parameters with a String argument" in {
35 | val uri = "/uris-in-scala.html" ? ("testOne" -> "1")
36 | val newUri = uri.replaceParams("testOne", "2")
37 | newUri.toString should equal ("/uris-in-scala.html?testOne=2")
38 | }
39 |
40 | "Replace param method" should "replace multiple parameters with a String argument" in {
41 | val uri = "/uris-in-scala.html" ? ("testOne" -> "1") & ("testOne" -> "2")
42 | val newUri = uri.replaceParams("testOne", "2")
43 | newUri.toString should equal ("/uris-in-scala.html?testOne=2")
44 | }
45 |
46 | "Replace param method" should "replace parameters with a Some argument" in {
47 | val uri = "/uris-in-scala.html" ? ("testOne" -> "1")
48 | val newUri = uri.replaceParams("testOne", Some("2"))
49 | newUri.toString should equal ("/uris-in-scala.html?testOne=2")
50 | }
51 |
52 | "Replace param method" should "not affect other parameters" in {
53 | val uri = "/uris-in-scala.html" ? ("testOne" -> "1") & ("testTwo" -> "2")
54 | val newUri = uri.replaceParams("testOne", "3")
55 | newUri.toString should equal ("/uris-in-scala.html?testTwo=2&testOne=3")
56 | }
57 |
58 | "Remove param method" should "remove multiple parameters" in {
59 | val uri = "/uris-in-scala.html" ? ("testOne" -> "1") & ("testOne" -> "2")
60 | val newUri = uri.removeParams("testOne")
61 | newUri.toString should equal ("/uris-in-scala.html")
62 | }
63 |
64 | "Replace all params method" should "replace all query params" in {
65 | val uri = "/uris-in-scala.html" ? ("testOne" -> "1") & ("testTwo" -> "2")
66 | val newUri = uri.replaceAllParams("testThree" -> Some("3"), "testFour" -> Some("4"))
67 | newUri.toString should equal ("/uris-in-scala.html?testThree=3&testFour=4")
68 | }
69 |
70 | "Remove param method" should "remove single parameters" in {
71 | val uri = "/uris-in-scala.html" ? ("testOne" -> "1")
72 | val newUri = uri.removeParams("testOne")
73 | newUri.toString should equal ("/uris-in-scala.html")
74 | }
75 |
76 | "Remove param method" should "not remove other parameters" in {
77 | val uri = "/uris-in-scala.html" ? ("testOne" -> "1") & ("testTwo" -> "2")
78 | val newUri = uri.removeParams("testOne")
79 | newUri.toString should equal ("/uris-in-scala.html?testTwo=2")
80 | }
81 |
82 | "Remove param method" should "remove parameters contained in SeqLike" in {
83 | val uri = "/uris-in-scala.html" ? ("testOne" -> "1") & ("testTwo" -> "2")
84 | val newUri = uri.removeParams(List("testOne", "testTwo"))
85 | newUri.toString should equal ("/uris-in-scala.html")
86 | }
87 |
88 | "Remove param method" should "not remove parameters uncontained in List" in {
89 | val uri = "/uris-in-scala.html" ? ("testOne" -> "1") & ("testTwo" -> "2")
90 | val newUri = uri.removeParams(List("testThree", "testFour"))
91 | newUri.toString should equal ("/uris-in-scala.html?testOne=1&testTwo=2")
92 | }
93 |
94 | "Remove param method" should "remove parameters contained in List and not remove parameters uncontained in List" in {
95 | val uri = "/uris-in-scala.html" ? ("testOne" -> "1") & ("testTwo" -> "2")
96 | val newUri = uri.removeParams(List("testOne", "testThree"))
97 | newUri.toString should equal ("/uris-in-scala.html?testTwo=2")
98 | }
99 |
100 | "Remove all params method" should "remove all query params" in {
101 | val uri = "/uris-in-scala.html" ? ("testOne" -> "1") & ("testTwo" -> "2")
102 | val newUri = uri.removeAllParams
103 | newUri.toString should equal ("/uris-in-scala.html")
104 | }
105 |
106 | "Scheme setter method" should "copy the URI with the new scheme" in {
107 | val uri = "http://coldplay.com/chris-martin.html" ? ("testOne" -> "1")
108 | val newUri = uri.withScheme("https")
109 | newUri.toString should equal ("https://coldplay.com/chris-martin.html?testOne=1")
110 | }
111 |
112 | "Host setter method" should "copy the URI with the new host" in {
113 | val uri = "http://coldplay.com/chris-martin.html" ? ("testOne" -> "1")
114 | val newUri = uri.withHost("jethrotull.com")
115 | newUri.toString should equal ("http://jethrotull.com/chris-martin.html?testOne=1")
116 | }
117 |
118 | "Port setter method" should "copy the URI with the new port" in {
119 | val uri = "http://coldplay.com/chris-martin.html" ? ("testOne" -> "1")
120 | val newUri = uri.withPort(8080)
121 | newUri.toString should equal ("http://coldplay.com:8080/chris-martin.html?testOne=1")
122 | }
123 |
124 | "Path with fragment" should "render correctly" in {
125 | val uri = "http://google.com/test" `#` "fragment"
126 | uri.toString should equal ("http://google.com/test#fragment")
127 | }
128 |
129 | "Path with query string and fragment" should "render correctly" in {
130 | val uri = "http://google.com/test" ? ("q" -> "scala-uri") `#` "fragment"
131 | uri.toString should equal ("http://google.com/test?q=scala-uri#fragment")
132 | }
133 |
134 | "hostParts" should "return the dot separated host" in {
135 | val uri = "http://theon.github.com/test" ? ("q" -> "scala-uri")
136 | uri.hostParts should equal (Vector("theon", "github", "com"))
137 | }
138 |
139 | "subdomain" should "return the first dot separated part of the host" in {
140 | val uri = "http://theon.github.com/test" ? ("q" -> "scala-uri")
141 | uri.subdomain should equal (Some("theon"))
142 | }
143 |
144 | "Uri with user info" should "render correctly" in {
145 | val uri = "http://user:password@moonpig.com/" `#` "hi"
146 | uri.toString should equal ("http://user:password@moonpig.com/#hi")
147 | }
148 |
149 | "Uri with a changed user" should "render correctly" in {
150 | val uri = "http://user:password@moonpig.com/" `#` "hi"
151 | uri.withUser("ian").toString should equal ("http://ian:password@moonpig.com/#hi")
152 | }
153 |
154 | "Uri with a changed password" should "render correctly" in {
155 | val uri = "http://user:password@moonpig.com/" `#` "hi"
156 | uri.withPassword("not-so-secret").toString should equal ("http://user:not-so-secret@moonpig.com/#hi")
157 | }
158 |
159 | "Matrix params" should "be added mid path" in {
160 | val uri = "http://stackoverflow.com/pathOne/pathTwo"
161 | val uriTwo = uri.addMatrixParam("pathOne", "name", "val")
162 |
163 | uriTwo.pathPart("pathOne").params should equal(Vector("name" -> Some("val")))
164 | uriTwo.toString should equal("http://stackoverflow.com/pathOne;name=val/pathTwo")
165 | }
166 |
167 | "Matrix params" should "be added to the end of the path" in {
168 | val uri = "http://stackoverflow.com/pathOne/pathTwo"
169 | val uriTwo = uri.addMatrixParam("name", "val")
170 |
171 | uriTwo.matrixParams should equal(Vector("name" -> Some("val")))
172 | uriTwo.pathPart("pathTwo").params should equal(Vector("name" -> Some("val")))
173 | uriTwo.toString should equal("http://stackoverflow.com/pathOne/pathTwo;name=val")
174 | }
175 |
176 | "A list of query params" should "get added successsfully" in {
177 | val p = ("name", true) :: ("key2", false) :: Nil
178 | val uri = "http://example.com".addParams(p)
179 | uri.query.params("name") should equal(Some("true") :: Nil)
180 | uri.query.params("key2") should equal(Some("false") :: Nil)
181 | uri.toString should equal("http://example.com?name=true&key2=false")
182 | }
183 |
184 | "A list of query params" should "get added to a URL already with query params successsfully" in {
185 | val p = ("name", true) :: ("key2", false) :: Nil
186 | val uri = ("http://example.com" ? ("name" -> Some("param1"))).addParams(p)
187 | uri.query.params("name") should equal(Vector(Some("param1"), Some("true")))
188 | uri.query.params("key2") should equal(Some("false") :: Nil)
189 | }
190 |
191 | "Path and query DSL" should "be possible to use together" in {
192 | val uri = "http://host" / "path" / "to" / "resource" ? ("a" -> "1" ) & ("b" -> "2")
193 | uri.toString should equal("http://host/path/to/resource?a=1&b=2")
194 | }
195 |
196 | "Path and fragment DSL" should "be possible to use together" in {
197 | val uri = "http://host" / "path" / "to" / "resource" `#` "hellyeah"
198 | uri.toString should equal("http://host/path/to/resource#hellyeah")
199 | }
200 |
201 | "Path and query and fragment DSL" should "be possible to use together" in {
202 | val uri = "http://host" / "path" / "to" / "resource" ? ("a" -> "1" ) & ("b" -> "2") `#` "wow"
203 | uri.toString should equal("http://host/path/to/resource?a=1&b=2#wow")
204 | }
205 |
206 | "Latter fragments DSLs" should "overwrite earlier fragments" in {
207 | val uri = "http://host" / "path" / "to" `#` "weird" / "resource" ? ("a" -> "1" ) & ("b" -> "2") `#` "wow"
208 | uri.toString should equal("http://host/path/to/resource?a=1&b=2#wow")
209 | }
210 |
211 | "/? operator" should "add a slash to the path and a query param" in {
212 | val uri = "http://host" /? ("a" -> "1" )
213 | uri.toString should equal("http://host/?a=1")
214 | }
215 |
216 | it should "work alongside the / operator" in {
217 | val uri = "http://host" / "path" /? ("a" -> "1" )
218 | uri.toString should equal("http://host/path/?a=1")
219 | }
220 |
221 | it should "work alongside the & operator" in {
222 | val uri = "http://host" /? ("a" -> "1" ) & ("b" -> "2" )
223 | uri.toString should equal("http://host/?a=1&b=2")
224 | }
225 |
226 | it should "work alongside the / and & operators together" in {
227 | val uri = "http://host" / "path" /? ("a" -> "1" ) & ("b" -> "2" )
228 | uri.toString should equal("http://host/path/?a=1&b=2")
229 | }
230 | }
231 |
--------------------------------------------------------------------------------
/src/test/scala/com/netaporter/uri/EncodingTests.scala:
--------------------------------------------------------------------------------
1 | package com.netaporter.uri
2 |
3 | import org.scalatest.{Matchers, FlatSpec}
4 | import com.netaporter.uri.config.UriConfig
5 |
6 | class EncodingTests extends FlatSpec with Matchers {
7 |
8 | import dsl._
9 | import encoding._
10 |
11 | "URI paths" should "be percent encoded" in {
12 | val uri: Uri = "http://theon.github.com" / "üris-in-scàla.html"
13 | uri.toString should equal ("http://theon.github.com/%C3%BCris-in-sc%C3%A0la.html")
14 | }
15 |
16 | "Raw paths" should "not be encoded" in {
17 | val uri: Uri = "http://theon.github.com" / "üris-in-scàla.html"
18 | uri.pathRaw should equal ("/üris-in-scàla.html")
19 | }
20 |
21 | "toStringRaw" should "not be encoded" in {
22 | val uri: Uri = ("http://theon.github.com" / "üris-in-scàla.html") ? ("càsh" -> "£50")
23 | uri.toStringRaw should equal ("http://theon.github.com/üris-in-scàla.html?càsh=£50")
24 | }
25 |
26 | "URI path spaces" should "be percent encoded by default" in {
27 | val uri: Uri = "http://theon.github.com" / "uri with space"
28 | uri.toString should equal ("http://theon.github.com/uri%20with%20space")
29 | }
30 |
31 | "URI path double quotes" should "be percent encoded when using conservative encoder" in {
32 | val uri: Uri = "http://theon.github.com" / "what-she-said" / """"that""""
33 | uri.toString(UriConfig.conservative) should equal ("http://theon.github.com/what-she-said/%22that%22")
34 | }
35 |
36 | "URI path spaces" should "be plus encoded if configured" in {
37 | implicit val config = UriConfig(encoder = percentEncode + spaceAsPlus)
38 | val uri: Uri = "http://theon.github.com" / "uri with space"
39 | uri.toString should equal ("http://theon.github.com/uri+with+space")
40 | }
41 |
42 | "Path chars" should "be encoded as custom strings if configured" in {
43 | implicit val config = UriConfig(encoder = percentEncode + encodeCharAs(' ', "_"))
44 | val uri: Uri = "http://theon.github.com" / "uri with space"
45 | uri.toString should equal ("http://theon.github.com/uri_with_space")
46 | }
47 |
48 | "Querystring parameters" should "be percent encoded" in {
49 | val uri = "http://theon.github.com/uris-in-scala.html" ? ("càsh" -> "+£50") & ("©opyright" -> "false")
50 | uri.toString should equal ("http://theon.github.com/uris-in-scala.html?c%C3%A0sh=%2B%C2%A350&%C2%A9opyright=false")
51 | }
52 |
53 | "Querystring double quotes" should "be percent encoded when using conservative encoder" in {
54 | val uri: Uri = "http://theon.github.com" ? ("what-she-said" -> """"that"""")
55 | uri.toString(UriConfig.conservative) should equal ("http://theon.github.com?what-she-said=%22that%22")
56 | }
57 |
58 | "Reserved characters" should "be percent encoded when using conservative encoder" in {
59 | val uri = "http://theon.github.com/uris-in-scala.html" ? ("reserved" -> ":/?#[]@!$&'()*+,;={}\\\n\r")
60 | uri.toString(UriConfig.conservative) should equal ("http://theon.github.com/uris-in-scala.html?reserved=%3A%2F%3F%23%5B%5D%40%21%24%26%27%28%29%2A%2B%2C%3B%3D%7B%7D%5C%0A%0D")
61 | }
62 |
63 | "Chinese characters" should "be percent encoded" in {
64 | val uri = "http://theon.github.com/uris-in-scala.html" ? ("chinese" -> "网址")
65 | uri.toString should equal ("http://theon.github.com/uris-in-scala.html?chinese=%E7%BD%91%E5%9D%80")
66 | }
67 |
68 | "Chinese characters with non-UTF8 encoding" should "be percent encoded" in {
69 | val uri = "http://theon.github.com/uris-in-scala.html" ? ("chinese" -> "网址")
70 | val conf = UriConfig(charset = "GB2312")
71 | uri.toString(conf) should equal ("http://theon.github.com/uris-in-scala.html?chinese=%CD%F8%D6%B7")
72 | }
73 |
74 | "Russian characters" should "be percent encoded" in {
75 | val uri = "http://theon.github.com/uris-in-scala.html" ? ("russian" -> "Скала")
76 | uri.toString should equal ("http://theon.github.com/uris-in-scala.html?russian=%D0%A1%D0%BA%D0%B0%D0%BB%D0%B0")
77 | }
78 |
79 | "Fragments" should "be percent encoded" in {
80 | val uri = "http://theon.github.com/uris-in-scala.html" ? ("chinese" -> "网址")
81 | uri.toString should equal ("http://theon.github.com/uris-in-scala.html?chinese=%E7%BD%91%E5%9D%80")
82 | }
83 |
84 | "Percent encoding with custom reserved characters" should "be easy" in {
85 | implicit val config = UriConfig(encoder = percentEncode('#'))
86 | val uri = "http://theon.github.com/uris-in-scala.html" ? ("reserved" -> ":/?#[]@!$&'()*+,;={}\\")
87 | uri.toString should equal ("http://theon.github.com/uris-in-scala.html?reserved=:/?%23[]@!$&'()*+,;={}\\")
88 | }
89 |
90 | "Percent encoding with a few less reserved characters that the defaults" should "be easy" in {
91 | implicit val config = UriConfig(encoder = percentEncode -- '+')
92 | val uri = "http://theon.github.com/uris-in-scala.html" ? ("reserved" -> ":/?#[]@!$&'()*+,;={}\\\n\r")
93 | uri.toString should equal ("http://theon.github.com/uris-in-scala.html?reserved=%3A%2F%3F%23%5B%5D%40%21%24%26%27%28%29%2A+%2C%3B%3D%7B%7D%5C%0A%0D")
94 | }
95 |
96 | "Percent encoding with a few extra reserved characters on top of the defaults" should "be easy" in {
97 | implicit val config = UriConfig(encoder = percentEncode() ++ ('a', 'b'))
98 | val uri: Uri = "http://theon.github.com/abcde"
99 | uri.toString should equal ("http://theon.github.com/%61%62cde")
100 | }
101 |
102 | "URI path pchars" should "not be encoded by default" in {
103 | val uri: Uri = "http://example.com" / "-._~!$&'()*+,;=:@/test"
104 | uri.toString should equal("http://example.com/-._~!$&'()*+,;=:@/test")
105 | }
106 |
107 | "Query parameters" should "have control characters encoded" in {
108 | val uri = "http://example.com/" ? ("control" -> "\u0019\u007F")
109 | uri.toString should equal("http://example.com/?control=%19%7F")
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/test/scala/com/netaporter/uri/GithubIssueTests.scala:
--------------------------------------------------------------------------------
1 | package com.netaporter.uri
2 |
3 | import org.scalatest.{Matchers, OptionValues, FlatSpec}
4 | import scala.Some
5 | import Uri._
6 | import com.netaporter.uri.decoding.PermissivePercentDecoder
7 | import com.netaporter.uri.config.UriConfig
8 | import com.netaporter.uri
9 | import org.parboiled2.ParseError
10 |
11 | /**
12 | * Test Suite to ensure that bugs raised by awesome github peeps NEVER come back
13 | */
14 | class GithubIssueTests extends FlatSpec with Matchers with OptionValues {
15 |
16 | import uri.dsl._
17 |
18 | "Github Issue #2" should "now be fixed. Pluses in querystrings should be encoded when using the conservative encoder" in {
19 | val uri = "http://theon.github.com/" ? ("+" -> "+")
20 | uri.toString(UriConfig.conservative) should equal ("http://theon.github.com/?%2B=%2B")
21 | }
22 |
23 | "Github Issue #4" should "now be fixed. Port numbers should be rendered by toString" in {
24 | val uri = "http://theon.github.com:8080/test" ? ("p" -> "1")
25 | uri.toString should equal ("http://theon.github.com:8080/test?p=1")
26 | }
27 |
28 | "Github Issue #5" should "now be fixed. The characters {} should now be percent encoded" in {
29 | val uri = ("http://theon.github.com" / "{}") ? ("{}" -> "{}")
30 | uri.toString should equal("http://theon.github.com/%7B%7D?%7B%7D=%7B%7D")
31 | }
32 |
33 | "Github Issue #6" should "now be fixed. No implicit Encoder val required for implicit Uri -> String conversion " in {
34 | val uri = "/blah" ? ("blah" -> "blah")
35 | val uriString: String = uri
36 | uriString should equal ("/blah?blah=blah")
37 | }
38 |
39 | "Github Issue #7" should "now be fixed. Calling uri.toString() (with parentheses) should now behave the same as uri.toString " in {
40 | val uri = "/blah" ? ("blah" -> "blah")
41 | uri.toString() should equal ("/blah?blah=blah")
42 | }
43 |
44 | "Github Issue #8" should "now be fixed. Parsed relative uris should have no scheme" in {
45 | val uri = parse("abc")
46 |
47 | uri.scheme should equal (None)
48 | uri.host should equal (None)
49 | uri.path should equal ("/abc")
50 | }
51 |
52 | "Github Issue #15" should "now be fixed. Empty Query String values are parsed" in {
53 | val uri = parse("http://localhost:8080/ping?oi=TscV16GUGtlU&ppc=&bpc=")
54 |
55 | uri.scheme.value should equal ("http")
56 | uri.host.value should equal ("localhost")
57 | uri.port.value should equal (8080)
58 | uri.path should equal ("/ping")
59 | println(uri.query.params)
60 | uri.query.params("oi") should equal (Vector(Some("TscV16GUGtlU")))
61 | uri.query.params("ppc") should equal (Vector(Some("")))
62 | uri.query.params("bpc") should equal (Vector(Some("")))
63 | }
64 |
65 | "Github Issue #12" should "now be fixed. Parsing URIs parse percent escapes" in {
66 | val source = new Uri(
67 | Some("http"),
68 | None,
69 | None,
70 | Some("xn--ls8h.example.net"),
71 | None,
72 | List(PathPart(""), PathPart("path with spaces")),
73 | QueryString(Vector("a b" -> Some("c d"))),
74 | None
75 | )
76 | val parsed = parse(source.toString)
77 | parsed should equal(source)
78 | }
79 |
80 | "Github Issue #19" should "now be fixed" in {
81 | val uri: Uri = "/coldplay.com?singer=chris%26will"
82 | uri.toString should equal ("/coldplay.com?singer=chris%26will")
83 | }
84 |
85 | "Github Issue #26" should "now be fixed" in {
86 | val uri = "http://lesswrong.com/index.php?query=abc%yum&john=hello"
87 | val conf = UriConfig(decoder = PermissivePercentDecoder)
88 | val u = parse(uri)(conf)
89 | u.query.param("query") should equal(Some("abc%yum"))
90 | }
91 |
92 | "Github Issue #37" should "now be fixed" in {
93 | val uri = "http://test.com:8080" / "something"
94 | uri.toString should equal("http://test.com:8080/something")
95 | }
96 |
97 | "Github Issue #51" should "now be fixed. Long URI should not throw StackOverflowException in scala 2.11" in {
98 | if(util.Properties.versionString contains "2.11") {
99 | Uri.parse("http://www.counselingeducation.com/preroll/d/?id=44022&pid=2289033&subid=&size=300x250&e=xKS%2BPDcZDew%2BLDcx7z7hjoWHrPUQRWmjqoNs30jyvuRTh8XP3dCxgaPcjsF5DGAMVAaTJNGqdCoSU2PV6eax0GqflO%2BEDnrj44T6m72%2B%2F0Tv%2B4NCKEMbshFTAv27Dr%2FXp18roLvPT6rwfRcD46jr2qrGL8ru5C%2Ba3QQVpioQ92QPYaU39vFSwrVPU8GWWZcLMuN1s%2BKtqcOx7KXSFBiOGpNNxryydPRzMjI%2BhwmaT1z4ijVnpDjYnicAcX%2BuKtJRqM%2F%2BN9DscOxSZr2x4oSzpkptlTeHvJrj5kJVcrqTzC16tRsuYUulPd7uQblmMxcTsHRkQoGtcB2oOkqRHbW5I6NNWYKQtAnr69gYGAcEXf2a46wi9s7CH8%2FI6uMRvjwvuO0XLFxGVr3hF1V7qfNEIpNo0Lt94LOwefZqnp8fae14taQ1Rb7TGdA9BLIhsbQBmC1WWK2mIUd%2FjERFPhGdoyjcQqGfpGPetYE1yyAdukcubhPcDTNfvPM5DzLENqkefmBNb808irgpulr%2BkUYj2jm6iLRxcGim0EsntciBOPbxVTm7LxfU7D34Z5APCbiyopazfj05Ks5EiD%2FiwsEAJmMXJjKFQlHcpgNLTju3Ct8%2B3TcRstIzoThYphqY8HfuoAtZWCOy9HsSLwXFCqQyZVGO5im%2Fn1BPrF%2Fx%2BnCUfG7yH0laqdQdxaPnz32Ax9H3dscs5E2MfyRTOJVhe0cJLP7T%2B0JuFvhy3NU9Jk2Jx%2BPnWN8wn47O9XJDfnM%2FScIjXpNtiiXxu8b72l14HaM56P2GoIOvaLq2M1Gfullar0dmSkXFmPoI2NMViA7EHBk1zer7AAh7pBq7y10uEh%2BbaDBowavM5c7HF6%2BJW1NBrcFIKBpNr2SvotpFlfqh629ROGLkL54AhzkFHfJzJev3UrXXlpYPP9XdDzTpxdzt1sVHf1Za9JkVPXzyxTVxT2tUDHpW1BJwduCytmxgu8OmVqTvlkbBObhHzLR4sLbOeRUeVNYxndkXn197%2Fxg5%2FsgBchZikNfaAnZhphGTQHiL4DhHjEG2vUaQAZkM8mQBRGkKAFsHn2i%2B%2BGb%2B13ZMKJwbzlmqXhvTBp3e6vmwLY7Ouw0PGR2Q3%2FNU6tqJbZhbUcE6x0rBCszsVwu4qzkUTVU0PyzXYaws%2BKSvWPD1AbLPfcb1NujrAkZKoDiXS%2BoHon9JMDucWOT1IiO01EshpfwkW4oUrgO9GcBtZUpFprmNm24zCzi975RzMzZb9JbilXy3McoBUHJccUPDcyJcNoRNXFL%2BWpt3kQk8sw%2Bf3QNHIWP%2FM5b3uxhgYqKjEql%2FH130ft6dhMcLAozVhEPBeaJiP2NAuMSRGm4HWJpSbIxZz2TcvcsUOYRyS3AXMjN6JEBZ8st29XSS2Gd1jD6SypKB3S9okfnwQApEm4gRLwssSozyyP%2B%2Bu%2FhYd6F%2Btd7vEz5WPaxgwPHLEORaayotzguPJoe2M1ilxUnz%2FruESHMXMHnypw9Tnoz3%2FEd1Lp%2FN1x%2B0Ngrk9IJ8Kzsxc6gtdHYsNablFG0T8Ls%2FfiH1kcM06yMmIEpoMTQc%2BUx4l2qxd5s%2FoUgWMuAcu7jbGesjWJxvCbMfdWZSI%2Fk%2FSXw3WUoIjOZxWT1I8gQzvJo7AsTjXAtvnwRwJIF8KqVFn2IxFT1EUQrhlMdL9l0cAjWdX7iETnCG0HHXCahFlatsZ6Z3OIA8%2BE3PgsiIvpe0Sijo0Bhj5fBs2tbdoHSegcOz%2FCv2j52vz%2Bh9Qm8pXI2mlHK%2B1H4GwFsq6jZ3EHlSsdmFzDb75yaUqhAqkXR%2BIdTTe9NFgoTJNOjdGZYzKjboxD7W2kYQByyM%2Foapfi%2B%2BZ1K6mH2q4Jc%2B2r0MzyGhwwrjxlVQXrJlpSfDodVIVvtETP7PGQO0%2FcOaNwIvksFKkOnOfZu5PsTB2uMdGGwpVevfeqO4EbUKgdYiURznwAZ7M1jLp%2B1IFIr6xgYCHYitfMpMlvu2mjsy1k89VQyOmLmtY9hx2HqI18GZCS%2FivyDsYysEHFR3xxDGu1K53YhSFVVEaKyAQO9lX3wfJLKnDpWnhZ%2Bru0yuXb1s1lUvJ9E1jJC3U%2B9z%2FfnizvH86TzLRLCbyYUWPOosYRGSeLJcXHskmD0hJSnEtivmJT%2FXcbQ3OL%2BuD97ni1NeQnOo3blVtc546ezMOWr5KScgNih%2FZ8zGaC6i2%2BasVMmLVJNVo9sKypQ0U%2B4pFL7QzR0teTnkMpxf3FRvM4ZPIxhsQrt1C%2FUbQCyRqCt0r6mNjzbw%2FV5NB3HccohOpEowGluk8NV40FwRGXuWgm%2BgIZT9jwMyRaZBR80q8JLdbjB6JXiROs5Fq%2BZtuBzMZ%2BV8QDon%2Bbzq7hcDi1kXVjLBxWxmpb%2FdgqfMkKPJ6Qop723HX4kVhNcGa03AaRwontrm2R%2FU9Dp1V9PtEYFKtaiy8csoTFwVYnNo6pcRnA%2BnEjgLyAYS287FlpF4eTHex97U3Rt%2FJCURSRVXfIF3WglXFB8aLDcvg%2BMRt2niW9Js34BVrapopCWZ%2BC8L3MMpzBlm6SVGlgiqzwt%2B1vVIkdMqu9v34Mc0qIay8ZlhtasYQDR5TXoX%2FBS8x3mZVXk36TEulqs%2Bys9XMJhz6QZ9L15LieRDwzCwyI1RHFu1IioKHwxXOOESlwtws5LotYme2UMQSjU2dEGvb3XrOqaAWODJT%2FwujSRoRVq0e8pzbcqrNPM1E0xnWKHnRCn44L0CJ4BXUM4J3BiE1es2RsxOh2e%2FNy%2FsfyFw%2FdYmt%2FUBVrx4rRfXalkbZFtzcNpoQNJo9blxAn5yvMIw8Bgo5aVRYwucWJaCH56jCCOsQkMarAb7Ob41gClaVJpY1ldVEACrnSsd4szk54E7I3R6PYQYp9tu8WbcN8hV%2FvSRvdY7FGZ249bFPbQx4LbjwTXd%2Bx%2FQmyIVbp2B85KeffWM71N8xLAIr1KaLAbX5Wm%2Bju4yuiyQv1ZJK8DpD9Unro5cWrcW1AOI4kXraeQvuWCgLZ%2FrZDhEo282hS6CR%2Fw5CjmOsEyMemjTHBIFacIo2i4t1ifbX5exKwCtME7CWg%2BdD4bBfvhFejqhxC%2BwpMTjejtr15Tekpup1gX1CmsFX4BHQ0fNb64k9Zr9DdaCna5hhVFoVhOEEMl67A2i9SvClr1uMIq7mfwYdn49esiWueotngWLpUBuMQsYBLKhXq72dur6BeA%2B4nv7L8OS5yQq1Q2UdyR573ONsHlKDQZAa34d1v1K%2BcnoUAq9t9VsI8HF6H8xvsH1Xlgv33S6IgRW4t%2FtQOjhmAhEDkkCaNHdVWj%2F72%2FXc%2BXTfu5E%2Ft1Z4DDBUXyylWrheUpGYDIzaWCDSnIudTw4YtLrT3URz5R49N85aAIJHvrOMFJgZcDoOgRqRMh%2FH2FiawltY6BW379Gx8ypM%2FSnJZY6h7pLhPRIJhTbU2eH8mfAF8kKJFVrCsmdWjqxxe4M1mJFSoYof%2FrZVvBbpcVHaf0KkBA2H4LQJz9aW1ZQiMRpt91agU7a6Ki9iSOnXPYu9jROCPMjrrm8udxqs9BBQtfwfFCuahvK6SDMIm81qE3vVYQHeDnQXqgCGQlHAtnobObxR6R0IEiDBCFnQCX2ZTicZ2wjbSq8Bm2GWnuA3IIV%2Fs9J3XLtA5sjcYQ%2FHoMcbPrCoFSd5AixaKlEIBQgtmIKtG3EHXTtlcudRoDKFTN0mdLd98DeSDHs71iXutTnfcppPYlduQFtfcG54GmAZbX7%2BxW4g%2FkZ1fBK%2BY0qDtwt1cnj0Okq3uezonM5GRa97MOvIN8B3k4PtAjUjyHGMo3TE%2FKWCb8NvF3VharBChKo7AL%2BpFzBw68GAsBKgEcyNzbavAMNfSwMXEIqKhQe9muUd0SPGrNvkWm0Rg7fltgvbsQbdpbpFFXLJetqgdZIE4Ux8959yIBfO3f2IJoTsx01kiFo0prRgYYBKo5Fl6FSyj92DpdA%2B4%2BJIynn%2FHggHxJFZvw1dnvvTcgpBR31j2mhLqg7%2F7mLkjYjh3ujVuXs6DynZSgIx2owCnU3peEEcTBvzFPj%2FJYY72QUI5EyrURwGKkkVG1fqrgrz%2F047e0957KicIxKMqm9F6pEsdskWSR3HVpp6jdRF%2F48GlVdcEuG%2FghloHKnQciUOLsZ6MlmOKS22rQ9yYKU0DuyWZOdtHGHB2O0FO4pjjmjf3IZX6lqk%2F909pEQve42JwS3u06nkL4LE8v%2BKLUDR%2B9cffor1PRKcw77BrT8TKM2ihSm1Ixx1RKjGG95matE6UDsq%2FzUVRrRrK5MsoS3eEAX5Mcgnp6AO2gts%2F7N6wzTVdPvyoiQVxknQcYHaZ9saAB4ZDmKPAyVFWopEi1XJ58Ah7EGruOc8NTAOynf8OlGXr2E9lFniz2oi05T4XrDKRaHcCmldQWn4NsiCbfcCEfwA99LMRCN0M7qMQVBpDvnwdkS04ih0AhiKzZuAWgoEVjsjYLrg9YBHuVjdvfaFejOJokXo3JllL2mkkqJP1Wxf02480xbB9xlVK7Ma4wGRH2hOfIbn%2FZExULl59HKnJFh5mBE4vjOg2lpDhJJ0OEMxrZ9xW2Vpi5OwpRP%2B66MOMmGeRv99pcFQ3sbLf0Utr4Z4IhQN2TyFYHPlmLZtdm6nJDTmZ1k4MqkL1WcXGChP3O64yhYQfN%2FgRL1B6fBxDPzuv8d%2F%2FG5wegQV%2Bdu75XXe7BWFzbPAjtvdhAtLaOwzJqCRBHigmDDgfKkvBUUvvivbYMuvzyHYN9czCwjKiCvukiyOvScpsreZ8yrVMs7S5ZETIfvQu95%2BzOXwVOa%2BL8f2vRKl7Z6Cs0YdjXBa4ZZ%2FFL2kiZ%2FSwsbZkYVeTYGO0K%2F8jzXHObqIf%2BwbeJnKf1k%2BZWYkkKQc6HP97hQbDRy34letmFX6IwC5YYgykLqUBk0Vwwc%2FAswtS5CNlEfZ%2FjOhjNcl%2FMc8FPqqKPtdbfSUX8pJr7U70eNKDToH888%2FBwTBn92MJnJQ%2Fj8AtRZcJRVwspbWHHeS56%2BCJ3%2FyvQdagxlirxC2x3nCOmY0FZkXufdoZh2OLaZfOWL6l3FYuyYSetM76mq1II4K%2B8dg3b2xIgCF0%2FP8eZj1uea9oE%2BqnaAiFKQ5EOygrZOjmsbL47USKWjjqYXrpFrFCQ3IN1bA0ivPiv5LWgS7f2GWS0A2qW4pEnR7OMEEMjwV0s2jC6ecaCExvcY7YGTBJ0xKqsIj%2F6TNjFgEo1Hj8DOKgyhFDv3Xl%2FYBme71CeEusAXEe0hNH%2BB%2B1JSDq8NKaz06J%2BCKH1p4n5F%2Bs1vTDuUGFg5W4LM6I7Jc1jvUJ%2FRj%2BL0g7nlrfSA1M1ESt3u%2BPb2w1C7%2BFgttSbf8GVo1JuybI8mG3r6diOzygBD5bsL%2F7E1UlIozRoILPz3eBq40Cny5JeoyrW4a0GO7E6wZZCkK%2BbQlvjuITDFqijTCvN6ElmaCZVLatgEH9sskRwzn4LvosWDk4sqj7j6WSxwRTFX12SVYabAhsq9he40711O7tLTMBxgi4xM5i6GEn7kOyPIU%2BRjLMUCTPyVny26fTTWlB%2F1WRhYuyD9H49Bs8xtTHkajxrn%2BMkkPLLnAuGKnwaLrtHU6EacS6AL2Y%2F%2BWvye6xHxfhV99Rk%2FMM1XCIOoV8O4i4Hlt2E4OkcM8tt7HEulI%2F7fzv%2BMjuHjpVnCiUIJY6jpeMOF2co62cIESfkePXeDLykAdGW88rxCiBHRV6CZMmvuiE6MIR6C3mIIOvBNteY7nl8wRbzla00qs7lNBWhsDPVIeyKt5Y6EPVKGIKhkzRUaugbK6rocl9HvyYJJ2Z2b7P%2BtQ3zFbNI2V4wc7oYe%2Ba1atiTQwflqUKezDILr20s5w%2FBaghY%2BjcjVeBkSXDue6r1FxvuSa22DDflc388g49a1QfC%2BOR0OPQCmKIB1xYSpY5Niw%2F7EkyMRDOEim0cOHxmsDDU3kokSA4DCWIVI0XdeMue1EojvycfKEeBBTw4Fuga6qx6XRZawiFY%2BSWT2Frb0C69qMm6xLvp1lZZkd1SaXzBGQ%2FcEQAl8lirNDJA1ZojtcZEQYqkpzaR7p47gg8IA2gQwxRNuYShzWoxEfn7q39CGqFxeuC6obeDPoLjf07VNnv9jGt%2FrMqHTmIu0SZOF1QKs1m7J5dsC6ABd3e0x5eI4eEHcjMzk0r6TidyIzD%2BkEBjOrkyjQ%2FbMvkAoL9ZqScdRS02oSPyOyDhtP8TN7HE3kWkOPOZeIyuH39pAYW%2FOv%2FRy7w3iKaSKWkqMPNhsNm8142VBspXtc3%2FoOWXCgM4G%2BCYrTBdDOiuhlY757YaPGjo1htNdau5yq89yN%2F89XfRQ%2Fc4pMh%2FNcPaZifxR1C2t69gV1qAwudCJ9BLinlvrUwQ1aUhLlW2W7uRrWrJ966kLVuIo2Nr%2BpE%2F9NINcw02QHbaY1hRbc7iBcCMNkUio7xF8xM9e%2FhYuD%2F2g%2BXF0NkxJOfQPOuClDTrw9cHhkKORhAgpujUiB%2Fri1yDEPuD7o%2FqdF713f7%2BqXn32iMD2fsEiEPBHOjQ8lG4OdhPJyyumzkv020jbNRGCCUa0KvgRZB6YZgGpCeJRvsRffLue8uzo59LNt9cSmEQyapdaHgzO2vCtN14OM6CnK5COET%2Bxkvch%2FQNnvTCcKYaMTv61lVlf4cy0SSmWnj8HGVCs3%2FJEcIkcECenFp4z4T%2F96uJQThSFITbARMePzOv%2Bj5dRnWPNf5o%2Fo35PFeQ3IwoO%2Bhf808s0%2FOEb1XCZxoAoUtcN3jSEX6FOt091jz4GDU1pM1VGLALMTVZVD997pdMA4udTpmQd%2BWz8E8qAlOES4HZ2Syi49g9MsO8LCSq0MffQZhVGyCa42TPympTcJN1OKVj4B5M%2FcDngK6194ksT2O0HHHNVRrHVXOiT3Jt1P20Hr7g6Ie7Mk%2BtSivxRx455BcuITjhafdTEW%2BgN570RNkdjX79nyQpHz%2BirIjiwNJi07pQ069MqHglf7P%2BIPiKzea%2BIprmHPZ96%2ByZfOFChzAnSvI86GuyCGiKmLNcI1zu6%2FHUC%2Fg11yVjIs5DOE4jN")
100 | }
101 | }
102 |
103 | "Github Issue #55" should "now be fixed" in {
104 | val uri: Uri = "http://localhost:9002/iefjiefjief-efefeffe-fefefee/toto?access_token=ijifjijef-fekieifj-fefoejfoef&gquery=filter(time_before_closing%3C=45)"
105 | uri.query.param("gquery") should equal(Some("filter(time_before_closing<=45)"))
106 | uri.toString should equal("http://localhost:9002/iefjiefjief-efefeffe-fefefee/toto?access_token=ijifjijef-fekieifj-fefoejfoef&gquery=filter(time_before_closing%3C%3D45)")
107 | }
108 |
109 | "Github Issue #56" should "now be fixed" in {
110 | val ex = the [java.net.URISyntaxException] thrownBy Uri.parse("http://test.net:8o8o")
111 | ex.getMessage should startWith("Invalid URI could not be parsed.")
112 | }
113 |
114 | "Github Issue #65 example 1" should "now be fixed" in {
115 | val uri = Uri.parse("http://localhost:9000/?foo=test&&bar=test")
116 | uri.toString should equal("http://localhost:9000/?foo=test&&bar=test")
117 | }
118 |
119 | "Github Issue #65 example 2" should "now be fixed" in {
120 | val uri = Uri.parse("http://localhost:9000/mlb/2014/06/15/david-wrights-slump-continues-why-new-york-mets-franchise-third-baseman-must-be-gone-before-seasons-end/?utm_source=RantSports&utm_medium=HUBRecirculation&utm_term=MLBNew York MetsGrid")
121 | uri.toString should equal("http://localhost:9000/mlb/2014/06/15/david-wrights-slump-continues-why-new-york-mets-franchise-third-baseman-must-be-gone-before-seasons-end/?utm_source=RantSports&utm_medium=HUBRecirculation&utm_term=MLBNew%20York%20MetsGrid")
122 | }
123 |
124 | "Github Issue #65 example 3" should "now be fixed" in {
125 | val uri = Uri.parse("http://localhost:9000/t?x=y%26")
126 | uri.query.param("x") should equal(Some("y&"))
127 | uri.toString should equal("http://localhost:9000/t?x=y%26")
128 | }
129 |
130 | "Github Issue #65 example 4" should "now be fixed" in {
131 | val uri = Uri.parse("http://localhost/offers.xml?&id=10748337&np=1")
132 | uri.toString should equal("http://localhost/offers.xml?&id=10748337&np=1")
133 | }
134 |
135 | "Github Issue #65 example 5" should "now be fixed" in {
136 | val uri = Uri.parse("http://localhost/offers.xml?id=10748337&np=1&")
137 | uri.toString should equal("http://localhost/offers.xml?id=10748337&np=1&")
138 | }
139 |
140 | "Github Issue #65 example 6" should "now be fixed" in {
141 | val uri = Uri.parse("http://localhost/offers.xml?id=10748337&np=1anchor")
142 | uri.toString should equal("http://localhost/offers.xml?id=10748337&np=1anchor")
143 | }
144 |
145 | "Github Issue #68" should "now be fixed" in {
146 | val uri = ("http://example.com/path" ? ("param" -> "something==")).toString
147 | uri.toString should equal("http://example.com/path?param=something%3D%3D")
148 | }
149 |
150 | "Github Issue #72" should "now be fixed" in {
151 | val uri = Uri.parse("http://hello.world?email=abc@xyz")
152 | uri.host should equal(Some("hello.world"))
153 | uri.query.param("email") should equal(Some("abc@xyz"))
154 | }
155 |
156 | "Github Issue #73" should "now be fixed" in {
157 | val uri = "http://somewhere.something".withUser("user:1@domain").withPassword("abc xyz")
158 | uri.toString should equal("http://user%3A1%40domain:abc%20xyz@somewhere.something")
159 | }
160 |
161 | "Github Issue #99" should "now be fixed" in {
162 | val uri = Uri.parse("https://www.foo.com/#/myPage?token=bar")
163 | uri.toString should equal("https://www.foo.com/#/myPage?token=bar")
164 | }
165 |
166 | "Github Issue #104" should "now be fixed" in {
167 | val uri = Uri.parse("a1+-.://localhost")
168 | uri.scheme should equal(Some("a1+-."))
169 | uri.host should equal(Some("localhost"))
170 | }
171 |
172 | "Github Issue #106" should "now be fixed" in {
173 | val p = "http://localhost:1234"
174 |
175 | val withPath = p / "some/path/segments"
176 | withPath.toString should equal("http://localhost:1234/some/path/segments")
177 |
178 | val withPathAndQuery = p / "some/path/segments" ? ("returnUrl" -> "http://localhost:1234/some/path/segments")
179 | withPathAndQuery.toString should equal("http://localhost:1234/some/path/segments?returnUrl=http://localhost:1234/some/path/segments")
180 | }
181 |
182 | "Github Issue #114" should "now be fixed" in {
183 | val uri = Uri.parse("https://krownlab.com/products/hardware-systems/baldur/#baldur-top-mount#1")
184 | uri.fragment should equal(Some("baldur-top-mount#1"))
185 | uri.toString should equal("https://krownlab.com/products/hardware-systems/baldur/#baldur-top-mount%231")
186 | }
187 |
188 | "Github Issue #124" should "now be fixed" in {
189 | val uri = Uri.parse("https://github.com")
190 | uri.matrixParams should equal(Seq.empty)
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/src/test/scala/com/netaporter/uri/ParsingTests.scala:
--------------------------------------------------------------------------------
1 | package com.netaporter.uri
2 |
3 | import org.scalatest.{Matchers, FlatSpec}
4 | import Uri._
5 | import scala._
6 | import scala.Some
7 | import com.netaporter.uri.parsing._
8 | import com.netaporter.uri.config.UriConfig
9 |
10 | class ParsingTests extends FlatSpec with Matchers {
11 |
12 | "Parsing an absolute URI" should "result in a valid Uri object" in {
13 | val uri = parse("http://theon.github.com/uris-in-scala.html")
14 | uri.scheme should equal (Some("http"))
15 | uri.host should equal (Some("theon.github.com"))
16 | uri.path should equal ("/uris-in-scala.html")
17 | }
18 |
19 | "Parsing a relative URI" should "result in a valid Uri object" in {
20 | val uri = parse("/uris-in-scala.html")
21 | uri.scheme should equal (None)
22 | uri.host should equal (None)
23 | uri.path should equal ("/uris-in-scala.html")
24 | }
25 |
26 | "Parsing a URI with querystring parameters" should "result in a valid Uri object" in {
27 | val uri = parse("/uris-in-scala.html?query_param_one=hello&query_param_one=goodbye&query_param_two=false")
28 | uri.query.params should equal (
29 | Vector (
30 | ("query_param_one" -> Some("hello")),
31 | ("query_param_one" -> Some("goodbye")),
32 | ("query_param_two" -> Some("false"))
33 | )
34 | )
35 | }
36 |
37 | "Parsing a URI with not properly URL-encoded querystring parameters" should "result in a valid Uri object" in {
38 | val uri = parse("/uris-in-scala.html?query_param_one=hello=world&query_param_two=false")
39 | uri.query.params should equal (
40 | Vector (
41 | ("query_param_one" -> Some("hello=world")),
42 | ("query_param_two" -> Some("false"))
43 | )
44 | )
45 | }
46 |
47 | "Parsing a URI with a zero-length querystring parameter" should "result in a valid Uri object" in {
48 | val uri = parse("/uris-in-scala.html?query_param_one=&query_param_two=false")
49 | uri.query.params should equal (
50 | Vector (
51 | ("query_param_one" -> Some("")),
52 | ("query_param_two" -> Some("false"))
53 | )
54 | )
55 | }
56 |
57 | "Parsing a url with relative scheme" should "result in a Uri with None for scheme" in {
58 | val uri = parse("//theon.github.com/uris-in-scala.html")
59 | uri.scheme should equal (None)
60 | uri.toString should equal ("//theon.github.com/uris-in-scala.html")
61 | }
62 |
63 | "Parsing a url with relative scheme" should "result in the correct host" in {
64 | val uri = parse("//theon.github.com/uris-in-scala.html")
65 | uri.host should equal(Some("theon.github.com"))
66 | }
67 |
68 | "Parsing a url with relative scheme" should "result in the correct path" in {
69 | val uri = parse("//theon.github.com/uris-in-scala.html")
70 | uri.pathParts should equal(Vector(PathPart("uris-in-scala.html")))
71 | }
72 |
73 | "Parsing a url with a fragment" should "result in a Uri with Some for fragment" in {
74 | val uri = parse("//theon.github.com/uris-in-scala.html#fragged")
75 | uri.fragment should equal (Some("fragged"))
76 | }
77 |
78 | "Parsing a url with a query string and fragment" should "result in a Uri with Some for fragment" in {
79 | val uri = parse("//theon.github.com/uris-in-scala.html?ham=true#fragged")
80 | uri.fragment should equal (Some("fragged"))
81 | }
82 |
83 | "Parsing a url without a fragment" should "result in a Uri with None for fragment" in {
84 | val uri = parse("//theon.github.com/uris-in-scala.html")
85 | uri.fragment should equal (None)
86 | }
87 |
88 | "Parsing a url without an empty fragment" should "result in a Uri with Some(empty string) for fragment" in {
89 | val uri = parse("//theon.github.com/uris-in-scala.html#")
90 | uri.fragment should equal (Some(""))
91 | }
92 |
93 | "Parsing a url with user" should "result in a Uri with the username" in {
94 | val uri = parse("mailto://theon@github.com")
95 | uri.scheme should equal(Some("mailto"))
96 | uri.user should equal(Some("theon"))
97 | uri.host should equal(Some("github.com"))
98 | }
99 |
100 | "Parsing a with user and password" should "result in a Uri with the user and password" in {
101 | val uri = parse("ftp://theon:password@github.com")
102 | uri.scheme should equal(Some("ftp"))
103 | uri.user should equal(Some("theon"))
104 | uri.password should equal(Some("password"))
105 | uri.host should equal(Some("github.com"))
106 | }
107 |
108 | "Parsing a with user and empty password" should "result in a Uri with the user and empty password" in {
109 | val uri = parse("ftp://theon:@github.com")
110 | uri.scheme should equal(Some("ftp"))
111 | uri.user should equal(Some("theon"))
112 | uri.password should equal(Some(""))
113 | uri.host should equal(Some("github.com"))
114 | }
115 |
116 | "Protocol relative url with authority" should "parse correctly" in {
117 | val uri = parse("//user:pass@www.mywebsite.com/index.html")
118 | uri.scheme should equal(None)
119 | uri.user should equal(Some("user"))
120 | uri.password should equal(Some("pass"))
121 | uri.subdomain should equal(Some("www"))
122 | uri.host should equal(Some("www.mywebsite.com"))
123 | uri.pathParts should equal(Vector(PathPart("index.html")))
124 | }
125 |
126 | "Url with @ in query string" should "parse correctly" in {
127 | val uri = parse("http://www.mywebsite.com?a=b@")
128 | uri.scheme should equal(Some("http"))
129 | uri.host should equal (Some("www.mywebsite.com"))
130 | }
131 |
132 | "Query string param with hash as value" should "be parsed as fragment" in {
133 | val uri = parse("http://stackoverflow.com?q=#frag")
134 | uri.query.params("q") should equal(Vector(Some("")))
135 | uri.fragment should equal(Some("frag"))
136 | }
137 |
138 | "Parsing a url with a query string that doesn't have a value" should "not throw an exception" in {
139 | val uri = parse("//theon.github.com/uris-in-scala.html?ham")
140 | uri.host should equal(Some("theon.github.com"))
141 | uri.query.params("ham") should equal(Vector(None))
142 | uri.toString should equal("//theon.github.com/uris-in-scala.html?ham")
143 |
144 | val uri2 = parse("//cythrawll.github.com/scala-uri.html?q=foo&ham")
145 | uri2.host should equal(Some("cythrawll.github.com"))
146 | uri2.query.params("ham") should equal(Vector(None))
147 | uri2.query.params("q") should equal(Vector(Some("foo")))
148 | uri2.toString should equal("//cythrawll.github.com/scala-uri.html?q=foo&ham")
149 |
150 |
151 | val uri3 = parse("//cythrawll.github.com/scala-uri.html?ham&q=foo")
152 | uri3.host should equal(Some("cythrawll.github.com"))
153 | uri3.query.params("ham") should equal(Vector(None))
154 | uri3.query.params("q") should equal(Vector(Some("foo")))
155 | uri3.toString should equal("//cythrawll.github.com/scala-uri.html?ham&q=foo")
156 | }
157 |
158 | "Parsing a url with two query strings that doesn't have a value in different ways" should "work and preserve the difference" in {
159 | val uri4 = parse("//cythrawll.github.com/scala-uri.html?ham&jam=&q=foo")
160 | uri4.host should equal (Some("cythrawll.github.com"))
161 | uri4.query.params("ham") should equal(Vector(None))
162 | uri4.query.params("jam") should equal(Vector(Some("")))
163 | uri4.query.params("q") should equal(Vector(Some("foo")))
164 | uri4.toString should equal("//cythrawll.github.com/scala-uri.html?ham&jam=&q=foo")
165 | }
166 |
167 |
168 | "Path with matrix params" should "be parsed when configured" in {
169 | implicit val config = UriConfig(matrixParams = true)
170 | val uri = parse("http://stackoverflow.com/path;paramOne=value;paramTwo=value2/pathTwo;paramOne=value")
171 | uri.pathParts should equal(Vector(
172 | MatrixParams("path", Vector("paramOne" -> Some("value"), "paramTwo" -> Some("value2"))),
173 | MatrixParams("pathTwo", Vector("paramOne" -> Some("value")))
174 | ))
175 | }
176 |
177 | "Path with matrix params" should "accept empty params and trailing semi-colons" in {
178 | implicit val config = UriConfig(matrixParams = true)
179 | val uri = parse("http://stackoverflow.com/path;;paramOne=value;paramTwo=value2;;paramThree=;")
180 | uri.pathParts should equal(Vector(
181 | MatrixParams("path", Vector("paramOne" -> Some("value"),
182 | "paramTwo" -> Some("value2"),
183 | "paramThree" -> Some("")))
184 | ))
185 | }
186 |
187 | it should "not be parsed by default" in {
188 | val uri = parse("http://stackoverflow.com/path;paramOne=value;paramTwo=value2/pathTwo;paramOne=value")
189 | uri.pathParts should equal(Vector(
190 | StringPathPart("path;paramOne=value;paramTwo=value2"),
191 | StringPathPart("pathTwo;paramOne=value")
192 | ))
193 | }
194 |
195 | "Empty path parts" should "be maintained during parsing" in {
196 | val uri = parse("http://www.example.com/hi//bye")
197 | uri.toString should equal("http://www.example.com/hi//bye")
198 | }
199 |
200 | "exotic/reserved characters in query string" should "be decoded" in {
201 | val q = "?weird%3D%26key=strange%25value&arrow=%E2%87%94"
202 | val parsedQueryString = new DefaultUriParser(q, config.UriConfig.default)._queryString.run().get
203 | parsedQueryString.params("weird=&key") should equal(Seq(Some("strange%value")))
204 | parsedQueryString.params("arrow") should equal(Seq(Some("⇔")))
205 | }
206 |
207 | "exotic/reserved characters in user info" should "be decoded" in {
208 | val userInfo = "user%3A:p%40ssword%E2%87%94@"
209 | val parsedUserInfo = new DefaultUriParser(userInfo, config.UriConfig.default)._userInfo.run().get
210 | parsedUserInfo.user should equal("user:")
211 | parsedUserInfo.pass should equal(Some("p@ssword⇔"))
212 | }
213 |
214 | "Uri.parse" should "provide paramMap as a Map of String to Seq of String" in {
215 | val parsed = Uri.parse("/?a=b&a=c&d=&e&f&f=g")
216 |
217 | parsed.query.paramMap should be (Map(
218 | "a" -> Seq("b", "c"),
219 | "d" -> Seq(""),
220 | "e" -> Seq.empty,
221 | "f" -> Seq("g")
222 | ))
223 | }
224 |
225 | "Uri.parseQuery" should "parse a query string starting with a ?" in {
226 | val parsed = Uri.parseQuery("?a=b&c=d")
227 | parsed should equal(QueryString.create("a" -> Some("b"), "c" -> Some("d")))
228 | }
229 |
230 | it should "parse a query string not starting with a ?" in {
231 | val parsed = Uri.parseQuery("a=b&c=d")
232 | parsed should equal(QueryString.create("a" -> Some("b"), "c" -> Some("d")))
233 | }
234 | }
--------------------------------------------------------------------------------
/src/test/scala/com/netaporter/uri/ProtocolTests.scala:
--------------------------------------------------------------------------------
1 | package com.netaporter.uri
2 |
3 | import org.scalatest.{Matchers, FlatSpec}
4 |
5 | class ProtocolTests extends FlatSpec with Matchers {
6 |
7 | import dsl._
8 |
9 | "A domain with no scheme" should "be rendered as a scheme relative url" in {
10 | val uri = Uri(host = "theon.github.com") / "uris-in-scala.html"
11 | uri.toString should equal ("//theon.github.com/uris-in-scala.html")
12 | }
13 |
14 | "A domain with a scheme" should "be rendered as a scheme absolute url" in {
15 | val uri = Uri(scheme = "ftp", host = "theon.github.com") / "uris-in-scala.html"
16 | uri.toString should equal ("ftp://theon.github.com/uris-in-scala.html")
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/test/scala/com/netaporter/uri/PublicSuffixTests.scala:
--------------------------------------------------------------------------------
1 | package com.netaporter.uri
2 |
3 | import org.scalatest.FlatSpec
4 | import org.scalatest.Matchers
5 |
6 | class PublicSuffixTests extends FlatSpec with Matchers {
7 |
8 | "Uri publicSuffix method" should "match the longest public suffix" in {
9 | val uri = Uri.parse("http://www.google.co.uk/blah")
10 | uri.publicSuffix should equal(Some("co.uk"))
11 | }
12 |
13 | it should "only return public suffixes that match full dot separated host parts" in {
14 | val uri = Uri.parse("http://www.bar.com")
15 |
16 | // Should not match ar.com
17 | // Github issue #110
18 | uri.publicSuffix should equal(Some("com"))
19 | }
20 |
21 | "Uri publicSuffixes method" should "match the all public suffixes" in {
22 | val uri = Uri.parse("http://www.google.co.uk/blah")
23 | uri.publicSuffixes should equal(Seq("co.uk", "uk"))
24 | }
25 |
26 | it should "return None for relative URLs" in {
27 | val uri = Uri.parse("/blah")
28 | uri.publicSuffix should equal(None)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/test/scala/com/netaporter/uri/ToUriTests.scala:
--------------------------------------------------------------------------------
1 | package com.netaporter.uri
2 |
3 | import org.scalatest.WordSpec
4 | import org.scalatest.Matchers
5 | import dsl._
6 | import java.net.URI
7 | import com.netaporter.uri.config.UriConfig
8 |
9 | class ToUriTests extends WordSpec with Matchers {
10 | "toUri" should {
11 | "handle simple URL" in {
12 | val strUri: String = "http://www.example.com"
13 | val uri: Uri = strUri
14 | val javaUri: URI = uri.toURI
15 | javaUri.getScheme() should equal("http")
16 | javaUri.getUserInfo() should be(null)
17 | javaUri.getHost() should equal("www.example.com")
18 | javaUri.getPath() should equal("")
19 | javaUri.getQuery() should be(null)
20 | javaUri.getFragment() should be(null)
21 | javaUri.toASCIIString() should equal(strUri)
22 | }
23 |
24 | "handle scheme-less URL" in {
25 | val strUri: String = "//www.example.com/test"
26 | val uri: Uri = strUri
27 | val javaUri: URI = uri.toURI
28 | javaUri.getScheme() should be(null)
29 | javaUri.getHost() should equal("www.example.com")
30 | javaUri.getPath() should equal("/test")
31 | javaUri.toASCIIString() should equal(strUri)
32 | }
33 |
34 | "handle authenticated URL" in {
35 | val strUri: String = "https://user:password@www.example.com/test"
36 | val uri: Uri = strUri
37 | val javaUri: URI = uri.toURI
38 | javaUri.getScheme() should equal("https")
39 | javaUri.getUserInfo() should equal("user:password")
40 | javaUri.getHost() should equal("www.example.com")
41 | javaUri.getPath() should equal("/test")
42 | javaUri.toASCIIString() should equal(strUri)
43 | }
44 |
45 | "handle exotic/reserved characters in query string" in {
46 | val uri: Uri = "http://www.example.com/test" ? ("weird=&key" -> "strange%value") & ("arrow" -> "⇔")
47 | val javaUri: URI = uri.toURI
48 | javaUri.getScheme() should equal("http")
49 | javaUri.getHost() should equal("www.example.com")
50 | javaUri.getPath() should equal("/test")
51 | javaUri.getQuery() should equal("weird=&key=strange%value&arrow=⇔")
52 | javaUri.getRawQuery() should equal("weird%3D%26key=strange%25value&arrow=%E2%87%94")
53 | javaUri.toString() should equal(uri.toString)
54 | javaUri.toASCIIString() should equal(uri.toString)
55 | }
56 | }
57 |
58 | "apply" should {
59 |
60 | "handle exotic/reserved characters in query string" in {
61 | val javaUri: URI = new URI("http://user:password@www.example.com/test?weird%3D%26key=strange%25value&arrow=%E2%87%94")
62 | val uri: Uri = Uri(javaUri)
63 | uri.scheme should equal(Some("http"))
64 | uri.host should equal(Some("www.example.com"))
65 | uri.user should equal(Some("user"))
66 | uri.password should equal(Some("password"))
67 | uri.path should equal("/test")
68 | uri.query.params should equal(Seq(("weird=&key", Some("strange%value")), ("arrow", Some("⇔"))))
69 | uri.toString(UriConfig.conservative) should equal(javaUri.toASCIIString())
70 | }
71 | }
72 | }
--------------------------------------------------------------------------------
/src/test/scala/com/netaporter/uri/TransformTests.scala:
--------------------------------------------------------------------------------
1 | package com.netaporter.uri
2 |
3 | import org.scalatest.{Matchers, WordSpec}
4 | import Uri._
5 |
6 | class TransformTests extends WordSpec with Matchers {
7 |
8 | "mapQuery" should {
9 |
10 | "transform query params" in {
11 | val uri = parse("/test?param_1=hello¶m_2=goodbye¶m_3=false")
12 | val uri2 = uri.mapQuery {
13 | case (k, v) => (k, v map (_+ "TEST"))
14 | }
15 | uri2.toString should equal("/test?param_1=helloTEST¶m_2=goodbyeTEST¶m_3=falseTEST")
16 | }
17 |
18 | "transform query param names" in {
19 | val uri = parse("/test?param_1=hello¶m_2=goodbye¶m_3=false")
20 | val uri2 = uri.mapQueryNames(_.split("_")(1))
21 | uri2.toString should equal("/test?1=hello&2=goodbye&3=false")
22 | }
23 |
24 | "flip query params" in {
25 | val uri = parse("/test?param_1=hello¶m_2=goodbye¶m_3=false")
26 | val uri2 = uri.mapQuery(_ match { case (k,Some(v)) => v->Some(k) case o => o})
27 | uri2.toString should equal("/test?hello=param_1&goodbye=param_2&false=param_3")
28 | }
29 |
30 | "transform query param values" in {
31 | val uri = parse("/test?param_1=hello¶m_2=goodbye¶m_3=false")
32 | val uri2 = uri.mapQueryValues(_.charAt(0).toString)
33 | uri2.toString should equal("/test?param_1=h¶m_2=g¶m_3=f")
34 | }
35 | }
36 |
37 | "filterQuery" should {
38 |
39 | "filter query params" in {
40 | val uri = parse("/test?param_1=hello¶m_2=goodbye¶m_3=false")
41 | val uri2 = uri.filterQuery {
42 | case (k, Some(v)) => (k + v).length > 13
43 | case (k, None) => k.length > 13
44 | }
45 | uri2.toString should equal("/test?param_2=goodbye")
46 | }
47 |
48 | "filter out all query params" in {
49 | val uri = parse("/test?param_1=hello¶m_2=goodbye¶m_3=false")
50 | val uri2 = uri.filterQuery(p => false)
51 | uri2.toString should equal("/test")
52 | }
53 |
54 | "filter query param names" in {
55 | val uri = parse("/test?param_1=hello¶m_2=goodbye¶m_3=false")
56 | val uri2 = uri.filterQueryNames(_ == "param_1")
57 | uri2.toString should equal("/test?param_1=hello")
58 | }
59 |
60 | "filter query param values" in {
61 | val uri = parse("/test?param_1=hello¶m_2=goodbye¶m_3=false")
62 | val uri2 = uri.filterQueryValues(_ == "false")
63 | uri2.toString should equal("/test?param_3=false")
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/test/scala/com/netaporter/uri/TypeTests.scala:
--------------------------------------------------------------------------------
1 | package com.netaporter.uri
2 |
3 | import org.scalatest.{Matchers, FlatSpec}
4 |
5 | class TypeTests extends FlatSpec with Matchers {
6 |
7 | import dsl._
8 |
9 | "String" should "render correctly" in {
10 | val uri = "/uris-in-scala.html" ? ("param" -> "hey")
11 | uri.toString should equal ("/uris-in-scala.html?param=hey")
12 | }
13 |
14 | "Booleans" should "render correctly" in {
15 | val uri = "/uris-in-scala.html" ? ("param" -> true)
16 | uri.toString should equal ("/uris-in-scala.html?param=true")
17 | }
18 |
19 | "Integers" should "render correctly" in {
20 | val uri = "/uris-in-scala.html" ? ("param" -> 1)
21 | uri.toString should equal ("/uris-in-scala.html?param=1")
22 | }
23 |
24 | "Floats" should "render correctly" in {
25 | val uri = "/uris-in-scala.html" ? ("param" -> 0.5f)
26 | uri.toString should equal ("/uris-in-scala.html?param=0.5")
27 | }
28 |
29 | "Options" should "render correctly" in {
30 | val uri = "/uris-in-scala.html" ? ("param" -> Some("some")) & ("param2" -> None)
31 | uri.toString should equal ("/uris-in-scala.html?param=some")
32 | }
33 | }
34 |
--------------------------------------------------------------------------------