diff --git a/docs/url.md b/docs/url.md index 21d54a29..23f0e29d 100644 --- a/docs/url.md +++ b/docs/url.md @@ -11,6 +11,8 @@ The above returns a dict, containing URL object: ```yaml scheme: 'http' host: 'server.com:8080' +hostname: 'server.com' +port: '8080' path: '/api' query: 'list=false' opaque: nil @@ -18,6 +20,24 @@ fragment: 'anchor' userinfo: 'admin:secret' ``` +IPv6 addresses are also supported: +``` +urlParse "http://[2001:db8::1]:8080/api" +``` + +Returns: +```yaml +scheme: 'http' +host: '[2001:db8::1]:8080' +hostname: '2001:db8::1' +port: '8080' +path: '/api' +query: '' +opaque: nil +fragment: '' +userinfo: '' +``` + For more info, check https://golang.org/pkg/net/url/#URL ## urlJoin @@ -31,3 +51,23 @@ The above returns the following string: ``` proto://host:80/path?query#fragment ``` + +Alternatively, you can use `hostname` and `port` instead of `host`: +``` +urlJoin (dict "fragment" "fragment" "hostname" "host" "port" "80" "path" "/path" "query" "query" "scheme" "http") +``` + +This will also produce: +``` +proto://host:80/path?query#fragment +``` + +IPv6 addresses are automatically detected and wrapped in square brackets when using `hostname` and `port`: +``` +urlJoin (dict "hostname" "2001:db8::1" "port" "8080" "path" "/path" "scheme" "http") +``` + +This produces: +``` +http://[2001:db8::1]:8080/path +``` diff --git a/url.go b/url.go index b8e120e1..d93e73ae 100644 --- a/url.go +++ b/url.go @@ -2,6 +2,7 @@ package sprig import ( "fmt" + "net" "net/url" "reflect" ) @@ -28,6 +29,7 @@ func urlParse(v string) map[string]interface{} { dict["scheme"] = parsedURL.Scheme dict["host"] = parsedURL.Host dict["hostname"] = parsedURL.Hostname() + dict["port"] = parsedURL.Port() dict["path"] = parsedURL.Path dict["query"] = parsedURL.RawQuery dict["opaque"] = parsedURL.Opaque @@ -43,9 +45,27 @@ func urlParse(v string) map[string]interface{} { // join given dict to URL string func urlJoin(d map[string]interface{}) string { + host := dictGetOrEmpty(d, "host") + // If host is not provided, try to construct it from hostname and port + if host == "" { + hostname := dictGetOrEmpty(d, "hostname") + port := dictGetOrEmpty(d, "port") + if port != "" { + // Check if hostname is an IPv6 address + ip := net.ParseIP(hostname) + if ip != nil && ip.To4() == nil { + // IPv6 address needs to be wrapped in square brackets + host = "[" + hostname + "]:" + port + } else { + host = hostname + ":" + port + } + } else { + host = hostname + } + } resURL := url.URL{ Scheme: dictGetOrEmpty(d, "scheme"), - Host: dictGetOrEmpty(d, "host"), + Host: host, Path: dictGetOrEmpty(d, "path"), RawQuery: dictGetOrEmpty(d, "query"), Opaque: dictGetOrEmpty(d, "opaque"), diff --git a/url_test.go b/url_test.go index f9c00b17..7ea10c66 100644 --- a/url_test.go +++ b/url_test.go @@ -11,6 +11,7 @@ var urlTests = map[string]map[string]interface{}{ "fragment": "fragment", "host": "host:80", "hostname": "host", + "port": "80", "opaque": "", "path": "/path", "query": "query", @@ -21,6 +22,7 @@ var urlTests = map[string]map[string]interface{}{ "fragment": "", "host": "host:80", "hostname": "host", + "port": "80", "opaque": "", "path": "/path", "query": "", @@ -31,6 +33,7 @@ var urlTests = map[string]map[string]interface{}{ "fragment": "", "host": "", "hostname": "", + "port": "", "opaque": "", "path": "something", "query": "", @@ -41,6 +44,7 @@ var urlTests = map[string]map[string]interface{}{ "fragment": "", "host": "host:80", "hostname": "host", + "port": "80", "opaque": "", "path": "/path", "query": "", @@ -51,12 +55,46 @@ var urlTests = map[string]map[string]interface{}{ "fragment": "", "host": "host:80", "hostname": "host", + "port": "80", "opaque": "", "path": "/pa th", "query": "key=val%20ue", "scheme": "proto", "userinfo": "", }, + "proto://[2001:db8::1]:8080/path": { + "fragment": "", + "host": "[2001:db8::1]:8080", + "hostname": "2001:db8::1", + "port": "8080", + "opaque": "", + "path": "/path", + "query": "", + "scheme": "proto", + "userinfo": "", + }, + "proto://[::1]:8080/path?query#fragment": { + "fragment": "fragment", + "host": "[::1]:8080", + "hostname": "::1", + "port": "8080", + "opaque": "", + "path": "/path", + "query": "query", + "scheme": "proto", + "userinfo": "", + }, + "proto://user@[2001:db8::1]:8080/path": { + "fragment": "", + "host": "[2001:db8::1]:8080", + "hostname": "2001:db8::1", + "port": "8080", + "opaque": "", + "path": "/path", + "query": "", + "scheme": "proto", + "userinfo": "user", + }, } func TestUrlParse(t *testing.T) { @@ -75,6 +113,9 @@ func TestUrlJoin(t *testing.T) { tests := map[string]string{ `{{ urlJoin (dict "fragment" "fragment" "host" "host:80" "path" "/path" "query" "query" "scheme" "proto") }}`: "proto://host:80/path?query#fragment", `{{ urlJoin (dict "fragment" "fragment" "host" "host:80" "path" "/path" "scheme" "proto" "userinfo" "ASDJKJSD") }}`: "proto://ASDJKJSD@host:80/path#fragment", + // Test IPv6 with hostname and port + `{{ urlJoin (dict "hostname" "2001:db8::1" "port" "8080" "path" "/path" "scheme" "proto") }}`: "proto://[2001:db8::1]:8080/path", + `{{ urlJoin (dict "hostname" "::1" "port" "8080" "path" "/path" "scheme" "proto") }}`: "proto://[::1]:8080/path", } for tpl, expected := range tests { assert.NoError(t, runt(tpl, expected))