Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ lint:
## modernize: ♻️ Check for outdated patterns
.PHONY: modernize
modernize:
go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest -test=false ./...
go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest -fix -test=false ./...

## test: 🚦 Execute all tests
.PHONY: test
Expand Down
7 changes: 7 additions & 0 deletions bytes.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,10 @@ func ToUpperBytes(b []byte) []byte {

return b
}

// AddTrailingSlashBytes appends a trailing '/' to b if it does not already end with one.
// If the input already ends with '/', the original slice is returned.
// A new slice is returned when a '/' is appended. The original slice is never modified.
func AddTrailingSlashBytes(b []byte) []byte {
return UnsafeBytes(AddTrailingSlashString(UnsafeString(b)))
}
73 changes: 73 additions & 0 deletions bytes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,76 @@ func Test_ToUpperBytes_Edge(t *testing.T) {
})
}
}

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

testCases := []struct {
name string
in []byte
want []byte
}{
{name: "empty", in: []byte(""), want: []byte("/")},
{name: "slash-only", in: []byte("/"), want: []byte("/")},
{name: "short-no-slash", in: []byte("abc"), want: []byte("abc/")},
{name: "short-with-slash", in: []byte("abc/"), want: []byte("abc/")},
{name: "path-no-slash", in: []byte("/api/v1/users"), want: []byte("/api/v1/users/")},
{name: "path-with-slash", in: []byte("/api/v1/users/"), want: []byte("/api/v1/users/")},
{name: "double-slash", in: []byte("abc//"), want: []byte("abc//")},
{name: "unicode", in: []byte("/日本語"), want: []byte("/日本語/")},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
result := AddTrailingSlashBytes(tc.in)
require.Equal(t, tc.want, result)
})
}
}

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

original := []byte("test")
originalCopy := make([]byte, len(original))
copy(originalCopy, original)

_ = AddTrailingSlashBytes(original)

require.Equal(t, originalCopy, original, "original slice should not be mutated")
}

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

input := []byte("test/")
result := AddTrailingSlashBytes(input)
require.Equal(t, input, result)
require.Same(t, &input[0], &result[0], "should return same slice instance")
}

func Benchmark_AddTrailingSlashBytes(b *testing.B) {
cases := []struct {
name string
input []byte
}{
{name: "empty", input: []byte("")},
{name: "slash-only", input: []byte("/")},
{name: "short-no-slash", input: []byte("abc")},
{name: "short-with-slash", input: []byte("abc/")},
{name: "path-no-slash", input: []byte("/api/v1/users")},
{name: "path-with-slash", input: []byte("/api/v1/users/")},
}

for _, tc := range cases {
b.Run(tc.name, func(b *testing.B) {
b.ReportAllocs()
var res []byte
for n := 0; n < b.N; n++ {
res = AddTrailingSlashBytes(tc.input)
}
_ = res
})
}
}
17 changes: 17 additions & 0 deletions strings.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,20 @@ func ToUpper(b string) string {

return b
}

// AddTrailingSlashString appends a trailing '/' to s if it does not already end with one.
// If the input already ends with '/', the original string is returned.
// A new string is returned only when a '/' needs to be appended.
func AddTrailingSlashString(s string) string {
n := len(s)
if n == 0 {
return "/"
}
if s[n-1] == '/' {
return s
}
buf := make([]byte, n+1)
copy(buf, s)
buf[n] = '/'
return UnsafeString(buf)
}
52 changes: 52 additions & 0 deletions strings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,55 @@ func Benchmark_ToLower(b *testing.B) {
})
}
}

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

testCases := []struct {
name string
in string
want string
}{
{name: "empty", in: "", want: "/"},
{name: "slash-only", in: "/", want: "/"},
{name: "short-no-slash", in: "abc", want: "abc/"},
{name: "short-with-slash", in: "abc/", want: "abc/"},
{name: "path-no-slash", in: "/api/v1/users", want: "/api/v1/users/"},
{name: "path-with-slash", in: "/api/v1/users/", want: "/api/v1/users/"},
{name: "double-slash", in: "abc//", want: "abc//"},
{name: "unicode", in: "/日本語", want: "/日本語/"},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
result := AddTrailingSlashString(tc.in)
require.Equal(t, tc.want, result)
})
}
}

func Benchmark_AddTrailingSlashString(b *testing.B) {
cases := []struct {
name string
input string
}{
{name: "empty", input: ""},
{name: "slash-only", input: "/"},
{name: "short-no-slash", input: "abc"},
{name: "short-with-slash", input: "abc/"},
{name: "path-no-slash", input: "/api/v1/users"},
{name: "path-with-slash", input: "/api/v1/users/"},
}

for _, tc := range cases {
b.Run(tc.name, func(b *testing.B) {
b.ReportAllocs()
var res string
for n := 0; n < b.N; n++ {
res = AddTrailingSlashString(tc.input)
}
_ = res
})
}
}
Loading