├── go.mod ├── example └── example.go ├── LICENSE ├── README.md ├── urlbuilder.go └── urlbuilder_test.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sheepla/go-urlbuilder 2 | 3 | go 1.21.2 4 | -------------------------------------------------------------------------------- /example/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | 7 | "github.com/sheepla/go-urlbuilder" 8 | ) 9 | 10 | var sourceURL = "https://localhost:8080/path/to/resource#helloworld?key1=value1&key2=value2" 11 | 12 | func main() { 13 | u := urlbuilder.MustParse(sourceURL) 14 | 15 | u.SetScheme("http"). 16 | SetHost("example.com:12345"). 17 | SetFragment("anotherFragment"). 18 | EditPath(func(elements []string) []string { 19 | return append(elements, "Go言語") 20 | }). 21 | EditQuery(func(q url.Values) url.Values { 22 | q.Set("key1", "key1-edited") 23 | q.Del("key2") 24 | q.Add("key3", "value3") 25 | 26 | return q 27 | }) 28 | 29 | // => http://example.com:12345/path/to/resource/Go%25E8%25A8%2580%25E8%25AA%259E?key1=key1-edited&key3=value3#anotherFragment 30 | fmt.Println(u.MustString()) 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 sheepla 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # 🔗 go-urlbuilder 4 | 5 |
6 | 7 | **go-urlbuilder** is a Go module based on `net/url` standard module, aimed at safely constructing URL strings with a concise syntax. 8 | 9 | ## Why? 10 | 11 | It is a good idea to use the `net/url` standard module when safely constructing URL strings. 12 | However, if you use `net/url` as is, you often need to prepare temporary variables, and have to write non-declaretive code which I felt was a bit cumbersome when building complex URLs over and over again. 13 | 14 | This module was created as a concise and easy way to construct URL strings based on `net/url`. 15 | 16 | ## Usage 17 | 18 | ```go 19 | package main 20 | 21 | import ( 22 | "fmt" 23 | "net/url" 24 | 25 | "github.com/sheepla/go-urlbuilder" 26 | ) 27 | 28 | var sourceURL = "https://localhost:8080/path/to/resource#helloworld?key1=value1&key2=value2" 29 | 30 | func main() { 31 | u := urlbuilder.MustParse(sourceURL) 32 | 33 | u.SetScheme("http"). 34 | SetHost("example.com:12345"). 35 | SetFragment("anotherFragment"). 36 | EditPath(func(elements []string) []string { 37 | return append(elements, "Go言語") 38 | }). 39 | EditQuery(func(q url.Values) url.Values { 40 | q.Set("key1", "key1-edited") 41 | q.Del("key2") 42 | q.Add("key3", "value3") 43 | 44 | return q 45 | }) 46 | 47 | // => http://example.com:12345/path/to/resource/Go%25E8%25A8%2580%25E8%25AA%259E?key1=key1-edited&key3=value3#anotherFragment 48 | fmt.Println(u.MustString()) 49 | } 50 | ``` 51 | 52 | ## License 53 | 54 | MIT 55 | 56 | ## Author 57 | 58 | [sheepla](https://github.com/sheepla) 59 | 60 | 61 | -------------------------------------------------------------------------------- /urlbuilder.go: -------------------------------------------------------------------------------- 1 | package urlbuilder 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | ) 7 | 8 | type URL struct { 9 | internal *url.URL 10 | err error 11 | } 12 | 13 | func Parse(s string) (*URL, error) { 14 | netURL, err := url.Parse(s) 15 | return &URL{ 16 | internal: netURL, 17 | err: err, 18 | }, err 19 | } 20 | 21 | func MustParse(s string) *URL { 22 | u, err := Parse(s) 23 | if err != nil { 24 | panic(err) 25 | } 26 | 27 | return u 28 | } 29 | 30 | func (u *URL) SetPath(base string, elements ...string) *URL { 31 | path, err := url.JoinPath(base, elements...) 32 | u.err = err 33 | u.internal.Path = path 34 | 35 | return u 36 | } 37 | 38 | func (u *URL) EditPath(editFunc func([]string) []string) *URL { 39 | elements := strings.Split(u.internal.Path, "/") 40 | 41 | // To prevent double escaping, 42 | // each path element is unescaped before being passed to the editing function. 43 | for i := 0; i < len(elements); i++ { 44 | if escaped, err := url.PathUnescape(elements[i]); err == nil { 45 | elements[i] = escaped 46 | } 47 | } 48 | 49 | elements = editFunc(elements) 50 | 51 | path, err := url.JoinPath("/", elements...) 52 | u.err = err 53 | u.internal.Path = path 54 | 55 | return u 56 | } 57 | 58 | func (u *URL) AppendPath(elements ...string) *URL { 59 | u.EditPath(func(current []string) []string { 60 | return append(current, elements...) 61 | }) 62 | 63 | return u 64 | } 65 | 66 | func (u *URL) SetScheme(scheme string) *URL { 67 | u.internal.Scheme = scheme 68 | 69 | return u 70 | } 71 | 72 | func (u *URL) SetHost(host string) *URL { 73 | u.internal.Host = host 74 | 75 | return u 76 | } 77 | 78 | func (u *URL) SetUser(userName string) *URL { 79 | u.internal.User = url.User(userName) 80 | 81 | return u 82 | } 83 | 84 | func (u *URL) SetUserWithPassword(userName, password string) *URL { 85 | u.internal.User = url.UserPassword(userName, password) 86 | 87 | return u 88 | } 89 | 90 | func (u *URL) SetFragment(fragment string) *URL { 91 | u.internal.Fragment = fragment 92 | 93 | return u 94 | } 95 | 96 | func (u *URL) EditQuery(editFunc func(url.Values) url.Values) *URL { 97 | edited := editFunc(u.internal.Query()) 98 | u.internal.RawQuery = edited.Encode() 99 | 100 | return u 101 | } 102 | 103 | func (u *URL) SetQuery(key, value string) *URL { 104 | u.EditQuery(func(q url.Values) url.Values { 105 | q.Set(key, value) 106 | return q 107 | }) 108 | 109 | return u 110 | } 111 | 112 | func (u *URL) AddQuery(key, value string) *URL { 113 | u.EditQuery(func(q url.Values) url.Values { 114 | q.Add(key, value) 115 | return q 116 | }) 117 | 118 | return u 119 | } 120 | 121 | func (u *URL) RemoveQuery(key string) *URL { 122 | u.EditQuery(func(q url.Values) url.Values { 123 | q.Del(key) 124 | return q 125 | }) 126 | 127 | return u 128 | } 129 | 130 | func (u *URL) String() (string, error) { 131 | return u.internal.String(), u.err 132 | } 133 | 134 | func (u *URL) MustString() string { 135 | s, err := u.String() 136 | if err != nil { 137 | panic(err) 138 | } 139 | 140 | return s 141 | } 142 | -------------------------------------------------------------------------------- /urlbuilder_test.go: -------------------------------------------------------------------------------- 1 | package urlbuilder_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "net/url" 7 | 8 | "github.com/sheepla/go-urlbuilder" 9 | ) 10 | 11 | func TestBasic(t *testing.T) { 12 | sourceURL := "https://localhost:8080/path/to/resource?key1=value1&key2=value2#helloWorld" 13 | u, err := urlbuilder.Parse(sourceURL) 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | 18 | u.SetScheme("http"). 19 | SetHost("another.example.com:12345"). 20 | SetFragment("anotherFragment"). 21 | SetUserWithPassword("u$er1", "P@$$w0rd!") 22 | 23 | have, err := u.String() 24 | if err != nil { 25 | t.Fatalf("an error occurred on constructing URL: %s\n", err) 26 | } 27 | 28 | want := "http://u$er1:P%40$$w0rd%21@another.example.com:12345/path/to/resource?key1=value1&key2=value2#anotherFragment" 29 | if have != want { 30 | t.Fatalf("have=%s\nwant=%s\n", have, want) 31 | } 32 | } 33 | 34 | func TestPathEditing(t *testing.T) { 35 | sourceURL := "https://localhost:8080/あ/progr@mm!ng" 36 | u, err := urlbuilder.Parse(sourceURL) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | 41 | u.EditPath(func(elements []string) []string { 42 | t.Log("current elements: ", elements) 43 | elements = append(elements, "Go言語") 44 | t.Log("edited elements: ", elements) 45 | 46 | return elements 47 | }). 48 | AppendPath("foo", "bar") 49 | 50 | have, err := u.String() 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | 55 | want := "https://localhost:8080/%25E3%2581%2582/progr@mm%2521ng/Go%25E8%25A8%2580%25E8%25AA%259E/foo/bar" 56 | if have != want { 57 | t.Fatalf("have=%s\nwant=%s\n", have, want) 58 | } 59 | 60 | } 61 | 62 | func TestQueryEditing(t *testing.T) { 63 | sourceURL := "https://localhost:8080/path/to/resource?key1=value1&key2=value2" 64 | u, err := urlbuilder.Parse(sourceURL) 65 | if err != nil { 66 | t.Fatal(err) 67 | } 68 | 69 | u.EditQuery(func(q url.Values) url.Values { 70 | t.Logf("current query: %s", q.Encode()) 71 | 72 | q.Set("key1", "value1-edited") 73 | q.Del("key2") 74 | q.Add("key3", "value3") 75 | 76 | t.Logf("key1: %s", q.Get("key1")) 77 | t.Logf("key2: %s", q.Get("key2")) 78 | t.Logf("key3: %s", q.Get("key3")) 79 | t.Logf("edited query: %s", q.Encode()) 80 | 81 | return q 82 | }) 83 | 84 | have, err := u.String() 85 | if err != nil { 86 | t.Fatalf("an error occurred on constructing URL: %s\n", err) 87 | } 88 | 89 | want := "https://localhost:8080/path/to/resource?key1=value1-edited&key3=value3" 90 | if have != want { 91 | t.Fatalf("have=%s\nwant=%s\n", have, want) 92 | } 93 | } 94 | 95 | func TestQueryEditingEx(t *testing.T) { 96 | sourceURL := "https://localhost:8080/path/to/resource?key1=value1&key2=value2" 97 | u, err := urlbuilder.Parse(sourceURL) 98 | if err != nil { 99 | t.Fatal(err) 100 | } 101 | 102 | u.SetQuery("key1", "key1-edited"). 103 | RemoveQuery("key2"). 104 | AddQuery("key3", "value3") 105 | 106 | have, err := u.String() 107 | if err != nil { 108 | t.Fatalf("an error occurred on constructing URL: %s\n", err) 109 | } 110 | 111 | want := "https://localhost:8080/path/to/resource?key1=key1-edited&key3=value3" 112 | if have != want { 113 | t.Fatalf("have=%s\nwant=%s\n", have, want) 114 | } 115 | } 116 | --------------------------------------------------------------------------------