feat: Join() reclaimed for strings — ErrorJoin for errors

core.Join("/", "deploy", "to", "homelab") → "deploy/to/homelab"
core.Join(".", "cmd", "deploy", "description") → "cmd.deploy.description"

Join builds via Concat — same hook point for security/validation.
errors.Join wrapper renamed to ErrorJoin.
JoinPath now delegates to Join("/", ...).

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-20 12:42:10 +00:00
parent 2fab391cc9
commit f1d6c2a174
6 changed files with 22 additions and 13 deletions

View file

@ -184,7 +184,7 @@ func (c *Core) Command(args ...any) any {
// Build parent chain — "deploy/to/homelab" creates "deploy" and "deploy/to" if missing
parts := Split(path, "/")
for i := len(parts) - 1; i > 0; i-- {
parentPath := StringJoin(parts[:i], "/")
parentPath := JoinPath(parts[:i]...)
if _, exists := c.commands.commands[parentPath]; !exists {
c.commands.commands[parentPath] = &Command{
name: parts[i-1],

View file

@ -138,9 +138,10 @@ func NewError(text string) error {
return errors.New(text)
}
// Join combines multiple errors into one.
// Wrapper around errors.Join for convenience.
func Join(errs ...error) error {
// ErrorJoin combines multiple errors into one.
//
// core.ErrorJoin(err1, err2, err3)
func ErrorJoin(errs ...error) error {
return errors.Join(errs...)
}

View file

@ -60,11 +60,19 @@ func SplitN(s, sep string, n int) []string {
return strings.SplitN(s, sep, n)
}
// StringJoin joins segments with separator.
// Join joins parts with a separator, building via Concat.
//
// core.StringJoin([]string{"a", "b", "c"}, "/") // "a/b/c"
func StringJoin(elems []string, sep string) string {
return strings.Join(elems, sep)
// core.Join("/", "deploy", "to", "homelab") // "deploy/to/homelab"
// core.Join(".", "cmd", "deploy", "description") // "cmd.deploy.description"
func Join(sep string, parts ...string) string {
if len(parts) == 0 {
return ""
}
result := parts[0]
for _, p := range parts[1:] {
result = Concat(result, sep, p)
}
return result
}
// Replace replaces all occurrences of old with new in s.

View file

@ -26,7 +26,7 @@ func Print(w io.Writer, format string, args ...any) {
//
// core.JoinPath("deploy", "to", "homelab") // → "deploy/to/homelab"
func JoinPath(segments ...string) string {
return StringJoin(segments, "/")
return Join("/", segments...)
}
// IsFlag returns true if the argument starts with a dash.

View file

@ -187,10 +187,10 @@ func TestNewError_Good(t *testing.T) {
assert.Equal(t, "simple error", err.Error())
}
func TestJoin_Good(t *testing.T) {
func TestErrorJoin_Good(t *testing.T) {
e1 := errors.New("first")
e2 := errors.New("second")
joined := Join(e1, e2)
joined := ErrorJoin(e1, e2)
assert.ErrorIs(t, joined, e1)
assert.ErrorIs(t, joined, e2)
}

View file

@ -43,8 +43,8 @@ func TestSplitN_Good(t *testing.T) {
assert.Equal(t, []string{"key", "value=extra"}, SplitN("key=value=extra", "=", 2))
}
func TestStringJoin_Good(t *testing.T) {
assert.Equal(t, "a/b/c", StringJoin([]string{"a", "b", "c"}, "/"))
func TestJoin_Good(t *testing.T) {
assert.Equal(t, "a/b/c", Join("/", "a", "b", "c"))
}
func TestReplace_Good(t *testing.T) {