Skip to content

Commit

Permalink
feat: add URL builder experimental package
Browse files Browse the repository at this point in the history
  • Loading branch information
a-h authored Sep 24, 2024
2 parents 2b69b31 + cd2b9fd commit a31c35c
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 0 deletions.
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/templ-go/x

go 1.22.4

require github.com/a-h/templ v0.2.747
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
github.com/a-h/templ v0.2.747 h1:D0dQ2lxC3W7Dxl6fxQ/1zZHBQslSkTSvl5FxP/CfdKg=
github.com/a-h/templ v0.2.747/go.mod h1:69ObQIbrcuwPCU32ohNaWce3Cb7qM5GMiqN1K+2yop4=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
69 changes: 69 additions & 0 deletions urlbuilder/urlbuilder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package urlbuilder

import (
"net/url"
"strings"

"github.com/a-h/templ"
)

// URLBuilder is a builder for constructing URLs
type URLBuilder struct {
scheme string
host string
path []string
query url.Values
fragment string
}

// New creates a new URLBuilder with the given scheme and host
func New(scheme string, host string) *URLBuilder {
return &URLBuilder{
scheme: scheme,
host: host,
query: url.Values{},
}
}

// Path adds a path segment to the URL
func (ub *URLBuilder) Path(segment string) *URLBuilder {
ub.path = append(ub.path, segment)
return ub
}

// Query adds a query parameter to the URL
func (ub *URLBuilder) Query(key string, value string) *URLBuilder {
ub.query.Add(key, value)
return ub
}

// Fragment sets the fragment (hash) part of the URL
func (ub *URLBuilder) Fragment(fragment string) *URLBuilder {
ub.fragment = fragment
return ub
}

// Build constructs the final URL as a SafeURL
func (ub *URLBuilder) Build() templ.SafeURL {
var buf strings.Builder
buf.WriteString(ub.scheme)
buf.WriteString("://")
buf.WriteString(ub.host)

for _, segment := range ub.path {
buf.WriteByte('/')
buf.WriteString(url.PathEscape(segment))
}

if len(ub.query) > 0 {
buf.WriteByte('?')
buf.WriteString(ub.query.Encode())
}

if ub.fragment != "" {
buf.WriteByte('#')
buf.WriteString(url.QueryEscape(ub.fragment))
}

return templ.URL(buf.String())
}
116 changes: 116 additions & 0 deletions urlbuilder/urlbuilder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package urlbuilder

import (
"testing"

"github.com/a-h/templ"
)

func BenchmarkURLBuilder(b *testing.B) {
b.ReportAllocs()

for i := 0; i < b.N; i++ {
New("https", "example.com").
Path("a").
Path("b").
Path("c").
Query("key1", "value1").
Query("key2", "value2").
Query("key with space", "value with slash").
Query("key/with/slash", "value/with/slash").
Path("a/b").
Query("key between paths", "value between paths").
Path("c d").
Fragment("fragment").Build()
}
}

func TestBasicURL(t *testing.T) {
t.Parallel()

got := New("https", "example.com").
Build()

expected := templ.URL("https://example.com")

if got != expected {
t.Fatalf("got %s, want %s", got, expected)
}
}

func TestURLWithPaths(t *testing.T) {
t.Parallel()

c := "c"
got := New("https", "example.com").
Path("a").
Path("b").
Path(c).
Query("key", "value").Build()

expected := templ.URL("https://example.com/a/b/c?key=value")

if got != expected {
t.Fatalf("got %s, want %s", got, expected)
}
}

func TestURLWithMultipleQueries(t *testing.T) {
t.Parallel()

got := New("https", "example.com").
Path("path").
Query("key1", "value1").
Query("key2", "value2").
Build()

expected := templ.URL("https://example.com/path?key1=value1&key2=value2")

if got != expected {
t.Fatalf("got %s, want %s", got, expected)
}
}

func TestURLWithNoPaths(t *testing.T) {
t.Parallel()

got := New("http", "example.org").
Query("search", "golang").
Build()

expected := templ.URL("http://example.org?search=golang")

if got != expected {
t.Fatalf("got %s, want %s", got, expected)
}
}

func TestURLEscapingPath(t *testing.T) {
t.Parallel()

got := New("https", "example.com").
Path("a/b").
Path("c d").
Build()

expected := templ.URL("https://example.com/a%2Fb/c%20d")

if got != expected {
t.Fatalf("got %s, want %s", got, expected)
}
}

func TestURLEscapingQuery(t *testing.T) {
t.Parallel()

got := New("https", "example.com").
Query("key with space", "value with space").
Query("key/with/slash", "value/with/slash").
Build()

expected := templ.URL("https://example.com?key+with+space=value+with+space&key%2Fwith%2Fslash=value%2Fwith%2Fslash")

if got != expected {
t.Fatalf("got %s, want %s", got, expected)
}
}

0 comments on commit a31c35c

Please sign in to comment.