Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,31 @@ which provides a workable but loose implementation of the RFC for URLs.

## What's new?

### V1.2 announcement
### v2.0.0

**Breaking changes**

Most users should not be affected by these breaking changes.

* `URI` and `Authority` become concrete types. Interfaces are discarded.
* `Parse()` and `ParseReference()` now return a `URI` value, no longer a pointer.
* The `Validate() error` methods have been removed: validation is carried out when parsing only.
* The `Builder` interface and `URI.Builder()` function have been removed.
`URI` exposes fluent builder methods instead.
* `UsesDNSHostValidation()` has been removed and replaced by a private default function.
Override is possible via `Option`. Similar custom behavior may be achieved for `DefaultPort()`.

**Features**

* `Parse(string, ...Option)` and `ParseReference(string, ...Option)` now support options to tune the
`URI` validation.

**Performances**

* perf: massive improvement due to giving up pointers (parsing now is a zero-allocation operation).
This boosts `Parse()` to be even faster than the standard library `net/url.Parse()`.

### V1.2 announcement
To do before I cut a v1.2.0:
* [] handle empty fragment, empty query.
Ex: `https://host?` is not equivalent to `http://host`.
Expand Down Expand Up @@ -123,11 +146,14 @@ V2 is getting closer to completion. It comes with:

### Building

The exposed type `URI` can be transformed into a fluent `Builder` to set the parts of an URI.
The exposed type `URI` can be used as a fluent builder to set the parts of an URI.

```go
aURI, _ := Parse("mailto://user@domain.com")
newURI := auri.Builder().SetUserInfo(test.name).SetHost("newdomain.com").SetScheme("http").SetPort("443")
newURI := auri.SetUserInfo(test.name).
SetHost("newdomain.com").
SetScheme("http").
SetPort("443")
```

### Canonicalization
Expand Down
22 changes: 11 additions & 11 deletions benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,31 +88,31 @@ func benchParseURLStdLib(payload []string) func(*testing.B) {

func Benchmark_String(b *testing.B) {
var ip ipType
tests := []*uri{
tests := []URI{
{
"foo", "//example.com:8042/over/there", "name=ferret", "nose",
authorityInfo{"//", "", "example.com", "8042", "/over/there", ip, nil},
nil,
"foo", "//example.com:8042/over/there", "name=ferret", "nose",
Authority{nil, "//", "", "example.com", "8042", "/over/there", ip},
},
{
"http", "//httpbin.org/get", "utf8=\xe2\x98\x83", "",
authorityInfo{"//", "", "httpbin.org", "", "/get", ip, nil},
nil,
"http", "//httpbin.org/get", "utf8=\xe2\x98\x83", "",
Authority{nil, "//", "", "httpbin.org", "", "/get", ip},
},
{
"mailto", "user@domain.com", "", "",
authorityInfo{"//", "user", "domain.com", "", "", ip, nil},
nil,
"mailto", "user@domain.com", "", "",
Authority{nil, "//", "user", "domain.com", "", "", ip},
},
{
"ssh", "//user@git.openstack.org:29418/openstack/keystone.git", "", "",
authorityInfo{"//", "user", "git.openstack.org", "29418", "/openstack/keystone.git", ip, nil},
nil,
"ssh", "//user@git.openstack.org:29418/openstack/keystone.git", "", "",
Authority{nil, "//", "user", "git.openstack.org", "29418", "/openstack/keystone.git", ip},
},
{
"https", "//willo.io/", "", "yolo",
authorityInfo{"//", "", "willo.io", "", "/", ip, nil},
nil,
"https", "//willo.io/", "", "yolo",
Authority{nil, "//", "", "willo.io", "", "/", ip},
},
}

Expand Down
137 changes: 109 additions & 28 deletions builder.go
Original file line number Diff line number Diff line change
@@ -1,59 +1,140 @@
package uri

// Builder builds URIs.
type Builder interface {
URI() URI
SetScheme(scheme string) Builder
SetUserInfo(userinfo string) Builder
SetHost(host string) Builder
SetPort(port string) Builder
SetPath(path string) Builder
SetQuery(query string) Builder
SetFragment(fragment string) Builder

// Returns the URI this Builder represents.
String() string
}

func (u *uri) SetScheme(scheme string) Builder {
// Builder methods

func (u URI) WithScheme(scheme string, opts ...Option) URI {
if u.Err() != nil {
return u
}

opts = append(opts, withValidationFlags(flagValidateScheme|flagValidateHost))
o, redeem := applyURIOptions(opts)
defer func() { redeem(o) }()

u.scheme = scheme
u.authority.ipType, u.err = u.validate(o)

return u
}

func (u *uri) SetUserInfo(userinfo string) Builder {
u.ensureAuthorityExists()
func (u URI) WithAuthority(authority Authority, opts ...Option) URI {
if u.Err() != nil {
return u
}

opts = append(opts, withValidationFlags(flagValidateHost|flagValidatePort|flagValidateUserInfo|flagValidatePath))
o, redeem := applyURIOptions(opts)
defer func() { redeem(o) }()

u.authority = authority
u.authority.ipType, u.err = u.validate(o)
u.authority.err = u.err

return u
}

func (u URI) WithUserInfo(userinfo string, opts ...Option) URI {
if u.Err() != nil {
return u
}

opts = append(opts, withValidationFlags(flagValidateUserInfo))
o, redeem := applyURIOptions(opts)
defer func() { redeem(o) }()

u.authority = u.authority.withEnsuredAuthority()
u.authority.userinfo = userinfo
u.authority.ipType, u.err = u.validate(o)
u.authority.err = u.err

return u
}

func (u *uri) SetHost(host string) Builder {
u.ensureAuthorityExists()
func (u URI) WithHost(host string, opts ...Option) URI {
if u.Err() != nil {
return u
}

opts = append(opts, withValidationFlags(flagValidateHost|flagValidatePort))
o, redeem := applyURIOptions(opts)
defer func() { redeem(o) }()

u.authority = u.authority.withEnsuredAuthority()
u.authority.host = host
u.authority.ipType, u.err = u.validate(o)
u.authority.err = u.err

return u
}

func (u *uri) SetPort(port string) Builder {
u.ensureAuthorityExists()
func (u URI) WithPort(port string, opts ...Option) URI { // TODO: port as int?
if u.Err() != nil {
return u
}

opts = append(opts, withValidationFlags(flagValidatePort))
o, redeem := applyURIOptions(opts)
defer func() { redeem(o) }()

u.authority = u.authority.withEnsuredAuthority()
u.authority.port = port
u.authority.ipType, u.err = u.validate(o)
u.authority.err = u.err

return u
}

func (u *uri) SetPath(path string) Builder {
u.ensureAuthorityExists()
func (u URI) WithPath(path string, opts ...Option) URI {
if u.Err() != nil {
return u
}

opts = append(opts, withValidationFlags(flagValidatePath))
o, redeem := applyURIOptions(opts)
defer func() { redeem(o) }()

u.authority = u.authority.withEnsuredAuthority()
u.authority.path = path
u.authority.ipType, u.err = u.validate(o)
u.authority.err = u.err

return u
}

func (u *uri) SetQuery(query string) Builder {
func (u URI) WithQuery(query string, opts ...Option) URI {
if u.Err() != nil {
return u
}

opts = append(opts, withValidationFlags(flagValidateQuery))
o, redeem := applyURIOptions(opts)
defer func() { redeem(o) }()

u.query = query
u.authority.ipType, u.err = u.validate(o)

return u
}

func (u *uri) SetFragment(fragment string) Builder {
func (u URI) WithFragment(fragment string, opts ...Option) URI {
if u.Err() != nil {
return u
}

opts = append(opts, withValidationFlags(flagValidateFragment))
o, redeem := applyURIOptions(opts)
defer func() { redeem(o) }()

u.fragment = fragment
u.authority.ipType, u.err = u.validate(o)

return u
}

func (u *uri) Builder() Builder {
return u
func (a Authority) withEnsuredAuthority() Authority {
if a.userinfo != "" || a.host != "" || a.port != "" {
a.prefix = authorityPrefix
}

return a
}
36 changes: 17 additions & 19 deletions builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,51 +40,49 @@ func Test_Builder(t *testing.T) {
"failed to parse uri: %v", err,
)

nuri := auri.Builder().SetUserInfo(test.name).SetHost("newdomain.com").SetScheme("http").SetPort("443")
zuri, ok := nuri.(URI)
require.True(t, ok)
assert.Equal(t, "//"+test.name+"@newdomain.com:443", zuri.Authority().String())
assert.Equal(t, "443", nuri.URI().Authority().Port())
nuri := auri.WithUserInfo(test.name).WithHost("newdomain.com").WithScheme("http").WithPort("443")
assert.Equal(t, "//"+test.name+"@newdomain.com:443", nuri.Authority().String())
assert.Equal(t, "443", nuri.Authority().Port())
val := nuri.String()

assert.Equalf(t, val, test.uriChanged,
"val: %#v", val,
"test: %#v", test.uriChanged,
"values don't match: %v != %v (actual: %#v, expected: %#v)", val, test.uriChanged,
)
assert.Equal(t, "http", nuri.URI().Scheme())
assert.Equal(t, "http", nuri.Scheme())

_ = nuri.SetPath("/abcd")
assert.Equal(t, "/abcd", nuri.URI().Authority().Path())
nuri = nuri.WithPath("/abcd")
assert.Equal(t, "/abcd", nuri.Authority().Path())

_ = nuri.SetQuery("a=b&x=5").SetFragment("chapter")
assert.Equal(t, url.Values{"a": []string{"b"}, "x": []string{"5"}}, nuri.URI().Query())
assert.Equal(t, "chapter", nuri.URI().Fragment())
assert.Equal(t, test.uriChanged+"/abcd?a=b&x=5#chapter", nuri.URI().String())
nuri = nuri.WithQuery("a=b&x=5").WithFragment("chapter")
assert.Equal(t, url.Values{"a": []string{"b"}, "x": []string{"5"}}, nuri.Query())
assert.Equal(t, "chapter", nuri.Fragment())
assert.Equal(t, test.uriChanged+"/abcd?a=b&x=5#chapter", nuri.String())
assert.Equal(t, test.uriChanged+"/abcd?a=b&x=5#chapter", nuri.String())
})
}
})

t.Run("when building from scratch", func(t *testing.T) {
u, _ := Parse("http:")
b := u.Builder()
u, err := Parse("http:")
require.NoError(t, err)

require.Empty(t, u.Authority())
assert.Equal(t, "", u.Authority().UserInfo())

b = b.SetUserInfo("user:pwd").SetHost("newdomain").SetPort("444")
assert.Equal(t, "http://user:pwd@newdomain:444", b.String())
v := u.WithUserInfo("user:pwd").WithHost("newdomain").WithPort("444")
assert.Equal(t, "http://user:pwd@newdomain:444", v.String())
})

t.Run("when overriding with an invalid value", func(t *testing.T) {
const uriRaw = "https://host:8080/a?query=value#fragment"

u, err := Parse(uriRaw)
require.NoError(t, err)
b := u.Builder()
b.SetPort("X8080")

u = u.WithPort("X8080")
auth := u.Authority()
require.Error(t, auth.Validate())
require.Error(t, auth.Err())
})
}
4 changes: 2 additions & 2 deletions default_ports.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
// the defaut port defined for this scheme (if any).
//
// For example, an URI like http://host:8080 would return false, since 80 is the default http port.
func (u uri) IsDefaultPort() bool {
func (u URI) IsDefaultPort() bool {
if len(u.authority.port) == 0 {
return true
}
Expand All @@ -23,7 +23,7 @@ func (u uri) IsDefaultPort() bool {
// or zero if no such default is known.
//
// For example, for scheme "https", the default port is 443.
func (u uri) DefaultPort() int {
func (u URI) DefaultPort() int {
return int(defaultPortForScheme(strings.ToLower(u.scheme)))
}

Expand Down
1 change: 1 addition & 0 deletions dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
// in case you need specific schemes to validate the host as a DNS name.
//
// See: https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml
// TODO: now pass it as an option. make private
var UsesDNSHostValidation = func(scheme string) bool {
switch scheme {
// prioritize early exit on most commonly used schemes
Expand Down
5 changes: 3 additions & 2 deletions dns_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ func TestDNSvsHost(t *testing.T) {
}

func TestValidateHostForScheme(t *testing.T) {
o := defaultOptions()
for _, host := range []string{
"a.b.c",
"a",
Expand All @@ -32,7 +33,7 @@ func TestValidateHostForScheme(t *testing.T) {
"a.b.c.d%30",
"a.b.c.%55",
} {
require.NoErrorf(t, validateHostForScheme(host, "http"),
require.NoErrorf(t, validateHostForScheme(host, "http", o),
"expected host %q to validate",
host,
)
Expand Down Expand Up @@ -62,7 +63,7 @@ func TestValidateHostForScheme(t *testing.T) {
"%",
"%X",
} {
require.Errorf(t, validateHostForScheme(host, "http"),
require.Errorf(t, validateHostForScheme(host, "http", o),
"expected host %q NOT to validate",
host,
)
Expand Down
Loading