├── 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 |
--------------------------------------------------------------------------------