feat: string.go — core string primitives, same pattern as array.go
HasPrefix, HasSuffix, TrimPrefix, TrimSuffix, Contains, Split, SplitN, StringJoin, Replace, Lower, Upper, Trim, RuneCount. utils.go and command.go now use string.go helpers — zero direct strings import in either file. 234 tests, 79.8% coverage. Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
c8ebf40e78
commit
e12526dca6
4 changed files with 190 additions and 21 deletions
|
|
@ -21,7 +21,6 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
|
|
@ -65,7 +64,7 @@ func (cmd *Command) I18nKey() string {
|
|||
if path == "" {
|
||||
path = cmd.name
|
||||
}
|
||||
return "cmd." + strings.ReplaceAll(path, "/", ".") + ".description"
|
||||
return "cmd." + Replace(path, "/", ".") + ".description"
|
||||
}
|
||||
|
||||
// Run executes the command's action with the given options.
|
||||
|
|
@ -183,9 +182,9 @@ func (c *Core) Command(args ...any) any {
|
|||
c.commands.commands[path] = cmd
|
||||
|
||||
// Build parent chain — "deploy/to/homelab" creates "deploy" and "deploy/to" if missing
|
||||
parts := strings.Split(path, "/")
|
||||
parts := Split(path, "/")
|
||||
for i := len(parts) - 1; i > 0; i-- {
|
||||
parentPath := strings.Join(parts[:i], "/")
|
||||
parentPath := StringJoin(parts[:i], "/")
|
||||
if _, exists := c.commands.commands[parentPath]; !exists {
|
||||
c.commands.commands[parentPath] = &Command{
|
||||
name: parts[i-1],
|
||||
|
|
@ -220,6 +219,6 @@ func (c *Core) Commands() []string {
|
|||
// pathName extracts the last segment of a path.
|
||||
// "deploy/to/homelab" → "homelab"
|
||||
func pathName(path string) string {
|
||||
parts := strings.Split(path, "/")
|
||||
parts := Split(path, "/")
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
|
|
|
|||
104
pkg/core/string.go
Normal file
104
pkg/core/string.go
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
// String operations for the Core framework.
|
||||
// Provides safe, predictable string helpers that downstream packages
|
||||
// use directly — same pattern as Array[T] for slices.
|
||||
|
||||
package core
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// HasPrefix returns true if s starts with prefix.
|
||||
//
|
||||
// core.HasPrefix("--verbose", "--") // true
|
||||
func HasPrefix(s, prefix string) bool {
|
||||
return strings.HasPrefix(s, prefix)
|
||||
}
|
||||
|
||||
// HasSuffix returns true if s ends with suffix.
|
||||
//
|
||||
// core.HasSuffix("test.go", ".go") // true
|
||||
func HasSuffix(s, suffix string) bool {
|
||||
return strings.HasSuffix(s, suffix)
|
||||
}
|
||||
|
||||
// TrimPrefix removes prefix from s.
|
||||
//
|
||||
// core.TrimPrefix("--verbose", "--") // "verbose"
|
||||
func TrimPrefix(s, prefix string) string {
|
||||
return strings.TrimPrefix(s, prefix)
|
||||
}
|
||||
|
||||
// TrimSuffix removes suffix from s.
|
||||
//
|
||||
// core.TrimSuffix("test.go", ".go") // "test"
|
||||
func TrimSuffix(s, suffix string) string {
|
||||
return strings.TrimSuffix(s, suffix)
|
||||
}
|
||||
|
||||
// Contains returns true if s contains substr.
|
||||
//
|
||||
// core.Contains("hello world", "world") // true
|
||||
func Contains(s, substr string) bool {
|
||||
return strings.Contains(s, substr)
|
||||
}
|
||||
|
||||
// Split splits s by separator.
|
||||
//
|
||||
// core.Split("a/b/c", "/") // ["a", "b", "c"]
|
||||
func Split(s, sep string) []string {
|
||||
return strings.Split(s, sep)
|
||||
}
|
||||
|
||||
// SplitN splits s by separator into at most n parts.
|
||||
//
|
||||
// core.SplitN("key=value=extra", "=", 2) // ["key", "value=extra"]
|
||||
func SplitN(s, sep string, n int) []string {
|
||||
return strings.SplitN(s, sep, n)
|
||||
}
|
||||
|
||||
// StringJoin joins segments with separator.
|
||||
//
|
||||
// core.StringJoin([]string{"a", "b", "c"}, "/") // "a/b/c"
|
||||
func StringJoin(elems []string, sep string) string {
|
||||
return strings.Join(elems, sep)
|
||||
}
|
||||
|
||||
// Replace replaces all occurrences of old with new in s.
|
||||
//
|
||||
// core.Replace("deploy/to/homelab", "/", ".") // "deploy.to.homelab"
|
||||
func Replace(s, old, new string) string {
|
||||
return strings.ReplaceAll(s, old, new)
|
||||
}
|
||||
|
||||
// Lower returns s in lowercase.
|
||||
//
|
||||
// core.Lower("HELLO") // "hello"
|
||||
func Lower(s string) string {
|
||||
return strings.ToLower(s)
|
||||
}
|
||||
|
||||
// Upper returns s in uppercase.
|
||||
//
|
||||
// core.Upper("hello") // "HELLO"
|
||||
func Upper(s string) string {
|
||||
return strings.ToUpper(s)
|
||||
}
|
||||
|
||||
// Trim removes leading and trailing whitespace.
|
||||
//
|
||||
// core.Trim(" hello ") // "hello"
|
||||
func Trim(s string) string {
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
|
||||
// RuneCount returns the number of runes (unicode characters) in s.
|
||||
//
|
||||
// core.RuneCount("hello") // 5
|
||||
// core.RuneCount("🔥") // 1
|
||||
func RuneCount(s string) int {
|
||||
return utf8.RuneCountInString(s)
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
// Utility functions for the Core framework.
|
||||
// Built on core string.go primitives.
|
||||
|
||||
package core
|
||||
|
||||
|
|
@ -8,8 +9,6 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Print writes a formatted line to a writer, defaulting to os.Stdout.
|
||||
|
|
@ -26,9 +25,8 @@ func Print(w io.Writer, format string, args ...any) {
|
|||
// JoinPath joins string segments into a path with "/" separator.
|
||||
//
|
||||
// core.JoinPath("deploy", "to", "homelab") // → "deploy/to/homelab"
|
||||
// core.JoinPath(args[:3]...) // → first 3 args as path
|
||||
func JoinPath(segments ...string) string {
|
||||
return strings.Join(segments, "/")
|
||||
return StringJoin(segments, "/")
|
||||
}
|
||||
|
||||
// IsFlag returns true if the argument starts with a dash.
|
||||
|
|
@ -37,7 +35,7 @@ func JoinPath(segments ...string) string {
|
|||
// core.IsFlag("-v") // true
|
||||
// core.IsFlag("deploy") // false
|
||||
func IsFlag(arg string) bool {
|
||||
return strings.HasPrefix(arg, "-")
|
||||
return HasPrefix(arg, "-")
|
||||
}
|
||||
|
||||
// FilterArgs removes empty strings and Go test runner flags from an argument list.
|
||||
|
|
@ -46,7 +44,7 @@ func IsFlag(arg string) bool {
|
|||
func FilterArgs(args []string) []string {
|
||||
var clean []string
|
||||
for _, a := range args {
|
||||
if a == "" || strings.HasPrefix(a, "-test.") {
|
||||
if a == "" || HasPrefix(a, "-test.") {
|
||||
continue
|
||||
}
|
||||
clean = append(clean, a)
|
||||
|
|
@ -66,12 +64,11 @@ func FilterArgs(args []string) []string {
|
|||
// "--v" → "", "", false (double dash, 1 char)
|
||||
// "hello" → "", "", false (not a flag)
|
||||
func ParseFlag(arg string) (key, value string, valid bool) {
|
||||
if strings.HasPrefix(arg, "--") {
|
||||
// Long flag: must be 2+ chars
|
||||
rest := strings.TrimPrefix(arg, "--")
|
||||
parts := strings.SplitN(rest, "=", 2)
|
||||
if HasPrefix(arg, "--") {
|
||||
rest := TrimPrefix(arg, "--")
|
||||
parts := SplitN(rest, "=", 2)
|
||||
name := parts[0]
|
||||
if utf8.RuneCountInString(name) < 2 {
|
||||
if RuneCount(name) < 2 {
|
||||
return "", "", false
|
||||
}
|
||||
if len(parts) == 2 {
|
||||
|
|
@ -80,12 +77,11 @@ func ParseFlag(arg string) (key, value string, valid bool) {
|
|||
return name, "", true
|
||||
}
|
||||
|
||||
if strings.HasPrefix(arg, "-") {
|
||||
// Short flag: must be exactly 1 char (rune)
|
||||
rest := strings.TrimPrefix(arg, "-")
|
||||
parts := strings.SplitN(rest, "=", 2)
|
||||
if HasPrefix(arg, "-") {
|
||||
rest := TrimPrefix(arg, "-")
|
||||
parts := SplitN(rest, "=", 2)
|
||||
name := parts[0]
|
||||
if utf8.RuneCountInString(name) != 1 {
|
||||
if RuneCount(name) != 1 {
|
||||
return "", "", false
|
||||
}
|
||||
if len(parts) == 2 {
|
||||
|
|
|
|||
70
tests/string_test.go
Normal file
70
tests/string_test.go
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
package core_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "forge.lthn.ai/core/go/pkg/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// --- String Operations ---
|
||||
|
||||
func TestHasPrefix_Good(t *testing.T) {
|
||||
assert.True(t, HasPrefix("--verbose", "--"))
|
||||
assert.True(t, HasPrefix("-v", "-"))
|
||||
assert.False(t, HasPrefix("hello", "-"))
|
||||
}
|
||||
|
||||
func TestHasSuffix_Good(t *testing.T) {
|
||||
assert.True(t, HasSuffix("test.go", ".go"))
|
||||
assert.False(t, HasSuffix("test.go", ".py"))
|
||||
}
|
||||
|
||||
func TestTrimPrefix_Good(t *testing.T) {
|
||||
assert.Equal(t, "verbose", TrimPrefix("--verbose", "--"))
|
||||
assert.Equal(t, "hello", TrimPrefix("hello", "--"))
|
||||
}
|
||||
|
||||
func TestTrimSuffix_Good(t *testing.T) {
|
||||
assert.Equal(t, "test", TrimSuffix("test.go", ".go"))
|
||||
assert.Equal(t, "test.go", TrimSuffix("test.go", ".py"))
|
||||
}
|
||||
|
||||
func TestContains_Good(t *testing.T) {
|
||||
assert.True(t, Contains("hello world", "world"))
|
||||
assert.False(t, Contains("hello world", "mars"))
|
||||
}
|
||||
|
||||
func TestSplit_Good(t *testing.T) {
|
||||
assert.Equal(t, []string{"a", "b", "c"}, Split("a/b/c", "/"))
|
||||
}
|
||||
|
||||
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 TestReplace_Good(t *testing.T) {
|
||||
assert.Equal(t, "deploy.to.homelab", Replace("deploy/to/homelab", "/", "."))
|
||||
}
|
||||
|
||||
func TestLower_Good(t *testing.T) {
|
||||
assert.Equal(t, "hello", Lower("HELLO"))
|
||||
}
|
||||
|
||||
func TestUpper_Good(t *testing.T) {
|
||||
assert.Equal(t, "HELLO", Upper("hello"))
|
||||
}
|
||||
|
||||
func TestTrim_Good(t *testing.T) {
|
||||
assert.Equal(t, "hello", Trim(" hello "))
|
||||
}
|
||||
|
||||
func TestRuneCount_Good(t *testing.T) {
|
||||
assert.Equal(t, 5, RuneCount("hello"))
|
||||
assert.Equal(t, 1, RuneCount("🔥"))
|
||||
assert.Equal(t, 0, RuneCount(""))
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue