1
0
Fork 0
mirror of https://github.com/laravel-ls/uri synced 2026-07-04 00:23:39 +02:00

uri: change all implemestations

This commit is contained in:
Koichi Shiraishi 2019-06-05 16:55:56 +09:00
parent e2736f1bb7
commit 695121e1f8
No known key found for this signature in database
GPG key ID: A71DFD3B4DA7A79B
2 changed files with 140 additions and 556 deletions

532
uri.go
View file

@ -5,13 +5,12 @@
package uri package uri
import ( import (
"bytes"
"encoding/json"
"fmt" "fmt"
"net/url" "net/url"
"path/filepath" "path/filepath"
"strconv" "runtime"
"strings" "strings"
"unicode"
"golang.org/x/xerrors" "golang.org/x/xerrors"
) )
@ -28,15 +27,10 @@ const (
) )
const ( const (
keyScheme = "scheme" hierPart = "://"
keyAuthority = "authority"
keyPath = "path"
keyFsPath = "fsPath"
keyQuery = "query"
keyFragment = "fragment"
) )
// URI Uniform Resource Identifier (URI) http://tools.ietf.org/html/rfc3986. // URI Uniform Resource Identifier (URI) https://tools.ietf.org/html/rfc3986.
// //
// This class is a simple parser which creates the basic component parts // This class is a simple parser which creates the basic component parts
// (http://tools.ietf.org/html/rfc3986#section-3) with minimal validation // (http://tools.ietf.org/html/rfc3986#section-3) with minimal validation
@ -49,453 +43,161 @@ const (
// | _____________________|__ // | _____________________|__
// / \ / \ // / \ / \
// urn:example:animal:ferret:nose // urn:example:animal:ferret:nose
type URI struct { type URI string
// Scheme is the 'http' part of 'http://www.msft.com/some/path?query#fragment'.
//
// The part before the first colon.
Scheme string `json:"scheme"`
// Authority is the 'www.msft.com' part of 'http://www.msft.com/some/path?query#fragment'. // Filename returns the file path for the given URI. It will return an error if
// The part between the first double slashes and the next slash. // the URI is invalid, or if the URI does not have the file scheme.
Authority string `json:"authority"` func (uri URI) Filename() (string, error) {
filename, err := filename(uri)
if err != nil {
return "", err
}
// Path is the '/some/path' part of 'http://www.msft.com/some/path?query#fragment'. return filepath.FromSlash(filename), nil
Path string `json:"path"` }
// FsPath returns a string representing the corresponding file system path of this URI. func filename(uri URI) (string, error) {
// u, err := url.ParseRequestURI(string(uri))
// Will handle UNC paths, normalizes windows drive letters to lower-case, and uses the if err != nil {
// platform specific path separator. return "", xerrors.Errorf("failed to parse request URI: %w", err)
// }
// * Will *not* validate the path for invalid characters and semantics.
// * Will *not* look at the scheme of this URI.
// * The result shall *not* be used for display purposes but for accessing a file on disk.
//
//
// The *difference* to `URI#path` is the use of the platform specific separator and the handling
// of UNC paths. See the below sample of a file-uri with an authority (UNC path).
//
// const u = URI.parse('file://server/c$/folder/file.txt')
// u.authority === 'server'
// u.path === '/shares/c$/file.txt'
// u.fsPath === '\\server\c$\folder\file.txt'
//
// Using `URI#path` to read a file (using fs-apis) would not be enough because parts of the path,
// namely the server name, would be missing.
//
// Therefore `URI#fsPath` exists - it's sugar to ease working with URIs that represent files on disk (`file` scheme).
FsPath string `json:"fsPath,omitempty"`
// Query is the 'query' part of 'http://www.msft.com/some/path?query#fragment'. if u.Scheme != FileScheme {
Query string `json:"query,omitempty"` return "", xerrors.Errorf("only file URIs are supported, got %v", u.Scheme)
}
// Fragment is the 'fragment' part of 'http://www.msft.com/some/path?query#fragment'. if isWindowsDriveURI(u.Path) {
Fragment string `json:"fragment,omitempty"` u.Path = u.Path[1:]
}
formatted string return u.Path, nil
skipEncoding bool
} }
// MarshalJSON implements json.Marshaler. // MarshalJSON implements json.Marshaler.
func (u *URI) MarshalJSON() ([]byte, error) { func (u URI) MarshalJSON() ([]byte, error) {
buf := new(bytes.Buffer) return nil, nil
buf.WriteString("{")
buf.WriteString(`"` + strconv.Quote(keyScheme) + `: "`)
scheme, err := json.Marshal(u.Scheme)
if err != nil {
return nil, err
}
buf.Write(scheme)
buf.WriteString(",")
buf.WriteString(`"` + strconv.Quote(keyAuthority) + `: "`)
authority, err := json.Marshal(u.Authority)
if err != nil {
return nil, err
}
buf.Write(authority)
buf.WriteString(",")
buf.WriteString(`"` + strconv.Quote(keyPath) + `: "`)
path, err := json.Marshal(u.Path)
if err != nil {
return nil, err
}
buf.Write(path)
if u.Query != "" {
buf.WriteString(",")
buf.WriteString(`"` + strconv.Quote(keyQuery) + `: "`)
query, err := json.Marshal(u.Query)
if err != nil {
return nil, err
}
buf.Write(query)
}
if u.FsPath != "" {
buf.WriteString(",")
buf.WriteString(`"` + strconv.Quote(keyFsPath) + `: "`)
fsPath, err := json.Marshal(u.FsPath)
if err != nil {
return nil, err
}
buf.Write(fsPath)
}
if u.Fragment != "" {
buf.WriteString(",")
buf.WriteString(`"` + strconv.Quote(keyFragment) + `: "`)
fragment, err := json.Marshal(u.Fragment)
if err != nil {
return nil, err
}
buf.Write(fragment)
}
buf.WriteString("}")
return buf.Bytes(), nil
} }
// UnmarshalJSON implements json.Unmarshaler. // UnmarshalJSON implements json.Unmarshaler.
func (u *URI) UnmarshalJSON(b []byte) error { func (u *URI) UnmarshalJSON(b []byte) error {
var schemeReceived bool
var authorityReceived bool
var pathReceived bool
var jm map[string]json.RawMessage
if err := json.Unmarshal(b, &jm); err != nil {
return err
}
// parse all the defined properties
for k, v := range jm {
switch k {
case keyScheme:
if err := json.Unmarshal([]byte(v), &u.Scheme); err != nil {
return err
}
schemeReceived = true
case keyAuthority:
if err := json.Unmarshal([]byte(v), &u.Authority); err != nil {
return err
}
authorityReceived = true
case keyPath:
if err := json.Unmarshal([]byte(v), &u.Path); err != nil {
return err
}
pathReceived = true
case keyFsPath:
if err := json.Unmarshal([]byte(v), &u.FsPath); err != nil {
return err
}
case keyQuery:
if err := json.Unmarshal([]byte(v), &u.Query); err != nil {
return err
}
case keyFragment:
if err := json.Unmarshal([]byte(v), &u.Fragment); err != nil {
return err
}
default:
return fmt.Errorf("additional property not allowed: \"" + k + "\"")
}
}
// check if scheme (a required property) was received
if !schemeReceived {
return xerrors.Errorf("%q is required but was not present", keyScheme)
}
// check if authority (a required property) was received
if !authorityReceived {
return xerrors.Errorf("%q is required but was not present", keyAuthority)
}
// check if path (a required property) was received
if !pathReceived {
return xerrors.Errorf("%q is required but was not present", keyPath)
}
return nil return nil
} }
// String implements fmt.Stringer. // New parses and creates a new URI from s.
func (u *URI) String() string { func New(s string) URI {
switch u.Scheme { if u, err := url.PathUnescape(s); err == nil {
case FileScheme, HTTPScheme, HTTPSScheme: s = u
if u.skipEncoding {
return u.formatted
}
u.formatted = format(*u, true)
u.skipEncoding = true
return u.formatted
default:
return "unknown scheme"
} }
if strings.HasPrefix(s, FileScheme+hierPart) {
return URI(s)
}
return File(s)
} }
var encodeTable = map[byte]string{ // File parses and creates a new filesystem URI from path.
Colon: "%3A", // gen-delims func File(path string) URI {
Slash: "%2F", const goRootPragma = "$GOROOT"
QuestionMark: "%3F", if len(path) >= len(goRootPragma) && strings.EqualFold(goRootPragma, path[:len(goRootPragma)]) {
Hash: "%23", path = runtime.GOROOT() + path[len(goRootPragma):]
OpenSquareBracket: "%5B", }
CloseSquareBracket: "%5D",
AtSign: "%40", if !isWindowsDrivePath(path) {
ExclamationMark: "%21", // sub-delims if abs, err := filepath.Abs(path); err == nil {
DollarSign: "%24", path = abs
Ampersand: "%26", }
SingleQuote: "%27", }
OpenParen: "%28",
CloseParen: "%29", if isWindowsDrivePath(path) {
Asterisk: "%2A", path = "/" + path
Plus: "%2B", }
Comma: "%2C",
Semicolon: "%3B", path = filepath.ToSlash(path)
Equals: "%3D", u := url.URL{
Space: "%20", Scheme: FileScheme,
Path: path,
}
return URI(u.String())
} }
func encodeFast(uriComponent string, allowSlash bool) string { // Parse parses and creates a new URI from s.
b := new(strings.Builder) func Parse(s string) (u URI, err error) {
nativeEncodePos := -1
for pos := 0; pos < len(uriComponent); pos++ {
code := uriComponent[pos]
switch {
case code >= LowerA && code <= LowerZ,
code >= UpperA && code <= UpperZ,
code >= Digit0 && code <= Digit9,
code == Dash,
code == Period,
code == Underline,
code == Tilde,
allowSlash && (code == Slash):
if nativeEncodePos != -1 {
b.WriteString(uriComponent[nativeEncodePos:pos])
nativeEncodePos = -1
}
str := b.String()
if str != "" {
b.WriteString(str)
b.WriteString(uriComponent[:pos])
}
default:
str := b.String()
if str != "" {
b.WriteString(str)
b.WriteString(uriComponent[0:pos])
}
escaped := encodeTable[code]
if escaped != "" {
if nativeEncodePos != -1 {
b.WriteString(uriComponent[nativeEncodePos:pos])
}
b.WriteString(escaped)
} else if nativeEncodePos == -1 {
nativeEncodePos = pos
}
}
}
if nativeEncodePos != -1 {
b.WriteString(uriComponent[:nativeEncodePos])
}
return b.String()
}
func encodeMinimal(path string, _ bool) string {
b := new(strings.Builder)
for i := 0; i < len(path); i++ {
code := path[i]
if code == Hash || code == QuestionMark {
if b.String() == "" {
b.WriteString(path[0:i])
}
b.WriteString(encodeTable[code])
} else if b.String() != "" {
b.WriteByte(path[i])
}
}
res := b.String()
if res == "" {
res = path
}
return res
}
func format(uri URI, skipEncoding bool) string {
var encoder func(string, bool) string
switch skipEncoding {
case true:
encoder = encodeMinimal
case false:
encoder = encodeFast
}
b := new(strings.Builder)
scheme := uri.Scheme
if scheme != "" {
b.WriteString(scheme)
b.WriteByte(':')
}
authority := uri.Authority
if authority != "" || scheme == FileScheme {
b.WriteRune(filepath.Separator)
b.WriteRune(filepath.Separator)
}
if authority != "" {
idx := strings.LastIndex(authority, "@")
if idx != -1 {
// <user>@<auth>
userinfo := authority[:idx]
authority = authority[idx+1:]
uiIdx := strings.Index(userinfo, ":")
if uiIdx == -1 {
b.WriteString(encoder(userinfo, false))
} else {
// <user>:<pass>@<auth>
b.WriteString(encoder(userinfo[:idx], false))
b.WriteRune(':')
b.WriteString(encoder(userinfo[idx+1:], false))
}
b.WriteRune('@')
}
authority = strings.ToLower(authority)
idx = strings.Index(authority, ":")
if idx == -1 {
b.WriteString(encoder(authority, false))
} else {
// <auth>:<port>
b.WriteString(encoder(authority[:idx], false))
b.WriteString(authority[idx:])
}
}
if path := uri.Path; path != "" {
// lower-case windows drive letters in /C:/fff or C:/fff
if len(path) >= 3 && path[0] == Slash && path[2] == Colon {
code := path[1]
if code >= UpperA && code <= UpperZ {
path = "/" + string(code+32) + ":" + string(path[3]) // "/c:".length == 3
}
} else if len(path) >= 2 && path[1] == Colon {
code := path[0]
if code >= UpperA && code <= UpperZ {
path = string(code+32) + ":" + string(path[2]) // "/c:".length == 3
}
}
// encode the rest of the path
b.WriteString(encoder(path, true))
}
if query := uri.Query; query != "" {
b.WriteRune('?')
b.WriteString(encoder(query, false))
}
if fragment := uri.Fragment; fragment != "" {
b.WriteRune('#')
if skipEncoding {
b.WriteString(fragment)
} else {
b.WriteString(encodeFast(fragment, false))
}
}
return b.String()
}
// Parse parses and creates a new URI from uri.
func Parse(s string) (u *URI, err error) {
us, err := url.Parse(s) us, err := url.Parse(s)
if err != nil { if err != nil {
return nil, xerrors.Errorf("url.Parse: %w\n", err) return u, xerrors.Errorf("url.Parse: %w\n", err)
} }
switch us.Scheme { switch us.Scheme {
case FileScheme: case FileScheme:
u = &URI{ ut := url.URL{
Scheme: FileScheme, Scheme: FileScheme,
Path: us.Path, Path: us.Path,
FsPath: filepath.FromSlash(us.Path), RawPath: filepath.FromSlash(us.Path),
} }
u = URI(ut.String())
case HTTPScheme, HTTPSScheme: case HTTPScheme, HTTPSScheme:
u = &URI{ ut := url.URL{
Scheme: us.Scheme, Scheme: us.Scheme,
Authority: us.Host, Host: us.Host,
Path: us.Path, Path: us.Path,
Query: us.Query().Encode(), RawQuery: us.Query().Encode(),
Fragment: us.Fragment, Fragment: us.Fragment,
} }
u = URI(ut.String())
default: default:
return nil, xerrors.New("unknown scheme") return u, xerrors.New("unknown scheme")
} }
return u, nil return
}
// File parses and creates a new URI filesystem path from path.
func File(path string) *URI {
return &URI{
Scheme: FileScheme,
Path: path,
FsPath: filepath.FromSlash(path),
}
} }
// From returns the new URI from args. // From returns the new URI from args.
func From(scheme, authority, path, query, fragment string) (u *URI) { func From(scheme, authority, path, query, fragment string) URI {
switch scheme { switch scheme {
case FileScheme: case FileScheme:
u = &URI{ u := url.URL{
Scheme: FileScheme, Scheme: FileScheme,
Path: path, Path: path,
FsPath: filepath.FromSlash(path), RawPath: filepath.FromSlash(path),
} }
return URI(u.String())
case HTTPScheme, HTTPSScheme: case HTTPScheme, HTTPSScheme:
u = &URI{ u := url.URL{
Scheme: scheme, Scheme: scheme,
Authority: authority, Host: authority,
Path: path, Path: path,
Query: url.QueryEscape(query), RawQuery: url.QueryEscape(query),
Fragment: fragment, Fragment: fragment,
} }
} return URI(u.String())
return u default:
panic(fmt.Sprintf("unknown scheme: %s", scheme))
}
}
// isWindowsDrivePath returns true if the file path is of the form used by Windows.
//
// We check if the path begins with a drive letter, followed by a ":".
func isWindowsDrivePath(path string) bool {
if len(path) < 4 {
return false
}
return unicode.IsLetter(rune(path[0])) && path[1] == ':'
}
// isWindowsDriveURI returns true if the file URI is of the format used by
// Windows URIs. The url.Parse package does not specially handle Windows paths
// (see https://golang.org/issue/6027). We check if the URI path has
// a drive prefix (e.g. "/C:"). If so, we trim the leading "/".
func isWindowsDriveURI(uri string) bool {
if len(uri) < 4 {
return false
}
return uri[0] == '/' && unicode.IsLetter(rune(uri[1])) && uri[2] == ':'
} }

View file

@ -2,61 +2,34 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package uri_test package uri
import ( import (
"path/filepath"
"testing" "testing"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/go-language-server/uri"
) )
func TestParse(t *testing.T) { func TestParse(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
s string s string
want *uri.URI want URI
wantErr bool
}{ }{
{ {
name: "ValidFileScheme", name: "ValidFileScheme",
s: "file://code.visualstudio.com/docs/extensions/overview.md", s: "file://code.visualstudio.com/docs/extensions/overview.md",
want: &uri.URI{ want: URI(FileScheme + hierPart + "/docs/extensions/overview.md"),
Scheme: uri.FileScheme,
Path: "/docs/extensions/overview.md",
FsPath: filepath.FromSlash("/docs/extensions/overview.md"),
},
wantErr: false,
}, },
{ {
name: "ValidHTTPScheme", name: "ValidHTTPScheme",
s: "http://code.visualstudio.com/docs/extensions/overview#frag", s: "http://code.visualstudio.com/docs/extensions/overview#frag",
want: &uri.URI{ want: URI(HTTPScheme + hierPart + "code.visualstudio.com/docs/extensions/overview#frag"),
Scheme: uri.HTTPScheme,
Authority: "code.visualstudio.com",
Path: "/docs/extensions/overview",
Fragment: "frag",
},
wantErr: false,
}, },
{ {
name: "ValidHTTPSScheme", name: "ValidHTTPSScheme",
s: "https://code.visualstudio.com/docs/extensions/overview#frag", s: "https://code.visualstudio.com/docs/extensions/overview#frag",
want: &uri.URI{ want: URI(HTTPSScheme + hierPart + "code.visualstudio.com/docs/extensions/overview#frag"),
Scheme: uri.HTTPSScheme,
Authority: "code.visualstudio.com",
Path: "/docs/extensions/overview",
Fragment: "frag",
},
wantErr: false,
},
{
name: "Invalid",
s: "foo://user@example.com:8042/over/there?name=ferret#nose",
want: new(uri.URI),
wantErr: true,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
@ -64,12 +37,13 @@ func TestParse(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
t.Parallel() t.Parallel()
got, err := uri.Parse(tt.s) got, err := Parse(tt.s)
if (err != nil) != tt.wantErr { if err != nil {
t.Errorf("wantErr: %t, err: %#v", tt.wantErr, err) t.Error(err)
return return
} }
if diff := cmp.Diff(got, tt.want, cmp.AllowUnexported(*tt.want)); (diff != "") != tt.wantErr {
if diff := cmp.Diff(got, tt.want); diff != "" {
t.Errorf("%s: (-got, +want)\n%s", tt.name, diff) t.Errorf("%s: (-got, +want)\n%s", tt.name, diff)
} }
}) })
@ -80,23 +54,19 @@ func TestFile(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
path string path string
want *uri.URI want URI
wantErr bool wantErr bool
}{ }{
{ {
name: "ValidFileScheme", name: "ValidFileScheme",
path: "/users/me/c#-projects/", path: "/users/me/c#-projects/",
want: &uri.URI{ want: URI(FileScheme + hierPart + "/users/me/c%23-projects"),
Scheme: uri.FileScheme,
Path: "/users/me/c#-projects/",
FsPath: filepath.FromSlash("/users/me/c#-projects/"),
},
wantErr: false, wantErr: false,
}, },
{ {
name: "Invalid", name: "Invalid",
path: "users-me-c#-projects", path: "users-me-c#-projects",
want: new(uri.URI), want: URI(""),
wantErr: true, wantErr: true,
}, },
} }
@ -105,7 +75,7 @@ func TestFile(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
t.Parallel() t.Parallel()
if diff := cmp.Diff(uri.File(tt.path), tt.want, cmp.AllowUnexported(*tt.want)); (diff != "") != tt.wantErr { if diff := cmp.Diff(File(tt.path), tt.want); (diff != "") != tt.wantErr {
t.Errorf("%s: (-got, +want)\n%s", tt.name, diff) t.Errorf("%s: (-got, +want)\n%s", tt.name, diff)
} }
}) })
@ -123,7 +93,7 @@ func TestFrom(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
args args args args
want *uri.URI want URI
}{ }{
{ {
name: "ValidFileScheme", name: "ValidFileScheme",
@ -134,11 +104,7 @@ func TestFrom(t *testing.T) {
query: "name=ferret", query: "name=ferret",
fragment: "nose", fragment: "nose",
}, },
want: &uri.URI{ want: URI(FileScheme + hierPart + "/over/there"),
Scheme: uri.FileScheme,
Path: "/over/there",
FsPath: filepath.FromSlash("/over/there"),
},
}, },
{ {
name: "ValidHTTPScheme", name: "ValidHTTPScheme",
@ -149,13 +115,7 @@ func TestFrom(t *testing.T) {
query: "name=ferret", query: "name=ferret",
fragment: "nose", fragment: "nose",
}, },
want: &uri.URI{ want: URI(HTTPScheme + hierPart + "example.com:8042/over/there?name%3Dferret#nose"),
Scheme: uri.HTTPScheme,
Authority: "example.com:8042",
Path: "/over/there",
Query: "name%3Dferret",
Fragment: "nose",
},
}, },
{ {
name: "ValidHTTPSScheme", name: "ValidHTTPSScheme",
@ -166,13 +126,7 @@ func TestFrom(t *testing.T) {
query: "name=ferret", query: "name=ferret",
fragment: "nose", fragment: "nose",
}, },
want: &uri.URI{ want: URI(HTTPSScheme + hierPart + "example.com:8042/over/there?name%3Dferret#nose"),
Scheme: uri.HTTPSScheme,
Authority: "example.com:8042",
Path: "/over/there",
Query: "name%3Dferret",
Fragment: "nose",
},
}, },
} }
for _, tt := range tests { for _, tt := range tests {
@ -180,81 +134,9 @@ func TestFrom(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
t.Parallel() t.Parallel()
if diff := cmp.Diff(uri.From(tt.args.scheme, tt.args.authority, tt.args.path, tt.args.query, tt.args.fragment), tt.want, cmp.AllowUnexported(*tt.want)); diff != "" { if diff := cmp.Diff(From(tt.args.scheme, tt.args.authority, tt.args.path, tt.args.query, tt.args.fragment), tt.want); diff != "" {
t.Errorf("%s: (-got, +want)\n%s", tt.name, diff) t.Errorf("%s: (-got, +want)\n%s", tt.name, diff)
} }
}) })
} }
} }
func TestURI_String(t *testing.T) {
type fields struct {
Scheme string
Authority string
Path string
FsPath string
Query string
Fragment string
}
tests := []struct {
name string
fields fields
want string
}{
{
name: "ValidFileScheme",
fields: fields{
Scheme: string(uri.FileScheme),
Path: "docs/extensions/overview.md",
FsPath: filepath.FromSlash("docs/extensions/overview.md"),
},
want: "file://docs/extensions/overview.md",
},
{
name: "ValidHTTPScheme",
fields: fields{
Scheme: string(uri.HTTPScheme),
Authority: "code.visualstudio.com",
Path: "/docs/extensions/overview",
FsPath: filepath.FromSlash("/docs/extensions/overview"),
Query: "test",
Fragment: "frag",
},
want: "http://code.visualstudio.com/docs/extensions/overview?test#frag",
},
{
name: "ValidHTTPSScheme",
fields: fields{
Scheme: string(uri.HTTPSScheme),
Authority: "code.visualstudio.com",
Path: "/docs/extensions/overview",
FsPath: filepath.FromSlash("/docs/extensions/overview"),
Query: "test",
Fragment: "frag",
},
want: "https://code.visualstudio.com/docs/extensions/overview?test#frag",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
u := &uri.URI{
Authority: tt.fields.Authority,
Fragment: tt.fields.Fragment,
FsPath: tt.fields.FsPath,
Path: tt.fields.Path,
Query: tt.fields.Query,
Scheme: tt.fields.Scheme,
}
if got := u.String(); !cmp.Equal(got, tt.want) {
t.Errorf("URI.String() = %v, want %v", got, tt.want)
}
if got2 := u.String(); !cmp.Equal(got2, tt.want) { // cache with u.formatted
t.Errorf("URI.String() = %v, want %v", got2, tt.want)
}
})
}
}