From a373923e3f19fd087250c058bc005df187acbb77 Mon Sep 17 00:00:00 2001 From: Yusuke Komatsu Date: Mon, 30 Jul 2018 14:32:25 +0900 Subject: [PATCH 1/2] add URL type --- convert.go | 15 + convert_test.go | 70 ++ json_marshal_benchmark_test.go | 14 + json_unmarshal_benchmark_test.go | 11 + type_url.go | 171 ++++ type_url_test.go | 1307 ++++++++++++++++++++++++++++++ 6 files changed, 1588 insertions(+) create mode 100644 convert_test.go create mode 100644 type_url.go create mode 100644 type_url_test.go diff --git a/convert.go b/convert.go index fa07850..dfa18ac 100644 --- a/convert.go +++ b/convert.go @@ -1,6 +1,7 @@ package generic import ( + "net/url" "strconv" "strings" "time" @@ -343,3 +344,17 @@ func asTimestampWithFunc(x interface{}, f func(i int64) time.Time) (result time. } return f(i), true, nil } + +func asURL(x interface{}) (result *url.URL, isValid ValidFlag, err error) { + switch x.(type) { + case nil: + return nil, false, nil + case *url.URL: + result = x.(*url.URL) + case string: + result, err = url.Parse(x.(string)) + default: + err = ErrInvalidGenericValue{Value: x} + } + return result, (err == nil), err +} diff --git a/convert_test.go b/convert_test.go new file mode 100644 index 0000000..c7f1afc --- /dev/null +++ b/convert_test.go @@ -0,0 +1,70 @@ +package generic + +import ( + "net/url" + "reflect" + "testing" +) + +func Test_asURL(t *testing.T) { + u, _ := url.Parse(testURLString) + + tests := []struct { + name string + args interface{} + wantResult *url.URL + wantIsValid ValidFlag + wantErr bool + }{ + { + name: "nil", + args: nil, + wantResult: nil, + wantIsValid: false, + wantErr: false, + }, + { + name: "string", + args: testURLString, + wantResult: u, + wantIsValid: true, + wantErr: false, + }, + { + name: "url.URL", + args: u, + wantResult: u, + wantIsValid: true, + wantErr: false, + }, + { + name: "int", + args: 1000, + wantResult: nil, + wantIsValid: false, + wantErr: true, + }, + { + name: "bool", + args: true, + wantResult: nil, + wantIsValid: false, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotResult, gotIsValid, err := asURL(tt.args) + if (err != nil) != tt.wantErr { + t.Errorf("asURL() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(gotResult, tt.wantResult) { + t.Errorf("asURL() gotResult = %v, want %v", gotResult, tt.wantResult) + } + if gotIsValid != tt.wantIsValid { + t.Errorf("asURL() gotIsValid = %v, want %v", gotIsValid, tt.wantIsValid) + } + }) + } +} diff --git a/json_marshal_benchmark_test.go b/json_marshal_benchmark_test.go index 1be9529..dafa027 100644 --- a/json_marshal_benchmark_test.go +++ b/json_marshal_benchmark_test.go @@ -1,6 +1,7 @@ package generic import ( + "net/url" "testing" "time" ) @@ -104,3 +105,16 @@ func BenchmarkMarshalJSONUint(b *testing.B) { x.MarshalJSON() } } + +func BenchmarkMarshalJSONURL(b *testing.B) { + x := URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "https", + Host: "www.google.com", + }, + } + for i := 0; i < b.N; i++ { + x.MarshalJSON() + } +} diff --git a/json_unmarshal_benchmark_test.go b/json_unmarshal_benchmark_test.go index ec1a221..95db3cc 100644 --- a/json_unmarshal_benchmark_test.go +++ b/json_unmarshal_benchmark_test.go @@ -78,6 +78,10 @@ func BenchmarkUnmarshalJSONTimeFromString(b *testing.B) { unmarshalJSONTimeBenchmark(b, []byte(`"`+now.String()+`"`)) } +func BenchmarkUnmarshalJSONURLFromString(b *testing.B) { + unmarshalJSONURLBenchmark(b, []byte(`"https://google.com"`)) +} + func unmarshalJSONBoolBenchmark(b *testing.B, bs []byte) { x := Bool{} for i := 0; i < b.N; i++ { @@ -119,3 +123,10 @@ func unmarshalJSONUintBenchmark(b *testing.B, bs []byte) { x.UnmarshalJSON(bs) } } + +func unmarshalJSONURLBenchmark(b *testing.B, bs []byte) { + x := URL{} + for i := 0; i < b.N; i++ { + x.UnmarshalJSON(bs) + } +} diff --git a/type_url.go b/type_url.go new file mode 100644 index 0000000..7fef9c9 --- /dev/null +++ b/type_url.go @@ -0,0 +1,171 @@ +package generic + +import ( + "database/sql/driver" + "encoding/json" + "net/url" +) + +// URL is generic url.URL type structure +type URL struct { + ValidFlag + url *url.URL +} + +// MarshalURL return generic.URL converting of request data +func MarshalURL(x interface{}) (URL, error) { + v := URL{} + err := v.Scan(x) + return v, err +} + +// Value implements the driver Valuer interface. +func (v URL) Value() (driver.Value, error) { + if !v.Valid() || v.url == nil { + return nil, nil + } + return v.url.String(), nil +} + +// Scan implements the sql.Scanner interface. +func (v *URL) Scan(x interface{}) (err error) { + v.url, v.ValidFlag, err = asURL(x) + return +} + +// Weak returns *url.URL, but if String.ValidFlag is false, returns nil. +func (v URL) Weak() interface{} { + return v.URL() +} + +// Set sets a specified value. +func (v *URL) Set(x interface{}) (err error) { + return v.Scan(x) +} + +// String implements the Stringer interface. +func (v URL) String() string { + if !v.Valid() || v.url == nil { + return "" + } + return v.url.String() +} + +// URL returns *url.URL, but if String.ValidFlag is false, returns nil. +func (v URL) URL() *url.URL { + if !v.Valid() || v.url == nil { + return nil + } + return v.url +} + +// MarshalJSON implements the json.Marshaler interface. +func (v URL) MarshalJSON() ([]byte, error) { + if !v.Valid() || v.url == nil { + return nullBytes, nil + } + return json.Marshal(v.url.String()) +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (v *URL) UnmarshalJSON(data []byte) error { + if len(data) == 0 { + return nil + } + var in interface{} + if err := json.Unmarshal(data, &in); err != nil { + return err + } + return v.Scan(in) +} + +// EscapedPath returns the escaped form of v.url.Path. +// In general there are multiple possible escaped forms of any path. +// +// EscapedPath returns v.url.RawPath when it is a valid escaping of v.url.Path. +// Otherwise EscapedPath ignores v.url.RawPath and computes an escaped form on its own. +// The String and RequestURI methods use EscapedPath to construct their results. +// In general, code should call EscapedPath instead of reading v.url.RawPath directly. +func (v URL) EscapedPath() string { + if !v.Valid() || v.url == nil { + return "" + } + return v.url.EscapedPath() +} + +// Hostname returns v.url.Host, without any port number. +// +// If Host is an IPv6 literal with a port number, Hostname returns the IPv6 literal without the square brackets. +// IPv6 literals may include a zone identifier. +func (v URL) Hostname() string { + if !v.Valid() || v.url == nil { + return "" + } + return v.url.Hostname() +} + +// IsAbs reports whether the URL is absolute. Absolute means that it has a non-empty scheme. +func (v URL) IsAbs() bool { + if !v.Valid() || v.url == nil { + return false + } + return v.url.IsAbs() +} + +// Port returns the port part of u.Host, without the leading colon. +// If u.Host doesn't contain a port, Port returns an empty string. +func (v URL) Port() string { + if !v.Valid() || v.url == nil { + return "" + } + return v.url.Port() +} + +// Query parses RawQuery and returns the corresponding values. +// It silently discards malformed value pairs. To check errors use ParseQuery. +func (v URL) Query() url.Values { + if !v.Valid() || v.url == nil { + return url.Values{} + } + return v.url.Query() +} + +// Parse parses a URL in the context of the receiver. +// The provided URL may be relative or absolute. +// Parse returns nil, err on parse failure, otherwise its return value is the same as ResolveReference. +func (v URL) Parse(ref string) (result URL, err error) { + if v.url == nil { + u := url.URL{} + v.url, err = u.Parse(ref) + } else { + v.url, err = v.url.Parse(ref) + } + if err != nil { + v = URL{} + return v, err + } + v.ValidFlag = true + return v, err +} + +// RequestURI returns the encoded path?query or opaque?query string that would be used in an HTTP request for v. +func (v URL) RequestURI() string { + if !v.Valid() || v.url == nil { + return "" + } + return v.url.RequestURI() +} + +// ResolveReference resolves a URI reference to an absolute URI from an absolute base URI, per RFC 3986 Section 5.2. +// The URI reference may be relative or absolute. ResolveReference always returns a new URL instance, even if the returned URL is identical to either the base or reference. +// If ref is an absolute URL, then ResolveReference ignores base and returns a copy of ref. +func (v URL) ResolveReference(ref *url.URL) URL { + if ref == nil { + v.ValidFlag = false + v.url = nil + } else { + v.ValidFlag = true + v.url = v.url.ResolveReference(ref) + } + return v +} diff --git a/type_url_test.go b/type_url_test.go new file mode 100644 index 0000000..a4e16ac --- /dev/null +++ b/type_url_test.go @@ -0,0 +1,1307 @@ +package generic + +import ( + "net/url" + "reflect" + "testing" +) + +type testURLStruct struct { + URL URL `json:"url"` + PATH URL `json:"path"` + NullValue URL `json:"null_value"` +} + +type testURLResult struct { + in string + uri URL + hostname string + abs bool + port string + queries url.Values + requestURI string +} + +const ( + testURLString = "https://foobar.test:8080?q=fizzbizz" + rootPath = "/" +) + +var ( + urltests = []testURLResult{ + // no path + testURLResult{ + in: "http://www.google.com", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + Host: "www.google.com", + }, + }, + hostname: "www.google.com", + abs: true, + port: "", + queries: url.Values{}, + requestURI: rootPath, + }, + // path + testURLResult{ + in: "http://www.google.com/", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + Host: "www.google.com", + Path: rootPath, + }, + }, + hostname: "www.google.com", + abs: true, + port: "", + queries: url.Values{}, + requestURI: rootPath, + }, + // path with hex escaping + testURLResult{ + in: "http://www.google.com/file%20one%26two", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + Host: "www.google.com", + Path: "/file one&two", + RawPath: "/file%20one%26two", + }, + }, + hostname: "www.google.com", + abs: true, + port: "", + queries: url.Values{}, + requestURI: "/file%20one%26two", + }, + // user + testURLResult{ + in: "ftp://webmaster@www.google.com/", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "ftp", + User: url.User("webmaster"), + Host: "www.google.com", + Path: rootPath, + }, + }, + hostname: "www.google.com", + abs: true, + port: "", + queries: url.Values{}, + requestURI: rootPath, + }, + // escape sequence in username + testURLResult{ + in: "ftp://john%20doe@www.google.com/", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "ftp", + User: url.User("john doe"), + Host: "www.google.com", + Path: rootPath, + }, + }, + hostname: "www.google.com", + abs: true, + port: "", + queries: url.Values{}, + requestURI: rootPath, + }, + // empty query + testURLResult{ + in: "http://www.google.com/?", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + Host: "www.google.com", + Path: "/", + ForceQuery: true, + }, + }, + hostname: "www.google.com", + abs: true, + port: "", + queries: url.Values{}, + requestURI: "/?", + }, + // query ending in question mark + // golang.org/issue/14573 + testURLResult{ + in: "http://www.google.com/?foo=bar?", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + Host: "www.google.com", + Path: "/", + RawQuery: "foo=bar?", + }, + }, + hostname: "www.google.com", + abs: true, + port: "", + queries: url.Values{ + "foo": []string{"bar?"}, + }, + requestURI: "/?foo=bar?", + }, + // query + testURLResult{ + in: "http://www.google.com/?q=go+language", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + Host: "www.google.com", + Path: "/", + RawQuery: "q=go+language", + }, + }, + hostname: "www.google.com", + abs: true, + port: "", + queries: url.Values{ + "q": []string{"go language"}, + }, + requestURI: "/?q=go+language", + }, + // query with hex escaping: NOT parsed + testURLResult{ + in: "http://www.google.com/?q=go%20language", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + Host: "www.google.com", + Path: "/", + RawQuery: "q=go%20language", + }, + }, + hostname: "www.google.com", + abs: true, + port: "", + queries: url.Values{ + "q": []string{"go language"}, + }, + requestURI: "/?q=go%20language", + }, + // %20 outside query + testURLResult{ + in: "http://www.google.com/a%20b?q=c+d", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + Host: "www.google.com", + Path: "/a b", + RawQuery: "q=c+d", + }, + }, + hostname: "www.google.com", + abs: true, + port: "", + queries: url.Values{ + "q": []string{"c d"}, + }, + requestURI: "/a%20b?q=c+d", + }, + // path without leading /, so no parsing + testURLResult{ + in: "http:www.google.com/?q=go+language", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + Opaque: "www.google.com/", + RawQuery: "q=go+language", + }, + }, + hostname: "", + abs: true, + port: "", + queries: url.Values{ + "q": []string{"go language"}, + }, + requestURI: "www.google.com/?q=go+language", + }, + // path without leading /, so no parsing + testURLResult{ + in: "http:%2f%2fwww.google.com/?q=go+language", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + Opaque: "%2f%2fwww.google.com/", + RawQuery: "q=go+language", + }, + }, + hostname: "", + abs: true, + port: "", + queries: url.Values{ + "q": []string{"go language"}, + }, + requestURI: "%2f%2fwww.google.com/?q=go+language", + }, + // non-authority with path + testURLResult{ + in: "mailto:/webmaster@golang.org", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "mailto", + Path: "/webmaster@golang.org", + }, + }, + hostname: "", + abs: true, + port: "", + queries: url.Values{}, + requestURI: "/webmaster@golang.org", + }, + // non-authority + testURLResult{ + in: "mailto:webmaster@golang.org", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "mailto", + Opaque: "webmaster@golang.org", + }, + }, + hostname: "", + abs: true, + port: "", + queries: url.Values{}, + requestURI: "webmaster@golang.org", + }, + // unescaped :// in query should not create a scheme + testURLResult{ + in: "/foo?query=http://bad", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Path: "/foo", + RawQuery: "query=http://bad", + }, + }, + hostname: "", + abs: false, + port: "", + queries: url.Values{ + "query": []string{"http://bad"}, + }, + requestURI: "/foo?query=http://bad", + }, + // leading // without scheme should create an authority + testURLResult{ + in: "//foo", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Host: "foo", + }, + }, + hostname: "foo", + abs: false, + port: "", + queries: url.Values{}, + requestURI: rootPath, + }, + // leading // without scheme, with userinfo, path, and query + testURLResult{ + in: "//user@foo/path?a=b", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + User: url.User("user"), + Host: "foo", + Path: "/path", + RawQuery: "a=b", + }, + }, + hostname: "foo", + abs: false, + port: "", + queries: url.Values{ + "a": []string{"b"}, + }, + requestURI: "/path?a=b", + }, + // Three leading slashes isn't an authority, but doesn't return an error. + // (We can't return an error, as this code is also used via + // ServeHTTP -> ReadRequest -> Parse, which is arguably a + // different URL parsing context, but currently shares the + // same codepath) + testURLResult{ + in: "///threeslashes", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Path: "///threeslashes", + }, + }, + hostname: "", + abs: false, + port: "", + queries: url.Values{}, + requestURI: "///threeslashes", + }, + testURLResult{ + in: "http://user:password@google.com", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + User: url.UserPassword("user", "password"), + Host: "google.com", + }, + }, + hostname: "google.com", + abs: true, + port: "", + queries: url.Values{}, + requestURI: rootPath, + }, + // unescaped @ in username should not confuse host + testURLResult{ + in: "http://j@ne:password@google.com", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + User: url.UserPassword("j@ne", "password"), + Host: "google.com", + }, + }, + hostname: "google.com", + abs: true, + port: "", + queries: url.Values{}, + requestURI: rootPath, + }, + // unescaped @ in password should not confuse host + testURLResult{ + in: "http://jane:p@ssword@google.com", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + User: url.UserPassword("jane", "p@ssword"), + Host: "google.com", + }, + }, + hostname: "google.com", + abs: true, + port: "", + queries: url.Values{}, + requestURI: rootPath, + }, + testURLResult{ + in: "http://j@ne:password@google.com/p@th?q=@go", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + User: url.UserPassword("j@ne", "password"), + Host: "google.com", + Path: "/p@th", + RawQuery: "q=@go", + }, + }, + hostname: "google.com", + abs: true, + port: "", + queries: url.Values{ + "q": []string{"@go"}, + }, + requestURI: "/p@th?q=@go", + }, + testURLResult{ + in: "http://www.google.com/?q=go+language#foo", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + Host: "www.google.com", + Path: "/", + RawQuery: "q=go+language", + Fragment: "foo", + }, + }, + hostname: "www.google.com", + abs: true, + port: "", + queries: url.Values{ + "q": []string{"go language"}, + }, + requestURI: "/?q=go+language", + }, + testURLResult{ + in: "http://www.google.com/?q=go+language#foo%26bar", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + Host: "www.google.com", + Path: "/", + RawQuery: "q=go+language", + Fragment: "foo&bar", + }, + }, + hostname: "www.google.com", + abs: true, + port: "", + queries: url.Values{ + "q": []string{"go language"}, + }, + requestURI: "/?q=go+language", + }, + testURLResult{ + in: "file:///home/adg/rabbits", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "file", + Host: "", + Path: "/home/adg/rabbits", + }, + }, + hostname: "", + abs: true, + port: "", + queries: url.Values{}, + requestURI: "/home/adg/rabbits", + }, + // "Windows" paths are no exception to the rule. + // See golang.org/issue/6027, especially comment #9. + testURLResult{ + in: "file:///C:/FooBar/Baz.txt", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "file", + Host: "", + Path: "/C:/FooBar/Baz.txt", + }, + }, + hostname: "", + abs: true, + port: "", + queries: url.Values{}, + requestURI: "/C:/FooBar/Baz.txt", + }, + // case-insensitive scheme + testURLResult{ + in: "MaIlTo:webmaster@golang.org", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "mailto", + Opaque: "webmaster@golang.org", + }, + }, + hostname: "", + abs: true, + port: "", + queries: url.Values{}, + requestURI: "webmaster@golang.org", + }, + // Relative path + testURLResult{ + in: "a/b/c", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Path: "/a/b/c", + }, + }, + hostname: "", + abs: false, + port: "", + queries: url.Values{}, + requestURI: "/a/b/c", + }, + // escaped '?' in username and password + testURLResult{ + in: "http://%3Fam:pa%3Fsword@google.com", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + User: url.UserPassword("?am", "pa?sword"), + Host: "google.com", + }, + }, + hostname: "google.com", + abs: true, + port: "", + queries: url.Values{}, + requestURI: rootPath, + }, + // host subcomponent; IPv4 address in RFC 3986 + testURLResult{ + in: "http://192.168.0.1/", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + Host: "192.168.0.1", + Path: "/", + }, + }, + hostname: "192.168.0.1", + abs: true, + port: "", + queries: url.Values{}, + requestURI: rootPath, + }, + // host and port subcomponents; IPv4 address in RFC 3986 + testURLResult{ + in: "http://192.168.0.1:8080/", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + Host: "192.168.0.1:8080", + Path: "/", + }, + }, + hostname: "192.168.0.1", + abs: true, + port: "8080", + queries: url.Values{}, + requestURI: rootPath, + }, + // host subcomponent; IPv6 address in RFC 3986 + testURLResult{ + in: "http://[fe80::1]/", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + Host: "[fe80::1]", + Path: "/", + }, + }, + hostname: "fe80::1", + abs: true, + port: "", + queries: url.Values{}, + requestURI: rootPath, + }, + // host and port subcomponents; IPv6 address in RFC 3986 + testURLResult{ + in: "http://[fe80::1]:8080/", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + Host: "[fe80::1]:8080", + Path: "/", + }, + }, + hostname: "fe80::1", + abs: true, + port: "8080", + queries: url.Values{}, + requestURI: rootPath, + }, + // host subcomponent; IPv6 address with zone identifier in RFC 6874 + testURLResult{ + in: "http://[fe80::1%25en0]/", // alphanum zone identifier + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + Host: "[fe80::1%en0]", + Path: "/", + }, + }, + hostname: "fe80::1%en0", + abs: true, + port: "", + queries: url.Values{}, + requestURI: rootPath, + }, + // host and port subcomponents; IPv6 address with zone identifier in RFC 6874 + testURLResult{ + in: "http://[fe80::1%25en0]:8080/", // alphanum zone identifier + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + Host: "[fe80::1%en0]:8080", + Path: "/", + }, + }, + hostname: "fe80::1%en0", + abs: true, + port: "8080", + queries: url.Values{}, + requestURI: rootPath, + }, + // host subcomponent; IPv6 address with zone identifier in RFC 6874 + testURLResult{ + in: "http://[fe80::1%25%65%6e%301-._~]/", // percent-encoded+unreserved zone identifier + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + Host: "[fe80::1%en01-._~]", + Path: "/", + }, + }, + hostname: "fe80::1%en01-._~", + abs: true, + port: "", + queries: url.Values{}, + requestURI: rootPath, + }, + // host and port subcomponents; IPv6 address with zone identifier in RFC 6874 + testURLResult{ + in: "http://[fe80::1%25%65%6e%301-._~]:8080/", // percent-encoded+unreserved zone identifier + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + Host: "[fe80::1%en01-._~]:8080", + Path: "/", + }, + }, + hostname: "fe80::1%en01-._~", + abs: true, + port: "8080", + queries: url.Values{}, + requestURI: rootPath, + }, + // alternate escapings of path survive round trip + testURLResult{ + in: "http://rest.rsc.io/foo%2fbar/baz%2Fquux?alt=media", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + Host: "rest.rsc.io", + Path: "/foo/bar/baz/quux", + RawPath: "/foo%2fbar/baz%2Fquux", + RawQuery: "alt=media", + }, + }, + hostname: "rest.rsc.io", + abs: true, + port: "", + queries: url.Values{ + "alt": []string{"media"}, + }, + requestURI: "/foo%2fbar/baz%2Fquux?alt=media", + }, + // issue 12036 + testURLResult{ + in: "mysql://a,b,c/bar", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "mysql", + Host: "a,b,c", + Path: "/bar", + }, + }, + hostname: "a,b,c", + abs: true, + port: "", + queries: url.Values{}, + requestURI: "/bar", + }, + // worst case host, still round trips + testURLResult{ + in: "scheme://!$&'()*+,;=hello!:port/path", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "scheme", + Host: "!$&'()*+,;=hello!:port", + Path: "/path", + }, + }, + hostname: "!$&'()*+,;=hello!", + abs: true, + port: "port", + queries: url.Values{}, + requestURI: "/path", + }, + // worst case path, still round trips + testURLResult{ + in: "http://host/!$&'()*+,;=:@[hello]", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + Host: "host", + Path: "/!$&'()*+,;=:@[hello]", + RawPath: "/!$&'()*+,;=:@[hello]", + }, + }, + hostname: "host", + abs: true, + port: "", + queries: url.Values{}, + requestURI: "/!$&'()*+,;=:@[hello]", + }, + // golang.org/issue/5684 + testURLResult{ + in: "http://example.com/oid/[order_id]", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + Host: "example.com", + Path: "/oid/[order_id]", + RawPath: "/oid/[order_id]", + }, + }, + hostname: "example.com", + abs: true, + port: "", + queries: url.Values{}, + requestURI: "/oid/[order_id]", + }, + // golang.org/issue/12200 (colon with empty port) + testURLResult{ + in: "http://192.168.0.2:8080/foo", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + Host: "192.168.0.2:8080", + Path: "/foo", + }, + }, + hostname: "192.168.0.2", + abs: true, + port: "8080", + queries: url.Values{}, + requestURI: "/foo", + }, + testURLResult{ + in: "http://192.168.0.2:/foo", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + Host: "192.168.0.2:", + Path: "/foo", + }, + }, + hostname: "192.168.0.2", + abs: true, + port: "", + queries: url.Values{}, + requestURI: "/foo", + }, + // Malformed IPv6 but still accepted. + testURLResult{ + in: "http://2b01:e34:ef40:7730:8e70:5aff:fefe:edac:8080/foo", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + Host: "2b01:e34:ef40:7730:8e70:5aff:fefe:edac:8080", + Path: "/foo", + }, + }, + hostname: "2b01", + abs: true, + port: "e34:ef40:7730:8e70:5aff:fefe:edac:8080", + queries: url.Values{}, + requestURI: "/foo", + }, + // Malformed IPv6 but still accepted. + testURLResult{ + in: "http://2b01:e34:ef40:7730:8e70:5aff:fefe:edac:/foo", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + Host: "2b01:e34:ef40:7730:8e70:5aff:fefe:edac:", + Path: "/foo", + }, + }, + hostname: "2b01", + abs: true, + port: "e34:ef40:7730:8e70:5aff:fefe:edac:", + queries: url.Values{}, + requestURI: "/foo", + }, + testURLResult{ + in: "http://[2b01:e34:ef40:7730:8e70:5aff:fefe:edac]:8080/foo", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + Host: "[2b01:e34:ef40:7730:8e70:5aff:fefe:edac]:8080", + Path: "/foo", + }, + }, + hostname: "2b01:e34:ef40:7730:8e70:5aff:fefe:edac", + abs: true, + port: "8080", + queries: url.Values{}, + requestURI: "/foo", + }, + testURLResult{ + in: "http://[2b01:e34:ef40:7730:8e70:5aff:fefe:edac]:/foo", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + Host: "[2b01:e34:ef40:7730:8e70:5aff:fefe:edac]:", + Path: "/foo", + }, + }, + hostname: "2b01:e34:ef40:7730:8e70:5aff:fefe:edac", + abs: true, + port: "", + queries: url.Values{}, + requestURI: "/foo", + }, + // golang.org/issue/7991 and golang.org/issue/12719 (non-ascii %-encoded in host) + testURLResult{ + in: "http://hello.世界.com/foo", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + Host: "hello.世界.com", + Path: "/foo", + }, + }, + hostname: "hello.世界.com", + abs: true, + port: "", + queries: url.Values{}, + requestURI: "/foo", + }, + testURLResult{ + in: "http://hello.%e4%b8%96%e7%95%8c.com/foo", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + Host: "hello.世界.com", + Path: "/foo", + }, + }, + hostname: "hello.世界.com", + abs: true, + port: "", + queries: url.Values{}, + requestURI: "/foo", + }, + testURLResult{ + in: "http://hello.%E4%B8%96%E7%95%8C.com/foo", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + Host: "hello.世界.com", + Path: "/foo", + }, + }, + hostname: "hello.世界.com", + abs: true, + port: "", + queries: url.Values{}, + requestURI: "/foo", + }, + // golang.org/issue/10433 (path beginning with //) + testURLResult{ + in: "http://example.com//foo", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "http", + Host: "example.com", + Path: "//foo", + }, + }, + hostname: "example.com", + abs: true, + port: "", + queries: url.Values{}, + requestURI: "//foo", + }, + // test that we can reparse the host names we accept. + testURLResult{ + in: "myscheme://authority<\"hi\">/foo", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "myscheme", + Host: "authority<\"hi\">", + Path: "/foo", + }, + }, + hostname: "authority<\"hi\">", + abs: true, + port: "", + queries: url.Values{}, + requestURI: "/foo", + }, + // spaces in hosts are disallowed but escaped spaces in IPv6 scope IDs are grudgingly OK. + // This happens on Windows. + // golang.org/issue/14002 + testURLResult{ + in: "tcp://[2020::2020:20:2020:2020%25Windows%20Loves%20Spaces]:2020", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "tcp", + Host: "[2020::2020:20:2020:2020%Windows Loves Spaces]:2020", + }, + }, + hostname: "2020::2020:20:2020:2020%Windows Loves Spaces", + abs: true, + port: "2020", + queries: url.Values{}, + requestURI: rootPath, + }, + testURLResult{ + in: "magnet:?xt=urn:btih:c12fe1c06bba254a9dc9f519b335aa7c1367a88a&dn", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "magnet", + Host: "", + Path: "", + RawQuery: "xt=urn:btih:c12fe1c06bba254a9dc9f519b335aa7c1367a88a&dn", + }, + }, + hostname: "", + abs: true, + port: "", + queries: url.Values{ + "xt": []string{"urn:btih:c12fe1c06bba254a9dc9f519b335aa7c1367a88a"}, + "dn": []string{""}, + }, + requestURI: "/?xt=urn:btih:c12fe1c06bba254a9dc9f519b335aa7c1367a88a&dn", + }, + testURLResult{ + in: "mailto:?subject=hi", + uri: URL{ + ValidFlag: true, + url: &url.URL{ + Scheme: "mailto", + Host: "", + Path: "", + RawQuery: "subject=hi", + }, + }, + hostname: "", + abs: true, + port: "", + queries: url.Values{ + "subject": []string{"hi"}, + }, + requestURI: "/?subject=hi", + }, + } + + // testURLResult{ + // in: "validFlag false", + // uri: URL{ + // ValidFlag: false, + // url: &url.URL{ + // Scheme: "http", + // Host: "www.google.com", + // Path: "/", + // RawQuery: "q=go+language", + // Fragment: "foo", + // }, + // }, + // hostname: "", + // abs: false, + // port: "", + // queries: url.Values{}, + // requestURI: "", + // }, + // testURLResult{ + // in: "nil", + // uri: URL{ + // ValidFlag: true, + // url: nil, + // }, + // hostname: "", + // abs: false, + // port: "", + // queries: url.Values{}, + // requestURI: "", + // }, +) + +func TestMarshalURL(t *testing.T) { + v, _ := url.Parse(testURLString) + + expected := URL{ + ValidFlag: true, + url: v, + } + + tests := []struct { + name string + args interface{} + want URL + wantErr bool + }{ + { + name: "URL", + args: v, + want: expected, + wantErr: false, + }, + { + name: "string", + args: testURLString, + want: expected, + wantErr: false, + }, + { + name: "invalid", + args: 10000, + want: URL{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := MarshalURL(tt.args) + if (err != nil) != tt.wantErr { + t.Errorf("MarshalURL() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("MarshalURL() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestURL_Value(t *testing.T) { + t.Skip() +} + +func TestURL_Scan(t *testing.T) { + t.Skip() +} + +func TestURL_Weak(t *testing.T) { + for _, tt := range urltests { + t.Run(tt.in, func(t *testing.T) { + if got := tt.uri.Weak(); got != tt.uri.url { + t.Errorf("URL.URL() = %v, want %v", got, tt.uri.url) + } + }) + } +} + +// func TestURL_Set(t *testing.T) { +// t.Skip() +// } + +func TestURL_String(t *testing.T) { + u1, _ := url.Parse(testURLString) + + s := "/foobar?fizz=bizz" + u2, _ := url.Parse(s) + + tests := []struct { + name string + fields URL + want string + }{ + { + name: "valid url", + fields: URL{ + ValidFlag: true, + url: u1, + }, + want: testURLString, + }, + { + name: "ValidFlag false", + fields: URL{ + ValidFlag: false, + url: u1, + }, + want: "", + }, + { + name: "only path", + fields: URL{ + ValidFlag: true, + url: u2, + }, + want: s, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.fields.String(); got != tt.want { + t.Errorf("URL.String() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestURL_URL(t *testing.T) { + for _, tt := range urltests { + t.Run(tt.in, func(t *testing.T) { + if got := tt.uri.URL(); got != tt.uri.url { + t.Errorf("URL.URL() = %v, want %v", got, tt.uri.url) + } + }) + } +} + +func TestURL_MarshalJSON_UnmarshalJSON(t *testing.T) { + for _, tt := range urltests { + t.Run(tt.in, func(t *testing.T) { + bs, err := tt.uri.MarshalJSON() + if err != nil { + t.Errorf("URL.MarshalJSON() error = %v, not expected", err) + return + } + v := URL{} + err = v.UnmarshalJSON(bs) + if err != nil { + t.Errorf("URL.UnmarshalJSON() error = %v, not expected", err) + return + } + if v.String() != tt.uri.String() { + t.Errorf("URL.UnmarshalJSON() = %v, want %v", v, tt.uri) + } + }) + } +} + +func TestURL_UnmarshalJSON(t *testing.T) { + t.Skip() +} + +func TestURL_EscapedPath(t *testing.T) { + t.Skip() +} + +func TestURL_Hostname(t *testing.T) { + for _, tt := range urltests { + t.Run(tt.in, func(t *testing.T) { + if got := tt.uri.Hostname(); got != tt.hostname { + t.Errorf("URL.Hostname() = %v, want %v", got, tt.hostname) + } + }) + } +} + +func TestURL_IsAbs(t *testing.T) { + for _, tt := range urltests { + t.Run(tt.in, func(t *testing.T) { + if got := tt.uri.IsAbs(); got != tt.abs { + t.Errorf("URL.IsAbs() = %v, want %v", got, tt.abs) + } + }) + } +} + +func TestURL_Port(t *testing.T) { + for _, tt := range urltests { + t.Run(tt.in, func(t *testing.T) { + if got := tt.uri.Port(); got != tt.port { + t.Errorf("URL.Port() = %v, want %v", got, tt.port) + } + }) + } +} + +func TestURL_Query(t *testing.T) { + for _, tt := range urltests { + t.Run(tt.in, func(t *testing.T) { + if got := tt.uri.Query(); !reflect.DeepEqual(got, tt.queries) { + t.Errorf("URL.Query() = %#v, want %#v", got, tt.queries) + } + }) + } +} + +func TestURL_Parse(t *testing.T) { + for _, tt := range urltests { + t.Run(tt.in, func(t *testing.T) { + v := URL{} + gotResult, err := v.Parse(tt.in) + if err != nil { + t.Errorf("URL.Parse() error = %v, not expected", err) + if gotResult != v { + t.Errorf("URL.Parse() = %v, want %v", gotResult, URL{}) + } + return + } + if !reflect.DeepEqual(gotResult, tt.uri) { + t.Errorf("URL.Parse() = %v, want %v", gotResult, tt.uri) + } + }) + } +} + +func TestURL_RequestURI(t *testing.T) { + for _, tt := range urltests { + t.Run(tt.in, func(t *testing.T) { + if got := tt.uri.RequestURI(); got != tt.requestURI { + t.Errorf("URL.RequestURI() = %v, want %v", got, tt.requestURI) + } + }) + } +} + +func TestURL_ResolveReference(t *testing.T) { + u, _ := url.Parse(testURLString) + ud, _ := url.Parse("https://notfound.test?q1=a&q2=b") + v := URL{ + ValidFlag: false, + url: ud, + } + tests := []struct { + name string + args *url.URL + want URL + }{ + { + name: "valid URL", + args: u, + want: URL{ + ValidFlag: true, + url: u, + }, + }, + { + name: "nil", + args: nil, + want: URL{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := v.ResolveReference(tt.args); !reflect.DeepEqual(got, tt.want) { + t.Errorf("URL.ResolveReference() = %v, want %v", got, tt.want) + } + }) + } +} From 19b8fc645679f476f98a9e9764c098173dc97be7 Mon Sep 17 00:00:00 2001 From: Yusuke Komatsu Date: Mon, 30 Jul 2018 14:40:42 +0900 Subject: [PATCH 2/2] fix travis.yml --- .travis.yml | 1 - README.md | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 2314af5..53ce17c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: go go: - - "1.7" - "1.8" - "1.9" - "1.10" diff --git a/README.md b/README.md index 1e49be5..a3d08f8 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ flexible data type for Go +support: Go 1.8+ + ## Install standard `go get`: