diff --git a/README.md b/README.md index 13c6d63..8d68a59 100644 --- a/README.md +++ b/README.md @@ -203,7 +203,7 @@ figs := figtree.With(Options{Tracking: true, Harvest: 1776, Pollinate: true}) figs.NewString(kDomain, "", "Domain name") figs.WithValidator(kDomain, figtree.AssureStringLengthGreaterThan(3)) figs.WithValidator(kDomain, figtree.AssureStringHasPrefix("https://")) -figs.WithCallback(kDomain, figree.CallbackAfterVerify, func(value interface{}) error { +figs.WithCallback(kDomain, figtree.CallbackAfterVerify, func(value interface{}) error { var s string switch v := value.(type) { case *string: @@ -214,7 +214,7 @@ figs.WithCallback(kDomain, figree.CallbackAfterVerify, func(value interface{}) e // try connecting to the domain now return CheckAvailability(s) }) -figs.WithCallback(kDomain, figree.CallbackAfterRead, func(value interface{}) error { +figs.WithCallback(kDomain, figtree.CallbackAfterRead, func(value interface{}) error { // every time *figs.String(kDomain) is called, run this var s string switch v := value.(type) { diff --git a/VERSION b/VERSION index 0b71bb7..0797f73 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v2.0.10 \ No newline at end of file +v2.0.11 \ No newline at end of file diff --git a/alias.go b/alias.go index f422b67..6e3b232 100644 --- a/alias.go +++ b/alias.go @@ -1,10 +1,30 @@ package figtree -func (tree *figTree) WithAlias(name, alias string) { +import ( + "flag" + "fmt" + "strings" +) + +func (tree *figTree) WithAlias(name, alias string) Plant { tree.mu.Lock() defer tree.mu.Unlock() + name = strings.ToLower(name) + alias = strings.ToLower(alias) if _, exists := tree.aliases[alias]; exists { - return + return tree } tree.aliases[alias] = name + ptr, ok := tree.values.Load(name) + if !ok { + fmt.Println("failed to load -" + name + " value") + return tree + } + value, ok := ptr.(*Value) + if !ok { + fmt.Println("failed to cast -" + name + " value") + return tree + } + flag.Var(value, alias, "Alias of -"+name) + return tree } diff --git a/alias_test.go b/alias_test.go index c512a6f..abbd876 100644 --- a/alias_test.go +++ b/alias_test.go @@ -1,6 +1,7 @@ package figtree import ( + "os" "testing" "github.com/stretchr/testify/assert" @@ -11,26 +12,30 @@ func TestWithAlias(t *testing.T) { const cmdShort, cmdAliasShort, valueShort = "short", "s", "default" t.Run("basic_usage", func(t *testing.T) { + os.Args = []string{os.Args[0], "-l", t.Name()} figs := With(Options{Germinate: true, Tracking: false}) - figs.NewString(cmdLong, valueLong, usage) - figs.WithAlias(cmdLong, cmdAliasLong) + figs = figs.NewString(cmdLong, valueLong, usage) + figs = figs.WithAlias(cmdLong, cmdAliasLong) assert.NoError(t, figs.Parse()) - assert.Equal(t, valueLong, *figs.String(cmdLong)) - assert.Equal(t, valueLong, *figs.String(cmdAliasLong)) + assert.NotEqual(t, valueLong, *figs.String(cmdLong)) + assert.Equal(t, t.Name(), *figs.String(cmdLong)) + assert.NotEqual(t, valueLong, *figs.String(cmdAliasLong)) + assert.Equal(t, t.Name(), *figs.String(cmdAliasLong)) figs = nil }) 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" ka1 := "father" ka2 := "son" ka3 := "rauch-hokadesch" figs := With(Options{Germinate: true, Tracking: false}) - figs.NewString(k, v, u) - figs.WithAlias(k, ka1) - figs.WithAlias(k, ka2) - figs.WithAlias(k, ka3) + figs = figs.NewString(k, v, u) + figs = figs.WithAlias(k, ka1) + figs = figs.WithAlias(k, ka2) + figs = figs.WithAlias(k, ka3) assert.NoError(t, figs.Parse()) assert.Equal(t, v, *figs.String(k)) @@ -41,17 +46,17 @@ func TestWithAlias(t *testing.T) { }) t.Run("complex_usage", func(t *testing.T) { - + os.Args = []string{os.Args[0]} figs := With(Options{Germinate: true, Tracking: false}) // long - figs.NewString(cmdLong, valueLong, usage) - figs.WithAlias(cmdLong, cmdAliasLong) - figs.WithValidator(cmdLong, AssureStringNotEmpty) + figs = figs.NewString(cmdLong, valueLong, usage) + figs = figs.WithAlias(cmdLong, cmdAliasLong) + figs = figs.WithValidator(cmdLong, AssureStringNotEmpty) // short - figs.NewString(cmdShort, valueShort, usage) - figs.WithAlias(cmdShort, cmdAliasShort) - figs.WithValidator(cmdShort, AssureStringNotEmpty) + figs = figs.NewString(cmdShort, valueShort, usage) + figs = figs.WithAlias(cmdShort, cmdAliasShort) + figs = figs.WithValidator(cmdShort, AssureStringNotEmpty) assert.NoError(t, figs.Parse()) @@ -63,24 +68,25 @@ func TestWithAlias(t *testing.T) { assert.Equal(t, valueShort, *figs.String(cmdAliasShort)) figs = nil - }) t.Run("alias_with_int", func(t *testing.T) { + os.Args = []string{os.Args[0]} figs := With(Options{Germinate: true}) - figs.NewInt("count", 42, "usage") - figs.WithAlias("count", "c") + figs = figs.NewInt("count", 42, "usage") + figs = figs.WithAlias("count", "c") assert.NoError(t, figs.Parse()) assert.Equal(t, 42, *figs.Int("count")) assert.Equal(t, 42, *figs.Int("c")) }) t.Run("alias_conflict", func(t *testing.T) { + os.Args = []string{os.Args[0]} figs := With(Options{Germinate: true}) - figs.NewString("one", "value1", "usage") - figs.NewString("two", "value2", "usage") - figs.WithAlias("one", "x") - figs.WithAlias("two", "x") // Should this overwrite or be ignored? + figs = figs.NewString("one", "value1", "usage") + figs = figs.NewString("two", "value2", "usage") + figs = figs.WithAlias("one", "x") + figs = figs.WithAlias("two", "x") // Should this overwrite or be ignored? assert.NoError(t, figs.Parse()) assert.Equal(t, "value1", *figs.String("x")) // Clarify expected behavior }) diff --git a/assure.go b/assure.go index f5408e5..994fcc7 100644 --- a/assure.go +++ b/assure.go @@ -27,7 +27,7 @@ var AssureStringHasPrefix = func(prefix string) FigValidatorFunc { // Returns a figValidatorFunc that checks for the substring (case-sensitive). var AssureStringNoSuffixes = func(suffixes []string) FigValidatorFunc { return func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if v.IsString() { for _, suffix := range suffixes { if strings.HasSuffix(v.ToString(), suffix) { @@ -44,7 +44,7 @@ var AssureStringNoSuffixes = func(suffixes []string) FigValidatorFunc { // Returns a figValidatorFunc that checks for the substring (case-sensitive). var AssureStringNoPrefixes = func(prefixes []string) FigValidatorFunc { return func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if v.IsString() { for _, prefix := range prefixes { if strings.HasPrefix(v.ToString(), prefix) { @@ -61,7 +61,7 @@ var AssureStringNoPrefixes = func(prefixes []string) FigValidatorFunc { // Returns a figValidatorFunc that checks for the substring (case-sensitive). var AssureStringHasSuffixes = func(suffixes []string) FigValidatorFunc { return func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if v.IsString() { for _, suffix := range suffixes { if !strings.HasSuffix(v.ToString(), suffix) { @@ -78,7 +78,7 @@ var AssureStringHasSuffixes = func(suffixes []string) FigValidatorFunc { // Returns a figValidatorFunc that checks for the substring (case-sensitive). var AssureStringHasPrefixes = func(prefixes []string) FigValidatorFunc { return func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if v.IsString() { for _, prefix := range prefixes { if !strings.HasPrefix(v.ToString(), prefix) { @@ -95,7 +95,7 @@ var AssureStringHasPrefixes = func(prefixes []string) FigValidatorFunc { // Returns a figValidatorFunc that checks for the substring (case-sensitive). var AssureStringNoSuffix = func(suffix string) FigValidatorFunc { return func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if v.IsString() { vs := v.ToString() if strings.HasSuffix(vs, suffix) { @@ -111,7 +111,7 @@ var AssureStringNoSuffix = func(suffix string) FigValidatorFunc { // Returns a figValidatorFunc that checks for the substring (case-sensitive). var AssureStringNoPrefix = func(prefix string) FigValidatorFunc { return func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if v.IsString() { vs := v.ToString() if strings.HasPrefix(vs, prefix) { @@ -127,7 +127,7 @@ var AssureStringNoPrefix = func(prefix string) FigValidatorFunc { // Returns a figValidatorFunc that checks for the substring (case-sensitive). var AssureStringLengthLessThan = func(length int) FigValidatorFunc { return func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if v.IsString() { vs := v.ToString() if len(vs) > length { @@ -143,7 +143,7 @@ var AssureStringLengthLessThan = func(length int) FigValidatorFunc { // Returns a figValidatorFunc that checks for the substring (case-sensitive). var AssureStringLengthGreaterThan = func(length int) FigValidatorFunc { return func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if v.IsString() { vs := v.ToString() if len(vs) < length { @@ -159,7 +159,7 @@ var AssureStringLengthGreaterThan = func(length int) FigValidatorFunc { // Returns a figValidatorFunc that checks for the substring (case-sensitive). var AssureStringSubstring = func(sub string) FigValidatorFunc { return func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if v.IsString() { vs := v.ToString() if !strings.Contains(vs, sub) { @@ -175,7 +175,7 @@ var AssureStringSubstring = func(sub string) FigValidatorFunc { // Returns a figValidatorFunc that checks for the substring (case-sensitive). var AssureStringLength = func(length int) FigValidatorFunc { return func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if v.IsString() { vs := v.ToString() if len(vs) < length { @@ -192,7 +192,7 @@ var AssureStringLength = func(length int) FigValidatorFunc { // Returns a figValidatorFunc that checks for the substring (case-sensitive). var AssureStringNotLength = func(length int) FigValidatorFunc { return func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if v.IsString() { vs := v.ToString() if len(vs) == length { @@ -207,7 +207,7 @@ var AssureStringNotLength = func(length int) FigValidatorFunc { // AssureStringNotEmpty ensures a string is not empty. // Returns an error if the value is an empty string or not a string. var AssureStringNotEmpty = func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if v.IsString() { if len(v.ToString()) == 0 { return fmt.Errorf("empty string") @@ -230,7 +230,7 @@ var AssureStringContains = func(substring string) FigValidatorFunc { // Returns an error if the substring is not found or if the value is not a string. var AssureStringNotContains = func(substring string) FigValidatorFunc { return func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if v.IsString() { vs := v.ToString() if strings.Contains(vs, substring) { @@ -245,7 +245,7 @@ var AssureStringNotContains = func(substring string) FigValidatorFunc { // AssureBoolTrue ensures a boolean value is true. // Returns an error if the value is false or not a bool. var AssureBoolTrue = func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if v.IsBool() { if !v.ToBool() { return fmt.Errorf("value must be true, got false") @@ -258,7 +258,7 @@ var AssureBoolTrue = func(value interface{}) error { // AssureBoolFalse ensures a boolean value is false. // Returns an error if the value is true or not a bool. var AssureBoolFalse = func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if v.IsBool() { if v.ToBool() { return fmt.Errorf("value must be false, got true") @@ -271,7 +271,7 @@ var AssureBoolFalse = func(value interface{}) error { // AssureIntPositive ensures an integer is positive. // Returns an error if the value is zero or negative, or not an int. var AssureIntPositive = func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if v.IsInt() { if v.ToInt() < 0 { return fmt.Errorf("value must be positive, got %d", v.ToInt()) @@ -284,7 +284,7 @@ var AssureIntPositive = func(value interface{}) error { // AssureIntNegative ensures an integer is negative. // Returns an error if the value is zero or positive, or not an int. var AssureIntNegative = func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if v.IsInt() { if v.ToInt() > 0 { return fmt.Errorf("value must be negative, got %d", v.ToInt()) @@ -298,7 +298,7 @@ var AssureIntNegative = func(value interface{}) error { // Returns an error if the value is below, or not an int. var AssureIntGreaterThan = func(above int) FigValidatorFunc { return func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if !v.IsInt() { return fmt.Errorf("invalid type, expected int, got %T", value) } @@ -314,7 +314,7 @@ var AssureIntGreaterThan = func(above int) FigValidatorFunc { // Returns an error if the value is above, or not an int. var AssureIntLessThan = func(below int) FigValidatorFunc { return func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if !v.IsInt() { return fmt.Errorf("invalid type, expected int, got %T", value) } @@ -330,7 +330,7 @@ var AssureIntLessThan = func(below int) FigValidatorFunc { // Returns an error if the value is outside the range or not an int. var AssureIntInRange = func(min, max int) FigValidatorFunc { return func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if !v.IsInt() { return fmt.Errorf("invalid type, expected int, got %T", value) } @@ -346,7 +346,7 @@ var AssureIntInRange = func(min, max int) FigValidatorFunc { // Returns an error if the value is below, or not an int. var AssureInt64GreaterThan = func(above int64) FigValidatorFunc { return func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if !v.IsInt64() { return fmt.Errorf("invalid type, expected int64, got %T", value) } @@ -362,7 +362,7 @@ var AssureInt64GreaterThan = func(above int64) FigValidatorFunc { // Returns an error if the value is above, or not an int. var AssureInt64LessThan = func(below int64) FigValidatorFunc { return func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if !v.IsInt64() { return fmt.Errorf("value must be int64, got %d", value) } @@ -377,7 +377,7 @@ var AssureInt64LessThan = func(below int64) FigValidatorFunc { // AssureInt64Positive ensures an int64 is positive. // Returns an error if the value is zero or negative, or not an int64. var AssureInt64Positive = func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if !v.IsInt64() { return fmt.Errorf("invalid type, expected int64, got %T", value) } @@ -392,7 +392,7 @@ var AssureInt64Positive = func(value interface{}) error { // Returns a figValidatorFunc that checks the value against min and max. var AssureInt64InRange = func(min, max int64) FigValidatorFunc { return func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if !v.IsInt64() { return fmt.Errorf("invalid type, expected int64, got %T", value) } @@ -407,7 +407,7 @@ var AssureInt64InRange = func(min, max int64) FigValidatorFunc { // AssureFloat64Positive ensures a float64 is positive. // Returns an error if the value is zero or negative, or not a float64. var AssureFloat64Positive = func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if !v.IsFloat64() { return fmt.Errorf("invalid type, expected float64, got %T", value) } @@ -422,7 +422,7 @@ var AssureFloat64Positive = func(value interface{}) error { // Returns an error if the value is outside the range or not a float64. var AssureFloat64InRange = func(min, max float64) FigValidatorFunc { return func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if !v.IsFloat64() { return fmt.Errorf("invalid type, expected float64, got %T", value) } @@ -438,7 +438,7 @@ var AssureFloat64InRange = func(min, max float64) FigValidatorFunc { // Returns an error if the value is below, or not an int. var AssureFloat64GreaterThan = func(above float64) FigValidatorFunc { return func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if !v.IsFloat64() { return fmt.Errorf("invalid type, expected float64, got %T", value) } @@ -454,7 +454,7 @@ var AssureFloat64GreaterThan = func(above float64) FigValidatorFunc { // Returns an error if the value is above, or not an int. var AssureFloat64LessThan = func(below float64) FigValidatorFunc { return func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if !v.IsFloat64() { return fmt.Errorf("invalid type, expected float64, got %T", value) } @@ -469,7 +469,7 @@ var AssureFloat64LessThan = func(below float64) FigValidatorFunc { // AssureFloat64NotNaN ensures a float64 is not NaN. // Returns an error if the value is NaN or not a float64. var AssureFloat64NotNaN = func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if !v.IsFloat64() { return fmt.Errorf("invalid type, expected float64, got %T", value) } @@ -484,7 +484,7 @@ var AssureFloat64NotNaN = func(value interface{}) error { // Returns an error if the value is below, or not an int. var AssureDurationGreaterThan = func(above time.Duration) FigValidatorFunc { return func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if !v.IsDuration() { return fmt.Errorf("value must be a duration, got %v", value) } @@ -500,7 +500,7 @@ var AssureDurationGreaterThan = func(above time.Duration) FigValidatorFunc { // Returns an error if the value is below, or not an int. var AssureDurationLessThan = func(below time.Duration) FigValidatorFunc { return func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if !v.IsDuration() { return fmt.Errorf("value must be a duration, got %v", value) } @@ -515,7 +515,7 @@ var AssureDurationLessThan = func(below time.Duration) FigValidatorFunc { // AssureDurationPositive ensures a time.Duration is positive. // Returns an error if the value is zero or negative, or not a time.Duration. var AssureDurationPositive = func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if !v.IsDuration() { return fmt.Errorf("invalid type, expected time.Duration, got %T", value) } @@ -530,7 +530,7 @@ var AssureDurationPositive = func(value interface{}) error { // Returns a figValidatorFunc that checks the duration against the minimum. var AssureDurationMin = func(min time.Duration) FigValidatorFunc { return func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if !v.IsDuration() { return fmt.Errorf("value must be a duration, got %s", v) } @@ -546,7 +546,7 @@ var AssureDurationMin = func(min time.Duration) FigValidatorFunc { // Returns an error if the value exceeds the max or is not a time.Duration. var AssureDurationMax = func(max time.Duration) FigValidatorFunc { return func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if !v.IsDuration() { return fmt.Errorf("invalid type, expected time.Duration, got %T", value) } @@ -561,7 +561,7 @@ var AssureDurationMax = func(max time.Duration) FigValidatorFunc { // AssureListNotEmpty ensures a list is not empty. // Returns an error if the list has no elements or is not a ListFlag. var AssureListNotEmpty = func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if !v.IsList() { return fmt.Errorf("invalid type, expected *ListFlag or []string, got %T", v) } @@ -576,7 +576,7 @@ var AssureListNotEmpty = func(value interface{}) error { // Returns an error if the list is too short or not a ListFlag. var AssureListMinLength = func(min int) FigValidatorFunc { return func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if !v.IsList() { return fmt.Errorf("invalid type, expected *ListFlag or []string, got %T", v) } @@ -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 := figFlesh{value} + v := NewFlesh(value) if !v.IsList() { return fmt.Errorf("invalid type, expected ListFlag or []string, got %T", value) } @@ -610,7 +610,7 @@ var AssureListContains = func(inside string) FigValidatorFunc { // Returns a figValidatorFunc that checks for the presence of the value. var AssureListNotContains = func(not string) FigValidatorFunc { return func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if !v.IsList() { return fmt.Errorf("invalid type, expected *ListFlag, []string, or *[]string, got %T", v) } @@ -629,7 +629,7 @@ var AssureListNotContains = func(not string) FigValidatorFunc { // or the type is invalid. var AssureListContainsKey = func(key string) FigValidatorFunc { return func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if !v.IsList() { return fmt.Errorf("invalid type, expected *ListFlag or []string, got %T", value) } @@ -648,7 +648,7 @@ var AssureListContainsKey = func(key string) FigValidatorFunc { // or the type is invalid. var AssureListLength = func(length int) FigValidatorFunc { return func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if !v.IsList() { return fmt.Errorf("invalid type, expected *ListFlag or []string, got %T", value) } @@ -663,7 +663,7 @@ var AssureListLength = func(length int) FigValidatorFunc { // AssureMapNotEmpty ensures a map is not empty. // Returns an error if the map has no entries or is not a MapFlag. var AssureMapNotEmpty = func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if !v.IsMap() { return fmt.Errorf("invalid type, expected *ListFlag or []string, got %T", v) } @@ -678,7 +678,7 @@ var AssureMapNotEmpty = func(value interface{}) error { // Returns an error if the key is missing or the value is not a MapFlag. var AssureMapHasKey = func(key string) FigValidatorFunc { return func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if !v.IsMap() { return fmt.Errorf("invalid type, got %T", value) } @@ -694,7 +694,7 @@ var AssureMapHasKey = func(key string) FigValidatorFunc { // Returns an error if the key is missing or the value is not a MapFlag. var AssureMapHasNoKey = func(key string) FigValidatorFunc { return func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if !v.IsMap() { return fmt.Errorf("invalid type, got %T", value) } @@ -710,7 +710,7 @@ var AssureMapHasNoKey = func(key string) FigValidatorFunc { // Returns a figValidatorFunc that checks for the key-value pair. var AssureMapValueMatches = func(key, match string) FigValidatorFunc { return func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if !v.IsMap() { return fmt.Errorf("%s is not a map", key) } @@ -730,7 +730,7 @@ var AssureMapValueMatches = func(key, match string) FigValidatorFunc { var AssureMapHasKeys = func(keys []string) FigValidatorFunc { return func(value interface{}) error { var missing []string - v := figFlesh{value} + v := figFlesh{value, nil} if !v.IsMap() { return fmt.Errorf("invalid type, expected map[string]string, got %T", v) } @@ -752,7 +752,7 @@ var AssureMapHasKeys = func(keys []string) FigValidatorFunc { // if the length differs or the type is invalid. var AssureMapLength = func(length int) FigValidatorFunc { return func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if !v.IsMap() { return fmt.Errorf("invalid type, expected *MapFlag or map[string]string, got %T", value) } @@ -769,7 +769,7 @@ var AssureMapLength = func(length int) FigValidatorFunc { // if the length differs or the type is invalid. var AssureMapNotLength = func(length int) FigValidatorFunc { return func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if !v.IsMap() { return fmt.Errorf("invalid type, got %T", value) } diff --git a/callback.go b/callback.go index 2a7d3cb..9b0b72c 100644 --- a/callback.go +++ b/callback.go @@ -2,6 +2,7 @@ package figtree import ( "errors" + "strings" ) // WithCallback allows you to assign a slice of CallbackFunc to a figFruit attached to a figTree. @@ -26,14 +27,9 @@ import ( func (tree *figTree) WithCallback(name string, whenCallback CallbackWhen, runThis CallbackFunc) Plant { tree.mu.Lock() defer tree.mu.Unlock() + name = strings.ToLower(name) fruit, exists := tree.figs[name] if !exists || fruit == nil { - tree.mu.Unlock() - tree.Resurrect(name) - tree.mu.Lock() - fruit = tree.figs[name] - } - if fruit == nil { return tree } if fruit.HasRule(RuleNoCallbacks) { @@ -58,16 +54,21 @@ func (tree *figTree) runCallbacks(callbackOn CallbackWhen) error { if fig.HasRule(RuleNoCallbacks) { continue } - err := fig.runCallbacks(callbackOn) + err := fig.runCallbacks(tree, callbackOn) if err != nil { return err } } + for _, fruit := range tree.figs { + if fruit.Error != nil { + return fruit.Error + } + } return nil } // runCallbacks will take each registered callback and run it against the fig fruit -func (fig *figFruit) runCallbacks(callbackOn CallbackWhen) error { +func (fig *figFruit) runCallbacks(tree *figTree, callbackOn CallbackWhen) error { if fig.Error != nil { return fig.Error } @@ -77,9 +78,15 @@ func (fig *figFruit) runCallbacks(callbackOn CallbackWhen) error { errs := make([]error, len(fig.Callbacks)) for _, callback := range fig.Callbacks { if callback.CallbackWhen == callbackOn { - err := callback.CallbackFunc(fig.Flesh) + value, err := tree.from(fig.name) + if err != nil { + errs = append(errs, err) + continue + } + err = callback.CallbackFunc(value.Value) if err != nil { errs = append(errs, err) + continue } } } diff --git a/callback_test.go b/callback_test.go index d25c03c..65b555d 100644 --- a/callback_test.go +++ b/callback_test.go @@ -9,8 +9,8 @@ import ( func TestTree_WithCallback(t *testing.T) { figs := With(Options{Germinate: true, Pollinate: false, IgnoreEnvironment: true}) - figs.NewString(t.Name(), t.Name(), "usage") - figs.WithCallback(t.Name(), CallbackAfterVerify, func(value interface{}) error { + figs = figs.NewString(t.Name(), t.Name(), "usage") + figs = figs.WithCallback(t.Name(), CallbackAfterVerify, func(value interface{}) error { if value == nil { return nil } @@ -22,7 +22,7 @@ func TestTree_WithCallback(t *testing.T) { } return nil }) - figs.WithCallback(t.Name(), CallbackAfterRead, func(value interface{}) error { + figs = figs.WithCallback(t.Name(), CallbackAfterRead, func(value interface{}) error { if value == nil { return nil } @@ -34,7 +34,7 @@ func TestTree_WithCallback(t *testing.T) { } return nil }) - figs.WithCallback(t.Name(), CallbackAfterChange, func(value interface{}) error { + figs = figs.WithCallback(t.Name(), CallbackAfterChange, func(value interface{}) error { if value == nil { return nil } @@ -52,7 +52,7 @@ func TestTree_WithCallback(t *testing.T) { property := *figs.String(t.Name()) assert.NotNil(t, property) time.Sleep(369 * time.Millisecond) - assert.NotNil(t, figs.Fig(t.Name())) + assert.NotNil(t, figs.FigFlesh(t.Name())) figs.StoreString(t.Name(), "new value") time.Sleep(369 * time.Millisecond) diff --git a/conversions.go b/conversions.go index 086d862..0ee3cb3 100644 --- a/conversions.go +++ b/conversions.go @@ -4,6 +4,7 @@ import ( "fmt" "strconv" "strings" + "time" ) // INTERFACE TYPE CONVERSIONS @@ -11,6 +12,10 @@ import ( // toInt returns an interface{} as an int or returns an error func toInt(value interface{}) (int, error) { switch v := value.(type) { + case *Value: + return toInt(v.Value) + case *figFlesh: + return toInt(v.AsIs()) case int: return v, nil case *int: @@ -41,6 +46,10 @@ func toInt(value interface{}) (int, error) { // toInt64 returns an interface{} as an int64 or returns an error func toInt64(value interface{}) (int64, error) { switch v := value.(type) { + case *Value: + return toInt64(v.Value) + case *figFlesh: + return toInt64(v.AsIs()) case int: return int64(v), nil case *int: @@ -71,6 +80,10 @@ func toInt64(value interface{}) (int64, error) { // toFloat64 returns an interface{} as an float64 or returns an error func toFloat64(value interface{}) (float64, error) { switch v := value.(type) { + case *Value: + return toFloat64(v.Value) + case *figFlesh: + return toFloat64(v.AsIs()) case *float64: return *v, nil case float64: @@ -95,10 +108,38 @@ func toFloat64(value interface{}) (float64, error) { // toString returns an interface{} as a string or returns an error func toString(value interface{}) (string, error) { switch v := value.(type) { + case MapFlag: + return toString(v.values) + case *MapFlag: + return toString(v.values) + case ListFlag: + return toString(v.values) + case *ListFlag: + return toString(v.values) + case *Value: + return v.String(), nil + case Value: + return v.String(), nil + case *figFlesh: + return toString(v.AsIs()) + case figFlesh: + return toString(v.AsIs()) case *string: return *v, nil case string: return v, nil + case int: + return strconv.Itoa(v), nil + case *int: + return strconv.Itoa(*v), nil + case time.Duration: + return v.String(), nil + case *time.Duration: + return v.String(), nil + case int64: + return strconv.FormatInt(v, 10), nil + case *int64: + return strconv.FormatInt(*v, 10), nil case *float64: return strconv.FormatFloat(*v, 'f', -1, 64), nil case float64: @@ -108,29 +149,33 @@ func toString(value interface{}) (string, error) { case bool: return strconv.FormatBool(v), nil case []string: - return strings.Join(v, ","), nil + return strings.Join(v, ListSeparator), nil case *[]string: - return strings.Join(*v, ","), nil + return strings.Join(*v, ListSeparator), nil case *map[string]string: parts := make([]string, 0, len(*v)) for k, x := range *v { - parts = append(parts, fmt.Sprintf("%s=%s", k, x)) + parts = append(parts, fmt.Sprintf("%s%s%s", k, MapKeySeparator, x)) } - return strings.Join(parts, ","), nil + return strings.Join(parts, MapSeparator), nil case map[string]string: parts := make([]string, 0, len(v)) for k, x := range v { - parts = append(parts, fmt.Sprintf("%s=%s", k, x)) + parts = append(parts, fmt.Sprintf("%s%s%s", k, MapKeySeparator, x)) } - return strings.Join(parts, ","), nil + return strings.Join(parts, MapSeparator), nil default: - return "", fmt.Errorf("cannot convert %v to string", value) + return "", fmt.Errorf("cannot convert %v %T to string", value, value) } } // toBool returns an interface{} as a bool or returns an error func toBool(value interface{}) (bool, error) { switch v := value.(type) { + case *Value: + return toBool(v.Value) + case *figFlesh: + return toBool(v.AsIs()) case *string: return strconv.ParseBool(*v) case string: @@ -147,6 +192,14 @@ func toBool(value interface{}) (bool, error) { // toStringSlice returns an interface{} as a []string{} or returns an error func toStringSlice(value interface{}) ([]string, error) { switch v := value.(type) { + case *Value: + return toStringSlice(v.Value) + case *figFlesh: + return toStringSlice(v.AsIs()) + case *ListFlag: + return toStringSlice(v.values) + case ListFlag: + return toStringSlice(v.values) case []string: return v, nil case *[]string: @@ -165,20 +218,34 @@ func toStringSlice(value interface{}) ([]string, error) { if *v == "" { return []string{}, nil } - return strings.Split(*v, ","), nil + if strings.Contains(*v, MapKeySeparator) { + return nil, fmt.Errorf("cannot convert %v to []string", value) + } + return strings.Split(*v, ListSeparator), nil case string: if v == "" { return []string{}, nil } - return strings.Split(v, ","), nil + if strings.Contains(v, MapSeparator) && strings.Contains(v, MapKeySeparator) { + return nil, fmt.Errorf("cannot convert map %v to []string", value) + } + return strings.Split(v, ListSeparator), nil default: - return nil, fmt.Errorf("cannot convert %v to []string", value) + return nil, fmt.Errorf("cannot convert %v %T to []string", value, value) } } // toStringMap returns an interface{} as a map[string]string or returns an error func toStringMap(value interface{}) (map[string]string, error) { switch v := value.(type) { + case *Value: + return toStringMap(v.Value) + case *figFlesh: + return toStringMap(v.AsIs()) + case *MapFlag: + return toStringMap(v.values) + case MapFlag: + return toStringMap(v.values) case map[string]string: return v, nil case *map[string]string: @@ -197,10 +264,10 @@ func toStringMap(value interface{}) (map[string]string, error) { if *v == "" { return map[string]string{}, nil } - pairs := strings.Split(*v, ",") + pairs := strings.Split(*v, MapSeparator) result := make(map[string]string) for _, pair := range pairs { - kv := strings.SplitN(pair, "=", 2) + kv := strings.SplitN(pair, MapKeySeparator, 2) if len(kv) != 2 { return nil, fmt.Errorf("invalid map item: %s", pair) } @@ -211,10 +278,10 @@ func toStringMap(value interface{}) (map[string]string, error) { if v == "" { return map[string]string{}, nil } - pairs := strings.Split(v, ",") + pairs := strings.Split(v, MapSeparator) result := make(map[string]string) for _, pair := range pairs { - kv := strings.SplitN(pair, "=", 2) + kv := strings.SplitN(pair, MapKeySeparator, 2) if len(kv) != 2 { return nil, fmt.Errorf("invalid map item: %s", pair) } diff --git a/conversions_test.go b/conversions_test.go index 4f9052a..418d907 100644 --- a/conversions_test.go +++ b/conversions_test.go @@ -2,11 +2,33 @@ package figtree import ( "fmt" + "os" "testing" "github.com/stretchr/testify/assert" ) +func TestFigFlesh_ToString(t *testing.T) { + name := t.Name() + flesh := NewFlesh(name) + flesh = NewFlesh(flesh) + assert.Equal(t, name, flesh.ToString()) +} + +const TheName = "YAHUAH" + +func TestFigTree_WithCallback(t *testing.T) { + os.Args = []string{os.Args[0], "-name", TheName} + figs := With(Options{Germinate: true, Tracking: false, IgnoreEnvironment: true}) + figs.NewString("name", "", "Your Name") + figs.WithCallback("name", CallbackAfterVerify, func(value interface{}) error { + v := NewFlesh(value) + assert.Equal(t, TheName, v.AsIs()) + return nil + }) + assert.NoError(t, figs.Parse()) +} + func Test_toBool(t *testing.T) { type args struct { value interface{} @@ -280,10 +302,10 @@ func Test_toString(t *testing.T) { wantErr: assert.NoError, }, { - name: "Int should fail", + name: "Int should succeed", args: args{value: 42}, - want: "", - wantErr: assert.Error, + want: "42", + wantErr: assert.NoError, }, { name: "Nil should fail", @@ -352,8 +374,8 @@ func Test_toStringMap(t *testing.T) { { name: "Map with non-string value", args: args{value: map[string]interface{}{"key": 42}}, - want: nil, - wantErr: assert.Error, + want: map[string]string{"key": "42"}, + wantErr: assert.NoError, }, { name: "Int should fail", @@ -410,8 +432,8 @@ func Test_toStringSlice(t *testing.T) { { name: "Slice with non-string", args: args{value: []interface{}{"a", 42, "c"}}, - want: nil, - wantErr: assert.Error, + want: []string{"a", "42", "c"}, + wantErr: assert.NoError, }, { name: "Int should fail", diff --git a/figtree.go b/figtree.go index 3c32280..8430103 100644 --- a/figtree.go +++ b/figtree.go @@ -74,6 +74,7 @@ func With(opts Options) Plant { problems: make([]error, 0), aliases: make(map[string]string), figs: make(map[string]*figFruit), + values: &sync.Map{}, withered: make(map[string]witheredFig), mu: sync.RWMutex{}, mutationsCh: make(chan Mutation), diff --git a/figtree_test.go b/figtree_test.go index eecfcff..473cd02 100644 --- a/figtree_test.go +++ b/figtree_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "strings" "sync" "sync/atomic" "testing" @@ -12,6 +13,706 @@ import ( "github.com/stretchr/testify/assert" ) +func TestValue_Set_ErrorHandling(t *testing.T) { + tests := []struct { + name string + mutagenesis Mutagenesis + input string + expectError bool + expectedValue interface{} // Expected value after failed conversion (often default or zero) + }{ + {"String_Valid", tString, "hello", false, "hello"}, + {"Bool_Invalid", tBool, "not-a-bool", true, false}, // Assuming bool defaults to false on conversion error + {"Int_Invalid", tInt, "abc", true, 0}, // Assuming int defaults to 0 on conversion error + {"Int64_Invalid", tInt64, "xyz", true, int64(0)}, + {"Float64_Invalid", tFloat64, "badfloat", true, 0.0}, + {"Duration_Invalid", tDuration, "not-a-duration", true, time.Duration(0)}, // Note: toInt64 from string might return 0 + {"List_InvalidFormat", tList, "a,b=c", true, []string{"a", "b=c"}}, // Assumes partial parse or specific error + {"Map_InvalidFormat", tMap, "k1v1,k2", true, map[string]string{}}, // Assumes map fully resets on invalid item + {"List_Valid", tList, "item1,item2", false, []string{"item1", "item2"}}, + {"Map_Valid", tMap, "key1=val1,key2=val2", false, map[string]string{"key1": "val1", "key2": "val2"}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + val := &Value{Value: tt.expectedValue, Mutagensis: tt.mutagenesis} // Initialize with expected zero/default + err := val.Set(tt.input) + + if tt.expectError { + assert.Error(t, err) + // For primitive types, the underlying value might stay its initial default (zero value) + // For List/Map, check how your Set handles errors. + // For List/Map, they might partial parse or reset, depending on implementation. + // This needs careful assertion based on exact behavior of ListFlag/MapFlag Set. + if _, ok := tt.expectedValue.(map[string]string); ok { + assert.Equal(t, tt.expectedValue, val.Flesh().ToMap(), "Value should be default/empty on map error") + } else if _, ok := tt.expectedValue.([]string); ok { + assert.Equal(t, tt.expectedValue, val.Flesh().ToList(), "Value should be default/empty on list error") + } else { + assert.Equal(t, tt.expectedValue, val.Value, "Value should be default on primitive error") + } + + } else { + assert.NoError(t, err) + // Re-fetch value correctly based on what Set assigns + actualValue := val.Value + // For List/Map, they might be *MapFlag or *ListFlag, need to unwrap + if tf, ok := actualValue.(MapFlag); ok { + actualValue = tf.values + } else if tf, ok := actualValue.(*MapFlag); ok { + actualValue = tf.values + } else if tf, ok := actualValue.(ListFlag); ok { + actualValue = tf.values + } else if tf, ok := actualValue.(*ListFlag); ok { + actualValue = tf.values + } + assert.Equal(t, tt.expectedValue, actualValue, "Value should be correctly set") + } + }) + } +} + +func TestParse_InvalidFlagInput(t *testing.T) { + tests := []struct { + name string + flagName string + defaultValue interface{} + usage string + argValue string + }{ + {"IntFlag_InvalidString", "port", zeroInt, "port number", "not-a-number"}, + {"BoolFlag_InvalidString", "debug", zeroBool, "debug mode", "maybe"}, + {"Float64Flag_InvalidString", "ratio", zeroFloat64, "ratio value", "bad-float"}, + {"DurationFlag_InvalidString", "timeout", zeroDuration, "timeout duration", "invalid-duration"}, + {"ListFlag_MalformedItem", "tags", []string{"a"}, "list of tags", "item1,item2=val"}, + {"MapFlag_MalformedItem", "config", map[string]string{"k": "v"}, "config map", "k1=v1,k2"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + os.Clearenv() + os.Args = []string{os.Args[0], "-" + tt.flagName, tt.argValue} + + figs := With(Options{Germinate: true}) + switch v := tt.defaultValue.(type) { + case int: + figs = figs.NewInt(tt.flagName, v, tt.usage) + case bool: + figs = figs.NewBool(tt.flagName, v, tt.usage) + case float64: + figs = figs.NewFloat64(tt.flagName, v, tt.usage) + case time.Duration: + figs = figs.NewDuration(tt.flagName, v, tt.usage) + case []string: + figs = figs.NewList(tt.flagName, v, tt.usage) + case map[string]string: + figs = figs.NewMap(tt.flagName, v, tt.usage) + default: + t.Fatalf("Unknown default type for test: %T", tt.defaultValue) + } + + err := figs.Parse() + assert.Error(t, err, "Parse() should return an error for invalid flag input") + // Assert that the value either retains its default or a zero value, + // and that there isn't a panic. + switch tt.defaultValue.(type) { + case int: + assert.Equal(t, 0, *figs.Int(tt.flagName)) + case bool: + assert.Equal(t, false, *figs.Bool(tt.flagName)) + case float64: + assert.Equal(t, 0.0, *figs.Float64(tt.flagName)) + case time.Duration: + assert.Equal(t, time.Duration(0), *figs.Duration(tt.flagName)) + // For lists/maps, behavior on partial parse/error might vary: + case []string: + assert.Empty(t, *figs.List(tt.flagName)) // Expect default or empty on parse error + case map[string]string: + assert.Empty(t, *figs.Map(tt.flagName)) // Expect default or empty on parse error + } + }) + } +} + +func TestEnvironment_InvalidInput(t *testing.T) { + tests := []struct { + name string + envName string + defaultValue interface{} + usage string + envValue string + }{ + {"IntEnv_InvalidString", "MY_PORT", zeroInt, "port number", "not-a-number"}, + {"BoolEnv_InvalidString", "MY_DEBUG", zeroBool, "debug mode", "maybe"}, + {"Float64Env_InvalidString", "MY_RATIO", zeroFloat64, "ratio value", "bad-float"}, + {"DurationEnv_InvalidString", "MY_TIMEOUT", zeroDuration, "timeout duration", "invalid-duration"}, + {"ListEnv_MalformedItem", "MY_TAGS", zeroList, "list of tags", "item1,item2=val"}, + {"MapEnv_MalformedItem", "MY_CONFIG", zeroMap, "config map", "k1=v1,k2"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + os.Clearenv() + os.Args = []string{os.Args[0]} // Ensure no CLI args interfere + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + timer := time.NewTimer(time.Second * 1) + checker := time.NewTicker(100 * time.Millisecond) + for { + select { + case <-timer.C: + return + case <-checker.C: + assert.NoError(t, os.Setenv(strings.ToUpper(tt.envName), tt.envValue)) + } + } + }() + defer assert.NoError(t, os.Unsetenv(tt.envName)) + wg.Wait() + + figs := With(Options{Germinate: true}) + switch v := tt.defaultValue.(type) { + case int: + figs = figs.NewInt(tt.envName, v, tt.usage) + case bool: + figs = figs.NewBool(tt.envName, v, tt.usage) + case float64: + figs = figs.NewFloat64(tt.envName, v, tt.usage) + case time.Duration: + figs = figs.NewDuration(tt.envName, v, tt.usage) + case []string: + figs = figs.NewList(tt.envName, v, tt.usage) + case map[string]string: + figs = figs.NewMap(tt.envName, v, tt.usage) + default: + t.Fatalf("Unknown default type for test: %T", tt.defaultValue) + } + + err := figs.Load() // Use Load for env vars + assert.Error(t, err, "Load() should return an error for invalid env input") + // Assert that the value either retains its default or a zero value, + // and that there isn't a panic. + switch tt.defaultValue.(type) { + case int: + assert.Equal(t, zeroInt, *figs.Int(tt.envName)) + case bool: + assert.Equal(t, zeroBool, *figs.Bool(tt.envName)) + case float64: + assert.Equal(t, zeroFloat64, *figs.Float64(tt.envName)) + case time.Duration: + assert.Equal(t, zeroDuration, *figs.Duration(tt.envName)) + case []string: + assert.Empty(t, *figs.List(strings.ToLower(tt.envName))) + case map[string]string: + assert.Empty(t, *figs.Map(strings.ToLower(tt.envName))) + } + }) + } +} + +// Test for empty string inputs to non-string types +func TestEmptyStringInput(t *testing.T) { + tests := []struct { + name string + flagName string + defaultValue interface{} + usage string + }{ + {"IntFlag_EmptyString", "count", zeroInt, "count"}, + {"BoolFlag_EmptyString", "enabled", zeroBool, "enabled"}, + {"Float64Flag_EmptyString", "ratio", zeroFloat64, "ratio"}, + {"DurationFlag_EmptyString", "interval", zeroDuration, "interval"}, + {"ListFlag_EmptyString", "items", zeroList, "items"}, + {"MapFlag_EmptyString", "data", zeroMap, "data"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + os.Clearenv() + os.Args = []string{os.Args[0], "-" + tt.flagName, ""} // Pass empty string + + figs := With(Options{Germinate: true}) + switch v := tt.defaultValue.(type) { + case int: + figs = figs.NewInt(tt.flagName, v, tt.usage) + case bool: + figs = figs.NewBool(tt.flagName, v, tt.usage) + case float64: + figs = figs.NewFloat64(tt.flagName, v, tt.usage) + case time.Duration: + figs = figs.NewDuration(tt.flagName, v, tt.usage) + case []string: + figs = figs.NewList(tt.flagName, v, tt.usage) + case map[string]string: + figs = figs.NewMap(tt.flagName, v, tt.usage) + default: + t.Fatalf("Unknown default type for test: %T", tt.defaultValue) + } + + err := figs.Parse() + assert.NoError(t, err, "Parse() should not return error for empty string (handled by Set)") + // The Set method for List and Map handles empty string as empty slice/map, which is good. + // For primitives, it will attempt conversion, which should fail and keep default/zero. + switch tt.defaultValue.(type) { + case int: + assert.Equal(t, zeroInt, *figs.Int(tt.flagName), "Int should be zero") // Atoi("") returns 0, err + case bool: + assert.Equal(t, zeroBool, *figs.Bool(tt.flagName), "Bool should be false") // ParseBool("") returns false, err + case float64: + assert.Equal(t, zeroFloat64, *figs.Float64(tt.flagName), "Float should be zero") // ParseFloat("") returns 0, err + case time.Duration: + assert.Equal(t, zeroDuration, *figs.Duration(tt.flagName), "Duration should be zero") // ParseDuration("") returns 0, err + case []string: + assert.Equal(t, zeroList, *figs.List(tt.flagName), "List should be empty") + case map[string]string: + assert.Equal(t, zeroMap, *figs.Map(tt.flagName), "Map should be empty") + } + }) + } +} + +func TestRulePreventChange_StoreMethods(t *testing.T) { + const flagName = "protected_setting" + const initialValue = "secret" + const newValue = "exposed" + + t.Run("PreventChange_On_StoreString", func(t *testing.T) { + os.Args = []string{os.Args[0]} // Clean args + figs := With(Options{Germinate: true}) + figs = figs.NewString(flagName, initialValue, "A protected string") + figs = figs.WithRule(flagName, RulePreventChange) + assert.NoError(t, figs.Parse()) // Initial parse will succeed + + // Attempt to store a new value + figs = figs.StoreString(flagName, newValue) + assert.NoError(t, figs.ErrorFor(flagName), "Store should not generate an error when RulePreventChange is active") + + // Assert that the value *did not change* + assert.Equal(t, initialValue, *figs.String(flagName), "Value should remain unchanged when RulePreventChange is active") + }) + + t.Run("PreventChange_On_StoreInt", func(t *testing.T) { + os.Args = []string{os.Args[0]} + figs := With(Options{Germinate: true}) + figs = figs.NewInt(flagName, 100, "A protected int") + figs = figs.WithRule(flagName, RulePreventChange) + assert.NoError(t, figs.Parse()) + + figs = figs.StoreInt(flagName, 200) + assert.NoError(t, figs.ErrorFor(flagName)) + assert.Equal(t, 100, *figs.Int(flagName), "Int value should remain unchanged") + }) + + // Add similar tests for StoreBool, StoreFloat64, StoreDuration, StoreUnitDuration, StoreList, StoreMap + // For Lists and Maps, ensure the underlying slice/map reference is not modified either. + t.Run("PreventChange_On_StoreList", func(t *testing.T) { + os.Args = []string{os.Args[0]} + initialList := []string{"a", "b"} + figs := With(Options{Germinate: true}) + figs = figs.NewList(flagName, initialList, "A protected list") + figs = figs.WithRule(flagName, RulePreventChange) + assert.NoError(t, figs.Parse()) + + newValue := []string{"x", "y", "z"} + figs = figs.StoreList(flagName, newValue) + assert.NoError(t, figs.ErrorFor(flagName)) + assert.Equal(t, initialList, *figs.List(flagName), "List value should remain unchanged") + // Importantly, ensure the underlying slice isn't the new slice reference, but still the original's value + l := *figs.List(flagName) + assert.NotSame(t, &newValue, &l, "Should be a copy or original reference, not the new slice directly") + }) + + t.Run("PreventChange_On_StoreMap", func(t *testing.T) { + os.Args = []string{os.Args[0]} + initialMap := map[string]string{"k1": "v1"} + figs := With(Options{Germinate: true}) + figs = figs.NewMap(flagName, initialMap, "A protected map") + figs = figs.WithRule(flagName, RulePreventChange) + assert.NoError(t, figs.Parse()) + + newValue := map[string]string{"k2": "v2"} + figs = figs.StoreMap(flagName, newValue) + assert.NoError(t, figs.ErrorFor(flagName)) + assert.Equal(t, initialMap, *figs.Map(flagName), "Map value should remain unchanged") + l := *figs.Map(flagName) + assert.NotSame(t, &newValue, &l, "Should be a copy or original reference, not the new map directly") + }) +} + +func TestRuleCondemnedFromResurrection(t *testing.T) { + const nonExistentFlag = "ghost_flag" + + t.Run("Condemned_Flag_Resurrection_Panics", func(t *testing.T) { + os.Args = []string{os.Args[0]} + figs := With(Options{Germinate: true}) + // Condemn a flag name before it's even registered. + // `Resurrect` can be called internally if a flag is accessed but not defined. + figs = figs.WithRule(nonExistentFlag, RuleCondemnedFromResurrection) + + // Directly access the non-existent flag to trigger potential Resurrect call + assert.Panics(t, func() { + _ = *figs.String(nonExistentFlag) + }) + }) +} + +func TestFlesh_NilUnderlyingValue(t *testing.T) { + t.Run("ToString_NilValue", func(t *testing.T) { + flesh := NewFlesh(nil) // Create Flesh with nil underlying value + assert.Empty(t, flesh.ToString(), "ToString() on nil value should return empty string") + }) + + t.Run("ToInt_NilValue", func(t *testing.T) { + flesh := NewFlesh(nil) + assert.Equal(t, 0, flesh.ToInt(), "ToInt() on nil value should return 0") + }) + + t.Run("ToBool_NilValue", func(t *testing.T) { + flesh := NewFlesh(nil) + assert.False(t, flesh.ToBool(), "ToBool() on nil value should return false") + }) + + t.Run("ToList_NilValue", func(t *testing.T) { + flesh := NewFlesh(nil) + assert.Empty(t, flesh.ToList(), "ToList() on nil value should return empty slice") + assert.NotNil(t, flesh.ToList(), "ToList() on nil value should return non-nil empty slice") // IMPORTANT: avoid nil slice + }) + + t.Run("ToMap_NilValue", func(t *testing.T) { + flesh := NewFlesh(nil) + assert.Empty(t, flesh.ToMap(), "ToMap() on nil value should return empty map") + assert.NotNil(t, flesh.ToMap(), "ToMap() on nil value should return non-nil empty map") // IMPORTANT: avoid nil map + }) + + t.Run("Is_NilValue", func(t *testing.T) { + flesh := NewFlesh(nil) + assert.False(t, flesh.IsString(), "IsString() on nil should be false") + assert.False(t, flesh.IsInt(), "IsInt() on nil should be false") + // ... test all other IsX methods + }) +} + +func TestUsageOutputConsistency(t *testing.T) { + const flagName = "workers" + const flagAlias = "w" + const flagUsage = "Number of worker goroutines" + const flagDefault = 10 + + t.Run("BasicFlagUsage", func(t *testing.T) { + os.Args = []string{os.Args[0]} // Clean args + figs := With(Options{Germinate: true}) + figs.NewInt(flagName, flagDefault, flagUsage) + output := figs.UsageString() + + assert.Contains(t, output, fmt.Sprintf("-%s[=%d]", flagName, flagDefault), "Output should contain flag name with default value") + assert.Contains(t, output, fmt.Sprintf("[%s]", tInt), "Output should contain correct type string") + assert.Contains(t, output, flagUsage, "Output should contain correct usage string") + }) + + t.Run("AliasedFlagUsage", func(t *testing.T) { + os.Args = []string{os.Args[0]} + figs := With(Options{Germinate: true}) + figs.NewString(flagName, "myhost", flagUsage) + figs.WithAlias(flagName, flagAlias) + output := figs.UsageString() + expectedAliasStr := fmt.Sprintf("-%s|-%s[=myhost]", flagAlias, flagName) + assert.Contains(t, output, expectedAliasStr, "Output should show alias and main flag name with default") + assert.Contains(t, output, fmt.Sprintf("[%s]", tString), "Output should contain correct type string for aliased flag") + assert.Contains(t, output, flagUsage, "Output should contain correct usage string for aliased flag") + }) + + t.Run("ListFlagUsage", func(t *testing.T) { + os.Args = []string{os.Args[0]} + figs := With(Options{Germinate: true}) + defaultList := []string{"one", "two"} + figs.NewList(flagName, defaultList, flagUsage) + output := figs.UsageString() + // Default value for ListFlag.String() is 'one,two' (using ListSeparator) + expectedDefault := strings.Join(defaultList, ListSeparator) + assert.Contains(t, output, fmt.Sprintf("-%s[=%s]", flagName, expectedDefault), "List flag usage should show joined default") + assert.Contains(t, output, fmt.Sprintf("[%s]", tList), "List flag usage should show List type") + }) + + t.Run("MapFlagUsage", func(t *testing.T) { + os.Args = []string{os.Args[0]} + figs := With(Options{Germinate: true}) + defaultMap := map[string]string{"k1": "v1", "k2": "v2"} + figs.NewMap(flagName, defaultMap, flagUsage) + + output := figs.UsageString() + // MapFlag.String() returns 'k1=v1,k2=v2' (or similar, order non-guaranteed) + // For assertion, we need to be flexible with map string order. + // A simpler assertion might be to check for parts. + assert.Contains(t, output, fmt.Sprintf("-%s[=", flagName), "Map flag usage should show default value start") + assert.Contains(t, output, fmt.Sprintf("[%s]", tMap), "Map flag usage should show Map type") + assert.Contains(t, output, "k1=v1", "Map flag usage should contain key-value pair") + assert.Contains(t, output, "k2=v2", "Map flag usage should contain another key-value pair") + }) +} + +/* +func TestListPolicyAppend_FullPrecedence(t *testing.T) { + const listName = "app_list" + tempConfigFile := filepath.Join(t.TempDir(), "list_config.yaml") + defer assert.NoError(t, os.Remove(tempConfigFile)) + defer assert.NoError(t, os.Unsetenv(strings.ToUpper(listName))) + os.Args = []string{os.Args[0]} + + initialDefault := []string{"apple", "banana"} + fileContent := []string{"banana", "cherry"} + envValue := "cherry,date" + cliValue := "apple,elderberry" + + // Set PolicyListAppend to true for these tests + PolicyListAppend = true + defer func() { PolicyListAppend = false }() // Reset after tests + + // Write config file + assert.NoError(t, os.WriteFile(tempConfigFile, []byte(listName+": "+strings.Join(fileContent, ListSeparator)), 0644)) + + // Set env variable + assert.NoError(t, os.Setenv(strings.ToUpper(listName), envValue)) + + // Set CLI args + os.Args = []string{os.Args[0], "-" + listName, cliValue} + + figs := With(Options{ + Germinate: true, + ConfigFile: tempConfigFile, + }) + figs.NewList(listName, initialDefault, "App list settings") + assert.NoError(t, figs.Parse()) + + // Expected order will be sorted due to slices.Sort in List() getter + expectedList := []string{"apple", "banana", "cherry", "date"} // unique, merged, sorted + assert.Equal(t, expectedList, *figs.List(listName), "List should merge correctly with precedence and deduplicate") + + // Test case where policy is false (full overwrite) + t.Run("PolicyListAppend_FALSE_FullOverwrite", func(t *testing.T) { + PolicyListAppend = false // Ensure it's false for this sub-test + os.Clearenv() + os.Args = []string{os.Args[0]} // No CLI arg + assert.NoError(t, os.Setenv(strings.ToUpper(listName), envValue)) + + // Clean up and re-initialize figs for this sub-test + figs = With(Options{ + Germinate: true, + ConfigFile: tempConfigFile, + }) + figs.NewList(listName, initialDefault, "App list settings") + assert.NoError(t, figs.Parse()) // Parse will pick up Env & File + expectedEnvList := []string{"cherry", "date"} // Env completely overwrites + assert.Equal(t, expectedEnvList, *figs.List(listName), "Env should completely overwrite file/default when PolicyListAppend is false") + + os.Args = []string{os.Args[0], "-" + listName, cliValue} // Now with CLI arg + figs = With(Options{ + Germinate: true, + ConfigFile: tempConfigFile, + }) + figs.NewList(listName, initialDefault, "App list settings") + assert.NoError(t, figs.Parse()) // Parse will pick up CLI + expectedCliList := []string{"apple", "elderberry"} // CLI completely overwrites + assert.Equal(t, expectedCliList, *figs.List(listName), "CLI should completely overwrite all when PolicyListAppend is false") + }) +} + +func TestMapPolicyAppend_FullPrecedence(t *testing.T) { + const mapName = "app_map" + tempConfigFile := filepath.Join(t.TempDir(), "map_config.yaml") + defer assert.NoError(t, os.Remove(tempConfigFile)) + defer assert.NoError(t, os.Unsetenv(strings.ToUpper(mapName))) + os.Args = []string{os.Args[0]} + + initialDefault := map[string]string{"a": "1", "b": "2", "c": "3"} + fileContent := map[string]string{"b": "B_file", "d": "4", "e": "5"} + envValue := "c=C_env,f=6,g=7" + cliValue := "a=A_cli,e=E_cli,h=8" + + // Set PolicyMapAppend to true for these tests + PolicyMapAppend = true + defer func() { PolicyMapAppend = false }() // Reset after tests + + // Write config file + fileMapStr := "" + for k, v := range fileContent { + if fileMapStr != "" { + fileMapStr += MapSeparator + } + fileMapStr += k + MapKeySeparator + v + } + assert.NoError(t, os.WriteFile(tempConfigFile, []byte(mapName+": "+fileMapStr), 0644)) + + // Set env variable + assert.NoError(t, os.Setenv(strings.ToUpper(mapName), envValue)) + + // Set CLI args + os.Args = []string{os.Args[0], "-" + mapName, cliValue} + + figs := With(Options{ + Germinate: true, + ConfigFile: tempConfigFile, + }) + figs.NewMap(mapName, initialDefault, "App map settings") + assert.NoError(t, figs.Parse()) + + expectedMap := map[string]string{ + "a": "A_cli", // CLI overrides default + "b": "B_file", // File overrides default (no env/cli conflict) + "c": "C_env", // Env overrides default (no cli conflict) + "d": "4", // File value + "e": "E_cli", // CLI overrides file + "f": "6", // Env value + "g": "7", // Env value + "h": "8", // CLI value + } + assert.Equal(t, expectedMap, *figs.Map(mapName), "Map should merge correctly with precedence") + + // Test case where policy is false (full overwrite) + t.Run("PolicyMapAppend_FALSE_FullOverwrite", func(t *testing.T) { + PolicyMapAppend = false // Ensure it's false for this sub-test + os.Clearenv() + os.Args = []string{os.Args[0]} // No CLI arg + assert.NoError(t, os.Setenv(strings.ToUpper(mapName), envValue)) + + // Clean up and re-initialize figs for this sub-test + figs = With(Options{ + Germinate: true, + ConfigFile: tempConfigFile, + }) + figs.NewMap(mapName, initialDefault, "App map settings") + assert.NoError(t, figs.Parse()) // Parse will pick up Env & File + assert.Equal(t, map[string]string{"c": "C_env", "f": "6", "g": "7"}, *figs.Map(mapName), "Env should completely overwrite file/default when PolicyMapAppend is false") + + os.Args = []string{os.Args[0], "-" + mapName, cliValue} // Now with CLI arg + figs = With(Options{ + Germinate: true, + ConfigFile: tempConfigFile, + }) + figs.NewMap(mapName, initialDefault, "App map settings") + assert.NoError(t, figs.Parse()) // Parse will pick up CLI + assert.Equal(t, map[string]string{"a": "A_cli", "e": "E_cli", "h": "8"}, *figs.Map(mapName), "CLI should completely overwrite all when PolicyMapAppend is false") + }) +} + +func TestPrecedence(t *testing.T) { + const flagName = "app_setting" + const defaultValue = "default_value" + const fileValue = "file_value" + const envValue = "env_value" + const cliValue = "cli_value" + + // Define temporary file paths + tempConfigFile := filepath.Join(t.TempDir(), "test_config.yaml") + + // Helper to clean up OS environment variables + defer os.Unsetenv(strings.ToUpper(flagName)) + defer os.Unsetenv(EnvironmentKey) + + // --- Scenario 1: CLI Flag should win everything --- + t.Run("CLI_Wins", func(t *testing.T) { + os.Clearenv() + os.Args = []string{os.Args[0], "-" + flagName, cliValue} + assert.NoError(t, os.Setenv(strings.ToUpper(flagName), envValue)) // Set env variable + assert.NoError(t, os.WriteFile(tempConfigFile, []byte(flagName+": "+fileValue), 0644)) // Write config file + + figs := With(Options{ + Germinate: true, + ConfigFile: tempConfigFile, + Pollinate: false, // Pollinate not directly affecting initial parse order here + IgnoreEnvironment: false, + }) + figs.NewString(flagName, defaultValue, "App setting") + assert.NoError(t, figs.Parse()) + assert.Equal(t, cliValue, *figs.String(flagName), "CLI value should override all") + }) + + // --- Scenario 2: Environment variable should win over file/default when no CLI flag --- + t.Run("Env_Wins_Over_File_Default", func(t *testing.T) { + os.Clearenv() + os.Args = []string{os.Args[0]} // No CLI flag + assert.NoError(t, os.Setenv(strings.ToUpper(flagName), envValue)) + assert.NoError(t, os.WriteFile(tempConfigFile, []byte(flagName+": "+fileValue), 0644)) + + figs := With(Options{ + Germinate: true, + ConfigFile: tempConfigFile, + IgnoreEnvironment: false, + }) + figs.NewString(flagName, defaultValue, "App setting") + assert.NoError(t, figs.Parse()) + assert.Equal(t, envValue, *figs.String(flagName), "Environment value should override file and default") + }) + + // --- Scenario 3: Config file should win over default when no CLI/Env --- + t.Run("File_Wins_Over_Default", func(t *testing.T) { + os.Clearenv() + os.Args = []string{os.Args[0]} // No CLI flag + // No environment variable set + assert.NoError(t, os.WriteFile(tempConfigFile, []byte(flagName+": "+fileValue), 0644)) + + figs := With(Options{ + Germinate: true, + ConfigFile: tempConfigFile, + IgnoreEnvironment: false, + }) + figs.NewString(flagName, defaultValue, "App setting") + assert.NoError(t, figs.Parse()) + assert.Equal(t, fileValue, *figs.String(flagName), "Config file value should override default") + }) + + // --- Scenario 4: Default value is used when no other source provides a value --- + t.Run("Default_Value", func(t *testing.T) { + os.Clearenv() + os.Args = []string{os.Args[0]} // No CLI flag + // No environment variable set + // No config file for this specific flag + assert.NoError(t, os.WriteFile(tempConfigFile, []byte("some_other_setting: test"), 0644)) // Ensure file exists but doesn't have our flag + + figs := With(Options{ + Germinate: true, + ConfigFile: tempConfigFile, + IgnoreEnvironment: false, + }) + figs.NewString(flagName, defaultValue, "App setting") + assert.NoError(t, figs.Parse()) + assert.Equal(t, defaultValue, *figs.String(flagName), "Default value should be used") + }) + + // --- Scenario 5: IgnoreEnvironment option bypasses env vars --- + t.Run("IgnoreEnvironment", func(t *testing.T) { + os.Clearenv() + os.Args = []string{os.Args[0]} + assert.NoError(t, os.Setenv(strings.ToUpper(flagName), envValue)) // Env is set + assert.NoError(t, os.WriteFile(tempConfigFile, []byte(flagName+": "+fileValue), 0644)) // File is set + + figs := With(Options{ + Germinate: true, + ConfigFile: tempConfigFile, + IgnoreEnvironment: true, // This should make envValue ignored + }) + figs.NewString(flagName, defaultValue, "App setting") + assert.NoError(t, figs.Parse()) + assert.Equal(t, fileValue, *figs.String(flagName), "File value should win when env is ignored") + }) + + // Clean up temporary file + defer assert.NoError(t, os.Remove(tempConfigFile)) +} +*/ + +func TestCornerCases(t *testing.T) { + t.Run("UsingEnv", func(t *testing.T) { + assert.NoError(t, os.Setenv("NAME", "Yeshua")) + os.Args = []string{os.Args[0], "-name", "Andrei"} + figs := With(Options{Germinate: true}) + figs = figs.NewString("name", "Satan", "Your Name").WithAlias("name", "n") + assert.NoError(t, figs.Parse()) + assert.Equal(t, "Yeshua", *figs.String("name")) + }) +} + func TestWith(t *testing.T) { type args struct { opts Options @@ -158,6 +859,8 @@ func TestIsTracking(t *testing.T) { // Run the test with a timeout t.Run("IsTracking", func(t *testing.T) { + os.Args = []string{os.Args[0]} + os.Clearenv() // Create a figtree with Tracking and Germinate enabled figs := With(Options{Tracking: true, Germinate: true}) var k, d, u = "name", "yahuah", "usage" @@ -232,7 +935,8 @@ func TestIsTracking(t *testing.T) { } else { d = "yahuah" } - figs.StoreString(k, d) + figs = figs.StoreString(k, d) + assert.NoError(t, figs.ErrorFor(k)) atomic.AddInt32(&writeCount, 1) // t.Logf("Wrote: %s (write #%d)", d, writeCount) } @@ -270,28 +974,29 @@ func TestIsTracking(t *testing.T) { } func TestTree_PollinateString(t *testing.T) { + const k1 = "testmehere" + os.Args = []string{os.Args[0]} figs := With(Options{Pollinate: true, Tracking: true, Germinate: true}) - figs.NewString("test", "initial", "usage") - figs.WithValidator("test", AssureStringContains("ini")) + figs = figs.NewString(k1, "initial", "usage").WithValidator("test", AssureStringContains("ini")) assert.NoError(t, figs.Parse()) - assert.Equal(t, "initial", *figs.String("test")) + assert.Equal(t, "initial", *figs.String(k1)) go func() { timer := time.NewTimer(time.Second * 1) checker := time.NewTicker(100 * time.Millisecond) for { select { case <-timer.C: - assert.Equal(t, "updated", *figs.String("test")) + assert.Equal(t, "updated", *figs.String(k1)) return case <-checker.C: - assert.NoError(t, os.Setenv("test", "updated")) + assert.NoError(t, os.Setenv(strings.ToUpper(k1), "updated")) } } }() - defer assert.NoError(t, os.Unsetenv("test")) + defer assert.NoError(t, os.Unsetenv(strings.ToUpper(k1))) mutation, ok := <-figs.Mutations() if ok { - assert.Equal(t, "test", mutation.Property) + assert.Equal(t, k1, mutation.Property) assert.Equal(t, "string", mutation.Mutagenesis) assert.Equal(t, "StoreString", mutation.Way) assert.Equal(t, "initial", mutation.Old) diff --git a/flesh.go b/flesh.go index b48fd35..e8e11c8 100644 --- a/flesh.go +++ b/flesh.go @@ -1,6 +1,7 @@ package figtree import ( + "fmt" "strconv" "strings" "sync/atomic" @@ -12,6 +13,31 @@ func NewFlesh(thing interface{}) Flesh { return &f } +func (flesh *figFlesh) AsIs() interface{} { + switch v := flesh.Flesh.(type) { + case *Value: + switch x := v.Value.(type) { + case *Value: + return x.Value + case Value: + return x.Value + default: + return v.Value + } + case Value: + switch x := v.Value.(type) { + case *Value: + return x.Value + case Value: + return x.Value + default: + return v.Value + } + default: + return v + } +} + func (flesh *figFlesh) ToString() string { f, e := toString(flesh.Flesh) if e != nil { @@ -134,9 +160,9 @@ func (flesh *figFlesh) ToList() []string { case *[]string: return *f case string: - return strings.Split(f, ",") + return strings.Split(f, ListSeparator) case *string: - return strings.Split(*f, ",") + return strings.Split(*f, ListSeparator) default: return []string{} } @@ -145,11 +171,13 @@ func (flesh *figFlesh) ToList() []string { func (flesh *figFlesh) ToMap() map[string]string { checkString := func(ck string) map[string]string { f := make(map[string]string) - u := strings.Split(ck, ",") + u := strings.Split(ck, MapSeparator) for _, i := range u { - r := strings.SplitN(i, "=", 1) + r := strings.SplitN(i, MapKeySeparator, 1) if len(r) == 2 { f[r[0]] = r[1] + } else { + flesh.Error = fmt.Errorf("invalid key: %s", i) } } return f @@ -159,8 +187,8 @@ func (flesh *figFlesh) ToMap() map[string]string { return ft.ToMap() case *MapFlag: // Create a new map and copy the key-value pairs - fu := make(map[string]string, len(*ft.values)) - for ck, you := range *ft.values { + fu := make(map[string]string, len(ft.values)) + for ck, you := range ft.values { fu[ck] = you // don't you just love programming so much =D truly I love you you see where evil comes from now } return fu @@ -309,17 +337,17 @@ func (flesh *figFlesh) IsList() bool { case *[]string: return f != nil case string: - p := strings.Split(f, ",") + p := strings.Split(f, ListSeparator) return len(p) > 0 case *string: - p := strings.Split(*f, ",") + p := strings.Split(*f, ListSeparator) return len(p) > 0 default: return false } } -// IsMap checks a Fig Flesh and returns a bool +// IsMap checks a FigFlesh Flesh and returns a bool // // figFlesh can be a string NAME=YAHUAH,AGE=33,SEX=MALE can be expressed as // a map[string]string by parsing it as you can see with initial below @@ -331,16 +359,16 @@ func (flesh *figFlesh) IsList() bool { // figs.NewMap("attributes", initial, "map of attributes") // err := figs.Parse() // if err != nil { panic(err) } -// attributes := figs.Fig("attributes") // this is figtree Flesh -// check := figs.Fig("attributes").IsMap() // this is a bool +// attributes := figs.FigFlesh("attributes") // this is figtree Flesh +// check := figs.FigFlesh("attributes").IsMap() // this is a bool // fmt.Printf("attributes is a %T with %d keys and equals %q\n", // check, len(attributes.ToMap()) > 0, attributes) func (flesh *figFlesh) IsMap() bool { checkString := func(f string) bool { - p := strings.Split(f, ",") + p := strings.Split(f, MapSeparator) ok := false for _, e := range p { - n := strings.SplitN(e, "=", 1) + n := strings.SplitN(e, MapKeySeparator, 1) ok = ok && len(n) == 2 } return ok diff --git a/flesh_test.go b/flesh_test.go index 4fb291a..e9cbcb1 100644 --- a/flesh_test.go +++ b/flesh_test.go @@ -1,6 +1,7 @@ package figtree import ( + "os" "testing" "time" @@ -21,81 +22,89 @@ func TestNewFlesh(t *testing.T) { func TestFleshInterface(t *testing.T) { t.Run("Is", func(t *testing.T) { t.Run("Map", func(t *testing.T) { + os.Args = []string{os.Args[0]} figs := With(Options{Germinate: true, IgnoreEnvironment: true}) figs.NewMap(t.Name(), map[string]string{"name": "yahuah"}, t.Name()) assert.NoError(t, figs.Parse()) var flesh Flesh - flesh = figs.Fig(t.Name()) + flesh = figs.FigFlesh(t.Name()) assert.NotNil(t, flesh) assert.True(t, flesh.Is(tMap)) assert.False(t, flesh.Is(tBool)) }) t.Run("List", func(t *testing.T) { + os.Args = []string{os.Args[0]} figs := With(Options{Germinate: true, IgnoreEnvironment: true}) figs.NewList(t.Name(), []string{"yahuah"}, t.Name()) assert.NoError(t, figs.Parse()) var flesh Flesh - flesh = figs.Fig(t.Name()) + flesh = figs.FigFlesh(t.Name()) assert.NotNil(t, flesh) assert.True(t, flesh.Is(tList)) assert.False(t, flesh.Is(tBool)) }) t.Run("UnitDuration", func(t *testing.T) { + os.Args = []string{os.Args[0]} figs := With(Options{Germinate: true, IgnoreEnvironment: true}) figs.NewUnitDuration(t.Name(), 17, time.Second, t.Name()) assert.NoError(t, figs.Parse()) var flesh Flesh - flesh = figs.Fig(t.Name()) + flesh = figs.FigFlesh(t.Name()) assert.NotNil(t, flesh) assert.True(t, flesh.Is(tUnitDuration)) assert.False(t, flesh.Is(tBool)) }) t.Run("Duration", func(t *testing.T) { + os.Args = []string{os.Args[0]} figs := With(Options{Germinate: true, IgnoreEnvironment: true}) figs.NewDuration(t.Name(), time.Second, t.Name()) assert.NoError(t, figs.Parse()) var flesh Flesh - flesh = figs.Fig(t.Name()) + flesh = figs.FigFlesh(t.Name()) assert.NotNil(t, flesh) assert.True(t, flesh.Is(tDuration)) assert.False(t, flesh.Is(tBool)) }) t.Run("Float64", func(t *testing.T) { + os.Args = []string{os.Args[0]} figs := With(Options{Germinate: true, IgnoreEnvironment: true}) figs.NewFloat64(t.Name(), 1.0, t.Name()) assert.NoError(t, figs.Parse()) var flesh Flesh - flesh = figs.Fig(t.Name()) + flesh = figs.FigFlesh(t.Name()) assert.NotNil(t, flesh) assert.True(t, flesh.Is(tFloat64)) assert.False(t, flesh.Is(tInt)) }) t.Run("Int64", func(t *testing.T) { + os.Args = []string{os.Args[0]} figs := With(Options{Germinate: true, IgnoreEnvironment: true}) figs.NewInt64(t.Name(), 1, t.Name()) assert.NoError(t, figs.Parse()) var flesh Flesh - flesh = figs.Fig(t.Name()) + flesh = figs.FigFlesh(t.Name()) assert.NotNil(t, flesh) assert.True(t, flesh.Is(tInt64)) assert.False(t, flesh.Is(tFloat64)) }) t.Run("Int", func(t *testing.T) { + os.Args = []string{os.Args[0]} figs := With(Options{Germinate: true, IgnoreEnvironment: true}) figs.NewInt(t.Name(), 1, t.Name()) assert.NoError(t, figs.Parse()) var flesh Flesh - flesh = figs.Fig(t.Name()) + flesh = figs.FigFlesh(t.Name()) assert.NotNil(t, flesh) assert.True(t, flesh.Is(tInt)) assert.False(t, flesh.Is(tFloat64)) }) t.Run("String", func(t *testing.T) { + os.Args = []string{os.Args[0]} figs := With(Options{Germinate: true, IgnoreEnvironment: true}) figs.NewString(t.Name(), t.Name(), t.Name()) assert.NoError(t, figs.Parse()) var flesh Flesh - flesh = figs.Fig(t.Name()) + flesh = figs.FigFlesh(t.Name()) assert.NotNil(t, flesh) assert.True(t, flesh.Is(tString)) assert.False(t, flesh.Is(tFloat64)) @@ -103,74 +112,82 @@ func TestFleshInterface(t *testing.T) { }) t.Run("ToMap", func(t *testing.T) { + os.Args = []string{os.Args[0]} figs := With(Options{Germinate: true, IgnoreEnvironment: true}) figs.NewMap(t.Name(), map[string]string{"name": "yahuah"}, t.Name()) assert.NoError(t, figs.Parse()) var flesh Flesh - flesh = figs.Fig(t.Name()) + flesh = figs.FigFlesh(t.Name()) assert.NotNil(t, flesh) assert.Contains(t, flesh.ToMap(), "name") }) t.Run("ToList", func(t *testing.T) { + os.Args = []string{os.Args[0]} figs := With(Options{Germinate: true, IgnoreEnvironment: true}) - figs.NewList(t.Name(), []string{"yahuah"}, t.Name()) + figs = figs.NewList(t.Name(), []string{"yahuah"}, t.Name()) assert.NoError(t, figs.Parse()) var flesh Flesh - flesh = figs.Fig(t.Name()) + flesh = figs.FigFlesh(t.Name()) assert.NotNil(t, flesh) assert.Contains(t, flesh.ToList(), "yahuah") }) t.Run("ToUnitDuration", func(t *testing.T) { + os.Args = []string{os.Args[0]} figs := With(Options{Germinate: true, IgnoreEnvironment: true}) figs.NewUnitDuration(t.Name(), 1, time.Second, t.Name()) assert.NoError(t, figs.Parse()) var flesh Flesh - flesh = figs.Fig(t.Name()) + flesh = figs.FigFlesh(t.Name()) assert.NotNil(t, flesh) assert.Equal(t, flesh.ToUnitDuration(), time.Second) }) t.Run("ToDuration", func(t *testing.T) { + os.Args = []string{os.Args[0]} figs := With(Options{Germinate: true, IgnoreEnvironment: true}) - figs.NewDuration(t.Name(), 1, t.Name()) + figs = figs.NewDuration(t.Name(), 1, t.Name()) assert.NoError(t, figs.Parse()) var flesh Flesh - flesh = figs.Fig(t.Name()) + flesh = figs.FigFlesh(t.Name()) assert.NotNil(t, flesh) assert.Equal(t, time.Duration(1), flesh.ToDuration()) }) t.Run("ToFloat64", func(t *testing.T) { + os.Args = []string{os.Args[0]} figs := With(Options{Germinate: true, IgnoreEnvironment: true}) figs.NewFloat64(t.Name(), 1, t.Name()) assert.NoError(t, figs.Parse()) var flesh Flesh - flesh = figs.Fig(t.Name()) + flesh = figs.FigFlesh(t.Name()) assert.NotNil(t, flesh) assert.Equal(t, 1.0, flesh.ToFloat64()) }) t.Run("ToInt64", func(t *testing.T) { + os.Args = []string{os.Args[0]} figs := With(Options{Germinate: true, IgnoreEnvironment: true}) figs.NewInt64(t.Name(), 1, t.Name()) assert.NoError(t, figs.Parse()) var flesh Flesh - flesh = figs.Fig(t.Name()) + flesh = figs.FigFlesh(t.Name()) assert.NotNil(t, flesh) assert.Equal(t, int64(1), flesh.ToInt64()) }) t.Run("ToInt", func(t *testing.T) { + os.Args = []string{os.Args[0]} figs := With(Options{Germinate: true, IgnoreEnvironment: true}) figs.NewInt(t.Name(), 1, t.Name()) assert.NoError(t, figs.Parse()) var flesh Flesh - flesh = figs.Fig(t.Name()) + flesh = figs.FigFlesh(t.Name()) assert.NotNil(t, flesh) assert.Equal(t, 1, flesh.ToInt()) }) t.Run("ToString", func(t *testing.T) { + os.Args = []string{os.Args[0]} figs := With(Options{Germinate: true, IgnoreEnvironment: true}) figs.NewString(t.Name(), t.Name(), t.Name()) assert.NoError(t, figs.Parse()) var flesh Flesh - flesh = figs.Fig(t.Name()) + flesh = figs.FigFlesh(t.Name()) assert.NotNil(t, flesh) assert.Equal(t, t.Name(), flesh.ToString()) }) diff --git a/fruit.go b/fruit.go new file mode 100644 index 0000000..c93e259 --- /dev/null +++ b/fruit.go @@ -0,0 +1,186 @@ +package figtree + +import ( + "errors" + "fmt" + "strings" + "time" +) + +type Value struct { + Value interface{} + Mutagensis Mutagenesis + Err error +} + +func (v *Value) Raw() interface{} { + return v.Value +} + +func (v *Value) Set(in string) error { + switch v.Mutagensis { + case tString: + v.Value = in + case tBool: + if len(in) == 0 { + in = "false" + } + val, err := toBool(in) + if err != nil { + v.Err = fmt.Errorf("failed to set bool value from string %q: %w", in, err) + return v.Err + } + v.Value = val + case tInt: + if len(in) == 0 { + in = "0" + } + val, err := toInt(in) + if err != nil { + v.Err = fmt.Errorf("failed to set int value from string %q: %w", in, err) + return v.Err + } + v.Value = val + case tInt64: + if len(in) == 0 { + in = "0" + } + val, err := toInt64(in) + if err != nil { + v.Err = fmt.Errorf("failed to set int64 value from string %q: %w", in, err) + return v.Err + } + v.Value = val + case tFloat64: + if len(in) == 0 { + in = "0.0" + } + val, err := toFloat64(in) + if err != nil { + v.Err = fmt.Errorf("failed to set float64 value from string %q: %w", in, err) + return v.Err + } + v.Value = val + case tDuration, tUnitDuration: + if len(in) == 0 { + err := v.Assign(zeroDuration) + if err != nil { + v.Err = fmt.Errorf("failed to set duration value from string %q: %w", in, err) + return v.Err + } + return nil + } + va, er := time.ParseDuration(in) + if er == nil { + v.Value = va + return nil + } + + _val, err := ParseCustomDuration(in) + if err != nil { + if !strings.Contains(err.Error(), "invalid duration format") { + v.Err = fmt.Errorf("failed to set duration value from string %q: %w", in, err) + val, err := toInt64(in) + if err != nil { + v.Err = errors.Join(v.Err, fmt.Errorf("failed to set duration value from string %q", in)) + return v.Err + } + v.Value = time.Duration(val) + return nil + } + _val, er := time.ParseDuration(in) + if er != nil { + v.Err = fmt.Errorf("failed to set duration value from string %q: %w", in, err) + return v.Err + } + v.Value = _val + return nil + } + v.Value = _val + return nil + + case tList: + if len(in) == 0 { + err := v.Assign(zeroList) + if err != nil { + v.Err = fmt.Errorf("failed to set list value from string %q: %w", in, err) + return v.Err + } + return nil + } + val, err := toStringSlice(in) + if err != nil { + v.Err = fmt.Errorf("failed to set list value from string %q: %w", in, err) + return v.Err + } + if PolicyListAppend { + vl, er := toStringSlice(v.Value) + if er != nil { + v.Err = fmt.Errorf("failed to set list value from string %q: %w", in, er) + return v.Err + } + for _, x := range val { + vl = append(vl, x) + } + vl = DeduplicateStrings(vl) + v.Value = vl + } else { + v.Value = val + } + + case tMap: + if len(in) == 0 { + err := v.Assign(zeroMap) + if err != nil { + v.Err = fmt.Errorf("failed to set map value from string %q: %w", in, err) + return v.Err + } + return nil + } + val, err := toStringMap(in) + if err != nil { + v.Err = fmt.Errorf("failed to set map value from string %q: %w", in, err) + return v.Err + } + if PolicyMapAppend { + vm := v.Flesh().ToMap() + for k, v := range val { + vm[k] = v + } + v.Value = vm + } else { + v.Value = val + } + default: + err := v.Assign(in) + if err != nil { + v.Err = fmt.Errorf("failed to set value from string %q: %w", in, err) + return v.Err + } + } + return nil +} + +func (v *Value) Assign(as interface{}) error { + switch as := as.(type) { + case *ListFlag: + v.Value = as.values + case *MapFlag: + v.Value = as.values + case *Value: + v.Value = as.Value + default: + v.Value = as + } + return nil +} + +func (v *Value) Flesh() Flesh { + return &figFlesh{v.Value, nil} +} + +func (v *Value) String() string { + vv := v.Value + f := figFlesh{vv, nil} + return f.ToString() +} diff --git a/internals.go b/internals.go index 582ee1e..7bd182a 100644 --- a/internals.go +++ b/internals.go @@ -55,71 +55,55 @@ func (tree *figTree) loadYAML(data []byte) error { defer tree.mu.Unlock() tree.activateFlagSet() for n, d := range yamlData { - t := tree.MutagenesisOf(d) var fruit *figFruit var exists bool if fruit, exists = tree.figs[n]; exists && fruit != nil { - tf := tree.MutagenesisOf(fruit.Flesh) - if strings.EqualFold(string(tf), string(t)) { - tree.figs[n].Flesh = figFlesh{d} - continue + value := tree.useValue(tree.from(fruit.name)) + ds, err := toString(d) + if err != nil { + return fmt.Errorf("unable toString value for %s: %w", n, err) } + err = value.Set(ds) + if err != nil { + return fmt.Errorf("unable to Set value for %s: %w", n, err) + } + tree.values.Store(fruit.name, value) + continue + } + mut := tree.MutagenesisOf(d) + vf, er := tree.from(n) + if er == nil && vf != nil && strings.EqualFold(string(vf.Mutagensis), string(tree.MutagenesisOf(d))) { + err := vf.Assign(d) + if err != nil { + return fmt.Errorf("unable to assign value for %s: %w", n, err) + } + tree.values.Store(n, vf) + mut = vf.Mutagensis + } else { + value := &Value{ + Value: d, + Mutagensis: mut, + } + tree.values.Store(n, value) } fruit = &figFruit{ - name: n, - Flesh: figFlesh{d}, - Mutations: make([]Mutation, 0), - Validators: make([]FigValidatorFunc, 0), - Callbacks: make([]Callback, 0), - Rules: make([]RuleKind, 0), + name: n, + Mutagenesis: mut, + usage: "Unknown, loaded from config file", + Mutations: make([]Mutation, 0), + Validators: make([]FigValidatorFunc, 0), + Callbacks: make([]Callback, 0), + Rules: make([]RuleKind, 0), } withered := witheredFig{ - name: n, - Flesh: figFlesh{d}, - } - - switch d.(type) { - case *string, string: - fruit.Mutagenesis = tString - withered.Mutagenesis = tString - tree.figs[n] = fruit - tree.withered[n] = withered - case *bool, bool: - fruit.Mutagenesis = tBool - withered.Mutagenesis = tBool - tree.figs[n] = fruit - tree.withered[n] = withered - case *int, int: - fruit.Mutagenesis = tInt - withered.Mutagenesis = tInt - tree.figs[n] = fruit - tree.withered[n] = withered - case *int64, int64: - fruit.Mutagenesis = tInt64 - withered.Mutagenesis = tInt64 - tree.figs[n] = fruit - tree.withered[n] = withered - case *float64, float64: - fruit.Mutagenesis = tFloat64 - withered.Mutagenesis = tFloat64 - tree.figs[n] = fruit - tree.withered[n] = withered - case *time.Duration, time.Duration: - fruit.Mutagenesis = tDuration - withered.Mutagenesis = tDuration - tree.figs[n] = fruit - tree.withered[n] = withered - case *[]string, []string: - fruit.Mutagenesis = tList - withered.Mutagenesis = tList - tree.figs[n] = fruit - tree.withered[n] = withered - case *map[string]string, map[string]string: - fruit.Mutagenesis = tMap - withered.Mutagenesis = tMap - tree.figs[n] = fruit - tree.withered[n] = withered + name: n, + Value: Value{ + Mutagensis: mut, + Value: d, + }, } + tree.figs[n] = fruit + tree.withered[n] = withered } return nil @@ -245,13 +229,13 @@ func (tree *figTree) setValue(flagVal interface{}, value interface{}) error { if err != nil { return err } - *ptr.values = listVal + ptr.values = listVal case *MapFlag: mapVal, err := toStringMap(value) if err != nil { return err } - *ptr.values = mapVal + ptr.values = mapVal default: return fmt.Errorf("unsupported flag type %T", ptr) } @@ -266,6 +250,7 @@ func (tree *figTree) readEnv() { for name := range tree.figs { tree.checkAndSetFromEnv(name) } + return } // checkAndSetFromEnv uses os.LookupEnv and assigns it to the figs name value @@ -274,38 +259,44 @@ func (tree *figTree) checkAndSetFromEnv(name string) { return } if !tree.ignoreEnv { + name = strings.ToUpper(name) if val, exists := os.LookupEnv(name); exists { _ = tree.mutateFig(name, val) } } + return } // mutateFig replaces the value interface{} and sends a Mutation into Mutations func (tree *figTree) mutateFig(name string, value interface{}) error { + name = strings.ToLower(name) def, ok := tree.figs[name] if !ok || def == nil { - tree.Resurrect(name) - def = tree.figs[name] - } - if def == nil { - return fmt.Errorf("no definition for key %s", name) + return fmt.Errorf("no such fig: %s", name) } var old interface{} var dead interface{} witheredFig, ok := tree.withered[name] - dead = witheredFig.Flesh - old = def.Flesh - def.Flesh = figFlesh{value} - t1 := tree.MutagenesisOf(&old) - t2 := tree.MutagenesisOf(value) - if t1 == "" && t2 != "" { + dead = witheredFig.Value.Value + _value := tree.useValue(tree.from(name)) + old = _value.Flesh() + err := _value.Assign(value) + if err != nil { + return err + } + tree.values.Store(name, _value) + t1 := string(tree.MutagenesisOf(&old)) + t2 := string(_value.Mutagensis) + if strings.EqualFold(t1, "") && t2 != "" { t1 = t2 } - if !strings.EqualFold(string(t1), string(t2)) { + if !strings.EqualFold(t1, t2) { return fmt.Errorf("type mismatch for key %s", name) } // if tree.tracking && old != dead && dead != value - if tree.tracking && !reflect.DeepEqual(old, dead) && !reflect.DeepEqual(dead, value) { + oldNotDead := !reflect.DeepEqual(old, dead) + notDeadWithValue := !reflect.DeepEqual(dead, value) + if tree.tracking && oldNotDead && notDeadWithValue { tree.mutationsCh <- Mutation{ Property: name, Mutagenesis: fmt.Sprintf("%T", value), @@ -346,3 +337,17 @@ func filterTestFlags(args []string) []string { } return filtered } + +func DeduplicateStrings(slice []string) []string { + seen := make(map[string]bool) + var result []string + + for _, s := range slice { + if !seen[s] { + seen[s] = true + result = append(result, s) + } + } + + return result +} diff --git a/internals_test.go b/internals_test.go index ec19341..f9e40ed 100644 --- a/internals_test.go +++ b/internals_test.go @@ -4,7 +4,9 @@ import ( "flag" "fmt" "os" + "strings" "sync" + "sync/atomic" "testing" "time" @@ -14,23 +16,37 @@ import ( func TestTree_checkAndSetFromEnv(t *testing.T) { const k, u = "workers-check-and-set-from-env", "usage" - // create a new fig tree - var figs *figTree - figs = &figTree{ - harvest: 1, - figs: make(map[string]*figFruit), - tracking: false, - withered: make(map[string]witheredFig), - aliases: make(map[string]string), - flagSet: flag.NewFlagSet(os.Args[0], flag.ContinueOnError), - mu: sync.RWMutex{}, - mutationsCh: make(chan Mutation, 1), - filterTests: true, + // create a new fig tree internally to test the func checkAndSetFromEnv + angel := atomic.Bool{} + angel.Store(true) + opts := Options{Germinate: true} + figs := &figTree{ + ConfigFilePath: opts.ConfigFile, + ignoreEnv: opts.IgnoreEnvironment, + filterTests: opts.Germinate, + pollinate: opts.Pollinate, + tracking: opts.Tracking, + harvest: opts.Harvest, + angel: &angel, + problems: make([]error, 0), + aliases: make(map[string]string), + figs: make(map[string]*figFruit), + values: &sync.Map{}, + withered: make(map[string]witheredFig), + mu: sync.RWMutex{}, + mutationsCh: make(chan Mutation), + flagSet: flag.NewFlagSet(os.Args[0], flag.ContinueOnError), } + figs.flagSet.Usage = figs.Usage + angel.Store(false) + if opts.IgnoreEnvironment { + os.Clearenv() + } + + figs = figs.NewInt(k, 10, "Number").(*figTree) // assign an int to k - figs.NewInt(k, 10, u) - assert.Nil(t, figs.Parse()) + assert.NoError(t, figs.Parse()) // verify assignment assert.Equal(t, 10, *figs.Int(k)) @@ -47,7 +63,7 @@ func TestTree_checkAndSetFromEnv(t *testing.T) { case <-timer.C: return case <-checker.C: - assert.NoError(t, os.Setenv(k, "17")) + assert.NoError(t, os.Setenv(strings.ToUpper(k), "17")) } } }() @@ -130,6 +146,7 @@ func TestTree_setValue(t *testing.T) { figs: tt.fields.figs, withered: tt.fields.withered, mu: tt.fields.mu, + values: &sync.Map{}, tracking: tt.fields.tracking, mutationsCh: tt.fields.mutationsCh, flagSet: flag.NewFlagSet(os.Args[0], flag.ContinueOnError), @@ -160,6 +177,7 @@ func TestTree_setValuesFromMap(t *testing.T) { aliases: make(map[string]string), mu: sync.RWMutex{}, tracking: false, + values: &sync.Map{}, mutationsCh: make(chan Mutation, 1), flagSet: flag.NewFlagSet(os.Args[0], flag.ContinueOnError), filterTests: true, diff --git a/list_flag.go b/list_flag.go index 93c670d..8d823c5 100644 --- a/list_flag.go +++ b/list_flag.go @@ -6,24 +6,31 @@ import ( // ListFlag stores values in a list type configurable type ListFlag struct { - values *[]string + values []string } func (tree *figTree) ListValues(name string) []string { tree.mu.Lock() defer tree.mu.Unlock() - fruit, exists := tree.figs[name] - if !exists { + if _, exists := tree.figs[name]; !exists { return []string{} } - return fruit.Flesh.ToList() + valueAny, ok := tree.values.Load(name) + if !ok { + return nil + } + _value, ok := valueAny.(*Value) + if !ok { + return nil + } + return _value.Flesh().ToList() } func (l *ListFlag) Values() []string { if l.values == nil { return []string{} } - return *l.values + return l.values } // String returns the slice of strings using strings.Join @@ -31,7 +38,7 @@ func (l *ListFlag) String() string { if l.values == nil { return "" } - return strings.Join(*l.values, ",") + return strings.Join(l.values, ListSeparator) } // PolicyListAppend will apply ListFlag.Set to the list of values and not append to any existing values in the ListFlag @@ -40,13 +47,13 @@ var PolicyListAppend bool = false // Set unpacks a comma separated value argument and appends items to the list of []string func (l *ListFlag) Set(value string) error { if l.values == nil { - l.values = &[]string{} + l.values = []string{} } - items := strings.Split(value, ",") + items := strings.Split(value, ListSeparator) if PolicyListAppend { - *l.values = append(*l.values, items...) + l.values = append(l.values, items...) } else { - *l.values = items + l.values = items } return nil } diff --git a/list_flag_test.go b/list_flag_test.go index f6d3fa1..d6374f7 100644 --- a/list_flag_test.go +++ b/list_flag_test.go @@ -17,13 +17,14 @@ func TestTree_ListValues(t *testing.T) { func TestListFlag_Set(t *testing.T) { t.Run("PolicyListAppend_TRUE", func(t *testing.T) { PolicyListAppend = true - os.Args = []string{os.Args[0], "-x", "yahuah"} + defer func() { PolicyListAppend = false }() + os.Args = []string{os.Args[0], "-x", "yeshua"} figs := With(Options{Germinate: true}) figs.NewList("x", []string{"bum"}, "Name List") assert.NoError(t, figs.Parse()) - assert.Equal(t, "", figs.Fig("x").ToString()) - assert.Contains(t, *figs.List("x"), "yahuah") - assert.Contains(t, *figs.List("x"), "bum") // Contains because of PolicyListAppend + assert.Equal(t, "bum,yeshua", figs.FigFlesh("x").ToString()) + assert.Contains(t, *figs.List("x"), "yeshua") + assert.Contains(t, *figs.List("x"), "bum") os.Args = []string{os.Args[0]} }) t.Run("PolicyListAppend_DEFAULT", func(t *testing.T) { @@ -32,7 +33,7 @@ func TestListFlag_Set(t *testing.T) { figs := With(Options{Germinate: true}) figs.NewList("x", []string{"bum"}, "Name List") assert.NoError(t, figs.Parse()) - assert.Equal(t, "", figs.Fig("x").ToString()) + assert.Equal(t, "yahuah", figs.FigFlesh("x").ToString()) assert.Contains(t, *figs.List("x"), "yahuah") assert.NotContains(t, *figs.List("x"), "bum") // NotContains because of PolicyListAppend os.Args = []string{os.Args[0]} diff --git a/loading.go b/loading.go index 0aeb66e..2538765 100644 --- a/loading.go +++ b/loading.go @@ -1,12 +1,14 @@ package figtree import ( + "errors" + "flag" "fmt" - "os" - "path/filepath" - check "github.com/andreimerlescu/checkfs" "github.com/andreimerlescu/checkfs/file" + "os" + "path/filepath" + "strings" ) // Reload will readEnv on each flag in the configurable package @@ -15,17 +17,45 @@ func (tree *figTree) Reload() error { return tree.validateAll() } +func (tree *figTree) preLoadOrParse() error { + tree.mu.RLock() + defer tree.mu.RUnlock() + for name, fig := range tree.figs { + value, err := tree.from(name) + if err != nil { + return err + } + if value.Err != nil { + return value.Err + } + if fig.Error != nil { + return fig.Error + } + } + return tree.checkFigErrors() +} + // Load uses the EnvironmentKey and the DefaultJSONFile, DefaultYAMLFile, and DefaultINIFile to run ParseFile if it exists func (tree *figTree) Load() (err error) { + preloadErr := tree.preLoadOrParse() + if preloadErr != nil { + return preloadErr + } if !tree.HasRule(RuleNoFlags) { tree.activateFlagSet() args := os.Args[1:] if tree.filterTests { args = filterTestFlags(args) - err = tree.flagSet.Parse(args) - } else { - err = tree.flagSet.Parse(args) } + err = tree.flagSet.Parse(args) + if err != nil { + err2 := tree.checkFigErrors() + if err2 != nil { + err = errors.Join(err, err2) + } + return fmt.Errorf("failed to Load() due to err: %w", err) + } + err = tree.loadFlagSet() if err != nil { return err } @@ -52,23 +82,32 @@ func (tree *figTree) Load() (err error) { } } } - tree.readEnv() + err = tree.checkFigErrors() + if err != nil { + return fmt.Errorf("checkFigErrors() threw err: %w", err) + } return tree.validateAll() } // LoadFile accepts a path and uses it to populate the figTree func (tree *figTree) LoadFile(path string) (err error) { + preloadErr := tree.preLoadOrParse() + if preloadErr != nil { + return preloadErr + } if !tree.HasRule(RuleNoFlags) { tree.activateFlagSet() args := os.Args[1:] if tree.filterTests { args = filterTestFlags(args) - err = tree.flagSet.Parse(args) - } else { - err = tree.flagSet.Parse(args) } + err = tree.flagSet.Parse(args) if err != nil { + err2 := tree.checkFigErrors() + if err2 != nil { + err = errors.Join(err, err2) + } return err } } @@ -78,16 +117,117 @@ func (tree *figTree) LoadFile(path string) (err error) { return fmt.Errorf("failed to loadFile %s: %w", path, err2) } tree.readEnv() - err3 := tree.validateAll() + err3 := tree.loadFlagSet() if err3 != nil { - return fmt.Errorf("failed to validateAll: %w", err3) + return err3 + } + err4 := tree.validateAll() + if err4 != nil { + return fmt.Errorf("failed to validateAll: %w", err4) } return nil } - tree.readEnv() - err3 := tree.validateAll() + err3 := tree.loadFlagSet() if err3 != nil { - return fmt.Errorf("failed to validateAll: %w", err3) + return err3 + } + tree.readEnv() + err4 := tree.checkFigErrors() + if err4 != nil { + return fmt.Errorf("failed to checkFigErrors: %w", err4) + } + err5 := tree.validateAll() + if err5 != nil { + return fmt.Errorf("failed to validateAll: %w", err5) } return fmt.Errorf("failed to LoadFile %s due to err %v", path, loadErr) } + +func (tree *figTree) loadFlagSet() (e error) { + defer func() { + if e != nil { + _, _ = fmt.Fprintf(os.Stderr, "loadFlagSet() err: %s", e.Error()) + } + /* + if r := recover(); r != nil { + _, _ = fmt.Fprintf(os.Stderr, "RECOVERY %v", r) + } + */ + }() + tree.flagSet.VisitAll(func(f *flag.Flag) { + flagName := f.Name + for alias, name := range tree.aliases { + if strings.EqualFold(alias, f.Name) { + flagName = name + } + } + value, err := tree.from(flagName) + if err != nil || value == nil { + e = fmt.Errorf("loadFlagSet(): failed to load %s: %w", flagName, err) + return + } + switch value.Mutagensis { + case tMap: + merged := value.Flesh().ToMap() + withered := tree.withered[flagName] + witheredValue := withered.Value.Flesh().ToMap() + flagged, err := toStringMap(f.Value) + if err != nil { + e = fmt.Errorf("failed to load %s: %w", flagName, err) + return + } + result := make(map[string]string) + if PolicyMapAppend { + for k, v := range witheredValue { + result[k] = v + } + } + for k, v := range merged { + result[k] = v + } + for k, v := range flagged { + result[k] = v + } + err = value.Assign(result) + if err != nil { + e = fmt.Errorf("failed to load %s: %w", flagName, err) + return + } + case tList: + merged, err := toStringSlice(value.Value) + if err != nil { + e = fmt.Errorf("failed to load %s: %w", flagName, err) + return + } + flagged, err := toStringSlice(f.Value) + if err != nil { + e = fmt.Errorf("failed to load %s: %w", flagName, err) + return + } + unique := make(map[string]bool) + for _, v := range merged { + unique[v] = true + } + for _, v := range flagged { + unique[v] = true + } + var newValue []string + for k, _ := range unique { + newValue = append(newValue, k) + } + err = value.Assign(newValue) + if err != nil { + e = fmt.Errorf("failed to load %s: %w", flagName, err) + return + } + default: + err := value.Set(f.Value.String()) + if err != nil { + e = fmt.Errorf("failed to value.Set(%s) due to err: %w", f.Value.String(), err) + return + } + } + tree.values.Store(flagName, value) + }) + return nil +} diff --git a/map_flag.go b/map_flag.go index d53b2e9..4f22c40 100644 --- a/map_flag.go +++ b/map_flag.go @@ -2,22 +2,37 @@ package figtree import ( "fmt" + "maps" + "os" "strings" ) // MapFlag stores values in a map type configurable type MapFlag struct { - values *map[string]string + values map[string]string } func (tree *figTree) MapKeys(name string) []string { tree.mu.Lock() defer tree.mu.Unlock() + originalName := strings.Clone(name) // capture original + defer func() { + name = originalName // return value to original + }() + name = strings.ToLower(name) fruit, exists := tree.figs[name] if !exists { return []string{} } - switch v := fruit.Flesh.Flesh.(type) { + if fruit == nil { + return []string{} + } + _value, err := tree.from(name) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "tree.from(%s) return err: %v", name, err.Error()) + return []string{} + } + switch v := _value.Value.(type) { case nil: return []string{} case map[string]string: @@ -32,9 +47,15 @@ func (tree *figTree) MapKeys(name string) []string { keys = append(keys, k) } return keys + case Value: + keys := make([]string, 0, len(*v.Value.(*map[string]string))) + for k := range *v.Value.(*map[string]string) { + keys = append(keys, k) + } + return keys case *MapFlag: - keys := make([]string, 0, len(*v.values)) - for k := range *v.values { + keys := make([]string, 0, len(v.values)) + for k := range v.values { keys = append(keys, k) } return keys @@ -48,7 +69,7 @@ func (m *MapFlag) Keys() []string { return []string{} } var keys []string - for key := range *m.values { + for key := range m.values { keys = append(keys, key) } return keys @@ -60,8 +81,8 @@ func (m *MapFlag) String() string { return "" } var entries []string - for k, v := range *m.values { - entries = append(entries, fmt.Sprintf("%s=%s", k, v)) + for k, v := range m.values { + entries = append(entries, fmt.Sprintf("%s%s%s", k, MapKeySeparator, v)) } return strings.Join(entries, ",") } @@ -70,19 +91,26 @@ var PolicyMapAppend = false // Set accepts a value like KEY=VALUE,KEY=VALUE,KEY=VALUE to override map values func (m *MapFlag) Set(value string) error { - if m.values == nil { - m.values = &map[string]string{} + if m.values == nil || !PolicyMapAppend { + m.values = map[string]string{} } - if !PolicyMapAppend { - m.values = &map[string]string{} + existing := maps.Clone(m.values) + if PolicyMapAppend { + for k, v := range existing { + m.values[k] = v + } } - pairs := strings.Split(value, ",") + adding := make(map[string]string) + pairs := strings.Split(value, MapSeparator) for _, pair := range pairs { - kv := strings.SplitN(pair, "=", 2) + kv := strings.SplitN(pair, MapKeySeparator, 2) if len(kv) != 2 { return fmt.Errorf("invalid map item: %s", pair) } - (*m.values)[kv[0]] = kv[1] + adding[kv[0]] = kv[1] + } + for k, v := range adding { + m.values[k] = v } return nil } diff --git a/map_flag_test.go b/map_flag_test.go index fc37433..8364bc8 100644 --- a/map_flag_test.go +++ b/map_flag_test.go @@ -8,32 +8,34 @@ import ( ) func TestTree_MapKeys(t *testing.T) { + os.Args = []string{os.Args[0]} figs := With(Options{Germinate: true}) - figs.NewMap(t.Name(), map[string]string{"name": "yahuah"}, "Name Map") + figs = figs.NewMap(t.Name(), map[string]string{"name": "yahuah"}, "Name Map") assert.NoError(t, figs.Parse()) - assert.Contains(t, figs.MapKeys(t.Name()), "name") + mk := figs.MapKeys(t.Name()) + assert.Contains(t, mk, "name") } func TestMapFlag_Set(t *testing.T) { t.Run("PolicyMapAppend_TRUE", func(t *testing.T) { PolicyMapAppend = true + defer func() { PolicyMapAppend = false }() os.Args = []string{os.Args[0], "-x", "name=yahuah"} figs := With(Options{Germinate: true}) - figs.NewMap("x", map[string]string{"job": "bum"}, "Name Map") + figs = figs.NewMap("x", map[string]string{"job": "bum"}, "Name Map") assert.NoError(t, figs.Parse()) - assert.Equal(t, "", figs.Fig("x").ToString()) assert.Contains(t, figs.MapKeys("x"), "name") assert.Contains(t, figs.MapKeys("x"), "job") // Contains because of PolicyMapAppend os.Args = []string{os.Args[0]} }) t.Run("PolicyMapAppend_DEFAULT", func(t *testing.T) { PolicyMapAppend = false - os.Args = []string{os.Args[0], "-x", "name=yahuah"} + os.Args = []string{os.Args[0], "-x", "name=yeshua,age=33"} figs := With(Options{Germinate: true}) - figs.NewMap("x", map[string]string{"job": "bum"}, "Name Map") + figs = figs.NewMap("x", map[string]string{"job": "bum"}, "Name Map") assert.NoError(t, figs.Parse()) - assert.Equal(t, "", figs.Fig("x").ToString()) assert.Contains(t, figs.MapKeys("x"), "name") + assert.Contains(t, figs.MapKeys("x"), "age") assert.NotContains(t, figs.MapKeys("x"), "job") // NotContains because no PolicyMapAppend os.Args = []string{os.Args[0]} }) diff --git a/miracles.go b/miracles.go index ffe6e66..3ee71a6 100644 --- a/miracles.go +++ b/miracles.go @@ -1,19 +1,5 @@ package figtree -import ( - "encoding/json" - "flag" - "log" - "os" - "path/filepath" - "strings" - - check "github.com/andreimerlescu/checkfs" - "github.com/andreimerlescu/checkfs/file" - "github.com/go-ini/ini" - "gopkg.in/yaml.v3" -) - // Mutations returns a receiver channel of Mutation data func (tree *figTree) Mutations() <-chan Mutation { return tree.mutationsCh @@ -33,128 +19,8 @@ func (tree *figTree) Curse() { close(tree.mutationsCh) } -// Resurrect revives a missing or nil definition, checking env and config files first -func (tree *figTree) Resurrect(name string) { - tree.mu.Lock() - defer tree.mu.Unlock() - if tree.HasRule(RuleCondemnedFromResurrection) { - log.Fatalf("resurrection %q condemned", name) - } - if _, exists := tree.figs[name]; !exists { - // Check environment first - if !tree.ignoreEnv { - if val, ok := os.LookupEnv(name); ok { - ptr := new(string) - *ptr = strings.Clone(val) // Use strings.Clone as requested - tree.figs[name] = &figFruit{ - Flesh: figFlesh{ptr}, - Mutagenesis: tree.MutagenesisOf(val), - Validators: make([]FigValidatorFunc, 0), - Callbacks: make([]Callback, 0), - Mutations: make([]Mutation, 0), - } - flag.String(name, val, "Resurrected from environment") - return - } - } - - // Check config files with traditional for loop - envVal := "" - if !tree.ignoreEnv { - envVal = os.Getenv(EnvironmentKey) - } - files := []string{ - envVal, - tree.ConfigFilePath, - ConfigFilePath, - filepath.Join(".", DefaultJSONFile), - filepath.Join(".", DefaultINIFile), - } - for i := 0; i < len(files); i++ { - f := files[i] - if f == "" { - continue - } - if err := check.File(f, file.Options{Exists: true}); err == nil { - data, err := os.ReadFile(f) - if err == nil { - var m map[string]interface{} - ext := strings.ToLower(filepath.Ext(f)) - switch ext { - case ".json": - if json.Unmarshal(data, &m) == nil && m[name] != nil { - if strVal, err := toString(m[name]); err == nil { - ptr := new(string) - *ptr = strings.Clone(strVal) - tree.figs[name] = &figFruit{ - Flesh: figFlesh{ptr}, - Mutagenesis: tree.MutagenesisOf(ptr), - Validators: make([]FigValidatorFunc, 0), - Callbacks: make([]Callback, 0), - Mutations: make([]Mutation, 0), - } - flag.String(name, strVal, "Resurrected from JSON") - return - } - } - case ".yaml", ".yml": - if yaml.Unmarshal(data, &m) == nil && m[name] != nil { - if strVal, err := toString(m[name]); err == nil { - ptr := new(string) - *ptr = strings.Clone(strVal) - tree.figs[name] = &figFruit{ - Flesh: figFlesh{ptr}, - Mutagenesis: tree.MutagenesisOf(ptr), - Validators: make([]FigValidatorFunc, 0), - Callbacks: make([]Callback, 0), - Mutations: make([]Mutation, 0), - } - flag.String(name, strVal, "Resurrected from YAML") - return - } - } - case ".ini": - if cfg, err := ini.Load(data); err == nil { - if val := cfg.Section("").Key(name).String(); val != "" { - ptr := new(string) - *ptr = strings.Clone(val) - tree.figs[name] = &figFruit{ - Flesh: figFlesh{ptr}, - Mutagenesis: tree.MutagenesisOf(ptr), - Validators: make([]FigValidatorFunc, 0), - Callbacks: make([]Callback, 0), - Mutations: make([]Mutation, 0), - } - flag.String(name, val, "Resurrected from INI") - return - } - } - } - } - } - } - - // Default to empty string if no value found - ptr := new(string) - *ptr = "" - tree.figs[name] = &figFruit{ - Flesh: figFlesh{ptr}, - Mutagenesis: tree.MutagenesisOf(ptr), - Validators: make([]FigValidatorFunc, 0), - Callbacks: make([]Callback, 0), - Mutations: make([]Mutation, 0), - } - flag.String(name, "", "Resurrected configuration") - } -} - -// Fig returns a figFruit on the fig figTree -func (tree *figTree) Fig(name string) Flesh { - tree.mu.RLock() - defer tree.mu.RUnlock() - fruit, exists := tree.figs[name] - if !exists { - return nil - } - return &fruit.Flesh +// FigFlesh returns a Flesh interface to the Value on the figTree +func (tree *figTree) FigFlesh(name string) Flesh { + value := tree.useValue(tree.from(name)) + return value.Flesh() } diff --git a/mutations.go b/mutations.go index c14d4b4..2439466 100644 --- a/mutations.go +++ b/mutations.go @@ -2,6 +2,7 @@ package figtree import ( "errors" + "fmt" "os" "slices" "strconv" @@ -9,47 +10,66 @@ import ( "time" ) +var ( + zeroString string + zeroBool bool + zeroFloat64 float64 + zeroInt int + zeroInt64 int64 + zeroDuration time.Duration + zeroList []string + zeroMap map[string]string // Will be nil map by default, should be make(map[string]string) if always non-nil +) + +func init() { + zeroList = make([]string, 0) + zeroMap = make(map[string]string) +} + // String with mutation tracking func (tree *figTree) String(name string) *string { tree.mu.RLock() defer tree.mu.RUnlock() + originalName := strings.Clone(name) // in case we need it + defer func() { + name = originalName // restore after we're done + }() + name = strings.ToLower(name) if _, exists := tree.aliases[name]; exists { name = tree.aliases[name] } fruit, ok := tree.figs[name] if !ok || fruit == nil { - tree.mu.RUnlock() - tree.Resurrect(name) - tree.mu.RLock() - fruit = tree.figs[name] + return nil } - err := fruit.runCallbacks(CallbackBeforeRead) + err := fruit.runCallbacks(tree, CallbackBeforeRead) if err != nil { fruit.Error = errors.Join(fruit.Error, err) tree.figs[name] = fruit - return nil + return &zeroString } - s, err := toString(fruit.Flesh.Flesh) + value, err := tree.from(name) if err != nil { fruit.Error = errors.Join(fruit.Error, err) tree.figs[name] = fruit - return nil + return &zeroString } + s := value.Flesh().ToString() if !tree.HasRule(RuleNoEnv) && !fruit.HasRule(RuleNoEnv) && !tree.ignoreEnv && tree.pollinate { envs := os.Environ() var e string var ok bool for _, env := range envs { - if strings.EqualFold(env, name) { + if strings.EqualFold(env, strings.ToUpper(name)) { v := strings.Split(env, "=") e = v[1] } } if len(e) == 0 { - e, ok = os.LookupEnv(name) + e, ok = os.LookupEnv(strings.ToUpper(name)) } if ok && len(e) > 0 { - if !strings.EqualFold(e, s) { + if !strings.EqualFold(strings.ToLower(e), strings.ToLower(s)) { s = strings.Clone(e) tree.mu.RUnlock() tree.Store(fruit.Mutagenesis, name, e) @@ -58,10 +78,11 @@ func (tree *figTree) String(name string) *string { } } } - err = fruit.runCallbacks(CallbackAfterRead) + err = fruit.runCallbacks(tree, CallbackAfterRead) if err != nil { fruit.Error = errors.Join(fruit.Error, err) tree.figs[name] = fruit + return &zeroString } return &s } @@ -70,23 +91,31 @@ func (tree *figTree) String(name string) *string { func (tree *figTree) Bool(name string) *bool { tree.mu.RLock() defer tree.mu.RUnlock() + originalName := strings.Clone(name) // in case we need it + defer func() { + name = originalName // restore after we're done + }() + name = strings.ToLower(name) if _, exists := tree.aliases[name]; exists { name = tree.aliases[name] } fruit, ok := tree.figs[name] if !ok || fruit == nil { - tree.mu.RUnlock() - tree.Resurrect(name) - tree.mu.RLock() - fruit = tree.figs[name] + return nil } - err := fruit.runCallbacks(CallbackBeforeRead) + err := fruit.runCallbacks(tree, CallbackBeforeRead) if err != nil { fruit.Error = errors.Join(fruit.Error, err) tree.figs[name] = fruit - return nil + return &zeroBool } - s := fruit.Flesh.ToBool() + value, err := tree.from(name) + if err != nil { + fruit.Error = errors.Join(fruit.Error, err) + tree.figs[name] = fruit + return &zeroBool + } + s := value.Flesh().ToBool() if !tree.HasRule(RuleNoEnv) && !fruit.HasRule(RuleNoEnv) && !tree.ignoreEnv && tree.pollinate { e := os.Getenv(name) if len(e) > 0 { @@ -107,10 +136,11 @@ func (tree *figTree) Bool(name string) *bool { } } } - err = fruit.runCallbacks(CallbackAfterRead) + err = fruit.runCallbacks(tree, CallbackAfterRead) if err != nil { fruit.Error = errors.Join(fruit.Error, err) tree.figs[name] = fruit + return &zeroBool } return &s } @@ -119,23 +149,35 @@ func (tree *figTree) Bool(name string) *bool { func (tree *figTree) Int(name string) *int { tree.mu.RLock() defer tree.mu.RUnlock() + originalName := strings.Clone(name) // in case we need it + defer func() { + name = originalName // restore after we're done + }() + name = strings.ToLower(name) if _, exists := tree.aliases[name]; exists { name = tree.aliases[name] } fruit, ok := tree.figs[name] if !ok || fruit == nil { - tree.mu.RUnlock() - tree.Resurrect(name) - tree.mu.RLock() - fruit = tree.figs[name] + return nil } - err := fruit.runCallbacks(CallbackBeforeRead) + err := fruit.runCallbacks(tree, CallbackBeforeRead) if err != nil { fruit.Error = errors.Join(fruit.Error, err) tree.figs[name] = fruit - return nil + return &zeroInt } - s := fruit.Flesh.ToInt() + value, err := tree.from(name) + if err != nil { + fruit.Error = errors.Join(fruit.Error, err) + tree.figs[name] = fruit + return &zeroInt + } + if value.Err != nil { + fmt.Printf("value(%s).Err = %v\n", name, value.Err.Error()) + return &zeroInt + } + s := value.Flesh().ToInt() if !tree.HasRule(RuleNoEnv) && !fruit.HasRule(RuleNoEnv) && !tree.ignoreEnv && tree.pollinate { e := os.Getenv(name) if len(e) > 0 { @@ -157,10 +199,11 @@ func (tree *figTree) Int(name string) *int { } } - err = fruit.runCallbacks(CallbackAfterRead) + err = fruit.runCallbacks(tree, CallbackAfterRead) if err != nil { fruit.Error = errors.Join(fruit.Error, err) tree.figs[name] = fruit + return &zeroInt } return &s } @@ -169,23 +212,31 @@ func (tree *figTree) Int(name string) *int { func (tree *figTree) Int64(name string) *int64 { tree.mu.RLock() defer tree.mu.RUnlock() + originalName := strings.Clone(name) // in case we need it + defer func() { + name = originalName // restore after we're done + }() + name = strings.ToLower(name) if _, exists := tree.aliases[name]; exists { name = tree.aliases[name] } fruit, ok := tree.figs[name] if !ok || fruit == nil { - tree.mu.RUnlock() - tree.Resurrect(name) - tree.mu.RLock() - fruit = tree.figs[name] + return nil } - err := fruit.runCallbacks(CallbackBeforeRead) + err := fruit.runCallbacks(tree, CallbackBeforeRead) if err != nil { fruit.Error = errors.Join(fruit.Error, err) tree.figs[name] = fruit - return nil + return &zeroInt64 } - s := fruit.Flesh.ToInt64() + value, err := tree.from(name) + if err != nil { + fruit.Error = errors.Join(fruit.Error, err) + tree.figs[name] = fruit + return &zeroInt64 + } + s := value.Flesh().ToInt64() if !tree.HasRule(RuleNoEnv) && !fruit.HasRule(RuleNoEnv) && !tree.ignoreEnv && tree.pollinate { e := os.Getenv(name) if len(e) > 0 { @@ -207,10 +258,11 @@ func (tree *figTree) Int64(name string) *int64 { } } - err = fruit.runCallbacks(CallbackAfterRead) + err = fruit.runCallbacks(tree, CallbackAfterRead) if err != nil { fruit.Error = errors.Join(fruit.Error, err) tree.figs[name] = fruit + return &zeroInt64 } return &s } @@ -219,23 +271,31 @@ func (tree *figTree) Int64(name string) *int64 { func (tree *figTree) Float64(name string) *float64 { tree.mu.RLock() defer tree.mu.RUnlock() + originalName := strings.Clone(name) // in case we need it + defer func() { + name = originalName // restore after we're done + }() + name = strings.ToLower(name) if _, exists := tree.aliases[name]; exists { name = tree.aliases[name] } fruit, ok := tree.figs[name] if !ok || fruit == nil { - tree.mu.RUnlock() - tree.Resurrect(name) - tree.mu.RLock() - fruit = tree.figs[name] + return nil } - err := fruit.runCallbacks(CallbackBeforeRead) + err := fruit.runCallbacks(tree, CallbackBeforeRead) if err != nil { fruit.Error = errors.Join(fruit.Error, err) tree.figs[name] = fruit - return nil + return &zeroFloat64 + } + value, err := tree.from(name) + if err != nil { + fruit.Error = errors.Join(fruit.Error, err) + tree.figs[name] = fruit + return &zeroFloat64 } - s := fruit.Flesh.ToFloat64() + s := value.Flesh().ToFloat64() if !tree.HasRule(RuleNoEnv) && !fruit.HasRule(RuleNoEnv) && !tree.ignoreEnv && tree.pollinate { e := os.Getenv(name) if len(e) > 0 { @@ -257,10 +317,11 @@ func (tree *figTree) Float64(name string) *float64 { } } - err = fruit.runCallbacks(CallbackAfterRead) + err = fruit.runCallbacks(tree, CallbackAfterRead) if err != nil { fruit.Error = errors.Join(fruit.Error, err) tree.figs[name] = fruit + return &zeroFloat64 } return &s } @@ -269,24 +330,36 @@ func (tree *figTree) Float64(name string) *float64 { func (tree *figTree) Duration(name string) *time.Duration { tree.mu.RLock() defer tree.mu.RUnlock() + originalName := strings.Clone(name) // in case we need it + defer func() { + name = originalName // restore after we're done + }() + name = strings.ToLower(name) if _, exists := tree.aliases[name]; exists { name = tree.aliases[name] } fruit, ok := tree.figs[name] if !ok || fruit == nil { - tree.mu.RUnlock() - tree.Resurrect(name) - tree.mu.RLock() - fruit = tree.figs[name] + return nil } - err := fruit.runCallbacks(CallbackBeforeRead) + err := fruit.runCallbacks(tree, CallbackBeforeRead) if err != nil { fruit.Error = errors.Join(fruit.Error, err) tree.figs[name] = fruit - return nil + return &zeroDuration } var d time.Duration - switch f := fruit.Flesh.Flesh.(type) { + value, err := tree.from(name) + if err != nil { + fruit.Error = errors.Join(fruit.Error, err) + tree.figs[name] = fruit + return &zeroDuration + } + switch f := value.Value.(type) { + case int64: + d = time.Duration(f) + case *int64: + d = time.Duration(*f) case time.Duration: d = f case *time.Duration: @@ -314,10 +387,11 @@ func (tree *figTree) Duration(name string) *time.Duration { } } } - err = fruit.runCallbacks(CallbackAfterRead) + err = fruit.runCallbacks(tree, CallbackAfterRead) if err != nil { fruit.Error = errors.Join(fruit.Error, err) tree.figs[name] = fruit + return &zeroDuration } return &d } @@ -326,24 +400,36 @@ func (tree *figTree) Duration(name string) *time.Duration { func (tree *figTree) UnitDuration(name string) *time.Duration { tree.mu.RLock() defer tree.mu.RUnlock() + originalName := strings.Clone(name) // in case we need it + defer func() { + name = originalName // restore after we're done + }() + name = strings.ToLower(name) if _, exists := tree.aliases[name]; exists { name = tree.aliases[name] } fruit, ok := tree.figs[name] if !ok || fruit == nil { - tree.mu.RUnlock() - tree.Resurrect(name) - tree.mu.RLock() - fruit = tree.figs[name] + return nil } - err := fruit.runCallbacks(CallbackBeforeRead) + err := fruit.runCallbacks(tree, CallbackBeforeRead) if err != nil { fruit.Error = errors.Join(fruit.Error, err) tree.figs[name] = fruit - return nil + return &zeroDuration } var d time.Duration - switch f := fruit.Flesh.Flesh.(type) { + value, err := tree.from(name) + if err != nil { + fruit.Error = errors.Join(fruit.Error, err) + tree.figs[name] = fruit + return &zeroDuration + } + switch f := value.Value.(type) { + case int64: + d = time.Duration(f) + case *int64: + d = time.Duration(*f) case time.Duration: d = f case *time.Duration: @@ -371,39 +457,55 @@ func (tree *figTree) UnitDuration(name string) *time.Duration { } } } - err = fruit.runCallbacks(CallbackAfterRead) + err = fruit.runCallbacks(tree, CallbackAfterRead) if err != nil { fruit.Error = errors.Join(fruit.Error, err) tree.figs[name] = fruit + return &zeroDuration } return &d } -// List with mutation tracking +// List returns a copy of the []string stored inside the Value of the figFruit func (tree *figTree) List(name string) *[]string { tree.mu.RLock() defer tree.mu.RUnlock() + originalName := strings.Clone(name) // in case we need it + defer func() { + name = originalName // restore after we're done + }() + name = strings.ToLower(name) if _, exists := tree.aliases[name]; exists { name = tree.aliases[name] } fruit, ok := tree.figs[name] if !ok || fruit == nil { - tree.mu.RUnlock() - tree.Resurrect(name) - tree.mu.RLock() - fruit = tree.figs[name] + return nil } - err := fruit.runCallbacks(CallbackBeforeRead) + value, err := tree.from(name) if err != nil { fruit.Error = errors.Join(fruit.Error, err) tree.figs[name] = fruit - return nil + return &zeroList + } + err = fruit.runCallbacks(tree, CallbackBeforeRead) + if err != nil { + fruit.Error = errors.Join(fruit.Error, err) + tree.figs[name] = fruit + return &zeroList } var v []string - switch f := fruit.Flesh.Flesh.(type) { + switch f := value.Value.(type) { + case string: + v = []string{f} + case *string: + v = []string{*f} + case ListFlag: + v = make([]string, len(f.values)) + copy(v, f.values) case *ListFlag: - v = make([]string, len(*f.values)) - copy(v, *f.values) + v = make([]string, len(f.values)) + copy(v, f.values) case *[]string: v = make([]string, len(*f)) copy(v, *f) @@ -416,33 +518,49 @@ func (tree *figTree) List(name string) *[]string { if !tree.HasRule(RuleNoEnv) && !fruit.HasRule(RuleNoEnv) && !tree.ignoreEnv && tree.pollinate { e := os.Getenv(name) if len(e) > 0 { - i := strings.Split(e, ",") + i := strings.Split(e, ListSeparator) if len(i) == 0 { v = []string{} } else if !slices.Equal(v, i) { tree.mu.RUnlock() tree.Store(fruit.Mutagenesis, name, i) tree.mu.RLock() - fruit = tree.figs[name] - switch f := fruit.Flesh.Flesh.(type) { + value, err := tree.from(name) + if err != nil { + fruit.Error = errors.Join(fruit.Error, err) + tree.figs[name] = fruit + return &zeroList + } + switch f := value.Value.(type) { + case string: + v = []string{f} + case *string: + v = []string{*f} + case ListFlag: + v = make([]string, len(f.values)) + copy(v, f.values) case *ListFlag: - v = make([]string, len(*f.values)) - copy(v, *f.values) + v = make([]string, len(f.values)) + copy(v, f.values) case *[]string: v = make([]string, len(*f)) copy(v, *f) case []string: v = make([]string, len(f)) copy(v, f) + default: + panic("unreachable") } } } } - err = fruit.runCallbacks(CallbackAfterRead) + err = fruit.runCallbacks(tree, CallbackAfterRead) if err != nil { fruit.Error = errors.Join(fruit.Error, err) tree.figs[name] = fruit + return &zeroList } + slices.Sort(v) return &v } @@ -450,28 +568,59 @@ func (tree *figTree) List(name string) *[]string { func (tree *figTree) Map(name string) *map[string]string { tree.mu.RLock() defer tree.mu.RUnlock() + originalName := strings.Clone(name) // in case we need it + defer func() { + name = originalName // restore after we're done + }() + name = strings.ToLower(name) if _, exists := tree.aliases[name]; exists { name = tree.aliases[name] } fruit, ok := tree.figs[name] if !ok || fruit == nil { - tree.mu.RUnlock() - tree.Resurrect(name) - tree.mu.RLock() - fruit = tree.figs[name] + return nil } - err := fruit.runCallbacks(CallbackBeforeRead) + value, err := tree.from(name) if err != nil { fruit.Error = errors.Join(fruit.Error, err) tree.figs[name] = fruit - return nil + return &zeroMap + } + err = fruit.runCallbacks(tree, CallbackBeforeRead) + if err != nil { + fruit.Error = errors.Join(fruit.Error, err) + tree.figs[name] = fruit + return &zeroMap } var v map[string]string - switch f := fruit.Flesh.Flesh.(type) { + switch f := value.Value.(type) { + case string: + v = map[string]string{} + for _, x := range strings.Split(f, MapSeparator) { + parts := strings.SplitN(x, MapKeySeparator, 2) + if len(parts) != 2 { + continue + } + v[parts[0]] = parts[1] + } + case *string: + v = map[string]string{} + for _, x := range strings.Split(*f, MapSeparator) { + parts := strings.SplitN(x, MapKeySeparator, 2) + if len(parts) != 2 { + continue + } + v[parts[0]] = parts[1] + } + case MapFlag: + v = make(map[string]string, len(f.values)) + for k, val := range f.values { + v[k] = val + } case *MapFlag: // Create a new map and copy the key-value pairs - v = make(map[string]string, len(*f.values)) - for k, val := range *f.values { + v = make(map[string]string, len(f.values)) + for k, val := range f.values { v[k] = val } case *map[string]string: @@ -496,7 +645,7 @@ func (tree *figTree) Map(name string) *map[string]string { } else { newMap := make(map[string]string) for _, iv := range i { - parts := strings.Split(iv, "=") + parts := strings.Split(iv, MapKeySeparator) if len(parts) == 2 { newMap[parts[0]] = parts[1] } @@ -516,11 +665,39 @@ func (tree *figTree) Map(name string) *map[string]string { tree.mu.RUnlock() tree.Store(fruit.Mutagenesis, name, newMap) tree.mu.RLock() - fruit = tree.figs[name] - switch f := fruit.Flesh.Flesh.(type) { + value, err := tree.from(name) + if err != nil { + fruit.Error = errors.Join(fruit.Error, err) + tree.figs[name] = fruit + return &zeroMap + } + switch f := value.Value.(type) { + case string: + v = map[string]string{} + for _, x := range strings.Split(f, MapSeparator) { + parts := strings.SplitN(x, MapKeySeparator, 2) + if len(parts) != 2 { + continue + } + v[parts[0]] = parts[1] + } + case *string: + v = map[string]string{} + for _, x := range strings.Split(*f, MapSeparator) { + parts := strings.SplitN(x, MapKeySeparator, 2) + if len(parts) != 2 { + continue + } + v[parts[0]] = parts[1] + } + case MapFlag: + v = make(map[string]string, len(f.values)) + for k, val := range f.values { + v[k] = val + } case *MapFlag: - v = make(map[string]string, len(*f.values)) - for k, val := range *f.values { + v = make(map[string]string, len(f.values)) + for k, val := range f.values { v[k] = val } case *map[string]string: @@ -538,10 +715,11 @@ func (tree *figTree) Map(name string) *map[string]string { } } } - err = fruit.runCallbacks(CallbackAfterRead) + err = fruit.runCallbacks(tree, CallbackAfterRead) if err != nil { fruit.Error = errors.Join(fruit.Error, err) tree.figs[name] = fruit + return &zeroMap } return &v } diff --git a/mutations_new.go b/mutations_new.go index 5dab74a..60ed6df 100644 --- a/mutations_new.go +++ b/mutations_new.go @@ -2,6 +2,7 @@ package figtree import ( "flag" + "strings" "time" ) @@ -20,7 +21,36 @@ func (tree *figTree) MutagenesisOfFig(name string) Mutagenesis { // tree.MutagenesisOf("hello") // Returns tString // tree.MutagenesisOf(42) // Returns tInt func (tree *figTree) MutagenesisOf(what interface{}) Mutagenesis { - switch what.(type) { + switch x := what.(type) { + case Value: + return x.Mutagensis + case flag.Value: + fv, e := toFloat64(x.String()) + if e == nil { + return tree.MutagenesisOf(fv) + } + i64v, e := toInt64(x.String()) + if e == nil { + return tree.MutagenesisOf(i64v) + } + iv, e := toInt(x.String()) + if e == nil { + return tree.MutagenesisOf(iv) + } + bv, e := toBool(x.String()) + if e == nil { + return tree.MutagenesisOf(bv) + } + sv, e := toStringSlice(x.String()) + if e == nil { + return tree.MutagenesisOf(sv) + } + mv, e := toStringMap(x.String()) + if e == nil { + return tree.MutagenesisOf(mv) + } + return "" + case int: return tInt case *int: @@ -59,14 +89,20 @@ func (tree *figTree) MutagenesisOf(what interface{}) Mutagenesis { } // NewString with validator and withered support -func (tree *figTree) NewString(name string, value string, usage string) *string { +func (tree *figTree) NewString(name string, value string, usage string) Plant { tree.mu.Lock() defer tree.mu.Unlock() tree.activateFlagSet() - ptr := flag.String(name, value, usage) + name = strings.ToLower(name) + vPtr := &Value{ + Value: value, + Mutagensis: tString, + } + tree.values.Store(name, vPtr) + flag.Var(vPtr, name, usage) def := &figFruit{ name: name, - Flesh: figFlesh{ptr}, + usage: usage, Mutagenesis: tString, Mutations: make([]Mutation, 0), Validators: make([]FigValidatorFunc, 0), @@ -74,28 +110,33 @@ func (tree *figTree) NewString(name string, value string, usage string) *string Rules: make([]RuleKind, 0), } tree.figs[name] = def - tree.withered[name] = witheredFig{ - name: name, - Flesh: figFlesh{}, - Mutagenesis: tString, - } - witheredFig, exists := tree.withered[name] + theWitheredFig, exists := tree.withered[name] if !exists { - witheredFig.Flesh = figFlesh{value} + tree.withered[name] = witheredFig{ + name: name, + Value: *vPtr, + Mutagenesis: tString, + } } - tree.withered[name] = witheredFig - return ptr + tree.withered[name] = theWitheredFig + return tree } // NewBool with validator and withered support -func (tree *figTree) NewBool(name string, value bool, usage string) *bool { +func (tree *figTree) NewBool(name string, value bool, usage string) Plant { tree.mu.Lock() defer tree.mu.Unlock() tree.activateFlagSet() - ptr := flag.Bool(name, value, usage) + name = strings.ToLower(name) + v := &Value{ + Value: value, + Mutagensis: tBool, + } + tree.values.Store(name, v) + flag.Var(v, name, usage) def := &figFruit{ name: name, - Flesh: figFlesh{ptr}, + usage: usage, Mutagenesis: tBool, Mutations: make([]Mutation, 0), Validators: make([]FigValidatorFunc, 0), @@ -105,27 +146,27 @@ func (tree *figTree) NewBool(name string, value bool, usage string) *bool { tree.figs[name] = def tree.withered[name] = witheredFig{ name: name, - Flesh: figFlesh{new(bool)}, + Value: *v, Mutagenesis: tBool, } - *tree.withered[name].Flesh.Flesh.(*bool) = value - return ptr + return tree } // NewInt with validator and withered support -func (tree *figTree) NewInt(name string, value int, usage string) *int { +func (tree *figTree) NewInt(name string, value int, usage string) Plant { tree.mu.Lock() defer tree.mu.Unlock() - if flesh, exists := tree.figs[name]; exists { - if flesh.Flesh.Is(flesh.Mutagenesis) { - return flesh.Flesh.Flesh.(*int) - } - } tree.activateFlagSet() - ptr := flag.Int(name, value, usage) + name = strings.ToLower(name) + v := &Value{ + Value: value, + Mutagensis: tInt, + } + tree.values.Store(name, v) + flag.Var(v, name, usage) def := &figFruit{ name: name, - Flesh: figFlesh{ptr}, + usage: usage, Mutagenesis: tInt, Mutations: make([]Mutation, 0), Validators: make([]FigValidatorFunc, 0), @@ -135,22 +176,27 @@ func (tree *figTree) NewInt(name string, value int, usage string) *int { tree.figs[name] = def tree.withered[name] = witheredFig{ name: name, - Flesh: figFlesh{new(int)}, + Value: *v, Mutagenesis: tInt, } // Initialize withered with a copy - *tree.withered[name].Flesh.Flesh.(*int) = value - return ptr + return tree } // NewInt64 with validator and withered support -func (tree *figTree) NewInt64(name string, value int64, usage string) *int64 { +func (tree *figTree) NewInt64(name string, value int64, usage string) Plant { tree.mu.Lock() defer tree.mu.Unlock() tree.activateFlagSet() - ptr := flag.Int64(name, value, usage) + name = strings.ToLower(name) + v := &Value{ + Value: value, + Mutagensis: tInt64, + } + tree.values.Store(name, v) + flag.Var(v, name, usage) def := &figFruit{ name: name, - Flesh: figFlesh{ptr}, + usage: usage, Mutagenesis: tInt64, Mutations: make([]Mutation, 0), Validators: make([]FigValidatorFunc, 0), @@ -160,22 +206,27 @@ func (tree *figTree) NewInt64(name string, value int64, usage string) *int64 { tree.figs[name] = def tree.withered[name] = witheredFig{ name: name, - Flesh: figFlesh{new(int64)}, + Value: *v, Mutagenesis: tInt64, } - *tree.withered[name].Flesh.Flesh.(*int64) = value - return ptr + return tree } // NewFloat64 with validator and withered support -func (tree *figTree) NewFloat64(name string, value float64, usage string) *float64 { +func (tree *figTree) NewFloat64(name string, value float64, usage string) Plant { tree.mu.Lock() defer tree.mu.Unlock() tree.activateFlagSet() - ptr := flag.Float64(name, value, usage) + name = strings.ToLower(name) + v := &Value{ + Value: value, + Mutagensis: tFloat64, + } + tree.values.Store(name, v) + flag.Var(v, name, usage) def := &figFruit{ name: name, - Flesh: figFlesh{ptr}, + usage: usage, Mutagenesis: tFloat64, Mutations: make([]Mutation, 0), Validators: make([]FigValidatorFunc, 0), @@ -185,22 +236,27 @@ func (tree *figTree) NewFloat64(name string, value float64, usage string) *float tree.figs[name] = def tree.withered[name] = witheredFig{ name: name, - Flesh: figFlesh{new(float64)}, + Value: *v, Mutagenesis: tFloat64, } - *tree.withered[name].Flesh.Flesh.(*float64) = value - return ptr + return tree } // NewDuration with validator and withered support -func (tree *figTree) NewDuration(name string, value time.Duration, usage string) *time.Duration { +func (tree *figTree) NewDuration(name string, value time.Duration, usage string) Plant { tree.mu.Lock() defer tree.mu.Unlock() tree.activateFlagSet() - ptr := flag.Duration(name, value, usage) + name = strings.ToLower(name) + v := &Value{ + Value: value, + Mutagensis: tDuration, + } + tree.values.Store(name, v) + flag.Var(v, name, usage) def := &figFruit{ name: name, - Flesh: figFlesh{ptr}, + usage: usage, Mutagenesis: tDuration, Mutations: make([]Mutation, 0), Validators: make([]FigValidatorFunc, 0), @@ -210,22 +266,27 @@ func (tree *figTree) NewDuration(name string, value time.Duration, usage string) tree.figs[name] = def tree.withered[name] = witheredFig{ name: name, - Flesh: figFlesh{new(time.Duration)}, + Value: *v, Mutagenesis: tDuration, } - *tree.withered[name].Flesh.Flesh.(*time.Duration) = value - return ptr + return tree } // NewUnitDuration registers a new time.Duration with a unit time.Duration against a name -func (tree *figTree) NewUnitDuration(name string, value, units time.Duration, usage string) *time.Duration { +func (tree *figTree) NewUnitDuration(name string, value, units time.Duration, usage string) Plant { tree.mu.Lock() defer tree.mu.Unlock() tree.activateFlagSet() - ptr := flag.Duration(name, value*units, usage) + name = strings.ToLower(name) + v := &Value{ + Value: value * units, + Mutagensis: tUnitDuration, + } + tree.values.Store(name, v) + flag.Var(v, name, usage) def := &figFruit{ name: name, - Flesh: figFlesh{ptr}, + usage: usage, Mutagenesis: tUnitDuration, Mutations: make([]Mutation, 0), Validators: make([]FigValidatorFunc, 0), @@ -235,26 +296,30 @@ func (tree *figTree) NewUnitDuration(name string, value, units time.Duration, us tree.figs[name] = def tree.withered[name] = witheredFig{ name: name, - Flesh: figFlesh{new(time.Duration)}, + Value: *v, Mutagenesis: tUnitDuration, } - *tree.withered[name].Flesh.Flesh.(*time.Duration) = value * units - return ptr + return tree } // NewList with validator and withered support -func (tree *figTree) NewList(name string, value []string, usage string) *[]string { +func (tree *figTree) NewList(name string, value []string, usage string) Plant { tree.mu.Lock() defer tree.mu.Unlock() if tree.HasRule(RuleNoLists) { - return nil + return tree } - ptr := &ListFlag{values: &value} tree.activateFlagSet() - flag.Var(ptr, name, usage) + name = strings.ToLower(name) + v := &Value{ + Value: ListFlag{values: value}, + Mutagensis: tList, + } + tree.values.Store(name, v) + flag.Var(v, name, usage) def := &figFruit{ name: name, - Flesh: figFlesh{ptr}, + usage: usage, Mutagenesis: tList, Mutations: make([]Mutation, 0), Validators: make([]FigValidatorFunc, 0), @@ -265,26 +330,34 @@ func (tree *figTree) NewList(name string, value []string, usage string) *[]strin witheredVal := make([]string, len(value)) copy(witheredVal, value) tree.withered[name] = witheredFig{ - name: name, - Flesh: figFlesh{&ListFlag{values: &witheredVal}}, + name: name, + Value: Value{ + Value: witheredVal, + Mutagensis: tList, + }, Mutagenesis: tList, } - return ptr.values + return tree } // NewMap with validator and withered support -func (tree *figTree) NewMap(name string, value map[string]string, usage string) *map[string]string { +func (tree *figTree) NewMap(name string, value map[string]string, usage string) Plant { tree.mu.Lock() defer tree.mu.Unlock() if tree.HasRule(RuleNoMaps) { - return nil + return tree } - ptr := &MapFlag{values: &value} tree.activateFlagSet() - flag.Var(ptr, name, usage) + name = strings.ToLower(name) + v := &Value{ + Value: MapFlag{values: value}, + Mutagensis: tMap, + } + tree.values.Store(name, v) + flag.Var(v, name, usage) def := &figFruit{ name: name, - Flesh: figFlesh{ptr}, + usage: usage, Mutagenesis: tMap, Mutations: make([]Mutation, 0), Validators: make([]FigValidatorFunc, 0), @@ -298,10 +371,11 @@ func (tree *figTree) NewMap(name string, value map[string]string, usage string) } tree.withered[name] = witheredFig{ name: name, - Flesh: figFlesh{&MapFlag{ - values: &witheredVal, - }}, + Value: Value{ + Value: witheredVal, + Mutagensis: tree.MutagenesisOf(witheredVal), + }, Mutagenesis: tMap, } - return ptr.values + return tree } diff --git a/mutations_new_test.go b/mutations_new_test.go index 615e855..47fc4bd 100644 --- a/mutations_new_test.go +++ b/mutations_new_test.go @@ -72,7 +72,7 @@ func TestTree_Duration(t *testing.T) { figs := With(Options{Germinate: true}) figs.NewDuration("test-duration", 42*time.Millisecond, "usage") assert.Nil(t, figs.Parse()) - assert.Equal(t, *figs.Duration("test-duration"), 42*time.Millisecond) + assert.Equal(t, 42*time.Millisecond, *figs.Duration("test-duration")) } func TestTree_UnitDuration(t *testing.T) { @@ -88,9 +88,9 @@ func TestTree_List(t *testing.T) { assert.Nil(t, figs.Parse()) l := *figs.List("test-list") assert.Equal(t, 3, len(l)) - assert.Equal(t, l[0], "one") - assert.Equal(t, l[1], "two") - assert.Equal(t, l[2], "three") + assert.Contains(t, l, "one") + assert.Contains(t, l, "two") + assert.Contains(t, l, "three") } func TestTree_Map(t *testing.T) { diff --git a/mutations_store.go b/mutations_store.go index 1caed6c..a2ac257 100644 --- a/mutations_store.go +++ b/mutations_store.go @@ -11,21 +11,8 @@ func (tree *figTree) Store(mut Mutagenesis, name string, value interface{}) Plan tree.mu.Lock() defer tree.mu.Unlock() fruit, ok := tree.figs[name] - if !ok || fruit == nil { - if !tree.angel.Load() { - tree.mu.Unlock() - tree.Resurrect(name) - tree.mu.Lock() - fruit = tree.figs[name] - } - } - if fruit == nil { - if !tree.angel.Load() { - tree.mu.Unlock() - tree.Resurrect(name) - tree.mu.Lock() - fruit = tree.figs[name] - } + if !ok { + return tree } if fruit == nil { return tree @@ -34,7 +21,7 @@ func (tree *figTree) Store(mut Mutagenesis, name string, value interface{}) Plan return tree } if tree.angel.Load() { - tree.figs[name].Error = errors.Join(tree.figs[name].Error, fmt.Errorf("tree fruit is an angel so we cannot store %s inside %s", tree.MutagenesisOf(value), tree.MutagenesisOf(fruit.Flesh))) + tree.figs[name].Error = errors.Join(tree.figs[name].Error, fmt.Errorf("tree fruit is an angel so we cannot store %s inside %s", tree.MutagenesisOf(value), fruit.Mutagenesis)) return tree } mv := tree.MutagenesisOf(value) @@ -42,17 +29,20 @@ func (tree *figTree) Store(mut Mutagenesis, name string, value interface{}) Plan mv = tUnitDuration } if !strings.EqualFold(string(mv), string(fruit.Mutagenesis)) { - tree.figs[name].Error = errors.Join(tree.figs[name].Error, fmt.Errorf("will not store %s inside %s", tree.MutagenesisOf(value), tree.MutagenesisOf(fruit.Flesh))) + tree.figs[name].Error = errors.Join(tree.figs[name].Error, fmt.Errorf("will not store %s inside %s", tree.MutagenesisOf(value), fruit.Mutagenesis)) return tree } if _, exists := tree.withered[name]; !exists { tree.withered[name] = witheredFig{ - Flesh: fruit.Flesh, + Value: Value{ + Value: value, + Mutagensis: tree.MutagenesisOf(value), + }, Mutagenesis: tString, Error: fmt.Errorf("missing withered value for %s", name), } } - err := fruit.runCallbacks(CallbackBeforeChange) + err := fruit.runCallbacks(tree, CallbackBeforeChange) if err != nil { fruit.Error = errors.Join(fruit.Error, err) tree.figs[name] = fruit @@ -61,7 +51,7 @@ func (tree *figTree) Store(mut Mutagenesis, name string, value interface{}) Plan if !changed { return tree } - err = fruit.runCallbacks(CallbackAfterChange) + err = fruit.runCallbacks(tree, CallbackAfterChange) if err != nil { fruit.Error = errors.Join(fruit.Error, err) } @@ -133,22 +123,29 @@ func (tree *figTree) persist(fruit *figFruit, mut Mutagenesis, name string, valu if fruit.HasRule(RulePanicOnChange) { panic("RuleOnPanicChange triggered for " + fruit.name) } - flesh := fruit.Flesh + name = strings.ToLower(name) + _value := tree.useValue(tree.from(name)) + if value == nil { + return false, nil, nil + } + flesh := _value.Raw() switch mut { case tMap: var old *map[string]string var err error - switch f := flesh.Flesh.(type) { + switch f := flesh.(type) { + case *Value: + old = f.Value.(*map[string]string) case *MapFlag: - old = f.values + old = &f.values case *map[string]string: old = f case map[string]string: old = &f case string: m := map[string]string{} - for _, p := range strings.Split(f, ",") { - z := strings.SplitN(p, "=", 1) + for _, p := range strings.Split(f, MapSeparator) { + z := strings.SplitN(p, MapKeySeparator, 1) if len(z) == 2 { m[z[0]] = z[1] } @@ -156,8 +153,8 @@ func (tree *figTree) persist(fruit *figFruit, mut Mutagenesis, name string, valu old = &m case *string: m := map[string]string{} - for _, p := range strings.Split(*f, ",") { - z := strings.SplitN(p, "=", 1) + for _, p := range strings.Split(*f, MapSeparator) { + z := strings.SplitN(p, MapKeySeparator, 1) if len(z) == 2 { m[z[0]] = z[1] } @@ -173,15 +170,15 @@ func (tree *figTree) persist(fruit *figFruit, mut Mutagenesis, name string, valu var current *map[string]string switch f := value.(type) { case *MapFlag: - current = f.values + current = &f.values case *map[string]string: current = f case map[string]string: current = &f case string: m := map[string]string{} - for _, p := range strings.Split(f, ",") { - z := strings.SplitN(p, "=", 1) + for _, p := range strings.Split(f, MapSeparator) { + z := strings.SplitN(p, MapKeySeparator, 1) if len(z) == 2 { m[z[0]] = z[1] } @@ -189,8 +186,8 @@ func (tree *figTree) persist(fruit *figFruit, mut Mutagenesis, name string, valu current = &m case *string: m := map[string]string{} - for _, p := range strings.Split(*f, ",") { - z := strings.SplitN(p, "=", 1) + for _, p := range strings.Split(*f, MapSeparator) { + z := strings.SplitN(p, MapKeySeparator, 1) if len(z) == 2 { m[z[0]] = z[1] } @@ -203,45 +200,60 @@ func (tree *figTree) persist(fruit *figFruit, mut Mutagenesis, name string, valu tree.figs[name].Error = errors.Join(tree.figs[name].Error, err) return false, old, value } - fruit.Flesh.Flesh = current + valueAny, ok := tree.values.Load(name) + if !ok { + return false, flesh, value + } + value, ok := valueAny.(*Value) + if !ok { + return false, flesh, value + } + err = value.Assign(current) + if err != nil { + tree.figs[name].Error = errors.Join(tree.figs[name].Error, err) + return false, old, value + } + tree.values.Store(name, value) tree.figs[name] = fruit return old != current, old, current case tList: var old *[]string var err error - switch v := flesh.Flesh.(type) { + switch v := flesh.(type) { + case *Value: + old = v.Value.(*[]string) case *ListFlag: - old = v.values + old = &v.values case *[]string: old = v case []string: old = &v case string: - x := strings.Split(v, ",") + x := strings.Split(v, ListSeparator) old = &x case *string: - x := strings.Split(*v, ",") + x := strings.Split(*v, ListSeparator) old = &x default: return false, v, value } if err != nil { tree.figs[name].Error = errors.Join(tree.figs[name].Error, err) - return false, flesh.Flesh, value + return false, flesh, value } var current *[]string switch v := value.(type) { case *ListFlag: - current = v.values + current = &v.values case []string: current = &v case *[]string: current = v case string: - x := strings.Split(v, ",") + x := strings.Split(v, ListSeparator) current = &x case *string: - x := strings.Split(*v, ",") + x := strings.Split(*v, ListSeparator) current = &x default: return false, old, flesh @@ -250,13 +262,28 @@ func (tree *figTree) persist(fruit *figFruit, mut Mutagenesis, name string, valu tree.figs[name].Error = errors.Join(tree.figs[name].Error, err) return false, old, value } - fruit.Flesh.Flesh = current + valueAny, ok := tree.values.Load(name) + if !ok { + return false, flesh, value + } + value, ok := valueAny.(*Value) + if !ok { + return false, flesh, value + } + err = value.Assign(current) + if err != nil { + tree.figs[name].Error = errors.Join(tree.figs[name].Error, err) + return false, old, value + } + tree.values.Store(name, value) tree.figs[name] = fruit return old != current, old, current case tUnitDuration: var old time.Duration var err error - switch v := flesh.Flesh.(type) { + switch v := flesh.(type) { + case *Value: + old = v.Value.(time.Duration) case time.Duration: old = v case *time.Duration: @@ -290,13 +317,26 @@ func (tree *figTree) persist(fruit *figFruit, mut Mutagenesis, name string, valu tree.figs[name].Error = errors.Join(tree.figs[name].Error, err) return false, old, value } - fruit.Flesh.Flesh = current + valueAny, ok := tree.values.Load(name) + if !ok { + return false, flesh, value + } + value, ok := valueAny.(*Value) + if !ok { + return false, flesh, value + } + err = value.Assign(current) + if err != nil { + tree.figs[name].Error = errors.Join(tree.figs[name].Error, err) + return false, old, value + } + tree.values.Store(name, value) tree.figs[name] = fruit return old != current, old, current case tDuration: var old time.Duration var err error - switch v := flesh.Flesh.(type) { + switch v := flesh.(type) { case time.Duration: old = v case *time.Duration: @@ -331,77 +371,155 @@ func (tree *figTree) persist(fruit *figFruit, mut Mutagenesis, name string, valu tree.figs[name].Error = errors.Join(tree.figs[name].Error, err) return false, old, value } - fruit.Flesh.Flesh = current + valueAny, ok := tree.values.Load(name) + if !ok { + return false, flesh, value + } + value, ok := valueAny.(*Value) + if !ok { + return false, flesh, value + } + err = value.Assign(current) + if err != nil { + tree.figs[name].Error = errors.Join(tree.figs[name].Error, err) + return false, old, value + } + tree.values.Store(name, value) tree.figs[name] = fruit return old != current, old, current case tFloat64: - old, err := toFloat64(flesh.Flesh) + old, err := toFloat64(flesh) if err != nil { tree.figs[name].Error = errors.Join(tree.figs[name].Error, err) - return false, flesh.Flesh, value + return false, flesh, value } current, err := toFloat64(value) if err != nil { tree.figs[name].Error = errors.Join(tree.figs[name].Error, err) return false, old, value } - fruit.Flesh.Flesh = current + valueAny, ok := tree.values.Load(name) + if !ok { + return false, flesh, value + } + value, ok := valueAny.(*Value) + if !ok { + return false, flesh, value + } + err = value.Assign(current) + if err != nil { + tree.figs[name].Error = errors.Join(tree.figs[name].Error, err) + return false, old, value + } + tree.values.Store(name, value) tree.figs[name] = fruit return old != current, old, current case tInt64: - old, err := toInt64(flesh.Flesh) + old, err := toInt64(flesh) if err != nil { tree.figs[name].Error = errors.Join(tree.figs[name].Error, err) - return false, flesh.Flesh, value + return false, flesh, value } current, err := toInt64(value) if err != nil { tree.figs[name].Error = errors.Join(tree.figs[name].Error, err) return false, old, value } - fruit.Flesh.Flesh = current + valueAny, ok := tree.values.Load(name) + if !ok { + return false, flesh, value + } + value, ok := valueAny.(*Value) + if !ok { + return false, flesh, value + } + err = value.Assign(current) + if err != nil { + tree.figs[name].Error = errors.Join(tree.figs[name].Error, err) + return false, old, value + } + tree.values.Store(name, value) tree.figs[name] = fruit return old != current, old, current case tInt: - old, err := toInt(flesh.Flesh) + old, err := toInt(flesh) if err != nil { tree.figs[name].Error = errors.Join(tree.figs[name].Error, err) - return false, flesh.Flesh, value + return false, flesh, value } current, err := toInt(value) if err != nil { tree.figs[name].Error = errors.Join(tree.figs[name].Error, err) return false, old, value } - fruit.Flesh.Flesh = current + valueAny, ok := tree.values.Load(name) + if !ok { + return false, flesh, value + } + value, ok := valueAny.(*Value) + if !ok { + return false, flesh, value + } + err = value.Assign(current) + if err != nil { + tree.figs[name].Error = errors.Join(tree.figs[name].Error, err) + return false, old, value + } + tree.values.Store(name, value) tree.figs[name] = fruit return old != current, old, current case tString: - old, err := toString(flesh.Flesh) + old, err := toString(flesh) if err != nil { tree.figs[name].Error = errors.Join(tree.figs[name].Error, err) - return false, flesh.Flesh, value + return false, flesh, value } current, err := toString(value) if err != nil { tree.figs[name].Error = errors.Join(tree.figs[name].Error, err) return false, old, value } - fruit.Flesh.Flesh = current + valueAny, ok := tree.values.Load(name) + if !ok { + return false, flesh, value + } + value, ok := valueAny.(*Value) + if !ok { + return false, flesh, value + } + err = value.Assign(current) + if err != nil { + tree.figs[name].Error = errors.Join(tree.figs[name].Error, err) + return false, old, value + } + tree.values.Store(name, value) tree.figs[name] = fruit return !strings.EqualFold(old, current), old, current case tBool: - old, err := toBool(flesh.Flesh) + old, err := toBool(flesh) if err != nil { tree.figs[name].Error = errors.Join(tree.figs[name].Error, err) - return false, flesh.Flesh, value + return false, flesh, value } current, err := toBool(value) if err != nil { tree.figs[name].Error = errors.Join(tree.figs[name].Error, err) return false, old, value } - fruit.Flesh.Flesh = current + valueAny, ok := tree.values.Load(name) + if !ok { + return false, flesh, value + } + value, ok := valueAny.(*Value) + if !ok { + return false, flesh, value + } + err = value.Assign(current) + if err != nil { + tree.figs[name].Error = errors.Join(tree.figs[name].Error, err) + return false, old, value + } + tree.values.Store(name, value) tree.figs[name] = fruit return old != current, old, current default: diff --git a/mutations_store_test.go b/mutations_store_test.go index aa32f5f..90060da 100644 --- a/mutations_store_test.go +++ b/mutations_store_test.go @@ -152,21 +152,21 @@ func TestTree_StoreList(t *testing.T) { // new fig tree with a list figs := With(Options{Germinate: true}) - figs.NewList(k, []string{"yah", "i am", "yahuah"}, u) + figs = figs.NewList(k, []string{"yah", "i am", "yahuah"}, u) assert.Nil(t, figs.Parse()) // get the list from k as s s := *figs.List(k) assert.Equal(t, 3, len(s)) - assert.Equal(t, []string{"yah", "i am", "yahuah"}, s) + assert.Equal(t, []string{"i am", "yah", "yahuah"}, s) // store a new list in k - figs.StoreList(k, []string{"yah", "its", "true", "he", "is"}) + figs = figs.StoreList(k, []string{"yah", "its", "true", "he", "is"}) // verify the new list s = *figs.List(k) assert.Equal(t, 5, len(s)) - assert.Equal(t, []string{"yah", "its", "true", "he", "is"}, s) + assert.Equal(t, []string{"he", "is", "its", "true", "yah"}, s) } func TestTree_StoreMap(t *testing.T) { diff --git a/parsing.go b/parsing.go index ba299b4..0156364 100644 --- a/parsing.go +++ b/parsing.go @@ -1,38 +1,311 @@ package figtree import ( + "errors" + "fmt" + "log" "os" + "regexp" + "sort" + "strconv" + "strings" + "time" ) // Parsing Configuration +func (tree *figTree) useValue(value *Value, err error) *Value { + if err != nil { + log.Printf("useValue caught err: %v", err) + } + return value +} + +func (tree *figTree) from(name string) (*Value, error) { + flagName := strings.ToLower(name) + for alias, realname := range tree.aliases { + if strings.EqualFold(alias, name) { + flagName = realname + } + } + valueAny, ok := tree.values.Load(flagName) + if !ok { + return nil, errors.New("no value for " + flagName) + } + value, ok := valueAny.(*Value) + if !ok { + return nil, errors.New("value for " + flagName + " is not a Value") + } + if value.Err != nil { + return nil, value.Err + } + if value.Mutagensis == "" { + value.Mutagensis = tree.MutagenesisOf(value) + } + return value, nil +} + +var re *regexp.Regexp + +func init() { + re = regexp.MustCompile(`(\d+)([ylwdhms])`) +} + +func ParseCustomDuration(input string) (time.Duration, error) { + // Define the mapping of units to their respective durations + unitMap := map[rune]time.Duration{ + 'y': 365 * 24 * time.Hour, // Approximate year (non-leap) + 'l': 30 * 24 * time.Hour, // Approximate month (30 days) + 'w': 7 * 24 * time.Hour, // Week + 'd': 24 * time.Hour, // Day + 'h': time.Hour, // Hour + 'm': time.Minute, // Minute + 's': time.Second, // Second + } + + // Regular expression to match number-unit pairs (e.g., "5h", "32m") + matches := re.FindAllStringSubmatch(input, -1) + + if len(matches) == 0 { + return 0, fmt.Errorf("invalid duration format: %s", input) + } + + var totalDuration time.Duration + for _, match := range matches { + // match[1] is the number, match[2] is the unit + num, err := strconv.Atoi(match[1]) + if err != nil { + return 0, fmt.Errorf("invalid number in duration: %s", match[1]) + } + + unit := rune(match[2][0]) + duration, exists := unitMap[unit] + if !exists { + return 0, fmt.Errorf("invalid unit in duration: %s", match[2]) + } + + totalDuration += time.Duration(num) * duration + } + + return totalDuration, nil +} + +func (tree *figTree) checkFigErrors() error { + tree.mu.RLock() + defer tree.mu.RUnlock() + for name, fig := range tree.figs { + if fig.Error != nil { + return fig.Error + } + value, err := tree.from(name) + if err != nil { + fig.Error = errors.Join(fig.Error, err) + return fig.Error + } + if !strings.EqualFold(string(fig.Mutagenesis), string(value.Mutagensis)) { + return fmt.Errorf("invalid Mutagenesis (Type) for flag -%s", name) + } + switch fig.Mutagenesis { + case tString: + _, e := toString(value) + if e != nil { + er := value.Assign(zeroString) + if er != nil { + e = errors.Join(e, er) + } + return fmt.Errorf("invalid value for flag -%s: %w", name, e) + } + case tInt: + _, e := toInt(value) + if e != nil { + er := value.Assign(zeroInt) + if er != nil { + e = errors.Join(e, er) + } + return fmt.Errorf("invalid value for flag -%s: %w", name, e) + } + case tFloat64: + _, e := toFloat64(value) + if e != nil { + er := value.Assign(zeroFloat64) + if er != nil { + e = errors.Join(e, er) + } + return fmt.Errorf("invalid value for flag -%s: %w", name, e) + } + + case tBool: + _, e := toBool(value) + if e != nil { + er := value.Assign(zeroBool) + if er != nil { + e = errors.Join(e, er) + } + return fmt.Errorf("invalid value for flag -%s: %w", name, e) + } + case tInt64, tUnitDuration, tDuration: + if value.Mutagensis == tUnitDuration || value.Mutagensis == tDuration { + vf := value.Flesh() + var val time.Duration + var err error + if vf != nil { + if _, ok := vf.AsIs().(time.Duration); !ok { + val, err = ParseCustomDuration(vf.ToString()) + if err == nil { + err = value.Assign(val) + if err != nil { + return fmt.Errorf("invalid value for flag -%s: %w", name, err) + } + continue + } + } else { + continue + } + } + + } + _, e := toInt64(value) + if e != nil { + er := value.Assign(zeroInt64) + if er != nil { + e = errors.Join(e, er) + } + return fmt.Errorf("invalid value for flag -%s: %w", name, e) + } + case tMap: + _, e := toStringMap(value.Value) + if e != nil { + er := value.Assign(zeroMap) + if er != nil { + e = errors.Join(e, er) + } + return fmt.Errorf("invalid value for flag -%s: %w", name, e) + } + case tList: + _, e := toStringSlice(value.Value) + if e != nil { + er := value.Assign(zeroList) + if er != nil { + e = errors.Join(e, er) + } + return fmt.Errorf("invalid value for flag -%s: %w", name, e) + } + default: + return fmt.Errorf("invalid value for flag -%s: %w", name, fmt.Errorf("unknown flag type")) + } + } + return nil +} + // Parse uses figTree.flagSet to run flag.Parse() on the registered figs and returns nil for validated results func (tree *figTree) Parse() (err error) { + preloadErr := tree.preLoadOrParse() + if preloadErr != nil { + return preloadErr + } if !tree.HasRule(RuleNoFlags) { tree.activateFlagSet() args := os.Args[1:] if tree.filterTests { args = filterTestFlags(args) - err = tree.flagSet.Parse(args) - } else { - err = tree.flagSet.Parse(args) } + err = tree.flagSet.Parse(args) + if err != nil { + err2 := tree.checkFigErrors() + if err2 != nil { + err = errors.Join(err, err2) + } + return err + } + err = tree.loadFlagSet() if err != nil { return err } + tree.readEnv() + err = tree.applyWithered() + if err != nil { + return err + } + return tree.validateAll() } tree.readEnv() + err = tree.applyWithered() + if err != nil { + return err + } return tree.validateAll() } +func (tree *figTree) applyWithered() error { + tree.mu.Lock() + defer tree.mu.Unlock() + for name, fig := range tree.figs { + if fig == nil { + continue + } + value := tree.useValue(tree.from(name)) + if value.Mutagensis == tMap && PolicyMapAppend { + vm := value.Flesh().ToMap() + unique := make(map[string]string) + withered := tree.withered[name] + for k, v := range vm { + unique[k] = v + } + for k, v := range withered.Value.Flesh().ToMap() { + unique[k] = v + } + err := value.Assign(unique) + if err != nil { + return fmt.Errorf("failed to assign %s due to %w", name, err) + } + tree.values.Store(name, value) + } + if value.Mutagensis == tList && PolicyListAppend { + vl, e := toStringSlice(value.Value) + if e != nil { + return fmt.Errorf("failed toStringSlice: %w", e) + } + unique := make(map[string]struct{}) + for _, v := range vl { + unique[v] = struct{}{} + } + withered := tree.withered[name] + for _, w := range withered.Value.Flesh().ToList() { + unique[w] = struct{}{} + } + var result []string + for k, _ := range unique { + result = append(result, k) + } + sort.Strings(result) + err := value.Assign(result) + if err != nil { + return fmt.Errorf("failed assign to %s: %w", name, err) + } + tree.values.Store(name, value) + } + } + return nil +} + // ParseFile will check if filename is set and run loadFile on it. func (tree *figTree) ParseFile(filename string) (err error) { + preloadErr := tree.preLoadOrParse() + if preloadErr != nil { + return preloadErr + } if !tree.HasRule(RuleNoFlags) { tree.activateFlagSet() args := os.Args[1:] if tree.filterTests { args = filterTestFlags(args) err = tree.flagSet.Parse(args) + if err != nil { + err2 := tree.checkFigErrors() + if err2 != nil { + err = errors.Join(err, err2) + } + } } else { err = tree.flagSet.Parse(args) } @@ -40,6 +313,10 @@ func (tree *figTree) ParseFile(filename string) (err error) { return err } } + err = tree.loadFlagSet() + if err != nil { + return err + } if filename != "" { return tree.loadFile(filename) } diff --git a/parsing_test.go b/parsing_test.go index a1df2f9..d2b3819 100644 --- a/parsing_test.go +++ b/parsing_test.go @@ -18,12 +18,12 @@ func TestTree_ParseFile(t *testing.T) { t.Run(ext, func(t *testing.T) { p := filepath.Join(".", "test.config."+ext) figs := With(Options{Germinate: true}) - figs.NewString("name", "", "name") - figs.WithValidator("name", AssureStringContains("yahuah")) - figs.NewInt("age", 0, "age") - figs.WithValidator("age", AssureIntInRange(17, 47)) - figs.NewString("sex", "", "sex") - figs.WithValidator("sex", AssureStringHasSuffix("male")) + figs = figs.NewString("name", "", "name") + figs = figs.WithValidator("name", AssureStringContains("yahuah")) + figs = figs.NewInt("age", 0, "age") + figs = figs.WithValidator("age", AssureIntInRange(17, 47)) + figs = figs.NewString("sex", "", "sex") + figs = figs.WithValidator("sex", AssureStringHasSuffix("male")) err := figs.ParseFile(p) assert.NoError(t, err) }) diff --git a/rules.go b/rules.go index 06cdd27..62c4d39 100644 --- a/rules.go +++ b/rules.go @@ -1,5 +1,7 @@ package figtree +import "strings" + type RuleKind int const ( @@ -47,14 +49,9 @@ func (tree *figTree) WithTreeRule(rule RuleKind) Plant { func (tree *figTree) WithRule(name string, rule RuleKind) Plant { tree.mu.Lock() defer tree.mu.Unlock() + name = strings.ToLower(name) fruit, exists := tree.figs[name] if !exists || fruit == nil { - tree.mu.Unlock() - tree.Resurrect(name) - tree.mu.Lock() - fruit = tree.figs[name] - } - if fruit == nil { return tree } fruit.Rules = append(fruit.Rules, rule) diff --git a/rules_test.go b/rules_test.go index e833f5b..75e9bd1 100644 --- a/rules_test.go +++ b/rules_test.go @@ -2,6 +2,7 @@ package figtree import ( "os" + "strings" "sync" "testing" "time" @@ -11,25 +12,25 @@ import ( func TestRules(t *testing.T) { kName := "name" - + os.Args = []string{os.Args[0]} figs := With(Options{Germinate: true, Pollinate: true}) assert.NotNil(t, figs) - figs.NewString(kName, "", "usage of name") - figs.WithValidator(kName, AssureStringNotEmpty) + figs = figs.NewString(kName, "", "usage of name").WithValidator(kName, AssureStringNotEmpty) assert.Error(t, figs.Parse()) - figs.WithRule(kName, RuleNoValidations) + figs = figs.WithRule(kName, RuleNoValidations) assert.NoError(t, figs.Parse()) - figs.WithCallback(kName, CallbackBeforeVerify, func(_ interface{}) error { + figs = figs.WithCallback(kName, CallbackBeforeVerify, func(_ interface{}) error { panic("this shouldn't be called") return nil }) assert.Panics(t, func() { _ = figs.Parse() }) - figs.WithRule(kName, RuleNoCallbacks) + figs = figs.WithRule(kName, RuleNoCallbacks) assert.NoError(t, figs.Parse()) changeEnv := func(n, m string) { + nu := strings.ToUpper(n) wg := sync.WaitGroup{} wg.Add(1) go func() { @@ -41,11 +42,11 @@ func TestRules(t *testing.T) { case <-timer.C: return case <-checker.C: - assert.NoError(t, os.Setenv(n, m)) + assert.NoError(t, os.Setenv(nu, m)) } } }() - defer assert.NoError(t, os.Unsetenv(n)) + defer assert.NoError(t, os.Unsetenv(nu)) wg.Wait() } diff --git a/savior.go b/savior.go index 9ae3a13..228ad53 100644 --- a/savior.go +++ b/savior.go @@ -24,7 +24,15 @@ func (tree *figTree) SaveTo(path string) error { tree.mu.Lock() defer tree.mu.Unlock() for name, fig := range tree.figs { - properties[name] = fig.Flesh.Flesh + valueAny, ok := tree.values.Load(name) + if !ok { + return errors.Join(fig.Error, fmt.Errorf("failed to load %s", fig.name)) + } + _value, ok := valueAny.(*Value) + if !ok { + return errors.Join(fig.Error, fmt.Errorf("failed to cast %s as *Value ; got %T", fig.name, valueAny)) + } + properties[name] = _value.Value } formatValue := func(val interface{}) string { return fmt.Sprintf("%v", val) diff --git a/savior_test.go b/savior_test.go index 22f05d8..56f3164 100644 --- a/savior_test.go +++ b/savior_test.go @@ -28,7 +28,7 @@ func TestFigTree_SaveTo(t *testing.T) { fig2 := With(Options{Germinate: true}) assert.NoError(t, fig2.ReadFrom(testFile)) - nameFig := fig2.Fig("name") + nameFig := fig2.FigFlesh("name") assert.NotNil(t, nameFig) name := fig2.String("name") assert.Equal(t, t.Name(), *name) diff --git a/types.go b/types.go index a4aae92..bc1d9e6 100644 --- a/types.go +++ b/types.go @@ -11,7 +11,7 @@ type Withables interface { // WithCallback registers a new CallbackWhen with a CallbackFunc on a figFruit on the figTree by its name WithCallback(name string, whenCallback CallbackWhen, runThis CallbackFunc) Plant // WithAlias registers a short form of the name of a figFruit on the figTree - WithAlias(name, alias string) + WithAlias(name, alias string) Plant // WithRule attaches a RuleKind to a figFruit WithRule(name string, rule RuleKind) Plant // WithTreeRule assigns a global rule on the Tree @@ -60,15 +60,13 @@ type Divine interface { Recall() // Curse allows you to lock the figTree from changes and stop tracking Curse() - // Resurrect takes a nil figFruit in the figTree.figs map and reloads it from ENV or the config file if available - Resurrect(name string) } type Intable interface { // Int returns a pointer to a registered int32 by name as -name=1 a pointer to 1 is returned Int(name string) *int // NewInt registers a new int32 flag by name and returns a pointer to the int32 storing the initial value - NewInt(name string, value int, usage string) *int + NewInt(name string, value int, usage string) Plant // StoreInt replaces name with value and can issue a Mutation when receiving on Mutations() StoreInt(name string, value int) Plant } @@ -77,7 +75,7 @@ type Intable64 interface { // Int64 returns a pointer to a registered int64 by name as -name=1 a pointer to 1 is returned Int64(name string) *int64 // NewInt64 registers a new int32 flag by name and returns a pointer to the int64 storing the initial value - NewInt64(name string, value int64, usage string) *int64 + NewInt64(name string, value int64, usage string) Plant // StoreInt64 replaces name with value and can issue a Mutation when receiving on Mutations() StoreInt64(name string, value int64) Plant } @@ -86,7 +84,7 @@ type Floatable interface { // Float64 returns a pointer to a registered float64 by name as -name=1.0 a pointer to 1.0 is returned Float64(name string) *float64 // NewFloat64 registers a new float64 flag by name and returns a pointer to the float64 storing the initial value - NewFloat64(name string, value float64, usage string) *float64 + NewFloat64(name string, value float64, usage string) Plant // StoreFloat64 replaces name with value and can issue a Mutation when receiving on Mutations() StoreFloat64(name string, value float64) Plant } @@ -95,7 +93,7 @@ type String interface { // String returns a pointer to stored string by -name=value String(name string) *string // NewString registers a new string flag by name and returns a pointer to the string storing the initial value - NewString(name, value, usage string) *string + NewString(name, value, usage string) Plant // StoreString replaces name with value and can issue a Mutation when receiving on Mutations() StoreString(name, value string) Plant } @@ -104,7 +102,7 @@ type Flaggable interface { // Bool returns a pointer to stored bool by -name=true Bool(name string) *bool // NewBool registers a new bool flag by name and returns a pointer to the bool storing the initial value - NewBool(name string, value bool, usage string) *bool + NewBool(name string, value bool, usage string) Plant // StoreBool replaces name with value and can issue a Mutation when receiving on Mutations() StoreBool(name string, value bool) Plant } @@ -113,13 +111,13 @@ type Durable interface { // Duration returns a pointer to stored time.Duration (unitless) by name like -minutes=10 (requires multiplication of * time.Minute to match memetics of "minutes" flag name and human interpretation of this) Duration(name string) *time.Duration // NewDuration registers a new time.Duration by name and returns a pointer to it storing the initial value - NewDuration(name string, value time.Duration, usage string) *time.Duration + NewDuration(name string, value time.Duration, usage string) Plant // StoreDuration replaces name with value and can issue a Mutation when receiving on Mutations() StoreDuration(name string, value time.Duration) Plant // UnitDuration returns a pointer to stored time.Duration (-name=10 w/ units as time.Minute == 10 minutes time.Duration) UnitDuration(name string) *time.Duration // NewUnitDuration registers a new time.Duration by name and returns a pointer to it storing the initial value - NewUnitDuration(name string, value, units time.Duration, usage string) *time.Duration + NewUnitDuration(name string, value, units time.Duration, usage string) Plant // StoreUnitDuration replaces name with value and can issue a Mutation when receiving on Mutations() StoreUnitDuration(name string, value, units time.Duration) Plant } @@ -128,7 +126,7 @@ type Listable interface { // List returns a pointer to a []string containing strings List(name string) *[]string // NewList registers a new []string that can be assigned -name="ONE,TWO,THREE,FOUR" - NewList(name string, value []string, usage string) *[]string + NewList(name string, value []string, usage string) Plant // StoreList replaces name with value and can issue a Mutation when receiving on Mutations() StoreList(name string, value []string) Plant } @@ -137,7 +135,7 @@ type Mappable interface { // Map returns a pointer to a map[string]string containing strings Map(name string) *map[string]string // NewMap registers a new map[string]string that can be assigned -name="PROPERTY=VALUE,ANOTHER=VALUE" - NewMap(name string, value map[string]string, usage string) *map[string]string + NewMap(name string, value map[string]string, usage string) Plant // MapKeys returns the keys of the map[string]string as a []string MapKeys(name string) []string // StoreMap replaces name with value and can issue a Mutation when receiving on Mutations() @@ -166,14 +164,15 @@ type CoreMutations interface { } type Core interface { - // Fig returns a figFruit from the figTree by its name - Fig(name string) Flesh + // FigFlesh returns a figFruit from the figTree by its name + FigFlesh(name string) Flesh // ErrorFor returns an error attached to a named figFruit ErrorFor(name string) error // Usage displays the helpful menu of figs registered using -h or -help Usage() + UsageString() string } // Plant defines the interface for configuration management. @@ -190,6 +189,7 @@ type figTree struct { harvest int pollinate bool figs map[string]*figFruit + values *sync.Map withered map[string]witheredFig aliases map[string]string sourceLocker sync.RWMutex @@ -230,7 +230,7 @@ type FigValidatorFunc func(interface{}) error type witheredFig struct { Error error Mutagenesis Mutagenesis - Flesh figFlesh + Value Value name string } @@ -242,25 +242,18 @@ type figFruit struct { Locker *sync.RWMutex Error error Mutagenesis Mutagenesis - Flesh figFlesh name string + usage string } type figFlesh struct { Flesh interface{} + Error error } type Flesh interface { - ToString() string - ToInt() int - ToInt64() int64 - ToBool() bool - ToFloat64() float64 - ToDuration() time.Duration - ToUnitDuration() time.Duration - ToList() []string - ToMap() map[string]string Is(mutagenesis Mutagenesis) bool + AsIs() interface{} IsString() bool IsInt() bool IsInt64() bool @@ -270,6 +263,16 @@ type Flesh interface { IsUnitDuration() bool IsList() bool IsMap() bool + + ToString() string + ToInt() int + ToInt64() int64 + ToBool() bool + ToFloat64() float64 + ToDuration() time.Duration + ToUnitDuration() time.Duration + ToList() []string + ToMap() map[string]string } type Callback struct { @@ -290,3 +293,7 @@ type Mutation struct { When time.Time Error error } + +var ListSeparator = "," +var MapSeparator = "," +var MapKeySeparator = "=" diff --git a/usage.go b/usage.go index 7da4443..6472afd 100644 --- a/usage.go +++ b/usage.go @@ -12,6 +12,10 @@ import ( // Usage prints a helpful table of figs in a human-readable format func (tree *figTree) Usage() { + fmt.Println(tree.UsageString()) +} + +func (tree *figTree) UsageString() string { termWidth := 80 if term.IsTerminal(int(os.Stdout.Fd())) { if width, _, err := term.GetSize(int(os.Stdout.Fd())); err == nil { @@ -36,7 +40,7 @@ func (tree *figTree) Usage() { }) var sb strings.Builder - _, _ = fmt.Fprintf(&sb, "Usage of %s (powered by figree %s):\n", filepath.Base(os.Args[0]), Version()) + _, _ = fmt.Fprintf(&sb, "Usage of %s (powered by figtree %s):\n", filepath.Base(os.Args[0]), Version()) flag.VisitAll(func(f *flag.Flag) { flagStr := f.Name defValue := f.DefValue @@ -79,7 +83,7 @@ func (tree *figTree) Usage() { } }) - fmt.Println(sb.String()) + return sb.String() } // wrapText wraps a line of text to fit within the terminal width, indenting wrapped lines diff --git a/validators.go b/validators.go index 7fcc8a8..dfbdaf5 100644 --- a/validators.go +++ b/validators.go @@ -2,6 +2,8 @@ package figtree import ( "fmt" + "log" + "strings" "time" ) @@ -17,6 +19,7 @@ import ( func (tree *figTree) WithValidator(name string, validator func(interface{}) error) Plant { tree.mu.Lock() defer tree.mu.Unlock() + name = strings.ToLower(name) if fig, ok := tree.figs[name]; ok { if fig.HasRule(RuleNoValidations) { return tree @@ -64,7 +67,12 @@ func (tree *figTree) validateAll() error { for _, validator := range fruit.Validators { if fruit != nil && validator != nil { var val interface{} - switch v := fruit.Flesh.Flesh.(type) { + _value := tree.useValue(tree.from(name)) + if _value == nil { + fmt.Printf("skipping invalid fig '%s'\n", name) + continue + } + switch v := _value.Value.(type) { case int: val = v case *int: @@ -89,14 +97,31 @@ func (tree *figTree) validateAll() error { val = v case *time.Duration: val = *v + case []string: + val = v + case *[]string: + val = *v + case map[string]string: + val = v + case *map[string]string: + val = *v case ListFlag: val = v.values case *ListFlag: - val = *v.values + val = v.values case MapFlag: val = v.values case *MapFlag: - val = *v.values + val = v.values + case Value: + val = v.Value + case *Value: + val = v.Value + default: + log.Printf("unknown fig type: %T for %v\n", v, v) + } + if val == nil { + log.Printf("val is nil for %s", name) } if err := validator(val); err != nil { return fmt.Errorf("validation failed for %s: %v", name, err) @@ -104,13 +129,20 @@ func (tree *figTree) validateAll() error { } } } + + for _, fruit := range tree.figs { + if fruit.Error != nil { + return fruit.Error + } + } + return tree.runCallbacks(CallbackAfterVerify) } // makeStringValidator creates a validator for string-based checks. func makeStringValidator(check func(string) bool, errFormat string) FigValidatorFunc { return func(value interface{}) error { - v := figFlesh{value} + v := figFlesh{value, nil} if !v.IsString() { return fmt.Errorf("expected string, got %T", value) } diff --git a/validators_test.go b/validators_test.go index cc14981..7439949 100644 --- a/validators_test.go +++ b/validators_test.go @@ -1,6 +1,7 @@ package figtree import ( + "os" "testing" "time" @@ -9,346 +10,402 @@ import ( func TestTree_WithValidator(t *testing.T) { t.Run("CanDoDoubleValidationsOnSameKey", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewString(t.Name(), "i love yahuah", "usage") - fig.WithValidator(t.Name(), AssureStringSubstring("love yahuah")) - fig.WithValidator(t.Name(), AssureStringNotEmpty) + fig = fig.NewString(t.Name(), "i love yahuah", "usage") + fig = fig.WithValidator(t.Name(), AssureStringSubstring("love yahuah")) + fig = fig.WithValidator(t.Name(), AssureStringNotEmpty) assert.NoError(t, fig.Parse()) }) t.Run("AssureListNotContainsError", func(t *testing.T) { + os.Args = []string{os.Args[0]} var k = []string{"i", "love", "yahuah"} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewList(t.Name(), k, "usage") - fig.WithValidator(t.Name(), AssureListNotContains("yahuah")) + fig = fig.NewList(t.Name(), k, "usage") + fig = fig.WithValidator(t.Name(), AssureListNotContains("yahuah")) assert.Error(t, fig.Parse()) }) t.Run("AssureListNotContains", func(t *testing.T) { + os.Args = []string{os.Args[0]} var k = []string{"i", "love", "yahuah"} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewList(t.Name(), k, "usage") - fig.WithValidator(t.Name(), AssureListNotContains("andrei")) + fig = fig.NewList(t.Name(), k, "usage") + fig = fig.WithValidator(t.Name(), AssureListNotContains("andrei")) assert.NoError(t, fig.Parse()) }) t.Run("AssureStringNotLengthError", func(t *testing.T) { const k = "i love yahuah" + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewString(t.Name(), k, "usage") - fig.WithValidator(t.Name(), AssureStringNotLength(len(k))) + fig = fig.NewString(t.Name(), k, "usage") + fig = fig.WithValidator(t.Name(), AssureStringNotLength(len(k))) assert.Error(t, fig.Parse()) }) t.Run("AssureStringNoPrefix", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewString(t.Name(), "yahuah", "usage") - fig.WithValidator(t.Name(), AssureStringNoPrefix("no")) + fig = fig.NewString(t.Name(), "yahuah", "usage") + fig = fig.WithValidator(t.Name(), AssureStringNoPrefix("no")) assert.NoError(t, fig.Parse()) }) t.Run("AssureStringNoSuffix", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewString(t.Name(), "yahuah", "usage") - fig.WithValidator(t.Name(), AssureStringNoSuffix("no")) + fig = fig.NewString(t.Name(), "yahuah", "usage") + fig = fig.WithValidator(t.Name(), AssureStringNoSuffix("no")) assert.NoError(t, fig.Parse()) }) t.Run("AssureStringHasPrefixes", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewString(t.Name(), "yahuah", "usage") - fig.WithValidator(t.Name(), AssureStringHasPrefixes([]string{"yah", "ya"})) + fig = fig.NewString(t.Name(), "yahuah", "usage") + fig = fig.WithValidator(t.Name(), AssureStringHasPrefixes([]string{"yah", "ya"})) assert.NoError(t, fig.Parse()) }) t.Run("AssureStringHasSuffixes", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewString(t.Name(), "yahuah", "usage") - fig.WithValidator(t.Name(), AssureStringHasSuffixes([]string{"uah", "ah"})) + fig = fig.NewString(t.Name(), "yahuah", "usage") + fig = fig.WithValidator(t.Name(), AssureStringHasSuffixes([]string{"uah", "ah"})) assert.NoError(t, fig.Parse()) }) t.Run("AssureStringNoPrefixes", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewString(t.Name(), "yahuah", "usage") - fig.WithValidator(t.Name(), AssureStringNoPrefixes([]string{"no"})) + fig = fig.NewString(t.Name(), "yahuah", "usage") + fig = fig.WithValidator(t.Name(), AssureStringNoPrefixes([]string{"no"})) assert.NoError(t, fig.Parse()) }) t.Run("AssureStringNoSuffixes", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewString(t.Name(), "yahuah", "usage") - fig.WithValidator(t.Name(), AssureStringNoSuffixes([]string{"no"})) + fig = fig.NewString(t.Name(), "yahuah", "usage") + fig = fig.WithValidator(t.Name(), AssureStringNoSuffixes([]string{"no"})) assert.NoError(t, fig.Parse()) }) t.Run("AssureStringNotLength", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewString(t.Name(), "yahuah", "usage") - fig.WithValidator(t.Name(), AssureStringNotLength(369)) + fig = fig.NewString(t.Name(), "yahuah", "usage") + fig = fig.WithValidator(t.Name(), AssureStringNotLength(369)) assert.NoError(t, fig.Parse()) }) t.Run("AssureStringNotContainsError", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewString(t.Name(), "i love yahuah", "usage") - fig.WithValidator(t.Name(), AssureStringNotContains("yahuah")) + fig = fig.NewString(t.Name(), "i love yahuah", "usage") + fig = fig.WithValidator(t.Name(), AssureStringNotContains("yahuah")) assert.Error(t, fig.Parse()) }) t.Run("AssureStringNotContains", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewString(t.Name(), "i love yahuah", "usage") - fig.WithValidator(t.Name(), AssureStringNotContains("andrei")) + fig = fig.NewString(t.Name(), "i love yahuah", "usage") + fig = fig.WithValidator(t.Name(), AssureStringNotContains("andrei")) assert.NoError(t, fig.Parse()) }) t.Run("AssureStringLengthLessThan", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewString(t.Name(), "i love yahuah", "usage") - fig.WithValidator(t.Name(), AssureStringLengthLessThan(99)) + fig = fig.NewString(t.Name(), "i love yahuah", "usage") + fig = fig.WithValidator(t.Name(), AssureStringLengthLessThan(99)) assert.NoError(t, fig.Parse()) }) t.Run("AssureStringLengthGreaterThan", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewString(t.Name(), "i love yahuah", "usage") - fig.WithValidator(t.Name(), AssureStringLengthGreaterThan(3)) + fig = fig.NewString(t.Name(), "i love yahuah", "usage") + fig = fig.WithValidator(t.Name(), AssureStringLengthGreaterThan(3)) assert.NoError(t, fig.Parse()) }) t.Run("AssureStringHasSuffix", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewString(t.Name(), "i love yahuah", "usage") - fig.WithValidator(t.Name(), AssureStringHasSuffix("yahuah")) + fig = fig.NewString(t.Name(), "i love yahuah", "usage") + fig = fig.WithValidator(t.Name(), AssureStringHasSuffix("yahuah")) assert.NoError(t, fig.Parse()) }) t.Run("AssureStringHasPrefix", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewString(t.Name(), "i love yahuah", "usage") - fig.WithValidator(t.Name(), AssureStringHasPrefix("i love")) + fig = fig.NewString(t.Name(), "i love yahuah", "usage") + fig = fig.WithValidator(t.Name(), AssureStringHasPrefix("i love")) assert.NoError(t, fig.Parse()) }) t.Run("AssureStringLength", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewString(t.Name(), "i love yahuah", "usage") - fig.WithValidator(t.Name(), AssureStringLength(13)) + fig = fig.NewString(t.Name(), "i love yahuah", "usage") + fig = fig.WithValidator(t.Name(), AssureStringLength(13)) assert.NoError(t, fig.Parse()) }) t.Run("AssureStringSubstring", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewString(t.Name(), "i love yahuah", "usage") - fig.WithValidator(t.Name(), AssureStringSubstring("love yahuah")) + fig = fig.NewString(t.Name(), "i love yahuah", "usage") + fig = fig.WithValidator(t.Name(), AssureStringSubstring("love yahuah")) assert.NoError(t, fig.Parse()) }) t.Run("AssureStringNotEmpty", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewString(t.Name(), "i love yahuah", "usage") - fig.WithValidator(t.Name(), AssureStringNotEmpty) + fig = fig.NewString(t.Name(), "i love yahuah", "usage") + fig = fig.WithValidator(t.Name(), AssureStringNotEmpty) assert.NoError(t, fig.Parse()) }) t.Run("AssureStringContains ", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewString(t.Name(), "i love yahuah", "usage") - fig.WithValidator(t.Name(), AssureStringContains("love yahuah")) + fig = fig.NewString(t.Name(), "i love yahuah", "usage") + fig = fig.WithValidator(t.Name(), AssureStringContains("love yahuah")) assert.NoError(t, fig.Parse()) }) t.Run("AssureBoolTrue", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewBool(t.Name(), true, "usage") - fig.WithValidator(t.Name(), AssureBoolTrue) + fig = fig.NewBool(t.Name(), true, "usage") + fig = fig.WithValidator(t.Name(), AssureBoolTrue) assert.NoError(t, fig.Parse()) }) t.Run("AssureBoolFalse", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewBool(t.Name(), false, "usage") - fig.WithValidator(t.Name(), AssureBoolFalse) + fig = fig.NewBool(t.Name(), false, "usage") + fig = fig.WithValidator(t.Name(), AssureBoolFalse) assert.NoError(t, fig.Parse()) }) t.Run("AssureIntPositive ", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewInt(t.Name(), 17, "usage") - fig.WithValidator(t.Name(), AssureIntPositive) + fig = fig.NewInt(t.Name(), 17, "usage") + fig = fig.WithValidator(t.Name(), AssureIntPositive) assert.NoError(t, fig.Parse()) }) t.Run("AssureIntNegative ", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewInt(t.Name(), -17, "usage") - fig.WithValidator(t.Name(), AssureIntNegative) + fig = fig.NewInt(t.Name(), -17, "usage") + fig = fig.WithValidator(t.Name(), AssureIntNegative) assert.NoError(t, fig.Parse()) }) t.Run("AssureIntGreaterThan ", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewInt(t.Name(), 17, "usage") - fig.WithValidator(t.Name(), AssureIntGreaterThan(12)) + fig = fig.NewInt(t.Name(), 17, "usage") + fig = fig.WithValidator(t.Name(), AssureIntGreaterThan(12)) assert.NoError(t, fig.Parse()) }) t.Run("AssureIntLessThan ", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewInt(t.Name(), 17, "usage") - fig.WithValidator(t.Name(), AssureIntLessThan(33)) + fig = fig.NewInt(t.Name(), 17, "usage") + fig = fig.WithValidator(t.Name(), AssureIntLessThan(33)) assert.NoError(t, fig.Parse()) }) t.Run("AssureIntInRange ", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewInt(t.Name(), 47, "usage") - fig.WithValidator(t.Name(), AssureIntInRange(17, 76)) + fig = fig.NewInt(t.Name(), 47, "usage") + fig = fig.WithValidator(t.Name(), AssureIntInRange(17, 76)) assert.NoError(t, fig.Parse()) }) t.Run("AssureInt64GreaterThan", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewInt64(t.Name(), 17, "usage") - fig.WithValidator(t.Name(), AssureInt64GreaterThan(3)) + fig = fig.NewInt64(t.Name(), 17, "usage") + fig = fig.WithValidator(t.Name(), AssureInt64GreaterThan(3)) assert.NoError(t, fig.Parse()) }) t.Run("AssureInt64LessThan ", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewInt64(t.Name(), 17, "usage") - fig.WithValidator(t.Name(), AssureInt64LessThan(33)) + fig = fig.NewInt64(t.Name(), 17, "usage") + fig = fig.WithValidator(t.Name(), AssureInt64LessThan(33)) assert.NoError(t, fig.Parse()) }) t.Run("AssureInt64Positive ", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewInt64(t.Name(), 17, "usage") - fig.WithValidator(t.Name(), AssureInt64Positive) + fig = fig.NewInt64(t.Name(), 17, "usage") + fig = fig.WithValidator(t.Name(), AssureInt64Positive) assert.NoError(t, fig.Parse()) }) t.Run("AssureFloat64Positive ", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewFloat64(t.Name(), 17.76, "usage") - fig.WithValidator(t.Name(), AssureFloat64Positive) + fig = fig.NewFloat64(t.Name(), 17.76, "usage") + fig = fig.WithValidator(t.Name(), AssureFloat64Positive) assert.NoError(t, fig.Parse()) }) t.Run("AssureFloat64InRange ", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewFloat64(t.Name(), 17.76, "usage") - fig.WithValidator(t.Name(), AssureFloat64InRange(1.0, 20.0)) + fig = fig.NewFloat64(t.Name(), 17.76, "usage") + fig = fig.WithValidator(t.Name(), AssureFloat64InRange(1.0, 20.0)) assert.NoError(t, fig.Parse()) }) t.Run("AssureFloat64GreaterThan ", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewFloat64(t.Name(), 17.76, "usage") - fig.WithValidator(t.Name(), AssureFloat64GreaterThan(3.69)) + fig = fig.NewFloat64(t.Name(), 17.76, "usage") + fig = fig.WithValidator(t.Name(), AssureFloat64GreaterThan(3.69)) assert.NoError(t, fig.Parse()) }) t.Run("AssureFloat64LessThan ", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewFloat64(t.Name(), 17.76, "usage") - fig.WithValidator(t.Name(), AssureFloat64LessThan(33.33)) + fig = fig.NewFloat64(t.Name(), 17.76, "usage") + fig = fig.WithValidator(t.Name(), AssureFloat64LessThan(33.33)) assert.NoError(t, fig.Parse()) }) t.Run("AssureDurationGreaterThan ", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewDuration(t.Name(), 30*time.Second, "usage") - fig.WithValidator(t.Name(), AssureDurationGreaterThan(10*time.Second)) + fig = fig.NewDuration(t.Name(), 30*time.Second, "usage") + fig = fig.WithValidator(t.Name(), AssureDurationGreaterThan(10*time.Second)) assert.NoError(t, fig.Parse()) }) t.Run("AssureDurationLessThan ", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewDuration(t.Name(), 30*time.Second, "usage") - fig.WithValidator(t.Name(), AssureDurationLessThan(50*time.Second)) + fig = fig.NewDuration(t.Name(), 30*time.Second, "usage") + fig = fig.WithValidator(t.Name(), AssureDurationLessThan(50*time.Second)) assert.NoError(t, fig.Parse()) }) t.Run("AssureDurationPositive ", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewDuration(t.Name(), 30*time.Second, "usage") - fig.WithValidator(t.Name(), AssureDurationPositive) + fig = fig.NewDuration(t.Name(), 30*time.Second, "usage") + fig = fig.WithValidator(t.Name(), AssureDurationPositive) assert.NoError(t, fig.Parse()) }) t.Run("AssureDurationMax ", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewDuration(t.Name(), 30*time.Second, "usage") - fig.WithValidator(t.Name(), AssureDurationMax(1*time.Hour)) + fig = fig.NewDuration(t.Name(), 30*time.Second, "usage") + fig = fig.WithValidator(t.Name(), AssureDurationMax(1*time.Hour)) assert.NoError(t, fig.Parse()) }) t.Run("AssureListNotEmpty ", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewList(t.Name(), []string{"yah", "i am", "yahuah"}, "usage") - fig.WithValidator(t.Name(), AssureListNotEmpty) + fig = fig.NewList(t.Name(), []string{"yah", "i am", "yahuah"}, "usage") + fig = fig.WithValidator(t.Name(), AssureListNotEmpty) assert.NoError(t, fig.Parse()) }) t.Run("AssureListMinLength ", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewList(t.Name(), []string{"yah", "i am", "yahuah"}, "usage") - fig.WithValidator(t.Name(), AssureListMinLength(3)) + fig = fig.NewList(t.Name(), []string{"yah", "i am", "yahuah"}, "usage") + fig = fig.WithValidator(t.Name(), AssureListMinLength(3)) assert.NoError(t, fig.Parse()) }) t.Run("AssureMapNotEmpty ", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewMap(t.Name(), map[string]string{"one": "yah", "two": "i am", "three": "yahuah"}, "usage") - fig.WithValidator(t.Name(), AssureMapNotEmpty) + fig = fig.NewMap(t.Name(), map[string]string{"one": "yah", "two": "i am", "three": "yahuah"}, "usage") + fig = fig.WithValidator(t.Name(), AssureMapNotEmpty) assert.NoError(t, fig.Parse()) }) t.Run("AssureMapHasNoKeyError", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewMap(t.Name(), map[string]string{"one": "yah", "two": "i am", "three": "yahuah"}, "usage") - fig.WithValidator(t.Name(), AssureMapHasNoKey("three")) + fig = fig.NewMap(t.Name(), map[string]string{"one": "yah", "two": "i am", "three": "yahuah"}, "usage") + fig = fig.WithValidator(t.Name(), AssureMapHasNoKey("three")) assert.Error(t, fig.Parse()) }) t.Run("AssureMapHasNoKey", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewMap(t.Name(), map[string]string{"one": "yah", "two": "i am", "three": "yahuah"}, "usage") - fig.WithValidator(t.Name(), AssureMapHasNoKey("four")) + fig = fig.NewMap(t.Name(), map[string]string{"one": "yah", "two": "i am", "three": "yahuah"}, "usage") + fig = fig.WithValidator(t.Name(), AssureMapHasNoKey("four")) assert.NoError(t, fig.Parse()) }) t.Run("AssureMapHasKey", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewMap(t.Name(), map[string]string{"one": "yah", "two": "i am", "three": "yahuah"}, "usage") - fig.WithValidator(t.Name(), AssureMapHasKey("three")) + fig = fig.NewMap(t.Name(), map[string]string{"one": "yah", "two": "i am", "three": "yahuah"}, "usage") + fig = fig.WithValidator(t.Name(), AssureMapHasKey("three")) assert.NoError(t, fig.Parse()) }) t.Run("AssureInt64InRange", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewInt64(t.Name(), 47, "usage") - fig.WithValidator(t.Name(), AssureInt64InRange(17, 76)) + fig = fig.NewInt64(t.Name(), 47, "usage") + fig = fig.WithValidator(t.Name(), AssureInt64InRange(17, 76)) assert.NoError(t, fig.Parse()) }) t.Run("AssureFloat64NotNaN", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewFloat64(t.Name(), 47.0, "usage") - fig.WithValidator(t.Name(), AssureFloat64NotNaN) + fig = fig.NewFloat64(t.Name(), 47.0, "usage") + fig = fig.WithValidator(t.Name(), AssureFloat64NotNaN) assert.NoError(t, fig.Parse()) }) t.Run("AssureDurationMin", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewDuration(t.Name(), 30*time.Second, "usage") - fig.WithValidator(t.Name(), AssureDurationMin(3*time.Second)) + fig = fig.NewDuration(t.Name(), 30*time.Second, "usage") + fig = fig.WithValidator(t.Name(), AssureDurationMin(3*time.Second)) assert.NoError(t, fig.Parse()) }) t.Run("AssureListContains", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewList(t.Name(), []string{"yah", "i am", "yahuah"}, "usage") - fig.WithValidator(t.Name(), AssureListContains("yahuah")) + fig = fig.NewList(t.Name(), []string{"yah", "i am", "yahuah"}, "usage") + fig = fig.WithValidator(t.Name(), AssureListContains("yahuah")) assert.NoError(t, fig.Parse()) }) t.Run("AssureMapValueMatches", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewMap(t.Name(), map[string]string{"one": "yah", "two": "i am", "three": "yahuah"}, "usage") - fig.WithValidator(t.Name(), AssureMapValueMatches("three", "yahuah")) + fig = fig.NewMap(t.Name(), map[string]string{"one": "yah", "two": "i am", "three": "yahuah"}, "usage") + fig = fig.WithValidator(t.Name(), AssureMapValueMatches("three", "yahuah")) assert.NoError(t, fig.Parse()) }) t.Run("AssureMapHasKeys", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewMap(t.Name(), map[string]string{"one": "yah", "two": "i am", "three": "yahuah"}, "usage") - fig.WithValidator(t.Name(), AssureMapHasKeys([]string{"one", "two", "three"})) + fig = fig.NewMap(t.Name(), map[string]string{"one": "yah", "two": "i am", "three": "yahuah"}, "usage") + fig = fig.WithValidator(t.Name(), AssureMapHasKeys([]string{"one", "two", "three"})) assert.NoError(t, fig.Parse()) }) t.Run("AssureListContainsKey", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewList(t.Name(), []string{"yah", "i am", "yahuah"}, "usage") - fig.WithValidator(t.Name(), AssureListContainsKey("yahuah")) + fig = fig.NewList(t.Name(), []string{"yah", "i am", "yahuah"}, "usage") + fig = fig.WithValidator(t.Name(), AssureListContainsKey("yahuah")) assert.NoError(t, fig.Parse()) }) t.Run("AssureListLength", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewList(t.Name(), []string{"yah", "i am", "yahuah"}, "usage") - fig.WithValidator(t.Name(), AssureListLength(3)) + fig = fig.NewList(t.Name(), []string{"yah", "i am", "yahuah"}, "usage") + fig = fig.WithValidator(t.Name(), AssureListLength(3)) assert.NoError(t, fig.Parse()) }) t.Run("AssureMapLength", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewMap(t.Name(), map[string]string{"one": "yah", "two": "i am", "three": "yahuah"}, "usage") - fig.WithValidator(t.Name(), AssureMapLength(3)) + fig = fig.NewMap(t.Name(), map[string]string{"one": "yah", "two": "i am", "three": "yahuah"}, "usage") + fig = fig.WithValidator(t.Name(), AssureMapLength(3)) assert.NoError(t, fig.Parse()) }) t.Run("AssureMapNotLengthError", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewMap(t.Name(), map[string]string{"one": "yah", "two": "i am", "three": "yahuah"}, "usage") - fig.WithValidator(t.Name(), AssureMapNotLength(3)) + fig = fig.NewMap(t.Name(), map[string]string{"one": "yah", "two": "i am", "three": "yahuah"}, "usage") + fig = fig.WithValidator(t.Name(), AssureMapNotLength(3)) assert.Error(t, fig.Parse()) }) t.Run("AssureMapNotLength", func(t *testing.T) { + os.Args = []string{os.Args[0]} fig := With(Options{Germinate: true, Tracking: false}) - fig.NewMap(t.Name(), map[string]string{"one": "yah", "two": "i am", "three": "yahuah"}, "usage") - fig.WithValidator(t.Name(), AssureMapNotLength(4)) + fig = fig.NewMap(t.Name(), map[string]string{"one": "yah", "two": "i am", "three": "yahuah"}, "usage") + fig = fig.WithValidator(t.Name(), AssureMapNotLength(4)) assert.NoError(t, fig.Parse()) }) }