├── .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=1&#anchor") 142 | uri.toString should equal("http://localhost/offers.xml?id=10748337&np=1&#anchor") 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 | --------------------------------------------------------------------------------