Skip to content

Commit 5f61c4b

Browse files
committed
feature: add AddTrailingSlash
1 parent d531ec6 commit 5f61c4b

File tree

2 files changed

+100
-0
lines changed

2 files changed

+100
-0
lines changed

byteseq.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,30 @@ func TrimRight[S byteSeq](s S, cutset byte) S {
8787
}
8888
return s[:lenStr]
8989
}
90+
91+
// AddTrailingSlash appends a trailing '/' to v if it does not already end with one.
92+
//
93+
// For string inputs, the result is always a new string.
94+
//
95+
// For []byte inputs, the result is always a new slice with the trailing '/' appended.
96+
// The original slice is never modified, even if it has sufficient capacity.
97+
//
98+
// This function guarantees that the returned value is independent of the input.
99+
func AddTrailingSlash[T byteSeq](v T) T {
100+
if len(v) > 0 && v[len(v)-1] == '/' {
101+
return v
102+
}
103+
104+
switch x := any(v).(type) {
105+
case string:
106+
return any(x + "/").(T) //nolint:forcetypeassert,errcheck // can't fail
107+
case []byte:
108+
result := make([]byte, len(x)+1)
109+
copy(result, x)
110+
result[len(x)] = '/'
111+
return any(result).(T) //nolint:forcetypeassert,errcheck // can't fail
112+
default:
113+
// impossible because of the constraint
114+
return v
115+
}
116+
}

byteseq_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,3 +303,76 @@ func Test_Trim_Edge(t *testing.T) {
303303
})
304304
}
305305
}
306+
307+
func Test_AddTrailingSlash_String(t *testing.T) {
308+
t.Parallel()
309+
310+
tests := []struct {
311+
in string
312+
want string
313+
}{
314+
{"", "/"},
315+
{"abc", "abc/"},
316+
{"abc/", "abc/"},
317+
{"/", "/"},
318+
}
319+
320+
for _, tt := range tests {
321+
require.Equal(t, tt.want, AddTrailingSlash(tt.in))
322+
}
323+
}
324+
325+
func Test_AddTrailingSlash_Bytes(t *testing.T) {
326+
t.Parallel()
327+
328+
tests := []struct {
329+
in []byte
330+
want []byte
331+
}{
332+
{[]byte(""), []byte("/")},
333+
{[]byte("abc"), []byte("abc/")},
334+
{[]byte("abc/"), []byte("abc/")},
335+
{[]byte("/"), []byte("/")},
336+
}
337+
338+
for _, tt := range tests {
339+
require.Equal(t, tt.want, AddTrailingSlash(tt.in))
340+
}
341+
}
342+
343+
func Benchmark_AddTrailingSlash(b *testing.B) {
344+
tests := []struct {
345+
name string
346+
in any
347+
}{
348+
{"StringSmallNoSlash", "abc"},
349+
{"StringSmallWithSlash", "abc/"},
350+
{"StringLargeNoSlash", string(make([]byte, 10_000))},
351+
{"StringLargeWithSlash", string(append(make([]byte, 10_000), '/'))},
352+
353+
{"BytesSmallNoSlash", []byte("abc")},
354+
{"BytesSmallWithSlash", []byte("abc/")},
355+
{"BytesLargeNoSlash", make([]byte, 10_000)},
356+
{"BytesLargeWithSlash", append(make([]byte, 10_000), '/')},
357+
}
358+
359+
for _, tt := range tests {
360+
b.Run(tt.name, func(b *testing.B) {
361+
switch v := tt.in.(type) {
362+
case string:
363+
var out string
364+
for i := 0; i < b.N; i++ {
365+
out = AddTrailingSlash(v)
366+
_ = out
367+
}
368+
369+
case []byte:
370+
var out []byte
371+
for i := 0; i < b.N; i++ {
372+
out = AddTrailingSlash(v)
373+
_ = out
374+
}
375+
}
376+
})
377+
}
378+
}

0 commit comments

Comments
 (0)