Skip to content
This repository has been archived by the owner on Apr 16, 2019. It is now read-only.

Commit

Permalink
Implement Path and Uri types.
Browse files Browse the repository at this point in the history
Converted `Uri` to abstract, and created case classes that extend `Uri`
to allow easier validation.
Extracted `Authority` and `UserInfo` classes from the UriParser to be
used fully.
Changed `PathPart` to `Segment`. This is more consistent with the RFC.
Also, "path" and "part" were too similar and being confused.
Changed `QueryString` to `Query`. This is more consistent with the RFC.
Created `Scheme` and `Fragment` classes. This seemed more consistent
with the rest of the structure and eased validation.
Created `Parameter` class. This was mentioned in issue NET-A-PORTER#34 and eased
validation.
Consistency changes throughout.
Deprecated as much as possible to ease transition of existing users.
Started updating the README, not complete.
No DSL changes (aside from compatibility changes) as this will be
completed as a separate task.
  • Loading branch information
evanbennett committed Jun 29, 2016
1 parent 818d220 commit 362193d
Show file tree
Hide file tree
Showing 55 changed files with 6,425 additions and 1,277 deletions.
56 changes: 42 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
To include it in your SBT project from maven central:

```scala
"com.netaporter" %% "scala-uri" % "0.4.14"
"com.netaporter" %% "scala-uri" % "1.0.0"
```

There is also a [demo project](https://github.com/NET-A-PORTER/scala-uri-demo) to help you get up and running quickly, from scratch.
Expand Down Expand Up @@ -89,10 +89,10 @@ import com.netaporter.uri.Uri.parse
val uri = parse("http://theon.github.com/scala-uri?param1=1&param2=2")
```

There also exists a `import com.netaporter.uri.Uri.parseQuery` for instances when you wish to parse a query string, not a full URI.

## Transforming URIs

TODO: This section needs to be updated.

### map

The `mapQuery` method will transform the Query String of a URI by applying the specified Function to each Query String Parameter
Expand Down Expand Up @@ -145,7 +145,7 @@ uri.filterQueryValues(_.length == 1) //Results in /scala-uri?p2=2

## URL Percent Encoding

By Default, `scala-uri` will URL percent encode paths and query string parameters. To prevent this, you can call the `uri.toStringRaw` method:
By Default, `scala-uri` will URL percent encode user info, paths, query strings and fragments. To prevent this, you can call the `uri.toStringRaw` method:

```scala
import com.netaporter.uri.dsl._
Expand All @@ -162,21 +162,21 @@ Only percent encode the hash character:

```scala
import com.netaporter.uri.encoding._
implicit val config = UriConfig(encoder = percentEncode('#'))
implicit val config = UriConfig(encoder = PercentEncoder('#'))
```

Percent encode all the default chars, except the plus character:

```scala
import com.netaporter.uri.encoding._
implicit val config = UriConfig(encoder = percentEncode -- '+')
implicit val config = UriConfig(encoder = PercentEncoder.default -- '+')
```

Encode all the default chars, and also encode the letters a and b:

```scala
import com.netaporter.uri.encoding._
implicit val config = UriConfig(encoder = percentEncode ++ ('a', 'b'))
implicit val config = UriConfig(encoder = PercentEncoder.default ++ ('a', 'b'))
```

### Encoding spaces as pluses
Expand All @@ -186,7 +186,7 @@ The default behaviour with scala uri, is to encode spaces as `%20`, however if y
```scala
import com.netaporter.uri.dsl._
import com.netaporter.uri.encoding._
implicit val config = UriConfig(encoder = percentEncode + spaceAsPlus)
implicit val config = UriConfig(encoder = PercentEncoder.default + EncodeCharAs.spaceAsPlus)

val uri: Uri = "http://theon.github.com/uri with space"
uri.toString //This is http://theon.github.com/uri+with+space
Expand All @@ -199,7 +199,7 @@ If you would like to do some custom encoding for specific characters, you can us
```scala
import com.netaporter.uri.dsl._
import com.netaporter.uri.encoding._
implicit val config = UriConfig(encoder = percentEncode + encodeCharAs(' ', "_"))
implicit val config = UriConfig(encoder = PercentEncoder.default + EncodeCharAs(' ', "_"))

val uri: Uri = "http://theon.github.com/uri with space"
uri.toString //This is http://theon.github.com/uri_with_space
Expand Down Expand Up @@ -232,6 +232,8 @@ uri.toStringRaw //This is: http://example.com/i-havent-%been%-percent-encoded

## Replacing Query String Parameters

TODO: This section needs to be updated. Move it to [Transforming URIs](#transforming-uris)?

If you wish to replace all existing query string parameters with a given name, you can use the `uri.replaceParams()` method:

```scala
Expand All @@ -244,6 +246,8 @@ newUri.toString //This is: http://example.com/path?param=2

## Removing Query String Parameters

TODO: This section needs to be updated. Move it to [Transforming URIs](#transforming-uris)?

If you wish to remove all existing query string parameters with a given name, you can use the `uri.removeParams()` method:

```scala
Expand All @@ -256,6 +260,8 @@ newUri.toString //This is: http://example.com/path?param2=2

## Get query string parameters

TODO: This section needs to be updated.

To get the query string parameters as a `Map[String,Seq[String]]` you can do the following:

```scala
Expand Down Expand Up @@ -305,6 +311,8 @@ uri.host //This is: Some("example.com")

## Matrix Parameters

TODO: This section needs to be updated.

[Matrix Parameters](http://www.w3.org/DesignIssues/MatrixURIs.html) are supported in `scala-uri`. Support is enabled
using a`UriConfig` with `matrixParams = true` like so:

Expand Down Expand Up @@ -368,14 +376,14 @@ These methods return `None` and `Seq.empty`, respectively for relative URIs

## Including scala-uri your project

`scala-uri` `0.4.x` is currently built with support for scala `2.10.x` and `2.11.x`
`scala-uri` `1.0.x` is currently built with support for scala `2.10.x` and `2.11.x`

For `2.9.x` support use `scala-uri` [`0.3.x`](https://github.com/net-a-porter/scala-uri/tree/0.3.x)

Release builds are available in maven central. For SBT users just add the following dependency:

```scala
"com.netaporter" %% "scala-uri" % "0.4.14"
"com.netaporter" %% "scala-uri" % "1.0.0"
```

For maven users you should use (for 2.11.x):
Expand All @@ -384,7 +392,7 @@ For maven users you should use (for 2.11.x):
<dependency>
<groupId>com.netaporter</groupId>
<artifactId>scala-uri_2.11</artifactId>
<version>0.4.14</version>
<version>1.0.0</version>
</dependency>
```

Expand All @@ -399,7 +407,7 @@ resolvers += "Sonatype OSS" at "https://oss.sonatype.org/content/repositories/sn
Add the following dependency:

```scala
"com.netaporter" %% "scala-uri" % "0.4.15-SNAPSHOT"
"com.netaporter" %% "scala-uri" % "1.0.0-SNAPSHOT"
```

# Contributions
Expand All @@ -416,13 +424,33 @@ Contributions to `scala-uri` are always welcome. Good ways to contribute include

The unit tests can be run from the sbt console by running the `test` command! Checking the unit tests all pass before sending pull requests will be much appreciated.

TODO: `scct:test` does not work for me.

Generate code coverage reports from the sbt console by running the `scct:test` command. The HTML reports should be generated at `target/scala-2.10/coverage-report/index.html`. Ideally pull requests shouldn't significantly decrease code coverage, but it's not the end of the world if they do. Contributions with no tests are better than no contributions :)

## Performance Tests

For the `scala-uri` performance tests head to the [scala-uri-benchmarks](https://github.com/net-a-porter/scala-uri-benchmarks) github project

# Migration guide from 0.3.x
# Migration guide

## From 1.0.x to 1.1.x

* Remove all deprecation warnings.

## From 0.4.x to 1.0.x

* Incompatibility: A query with no parameters now `toString`s as "?", and `Uri` can have no query.
* `Uri` method changes:
* `scheme` use `protocol` instead
* `query` use `queryValue` instead
* `fragment` use `fragmentString` instead
* `copy` use `copyOld` instead
* `path` use `pathToString` instead
* `unapply` returns the new arguments
* Lots of deprecation warnings.

## From 0.3.x to 0.4.x

* Package changes / import changes
* All code moved from `com.github.theon` package to `com.netaporter` package
Expand Down
43 changes: 43 additions & 0 deletions src/main/scala/com/netaporter/uri/Authority.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.netaporter.uri

import com.netaporter.uri.config.UriConfig

sealed abstract case class Authority(userInfo: Option[UserInfo], host: String, port: Option[Int]) {

def user: Option[String] = userInfo.map(_.user)

def password: Option[String] = userInfo.flatMap(_.password)

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

def copy(userInfo: Option[UserInfo] = userInfo, host: String = host, port: Option[Int] = port): Authority = Authority(userInfo, host, port)

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

def toString(implicit c: UriConfig): String =
"//" + userInfo.map(_.toString).getOrElse("") + host + port.map(":" + _).getOrElse("")

def toStringRaw(implicit c: UriConfig): String = toString(c.withNoEncoding)
}

object Authority {

def apply(userInfo: Option[UserInfo], host: String, port: Option[Int]): Authority = {
if (host == null || host.isEmpty) throw new IllegalArgumentException("`host` cannot be `null`.")
if (port.nonEmpty && port.exists(port => port < 1 || port > 65535)) throw new IllegalArgumentException("Invalid `port`. [" + port + "]")
new Authority(userInfo, host, port) {}
}

def apply(user: String = null, password: String = null, host: String, port: Int = 0): Authority =
apply(UserInfo.option(user, password), host, if (port == 0) None else Option(port))

def option(userInfo: Option[UserInfo], host: String, port: Option[Int]): Option[Authority] = {
if (host == null || host.isEmpty) {
if (userInfo.nonEmpty || port.nonEmpty) throw new IllegalArgumentException("`host` cannot be `null`.")
None
} else Option(apply(userInfo, host, port))
}

def option(user: String = null, password: String = null, host: String, port: Int = 0): Option[Authority] =
option(UserInfo.option(user, password), host, if (port == 0) None else Option(port))
}
31 changes: 31 additions & 0 deletions src/main/scala/com/netaporter/uri/Fragment.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.netaporter.uri

import com.netaporter.uri.config.UriConfig

sealed abstract case class Fragment(fragment: String) {

def copy(fragment: String = fragment): Fragment = Fragment(fragment)

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

def toString(implicit c: UriConfig): String =
"#" + c.fragmentEncoder.encode(fragment, c.charset)

def toStringRaw(implicit c: UriConfig): String = toString(c.withNoEncoding)
}

// TODO: Make `Fragment` abstract, and add `StringFragment` and `ParameterFragment`? GitHub Issue 14; https://en.wikipedia.org/wiki/Fragment_identifier

object Fragment {

def apply(fragment: String): Fragment = {
if (fragment == null) throw new IllegalArgumentException("`fragment` cannot be `null`.")
if (fragment == "") EmptyFragment else new Fragment(fragment) {}
}

def option(fragment: String): Option[Fragment] = {
if (fragment == null) None else Option(apply(fragment))
}
}

object EmptyFragment extends Fragment("")
37 changes: 37 additions & 0 deletions src/main/scala/com/netaporter/uri/Parameter.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.netaporter.uri

import com.netaporter.uri.encoding.UriEncoder

/**
* Regular Expression used to replace existing `"???" -> Some("???")`:
* ("[^"]+") -> ("[^"]*"|None|Some\("[^"]*"\)|Option\("[^"]*"\))
* Parameter($1, $2)
*/
sealed abstract case class Parameter(key: String, value: Option[String]) {

def mapKey(f: String => String): Parameter = Parameter(f(key), value)

def mapValue(f: Option[String] => Option[String]): Parameter = Parameter(key, f(value))

def copy(key: String = key, value: Option[String] = value): Parameter = Parameter(key, value)

def withValue(newValue: String): Parameter = Parameter(key, newValue)

def withValue(newValue: Option[String]): Parameter = Parameter(key, newValue)

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

def toString(encoder: UriEncoder, charset: String): String =
encoder.encode(key, charset) + value.map("=" + encoder.encode(_, charset)).getOrElse("")
}

object Parameter {

def apply(key: String, value: Option[String] = None): Parameter = {
if (key == null || key.isEmpty) throw new IllegalArgumentException("`key` cannot be `null` and cannot be empty.")
if (value == null) throw new IllegalArgumentException("`value` cannot be `null`.")
new Parameter(key, value) {}
}

def apply(key: String, value: String): Parameter = apply(key, Option(value))
}
Loading

0 comments on commit 362193d

Please sign in to comment.