Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
dfbe8ec
WIP Handling WithAlias conversion error
andreimerlescu Jun 16, 2025
41c0e08
WIP Handling WithAlias conversion error
andreimerlescu Jun 16, 2025
a14eba4
Fixed bug in StoreMap and enhanced testing on alias usage
andreimerlescu Apr 7, 2026
ecc1c6a
Integrated Copilot Code Review into hotfix branch after Claude analys…
andreimerlescu Apr 7, 2026
f35435c
Added test corner case for re-entry attempts with duplicate aliases
andreimerlescu Apr 7, 2026
4e29a8e
Ensure ListSeparator is being respected in List() and MapSeparator in…
andreimerlescu Apr 7, 2026
b24c9c6
Added removed error handling from merge conflict resolution that got …
andreimerlescu Apr 7, 2026
65d464a
Fix AssureBoolTrue/False invalid type, improve mutations_store commen…
Copilot Apr 7, 2026
7cbf644
Normalized the harvest option
andreimerlescu Apr 8, 2026
2c761a4
Added additional tests and added WithValidators for Plant use
andreimerlescu Apr 8, 2026
b9337d6
Update assure.go
andreimerlescu Apr 8, 2026
60969c8
Update assure.go
andreimerlescu Apr 8, 2026
76a427c
Fixed nil pointer possibility in persist() method
andreimerlescu Apr 8, 2026
123530e
updated equal check in mutation store and added checkMapString checkB…
andreimerlescu Apr 8, 2026
8b28af1
Update mutations_store.go
andreimerlescu Apr 8, 2026
2f31263
Update parsing.go
andreimerlescu Apr 8, 2026
518a2c1
Fixed a classic lock while sending to a channel whose consumer needs …
andreimerlescu Apr 8, 2026
faeea9d
Added tree.problem when duplicate alias is registered
andreimerlescu Apr 8, 2026
20f1c63
Improved the problem statement by adding existing to duplicate regist…
andreimerlescu Apr 8, 2026
ccb2c81
Reduced locking complexity in MutagenesisOfFig
andreimerlescu Apr 8, 2026
5a920d7
Reduced locking complexity in MutagenesisOfFig
andreimerlescu Apr 8, 2026
7e42769
addressed a bug in the SaveTo switch on the properties map
andreimerlescu Apr 8, 2026
0872de2
moved loadYAML to loading.go
andreimerlescu Apr 8, 2026
a0794e9
Exposed Divine Problems
andreimerlescu Apr 8, 2026
44ee001
renamed funcs and addressed data inconsistency in List() switch
andreimerlescu Apr 8, 2026
94125d5
Update alias_test.go
andreimerlescu Apr 8, 2026
5a097ae
Update savior_test.go
andreimerlescu Apr 8, 2026
9bee2fa
Restore expected list order in TestFigTree_SaveTo_ListRoundTrip to ma…
Copilot Apr 9, 2026
87e2417
Update VERSION
andreimerlescu Apr 9, 2026
5726ea2
Update VERSION
andreimerlescu Apr 9, 2026
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
34 changes: 31 additions & 3 deletions alias.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ import (
"strings"
)

// resolveName returns the canonical fig name for a given name or alias.
// Callers must hold tree.mu (read or write) before calling this.
func (tree *figTree) resolveName(name string) string {
name = strings.ToLower(name)
if canonical, exists := tree.aliases[name]; exists {
return canonical
}
return name
}

func (tree *figTree) Problems() []error {
tree.mu.RLock()
defer tree.mu.RUnlock()
return append([]error(nil), tree.problems...)
}

func (tree *figTree) WithAlias(name, alias string) Plant {
tree.mu.Lock()
defer tree.mu.Unlock()
Expand All @@ -13,17 +29,29 @@ func (tree *figTree) WithAlias(name, alias string) Plant {
if _, exists := tree.aliases[alias]; exists {
return tree
}
tree.aliases[alias] = name
if _, exists := tree.figs[name]; !exists {
tree.problems = append(tree.problems, fmt.Errorf("WithAlias: no fig named -%s", name))
return tree
}
ptr, ok := tree.values.Load(name)
if !ok {
fmt.Println("failed to load -" + name + " value")
tree.problems = append(tree.problems, fmt.Errorf("WithAlias: no value found for -%s", name))
return tree
}
value, ok := ptr.(*Value)
if !ok {
fmt.Println("failed to cast -" + name + " value")
tree.problems = append(tree.problems, fmt.Errorf("WithAlias: failed to cast value for -%s", name))
return tree
}
if _, exists := tree.figs[alias]; exists {
tree.problems = append(tree.problems, fmt.Errorf("WithAlias: alias -%s conflicts with existing fig name", alias))
return tree
}
if tree.flagSet.Lookup(alias) != nil {
tree.problems = append(tree.problems, fmt.Errorf("WithAlias: alias -%s conflicts with existing flag", alias))
return tree
}
tree.aliases[alias] = name // only register after all validations pass
tree.flagSet.Var(value, alias, "Alias of -"+name)
return tree
Comment thread
andreimerlescu marked this conversation as resolved.
Outdated
}
103 changes: 102 additions & 1 deletion alias_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,77 @@
package figtree

import (
"context"
"os"
"sync"
"testing"
"time"

"github.com/stretchr/testify/assert"
)

func TestWithAlias_ConflictsWithExistingFig(t *testing.T) {
os.Args = []string{os.Args[0]}
figs := With(Options{Germinate: true})
figs = figs.NewString("long", "default", "usage")
figs = figs.NewString("short", "default", "usage")

// Attempt to register "short" as an alias for "long" — but "short" is
// already a registered fig name, so this should record a problem and
// not panic.
figs = figs.WithAlias("long", "short")

assert.NoError(t, figs.Parse())
problems := figs.(*figTree).Problems()
assert.Len(t, problems, 1, "expected one problem recorded for alias conflict")
assert.Contains(t, problems[0].Error(), "conflicts with existing fig name")
}

func TestConcurrentPollinateReads(t *testing.T) {
os.Args = []string{os.Args[0]}
figs := With(Options{Pollinate: true, Germinate: true, Tracking: false})
figs.NewString("concurrent_key", "initial", "usage")
assert.NoError(t, figs.Parse())

ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()

go func() {
vals := []string{"alpha", "beta", "gamma"}
i := 0
ticker := time.NewTicker(5 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
os.Setenv("CONCURRENT_KEY", vals[i%3])
i++
Comment thread
andreimerlescu marked this conversation as resolved.
}
}
}()

var wg sync.WaitGroup
for n := 0; n < 10; n++ {
wg.Add(1)
go func() {
defer wg.Done()
ticker := time.NewTicker(10 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
_ = figs.String("concurrent_key")
}
}
}()
}
wg.Wait()
}

func TestWithAlias(t *testing.T) {
const cmdLong, cmdAliasLong, valueLong, usage = "long", "l", "default", "usage"
const cmdShort, cmdAliasShort, valueShort = "short", "s", "default"
Expand All @@ -25,6 +90,15 @@ func TestWithAlias(t *testing.T) {
figs = nil
})

t.Run("shorthand_notation", func(t *testing.T) {
os.Args = []string{os.Args[0], "-" + cmdAliasLong, valueLong}
figs := With(Options{Germinate: true, Tracking: false})
figs.NewString(cmdLong, valueLong, usage)
figs.WithAlias(cmdLong, cmdAliasLong)
assert.NoError(t, figs.Parse())
assert.Equal(t, valueLong, *figs.String(cmdLong))
})

t.Run("multiple_aliases", func(t *testing.T) {
os.Args = []string{os.Args[0]}
const k, v, u = "name", "yeshua", "the real name of god"
Expand All @@ -46,7 +120,11 @@ func TestWithAlias(t *testing.T) {
})

t.Run("complex_usage", func(t *testing.T) {
os.Args = []string{os.Args[0]}
os.Args = []string{
os.Args[0],
"-list", "three,four,five",
"-map", "four=4,five=5,six=6",
}
figs := With(Options{Germinate: true, Tracking: false})
// long
figs = figs.NewString(cmdLong, valueLong, usage)
Expand All @@ -58,6 +136,16 @@ func TestWithAlias(t *testing.T) {
figs = figs.WithAlias(cmdShort, cmdAliasShort)
figs = figs.WithValidator(cmdShort, AssureStringNotEmpty)

// list
figs.NewList("myList", []string{"one", "two", "three"}, "usage")
figs.WithValidator("myList", AssureListNotEmpty)
figs.WithAlias("myList", "list")

// map
figs.NewMap("myMap", map[string]string{"one": "1", "two": "2", "three": "3"}, "usage")
figs.WithValidator("myMap", AssureMapNotEmpty)
figs.WithAlias("myMap", "map")

assert.NoError(t, figs.Parse())

// long
Expand All @@ -66,6 +154,19 @@ func TestWithAlias(t *testing.T) {
// short
assert.Equal(t, valueShort, *figs.String(cmdShort))
assert.Equal(t, valueShort, *figs.String(cmdAliasShort))
// list
assert.NotEqual(t, []string{"one", "two", "three"}, *figs.List("myList"))
assert.Equal(t, []string{"five", "four", "three"}, *figs.List("myList"))

// list alias
assert.NotEqual(t, []string{"one", "two", "three"}, *figs.List("list"))
assert.Equal(t, []string{"five", "four", "three"}, *figs.List("list"))
Comment thread
andreimerlescu marked this conversation as resolved.
Outdated
// map
assert.NotEqual(t, map[string]string{"one": "1", "two": "2", "three": "3"}, *figs.Map("myMap"))
assert.Equal(t, map[string]string{"four": "4", "five": "5", "six": "6"}, *figs.Map("myMap"))
// map alias
assert.NotEqual(t, map[string]string{"one": "1", "two": "2", "three": "3"}, *figs.Map("map"))
assert.Equal(t, map[string]string{"four": "4", "five": "5", "six": "6"}, *figs.Map("map"))

figs = nil
})
Expand Down
28 changes: 14 additions & 14 deletions assure.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ import (
var AssureStringHasSuffix = func(suffix string) FigValidatorFunc {
return makeStringValidator(
func(s string) bool { return strings.HasSuffix(s, suffix) },
"string must have suffix %q, got %q",
fmt.Sprintf("string must have suffix %q, got %%q", suffix),
)
}

// AssureStringHasPrefix ensures a string starts with the given prefix.
var AssureStringHasPrefix = func(prefix string) FigValidatorFunc {
return makeStringValidator(
func(s string) bool { return strings.HasPrefix(s, prefix) },
"string must have prefix %q, got %q",
fmt.Sprintf("string must have prefix %q, got %%q", prefix),
)
}

Expand Down Expand Up @@ -57,7 +57,7 @@ var AssureStringNoPrefixes = func(prefixes []string) FigValidatorFunc {
}
}

// AssureStringHasSuffixes ensures a string ends with a suffix
// AssureStringHasSuffixes ensures a string ends with all suffixes in the list provided
// Returns a figValidatorFunc that checks for the substring (case-sensitive).
var AssureStringHasSuffixes = func(suffixes []string) FigValidatorFunc {
return func(value interface{}) error {
Expand Down Expand Up @@ -222,7 +222,7 @@ var AssureStringNotEmpty = func(value interface{}) error {
var AssureStringContains = func(substring string) FigValidatorFunc {
return makeStringValidator(
func(s string) bool { return strings.Contains(s, substring) },
"string must contain %q, got %q",
fmt.Sprintf("string must contain %q, got %%q", substring),
)
}

Expand All @@ -248,11 +248,11 @@ var AssureBoolTrue = func(value interface{}) error {
v := figFlesh{value, nil}
if v.IsBool() {
if !v.ToBool() {
return fmt.Errorf("value must be true, got false")
return ErrValue{ErrWayBeNegative, v.ToBool(), true}
Comment thread
andreimerlescu marked this conversation as resolved.
Outdated
}
return nil
}
return ErrInvalidType{tBool, value}
return ErrInvalidType{tString, value}
}
Comment thread
andreimerlescu marked this conversation as resolved.

// AssureBoolFalse ensures a boolean value is false.
Expand All @@ -261,11 +261,11 @@ var AssureBoolFalse = func(value interface{}) error {
v := figFlesh{value, nil}
if v.IsBool() {
if v.ToBool() {
return fmt.Errorf("value must be false, got true")
return ErrValue{ErrWayBePositive, v.ToBool(), false}
Comment thread
andreimerlescu marked this conversation as resolved.
Outdated
}
return nil
}
return ErrInvalidType{tBool, value}
return ErrInvalidType{tString, value}
}
Comment thread
andreimerlescu marked this conversation as resolved.

// AssureIntPositive ensures an integer is positive.
Expand Down Expand Up @@ -364,7 +364,7 @@ var AssureInt64LessThan = func(below int64) FigValidatorFunc {
return func(value interface{}) error {
v := figFlesh{value, nil}
if !v.IsInt64() {
return fmt.Errorf("value must be int64, got %d", value)
return ErrInvalidType{tInt64, value}
}
Comment thread
andreimerlescu marked this conversation as resolved.
i := v.ToInt64()
if i > below {
Expand Down Expand Up @@ -486,7 +486,7 @@ var AssureDurationGreaterThan = func(above time.Duration) FigValidatorFunc {
return func(value interface{}) error {
v := figFlesh{value, nil}
if !v.IsDuration() {
return fmt.Errorf("value must be a duration, got %v", value)
return ErrInvalidType{tDuration, value}
}
t := v.ToDuration()
if t < above {
Expand All @@ -502,7 +502,7 @@ var AssureDurationLessThan = func(below time.Duration) FigValidatorFunc {
return func(value interface{}) error {
v := figFlesh{value, nil}
if !v.IsDuration() {
return fmt.Errorf("value must be a duration, got %v", value)
return ErrInvalidType{tDuration, value}
}
t := v.ToDuration()
if t > below {
Expand Down Expand Up @@ -582,7 +582,7 @@ var AssureListMinLength = func(min int) FigValidatorFunc {
}
l := v.ToList()
if len(l) < min {
return fmt.Errorf("list is empty")
return fmt.Errorf("list must have at least %d elements, got %d", min, len(l))
}
return nil
}
Expand All @@ -592,7 +592,7 @@ var AssureListMinLength = func(min int) FigValidatorFunc {
// Returns a figValidatorFunc that checks for the presence of the value.
var AssureListContains = func(inside string) FigValidatorFunc {
return func(value interface{}) error {
v := NewFlesh(value)
v := figFlesh{value, nil}
if !v.IsList() {
return ErrInvalidType{tList, value}
}
Expand Down Expand Up @@ -712,7 +712,7 @@ var AssureMapValueMatches = func(key, match string) FigValidatorFunc {
return func(value interface{}) error {
v := figFlesh{value, nil}
if !v.IsMap() {
return fmt.Errorf("%s is not a map", key)
return ErrInvalidType{tMap, value}
}
m := v.ToMap()
if val, exists := m[key]; exists {
Expand Down
3 changes: 1 addition & 2 deletions callback.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package figtree

import (
"errors"
"strings"
)

// WithCallback allows you to assign a slice of CallbackFunc to a figFruit attached to a figTree.
Expand All @@ -27,7 +26,7 @@ import (
func (tree *figTree) WithCallback(name string, whenCallback CallbackWhen, runThis CallbackFunc) Plant {
tree.mu.Lock()
defer tree.mu.Unlock()
name = strings.ToLower(name)
name = tree.resolveName(name)
fruit, exists := tree.figs[name]
if !exists || fruit == nil {
return tree
Expand Down
1 change: 1 addition & 0 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
func (tree *figTree) ErrorFor(name string) error {
tree.mu.RLock()
defer tree.mu.RUnlock()
name = tree.resolveName(name)
fruit, exists := tree.figs[name]
if !exists || fruit == nil {
return fmt.Errorf("no tree named %s", name)
Expand Down
8 changes: 7 additions & 1 deletion figtree.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ func Grow() Plant {
func With(opts Options) Plant {
angel := atomic.Bool{}
angel.Store(true)
chBuf := 0
if opts.Harvest <= 0 && opts.Tracking {
chBuf = 1
} else if opts.Tracking {
chBuf = opts.Harvest
}
Comment thread
andreimerlescu marked this conversation as resolved.
Outdated
fig := &figTree{
ConfigFilePath: opts.ConfigFile,
ignoreEnv: opts.IgnoreEnvironment,
Expand All @@ -77,7 +83,7 @@ func With(opts Options) Plant {
values: &sync.Map{},
withered: make(map[string]witheredFig),
mu: sync.RWMutex{},
mutationsCh: make(chan Mutation),
mutationsCh: make(chan Mutation, chBuf),
flagSet: flag.NewFlagSet(os.Args[0], flag.ContinueOnError),
}
fig.flagSet.Usage = fig.Usage
Expand Down
10 changes: 7 additions & 3 deletions internals.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,13 @@ func (tree *figTree) loadINI(data []byte) error {

// setValuesFromMap uses the data map to store the configurable figs
func (tree *figTree) setValuesFromMap(data map[string]interface{}) error {
tree.mu.Lock()
defer tree.mu.Unlock()
for key, value := range data {
if _, exists := tree.figs[key]; exists {
if err := tree.mutateFig(key, value); err != nil {
name := tree.resolveName(key)
_, exists := tree.figs[name]
if exists {
if err := tree.mutateFig(name, value); err != nil {
return fmt.Errorf("error setting key %s: %w", key, err)
}
Comment thread
andreimerlescu marked this conversation as resolved.
}
Expand Down Expand Up @@ -269,7 +273,7 @@ func (tree *figTree) checkAndSetFromEnv(name string) {

// mutateFig replaces the value interface{} and sends a Mutation into Mutations
func (tree *figTree) mutateFig(name string, value interface{}) error {
name = strings.ToLower(name)
name = tree.resolveName(name)
def, ok := tree.figs[name]
if !ok || def == nil {
Comment thread
andreimerlescu marked this conversation as resolved.
return fmt.Errorf("no such fig: %s", name)
Expand Down
1 change: 1 addition & 0 deletions list_flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type ListFlag struct {
func (tree *figTree) ListValues(name string) []string {
tree.mu.Lock()
defer tree.mu.Unlock()
name = tree.resolveName(name)
if _, exists := tree.figs[name]; !exists {
return []string{}
}
Expand Down
Loading
Loading