From 42f9b5c1d385230b77b99b1295b3530485297779 Mon Sep 17 00:00:00 2001 From: gi8 Date: Fri, 4 Jul 2025 16:23:49 +0200 Subject: [PATCH] complete refactor --- bool.go | 56 --------- bool_slice.go | 71 ------------ bool_slice_test.go | 146 ------------------------ bool_test.go | 146 ------------------------ builder.go | 39 +++++++ defaults.go | 75 ------------ defaults_test.go | 199 -------------------------------- duration.go | 56 --------- duration_slice.go | 72 ------------ duration_slice_test.go | 147 ------------------------ duration_test.go | 123 -------------------- dynflags.go | 129 +++++++++++++-------- dynflags_test.go | 234 +++++++++++++++++++++++--------------- examples/advanced/main.go | 206 --------------------------------- examples/simple/main.go | 71 ------------ flag.go | 66 ++++------- flag_test.go | 30 ----- flags_config.go | 55 --------- flags_config_test.go | 135 ---------------------- flags_parsed.go | 70 ------------ flags_parsed_test.go | 167 --------------------------- float64.go | 56 --------- float64_slice.go | 72 ------------ float64_slice_test.go | 140 ----------------------- float64_test.go | 130 --------------------- int.go | 56 --------- int_slice.go | 71 ------------ int_slice_test.go | 140 ----------------------- int_test.go | 124 -------------------- ip.go | 68 ----------- ip_slice.go | 72 ------------ ip_slice_test.go | 146 ------------------------ ip_test.go | 140 ----------------------- listen_addr.go | 65 ----------- listen_addr_slice.go | 75 ------------ listen_addr_slice_test.go | 151 ------------------------ listen_addr_test.go | 136 ---------------------- parser.go | 102 +++++++---------- parser_test.go | 158 ------------------------- string.go | 54 --------- string_slice.go | 62 ---------- string_slice_test.go | 169 --------------------------- string_test.go | 116 ------------------- types.go | 44 +++++++ url.go | 64 ----------- url_slice.go | 72 ------------ url_slice_test.go | 154 ------------------------- url_test.go | 146 ------------------------ usage.go | 138 ++++++++++++++++++++++ value_base.go | 62 ++++++++++ value_base_slice.go | 76 +++++++++++++ value_bool.go | 25 ++++ value_string.go | 25 ++++ wrap.go | 23 ++++ 54 files changed, 724 insertions(+), 4701 deletions(-) delete mode 100644 bool.go delete mode 100644 bool_slice.go delete mode 100644 bool_slice_test.go delete mode 100644 bool_test.go create mode 100644 builder.go delete mode 100644 defaults.go delete mode 100644 defaults_test.go delete mode 100644 duration.go delete mode 100644 duration_slice.go delete mode 100644 duration_slice_test.go delete mode 100644 duration_test.go delete mode 100644 examples/advanced/main.go delete mode 100644 examples/simple/main.go delete mode 100644 flag_test.go delete mode 100644 flags_config.go delete mode 100644 flags_config_test.go delete mode 100644 flags_parsed.go delete mode 100644 flags_parsed_test.go delete mode 100644 float64.go delete mode 100644 float64_slice.go delete mode 100644 float64_slice_test.go delete mode 100644 float64_test.go delete mode 100644 int.go delete mode 100644 int_slice.go delete mode 100644 int_slice_test.go delete mode 100644 int_test.go delete mode 100644 ip.go delete mode 100644 ip_slice.go delete mode 100644 ip_slice_test.go delete mode 100644 ip_test.go delete mode 100644 listen_addr.go delete mode 100644 listen_addr_slice.go delete mode 100644 listen_addr_slice_test.go delete mode 100644 listen_addr_test.go delete mode 100644 parser_test.go delete mode 100644 string.go delete mode 100644 string_slice.go delete mode 100644 string_slice_test.go delete mode 100644 string_test.go create mode 100644 types.go delete mode 100644 url.go delete mode 100644 url_slice.go delete mode 100644 url_slice_test.go delete mode 100644 url_test.go create mode 100644 usage.go create mode 100644 value_base.go create mode 100644 value_base_slice.go create mode 100644 value_bool.go create mode 100644 value_string.go create mode 100644 wrap.go diff --git a/bool.go b/bool.go deleted file mode 100644 index efb4b56..0000000 --- a/bool.go +++ /dev/null @@ -1,56 +0,0 @@ -package dynflags - -import ( - "fmt" - "strconv" -) - -type BoolValue struct { - Bound *bool -} - -func (b *BoolValue) GetBound() any { - if b.Bound == nil { - return nil - } - return *b.Bound -} - -func (b *BoolValue) Parse(value string) (any, error) { - return strconv.ParseBool(value) -} - -func (b *BoolValue) Set(value any) error { - if val, ok := value.(bool); ok { - *b.Bound = val - return nil - } - return fmt.Errorf("invalid value type: expected bool") -} - -// Bool defines a boolean flag with the specified name, default value, and usage description. -// The flag is added to the group's flag list and returned as a *Flag instance. -func (g *ConfigGroup) Bool(name string, value bool, usage string) *Flag { - bound := &value - flag := &Flag{ - Type: FlagTypeBool, - Default: value, - Usage: usage, - value: &BoolValue{Bound: bound}, - } - g.Flags[name] = flag - g.flagOrder = append(g.flagOrder, name) - return flag -} - -// GetBool returns the bool value of a flag with the given name -func (pg *ParsedGroup) GetBool(flagName string) (bool, error) { - value, exists := pg.Values[flagName] - if !exists { - return false, fmt.Errorf("flag '%s' not found in group '%s'", flagName, pg.Name) - } - if boolVal, ok := value.(bool); ok { - return boolVal, nil - } - return false, fmt.Errorf("flag '%s' is not a bool", flagName) -} diff --git a/bool_slice.go b/bool_slice.go deleted file mode 100644 index 48bedc2..0000000 --- a/bool_slice.go +++ /dev/null @@ -1,71 +0,0 @@ -package dynflags - -import ( - "fmt" - "strconv" - "strings" -) - -type BoolSlicesValue struct { - Bound *[]bool -} - -func (b *BoolSlicesValue) GetBound() any { - if b.Bound == nil { - return nil - } - return *b.Bound -} - -func (b *BoolSlicesValue) Parse(value string) (any, error) { - parsed, err := strconv.ParseBool(value) - if err != nil { - return nil, fmt.Errorf("invalid boolean value: %s, error: %w", value, err) - } - return parsed, nil -} - -func (b *BoolSlicesValue) Set(value any) error { - if parsedBool, ok := value.(bool); ok { - *b.Bound = append(*b.Bound, parsedBool) - return nil - } - return fmt.Errorf("invalid value type: expected bool") -} - -// BoolSlices defines a boolean slice flag with the specified name, default value, and usage description. -// The flag is added to the group's flag list and returned as a *Flag instance. -func (g *ConfigGroup) BoolSlices(name string, value []bool, usage string) *Flag { - bound := &value - defaultValue := make([]string, len(value)) - for i, v := range value { - defaultValue[i] = strconv.FormatBool(v) - } - flag := &Flag{ - Type: FlagTypeBoolSlice, - Default: strings.Join(defaultValue, ","), - Usage: usage, - value: &BoolSlicesValue{Bound: bound}, - } - g.Flags[name] = flag - g.flagOrder = append(g.flagOrder, name) - - return flag -} - -// GetBoolSlices returns the []bool value of a flag with the given name -func (pg *ParsedGroup) GetBoolSlices(flagName string) ([]bool, error) { - value, exists := pg.Values[flagName] - if !exists { - return nil, fmt.Errorf("flag '%s' not found in group '%s'", flagName, pg.Name) - } - if slice, ok := value.([]bool); ok { - return slice, nil - } - - if b, ok := value.(bool); ok { - return []bool{b}, nil - } - - return nil, fmt.Errorf("flag '%s' is not a []bool", flagName) -} diff --git a/bool_slice_test.go b/bool_slice_test.go deleted file mode 100644 index a20e123..0000000 --- a/bool_slice_test.go +++ /dev/null @@ -1,146 +0,0 @@ -package dynflags_test - -import ( - "testing" - - "github.com/containeroo/dynflags" - "github.com/stretchr/testify/assert" -) - -func TestBoolSlicesValue(t *testing.T) { - t.Parallel() - - t.Run("Parse valid bool value", func(t *testing.T) { - t.Parallel() - - boolSlicesValue := dynflags.BoolSlicesValue{Bound: &[]bool{}} - parsed, err := boolSlicesValue.Parse("true") - assert.NoError(t, err) - assert.Equal(t, true, parsed) - }) - - t.Run("Parse invalid bool value", func(t *testing.T) { - t.Parallel() - - boolSlicesValue := dynflags.BoolSlicesValue{Bound: &[]bool{}} - parsed, err := boolSlicesValue.Parse("invalid") - assert.Error(t, err) - assert.Nil(t, parsed) - }) - - t.Run("Set valid bool value", func(t *testing.T) { - t.Parallel() - - bound := []bool{true} - boolSlicesValue := dynflags.BoolSlicesValue{Bound: &bound} - - err := boolSlicesValue.Set(false) - assert.NoError(t, err) - assert.Equal(t, []bool{true, false}, bound) - }) - - t.Run("Set invalid type", func(t *testing.T) { - t.Parallel() - - bound := []bool{} - boolSlicesValue := dynflags.BoolSlicesValue{Bound: &bound} - - err := boolSlicesValue.Set("invalid") - assert.Error(t, err) - assert.EqualError(t, err, "invalid value type: expected bool") - }) -} - -func TestGroupConfigBoolSlices(t *testing.T) { - t.Parallel() - - t.Run("Define bool slices flag", func(t *testing.T) { - t.Parallel() - - group := &dynflags.ConfigGroup{Flags: make(map[string]*dynflags.Flag)} - defaultValue := []bool{true, false} - group.BoolSlices("boolSliceFlag", defaultValue, "A bool slices flag") - - assert.Contains(t, group.Flags, "boolSliceFlag") - assert.Equal(t, "A bool slices flag", group.Flags["boolSliceFlag"].Usage) - assert.Equal(t, "true,false", group.Flags["boolSliceFlag"].Default) - }) -} - -func TestGetBoolSlices(t *testing.T) { - t.Parallel() - - t.Run("Retrieve []bool value", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Name: "testGroup", - Values: map[string]any{ - "flag1": []bool{true, false, true}, - }, - } - - result, err := parsedGroup.GetBoolSlices("flag1") - assert.NoError(t, err) - assert.Equal(t, []bool{true, false, true}, result) - }) - - t.Run("Retrieve single bool value as []bool", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Name: "testGroup", - Values: map[string]any{ - "flag1": true, - }, - } - - result, err := parsedGroup.GetBoolSlices("flag1") - assert.NoError(t, err) - assert.Equal(t, []bool{true}, result) - }) - - t.Run("Flag not found", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Name: "testGroup", - Values: map[string]any{}, - } - - result, err := parsedGroup.GetBoolSlices("nonExistentFlag") - assert.Error(t, err) - assert.Nil(t, result) - assert.EqualError(t, err, "flag 'nonExistentFlag' not found in group 'testGroup'") - }) - - t.Run("Flag value is invalid type", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Name: "testGroup", - Values: map[string]any{ - "flag1": "invalid", - }, - } - - result, err := parsedGroup.GetBoolSlices("flag1") - assert.Error(t, err) - assert.Nil(t, result) - assert.EqualError(t, err, "flag 'flag1' is not a []bool") - }) -} - -func TestBoolSlicesGetBound(t *testing.T) { - t.Run("BoolSlicesValue - GetBound", func(t *testing.T) { - var slices *[]bool - val := []bool{true, false, true} - slices = &val - - boolSlicesValue := dynflags.BoolSlicesValue{Bound: slices} - assert.Equal(t, val, boolSlicesValue.GetBound()) - - boolSlicesValue = dynflags.BoolSlicesValue{Bound: nil} - assert.Nil(t, boolSlicesValue.GetBound()) - }) -} diff --git a/bool_test.go b/bool_test.go deleted file mode 100644 index f30e9cc..0000000 --- a/bool_test.go +++ /dev/null @@ -1,146 +0,0 @@ -package dynflags_test - -import ( - "testing" - - "github.com/containeroo/dynflags" - "github.com/stretchr/testify/assert" -) - -func TestBoolValue_Parse(t *testing.T) { - t.Parallel() - - t.Run("ValidTrueValue", func(t *testing.T) { - t.Parallel() - - b := &dynflags.BoolValue{Bound: new(bool)} - value, err := b.Parse("true") - assert.NoError(t, err) - assert.Equal(t, true, value) - }) - - t.Run("ValidFalseValue", func(t *testing.T) { - t.Parallel() - - b := &dynflags.BoolValue{Bound: new(bool)} - value, err := b.Parse("false") - assert.NoError(t, err) - assert.Equal(t, false, value) - }) - - t.Run("InvalidValue", func(t *testing.T) { - t.Parallel() - - b := &dynflags.BoolValue{Bound: new(bool)} - value, err := b.Parse("invalid") - assert.Error(t, err) - assert.Equal(t, value, false) - }) -} - -func TestBoolValue_Set(t *testing.T) { - t.Parallel() - - t.Run("SetValidTrue", func(t *testing.T) { - t.Parallel() - - bound := new(bool) - b := &dynflags.BoolValue{Bound: bound} - err := b.Set(true) - assert.NoError(t, err) - assert.Equal(t, true, *bound) - }) - - t.Run("SetValidFalse", func(t *testing.T) { - t.Parallel() - - bound := new(bool) - b := &dynflags.BoolValue{Bound: bound} - err := b.Set(false) - assert.NoError(t, err) - assert.Equal(t, false, *bound) - }) - - t.Run("SetInvalidValue", func(t *testing.T) { - t.Parallel() - - bound := new(bool) - b := &dynflags.BoolValue{Bound: bound} - err := b.Set(123) // Invalid type - assert.Error(t, err) - assert.EqualError(t, err, "invalid value type: expected bool") - }) -} - -func TestGroupConfig_Bool(t *testing.T) { - t.Parallel() - - t.Run("DefaultBool", func(t *testing.T) { - t.Parallel() - - group := &dynflags.ConfigGroup{ - Flags: make(map[string]*dynflags.Flag), - } - group.Bool("testBool", true, "Test boolean flag") - flag := group.Flags["testBool"] - assert.NotNil(t, flag) - assert.Equal(t, dynflags.FlagTypeBool, flag.Type) - assert.Equal(t, true, flag.Default) - }) -} - -func TestParsedGroup_GetBool(t *testing.T) { - t.Parallel() - - t.Run("GetExistingBool", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Name: "testGroup", - Values: map[string]any{"testBool": true}, - } - value, err := parsedGroup.GetBool("testBool") - assert.NoError(t, err) - assert.Equal(t, true, value) - }) - - t.Run("GetNonExistentBool", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Name: "testGroup", - Values: map[string]any{}, - } - value, err := parsedGroup.GetBool("nonExistent") - assert.Error(t, err) - assert.Equal(t, false, value) - assert.EqualError(t, err, "flag 'nonExistent' not found in group 'testGroup'") - }) - - t.Run("GetInvalidBoolType", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Name: "testGroup", - Values: map[string]any{"invalidBool": "notABool"}, - } - value, err := parsedGroup.GetBool("invalidBool") - assert.Error(t, err) - assert.Equal(t, false, value) - assert.EqualError(t, err, "flag 'invalidBool' is not a bool") - }) -} - -func TestBoolGetBound(t *testing.T) { - t.Run("BoolValue - GetBound", func(t *testing.T) { - var b *bool - val := true - b = &val - - boolValue := dynflags.BoolValue{Bound: b} - assert.Equal(t, true, boolValue.GetBound()) - - boolValue = dynflags.BoolValue{Bound: nil} - assert.Nil(t, boolValue.GetBound()) - }) -} diff --git a/builder.go b/builder.go new file mode 100644 index 0000000..e73f8fa --- /dev/null +++ b/builder.go @@ -0,0 +1,39 @@ +package dynflags + +// FlagBuilder is the base builder for scalar and slice dynamic flags. +type FlagBuilder[T any] struct { + df *DynFlags + bf *Flag + ptr *T +} + +// Required marks the flag as required. +func (b *FlagBuilder[T]) Required() *FlagBuilder[T] { + b.bf.required = true + return b +} + +// Metavar sets the metavar used in help output for this flag. +func (b *FlagBuilder[T]) Metavar(s string) *FlagBuilder[T] { + b.bf.metavar = s + return b +} + +// Deprecated marks the flag as deprecated with a reason. +func (b *FlagBuilder[T]) Deprecated(reason string) *FlagBuilder[T] { + b.bf.deprecated = reason + return b +} + +// SliceFlagBuilder extends FlagBuilder with slice-specific options. +type SliceFlagBuilder[T any] struct { + FlagBuilder[T] +} + +// Delimiter sets the delimiter used to split string input for slice flags. +func (b *SliceFlagBuilder[T]) Delimiter(sep string) *SliceFlagBuilder[T] { + if d, ok := b.bf.value.(DelimiterSetter); ok { + d.SetDelimiter(sep) + } + return b +} diff --git a/defaults.go b/defaults.go deleted file mode 100644 index 39bb4c3..0000000 --- a/defaults.go +++ /dev/null @@ -1,75 +0,0 @@ -package dynflags - -import ( - "fmt" - "sort" - "strings" - "text/tabwriter" -) - -// PrintDefaults prints all registered flags -func (df *DynFlags) PrintDefaults() { - w := tabwriter.NewWriter(df.output, 0, 8, 2, ' ', 0) - - // Print title if present - if df.title != "" { - fmt.Fprintln(df.output, df.title) // nolint:errcheck - fmt.Fprintln(df.output) // nolint:errcheck - } - - // Print description if present - if df.description != "" { - fmt.Fprintln(df.output, df.description) // nolint:errcheck - fmt.Fprintln(df.output) // nolint:errcheck - } - - // Sort group names - if df.SortGroups { - sort.Strings(df.groupOrder) - } - - // Iterate over groups in the order they were added - for _, groupName := range df.groupOrder { - group := df.configGroups[groupName] - - // Print group usage or fallback to uppercase group name - if group.usage != "" { - fmt.Fprintln(w, group.usage) // nolint:errcheck - } else { - fmt.Fprintln(w, strings.ToUpper(groupName)) // nolint:errcheck - } - - // Sort flag names - if df.SortFlags { - sort.Strings(group.flagOrder) - } - - // Print flags for the group - if len(group.flagOrder) > 0 { - fmt.Fprintln(w, " Flag\tUsage") // nolint:errcheck - for _, flagName := range group.flagOrder { - flag := group.Flags[flagName] - usage := flag.Usage - if flag.Default != nil && flag.Default != "" { - usage = fmt.Sprintf("%s (default: %v)", flag.Usage, flag.Default) - } - metavar := string(flag.Type) - if flag.metaVar != "" { - metavar = flag.metaVar - } - - fmt.Fprintf(w, " --%s..%s %s\t%s\n", groupName, flagName, metavar, usage) // nolint:errcheck - } - fmt.Fprintln(w, "") // nolint:errcheck - } - } - - // tabwriter buffers output for alignment; flush now to ensure aligned flag output is printed before the epilog - w.Flush() // nolint:errcheck - - // Print epilog if present - if df.epilog != "" { - fmt.Fprintln(df.output) // nolint:errcheck - fmt.Fprintln(df.output, df.epilog) // nolint:errcheck - } -} diff --git a/defaults_test.go b/defaults_test.go deleted file mode 100644 index 7fafbe8..0000000 --- a/defaults_test.go +++ /dev/null @@ -1,199 +0,0 @@ -package dynflags_test - -import ( - "bytes" - "testing" - - "github.com/containeroo/dynflags" - "github.com/stretchr/testify/assert" -) - -func TestPrintDefaults(t *testing.T) { - t.Parallel() - - t.Run("No groups, title, description, or epilog", func(t *testing.T) { - t.Parallel() - - var buf bytes.Buffer - df := dynflags.New(dynflags.ContinueOnError) - df.SetOutput(&buf) - - df.PrintDefaults() - - output := buf.String() - assert.Empty(t, output) - }) - - t.Run("Only title is present", func(t *testing.T) { - t.Parallel() - - var buf bytes.Buffer - df := dynflags.New(dynflags.ContinueOnError) - df.SetOutput(&buf) - df.Title("Test Title") - - df.PrintDefaults() - - output := buf.String() - assert.Contains(t, output, "Test Title") - assert.NotContains(t, output, "Usage:") - }) - - t.Run("Only description is present", func(t *testing.T) { - t.Parallel() - - var buf bytes.Buffer - df := dynflags.New(dynflags.ContinueOnError) - df.SetOutput(&buf) - df.Description("Test Description") - - df.PrintDefaults() - - output := buf.String() - assert.Contains(t, output, "Test Description") - assert.NotContains(t, output, "Usage:") - }) - - t.Run("Only epilog is present", func(t *testing.T) { - t.Parallel() - - var buf bytes.Buffer - df := dynflags.New(dynflags.ContinueOnError) - df.SetOutput(&buf) - df.Epilog("Test Epilog") - - df.PrintDefaults() - - output := buf.String() - assert.Contains(t, output, "Test Epilog") - }) - - t.Run("Title, description, and epilog are all present", func(t *testing.T) { - t.Parallel() - - var buf bytes.Buffer - df := dynflags.New(dynflags.ContinueOnError) - df.SetOutput(&buf) - df.Title("Test Title") - df.Description("Test Description") - df.Epilog("Test Epilog") - - df.PrintDefaults() - - output := buf.String() - assert.Contains(t, output, "Test Title") - assert.Contains(t, output, "Test Description") - assert.Contains(t, output, "Test Epilog") - }) - - t.Run("Single group with unsorted flags", func(t *testing.T) { - t.Parallel() - - var buf bytes.Buffer - df := dynflags.New(dynflags.ContinueOnError) - df.SetOutput(&buf) - group := df.Group("test") - group.String("flag2", "", "Second flag") - group.String("flag1", "", "First flag") - - df.PrintDefaults() - - output := buf.String() - assert.Contains(t, output, "TEST") - assert.Contains(t, output, "--test..flag2") - assert.Contains(t, output, "--test..flag1") - }) - - t.Run("Multiple groups with sorted flags", func(t *testing.T) { - t.Parallel() - - var buf bytes.Buffer - df := dynflags.New(dynflags.ContinueOnError) - df.SetOutput(&buf) - df.SortFlags = true - df.SortGroups = true - group1 := df.Group("test1") - group1.String("flagA", "", "Flag A") - group1.String("flagB", "", "Flag B") - - group2 := df.Group("test2") - group2.String("flagX", "", "Flag X") - - df.PrintDefaults() - - output := buf.String() - assert.Contains(t, output, "TEST1") - assert.Contains(t, output, "--test1..flagA") - assert.Contains(t, output, "--test1..flagB") - assert.Contains(t, output, "TEST2") - assert.Contains(t, output, "--test2..flagX") - }) - - t.Run("Group with usage text", func(t *testing.T) { - t.Parallel() - - var buf bytes.Buffer - df := dynflags.New(dynflags.ContinueOnError) - df.SetOutput(&buf) - group := df.Group("test") - group.Usage("Test Group Usage") - group.String("flag", "", "Test flag") - - df.PrintDefaults() - - output := buf.String() - assert.Contains(t, output, "Test Group Usage") - assert.Contains(t, output, "--test..flag") - }) - - t.Run("Flags with and without default values", func(t *testing.T) { - t.Parallel() - - var buf bytes.Buffer - df := dynflags.New(dynflags.ContinueOnError) - df.SetOutput(&buf) - group := df.Group("test") - group.String("flag1", "default1", "Flag with default") - group.String("flag2", "", "Flag without default") - - df.PrintDefaults() - - output := buf.String() - assert.Contains(t, output, "--test..flag1") - assert.Contains(t, output, "(default: default1)") - assert.Contains(t, output, "--test..flag2") - assert.NotContains(t, output, "(default: )") - }) - - t.Run("Empty group with no flags", func(t *testing.T) { - t.Parallel() - - var buf bytes.Buffer - df := dynflags.New(dynflags.ContinueOnError) - df.SetOutput(&buf) - df.Group("test") - - df.PrintDefaults() - - output := buf.String() - assert.Contains(t, output, "TEST") - assert.NotContains(t, output, "Flag\tUsage") - }) - - t.Run("Metavar", func(t *testing.T) { - t.Parallel() - - var buf bytes.Buffer - df := dynflags.New(dynflags.ContinueOnError) - df.SetOutput(&buf) - g := df.Group("test") - s := g.String("test", "", "Test flag") - s.MetaVar("CUSTOM") - - df.PrintDefaults() - - output := buf.String() - assert.Contains(t, output, "CUSTOM") - assert.NotContains(t, output, "Flag\tUsage") - }) -} diff --git a/duration.go b/duration.go deleted file mode 100644 index fe87595..0000000 --- a/duration.go +++ /dev/null @@ -1,56 +0,0 @@ -package dynflags - -import ( - "fmt" - "time" -) - -type DurationValue struct { - Bound *time.Duration -} - -func (d *DurationValue) GetBound() any { - if d.Bound == nil { - return nil - } - return *d.Bound -} - -func (d *DurationValue) Parse(value string) (any, error) { - return time.ParseDuration(value) -} - -func (d *DurationValue) Set(value any) error { - if dur, ok := value.(time.Duration); ok { - *d.Bound = dur - return nil - } - return fmt.Errorf("invalid value type: expected duration") -} - -// Duration defines a duration flag with the specified name, default value, and usage description. -// The flag is added to the group's flag list and returned as a *Flag instance. -func (g *ConfigGroup) Duration(name string, value time.Duration, usage string) *Flag { - bound := &value - flag := &Flag{ - Type: FlagTypeDuration, - Default: value, - Usage: usage, - value: &DurationValue{Bound: bound}, - } - g.Flags[name] = flag - g.flagOrder = append(g.flagOrder, name) - return flag -} - -// GetDuration returns the time.Duration value of a flag with the given name -func (pg *ParsedGroup) GetDuration(flagName string) (time.Duration, error) { - vaue, exists := pg.Values[flagName] - if !exists { - return 0, fmt.Errorf("flag '%s' not found in group '%s'", flagName, pg.Name) - } - if durationVal, ok := vaue.(time.Duration); ok { - return durationVal, nil - } - return 0, fmt.Errorf("flag '%s' is not a time.Duration", flagName) -} diff --git a/duration_slice.go b/duration_slice.go deleted file mode 100644 index 1401538..0000000 --- a/duration_slice.go +++ /dev/null @@ -1,72 +0,0 @@ -package dynflags - -import ( - "fmt" - "strings" - "time" -) - -type DurationSlicesValue struct { - Bound *[]time.Duration -} - -func (d *DurationSlicesValue) GetBound() any { - if d.Bound == nil { - return nil - } - return *d.Bound -} - -func (d *DurationSlicesValue) Parse(value string) (any, error) { - parsed, err := time.ParseDuration(value) - if err != nil { - return nil, fmt.Errorf("invalid duration value: %s, error: %w", value, err) - } - return parsed, nil -} - -func (d *DurationSlicesValue) Set(value any) error { - if parsedDuration, ok := value.(time.Duration); ok { - *d.Bound = append(*d.Bound, parsedDuration) - return nil - } - return fmt.Errorf("invalid value type: expected time.Duration") -} - -// DurationSlices defines a duration slice flag with the specified name, default value, and usage description. -// The flag is added to the group's flag list and returned as a *Flag instance. -func (g *ConfigGroup) DurationSlices(name string, value []time.Duration, usage string) *Flag { - bound := &value - defaultValue := make([]string, len(value)) - for i, v := range value { - defaultValue[i] = v.String() - } - - flag := &Flag{ - Type: FlagTypeDurationSlice, - Default: strings.Join(defaultValue, ","), - Usage: usage, - value: &DurationSlicesValue{Bound: bound}, - } - g.Flags[name] = flag - g.flagOrder = append(g.flagOrder, name) - return flag -} - -// GetDurationSlices returns the []time.Duration value of a flag with the given name -func (pg *ParsedGroup) GetDurationSlices(flagName string) ([]time.Duration, error) { - value, exists := pg.Values[flagName] - if !exists { - return nil, fmt.Errorf("flag '%s' not found in group '%s'", flagName, pg.Name) - } - - if slice, ok := value.([]time.Duration); ok { - return slice, nil - } - - if d, ok := value.(time.Duration); ok { - return []time.Duration{d}, nil - } - - return nil, fmt.Errorf("flag '%s' is not a []time.Duration", flagName) -} diff --git a/duration_slice_test.go b/duration_slice_test.go deleted file mode 100644 index a7118fd..0000000 --- a/duration_slice_test.go +++ /dev/null @@ -1,147 +0,0 @@ -package dynflags_test - -import ( - "testing" - "time" - - "github.com/containeroo/dynflags" - "github.com/stretchr/testify/assert" -) - -func TestDurationSlicesValue(t *testing.T) { - t.Parallel() - - t.Run("Parse valid duration slice value", func(t *testing.T) { - t.Parallel() - - durationSlicesValue := dynflags.DurationSlicesValue{Bound: &[]time.Duration{}} - parsed, err := durationSlicesValue.Parse("5s") - assert.NoError(t, err) - assert.Equal(t, 5*time.Second, parsed) - }) - - t.Run("Parse invalid duration value", func(t *testing.T) { - t.Parallel() - - durationSlicesValue := dynflags.DurationSlicesValue{Bound: &[]time.Duration{}} - parsed, err := durationSlicesValue.Parse("invalid") - assert.Error(t, err) - assert.Nil(t, parsed) - }) - - t.Run("Set valid duration value", func(t *testing.T) { - t.Parallel() - - bound := []time.Duration{1 * time.Second} - durationSlicesValue := dynflags.DurationSlicesValue{Bound: &bound} - - err := durationSlicesValue.Set(2 * time.Second) - assert.NoError(t, err) - assert.Equal(t, []time.Duration{1 * time.Second, 2 * time.Second}, bound) - }) - - t.Run("Set invalid type", func(t *testing.T) { - t.Parallel() - - bound := []time.Duration{} - durationSlicesValue := dynflags.DurationSlicesValue{Bound: &bound} - - err := durationSlicesValue.Set("invalid") - assert.Error(t, err) - assert.EqualError(t, err, "invalid value type: expected time.Duration") - }) -} - -func TestGroupConfigDurationSlices(t *testing.T) { - t.Parallel() - - t.Run("Define duration slices flag", func(t *testing.T) { - t.Parallel() - - group := &dynflags.ConfigGroup{Flags: make(map[string]*dynflags.Flag)} - defaultValue := []time.Duration{1 * time.Second, 2 * time.Second} - group.DurationSlices("durationSliceFlag", defaultValue, "A duration slices flag") - - assert.Contains(t, group.Flags, "durationSliceFlag") - assert.Equal(t, "A duration slices flag", group.Flags["durationSliceFlag"].Usage) - assert.Equal(t, "1s,2s", group.Flags["durationSliceFlag"].Default) - }) -} - -func TestGetDurationSlices(t *testing.T) { - t.Parallel() - - t.Run("Retrieve []time.Duration value", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Name: "testGroup", - Values: map[string]any{ - "flag1": []time.Duration{1 * time.Second, 2 * time.Second, 3 * time.Second}, - }, - } - - result, err := parsedGroup.GetDurationSlices("flag1") - assert.NoError(t, err) - assert.Equal(t, []time.Duration{1 * time.Second, 2 * time.Second, 3 * time.Second}, result) - }) - - t.Run("Retrieve single time.Duration value as []time.Duration", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Name: "testGroup", - Values: map[string]any{ - "flag1": 5 * time.Second, - }, - } - - result, err := parsedGroup.GetDurationSlices("flag1") - assert.NoError(t, err) - assert.Equal(t, []time.Duration{5 * time.Second}, result) - }) - - t.Run("Flag not found", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Name: "testGroup", - Values: map[string]any{}, - } - - result, err := parsedGroup.GetDurationSlices("nonExistentFlag") - assert.Error(t, err) - assert.Nil(t, result) - assert.EqualError(t, err, "flag 'nonExistentFlag' not found in group 'testGroup'") - }) - - t.Run("Flag value is invalid type", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Name: "testGroup", - Values: map[string]any{ - "flag1": "invalid", - }, - } - - result, err := parsedGroup.GetDurationSlices("flag1") - assert.Error(t, err) - assert.Nil(t, result) - assert.EqualError(t, err, "flag 'flag1' is not a []time.Duration") - }) -} - -func TestDurationSlicesGetBound(t *testing.T) { - t.Run("DurationSlicesValue - GetBound", func(t *testing.T) { - var slices *[]time.Duration - val := []time.Duration{1 * time.Second, 2 * time.Second} - slices = &val - - durationSlicesValue := dynflags.DurationSlicesValue{Bound: slices} - assert.Equal(t, val, durationSlicesValue.GetBound()) - - durationSlicesValue = dynflags.DurationSlicesValue{Bound: nil} - assert.Nil(t, durationSlicesValue.GetBound()) - }) -} diff --git a/duration_test.go b/duration_test.go deleted file mode 100644 index 728284b..0000000 --- a/duration_test.go +++ /dev/null @@ -1,123 +0,0 @@ -package dynflags_test - -import ( - "testing" - "time" - - "github.com/containeroo/dynflags" - - "github.com/stretchr/testify/assert" -) - -func TestDurationValue_Parse(t *testing.T) { - t.Parallel() - - t.Run("ValidDuration", func(t *testing.T) { - t.Parallel() - - d := &dynflags.DurationValue{} - value, err := d.Parse("2h") - assert.NoError(t, err) - assert.Equal(t, 2*time.Hour, value) - }) - - t.Run("InvalidDuration", func(t *testing.T) { - t.Parallel() - - d := &dynflags.DurationValue{} - _, err := d.Parse("invalid") - assert.Error(t, err) - }) -} - -func TestDurationValue_Set(t *testing.T) { - t.Parallel() - - t.Run("SetValidDuration", func(t *testing.T) { - t.Parallel() - - var bound time.Duration - d := &dynflags.DurationValue{Bound: &bound} - err := d.Set(1 * time.Minute) - assert.NoError(t, err) - assert.Equal(t, 1*time.Minute, bound) - }) - - t.Run("SetInvalidType", func(t *testing.T) { - t.Parallel() - - var bound time.Duration - d := &dynflags.DurationValue{Bound: &bound} - err := d.Set("not a duration") - assert.Error(t, err) - assert.Equal(t, time.Duration(0), bound) - }) -} - -func TestGroupConfig_Duration(t *testing.T) { - t.Parallel() - - t.Run("DurationDefault", func(t *testing.T) { - t.Parallel() - - group := &dynflags.ConfigGroup{Flags: make(map[string]*dynflags.Flag)} - defaultValue := 5 * time.Second - bound := group.Duration("timeout", defaultValue, "Timeout duration") - assert.Equal(t, defaultValue, bound.Default) - assert.Contains(t, group.Flags, "timeout") - assert.Equal(t, defaultValue, group.Flags["timeout"].Default) - assert.Equal(t, dynflags.FlagTypeDuration, group.Flags["timeout"].Type) - }) -} - -func TestParsedGroup_GetDuration(t *testing.T) { - t.Parallel() - - t.Run("GetValidDuration", func(t *testing.T) { - t.Parallel() - - parsed := &dynflags.ParsedGroup{ - Name: "test", - Values: map[string]any{"timeout": 30 * time.Second}, - } - dur, err := parsed.GetDuration("timeout") - assert.NoError(t, err) - assert.Equal(t, 30*time.Second, dur) - }) - - t.Run("GetDurationNotFound", func(t *testing.T) { - t.Parallel() - - parsed := &dynflags.ParsedGroup{ - Name: "test", - Values: map[string]any{}, - } - _, err := parsed.GetDuration("missing") - assert.Error(t, err) - }) - - t.Run("GetDurationWrongType", func(t *testing.T) { - t.Parallel() - - parsed := &dynflags.ParsedGroup{ - Name: "test", - Values: map[string]any{"timeout": "not a duration"}, - } - _, err := parsed.GetDuration("timeout") - assert.Error(t, err) - }) -} - -func TestDurationGetBound(t *testing.T) { - t.Run("DurationValue - GetBound", func(t *testing.T) { - var d *time.Duration - val := 2 * time.Second - d = &val - - durationValue := dynflags.DurationValue{Bound: d} - assert.Equal(t, val, durationValue.GetBound()) - - durationValue = dynflags.DurationValue{Bound: nil} - assert.Nil(t, durationValue.GetBound()) - }) -} diff --git a/dynflags.go b/dynflags.go index 7697045..0b65b70 100644 --- a/dynflags.go +++ b/dynflags.go @@ -1,89 +1,128 @@ package dynflags import ( - "fmt" "io" "os" ) -// ParseBehavior defines how Parse should behave on errors. +// ParseBehavior controls how parsing errors are handled. type ParseBehavior int const ( - ContinueOnError ParseBehavior = iota // Continue parsing on error - ExitOnError // Exit on error + ContinueOnError ParseBehavior = iota + ExitOnError + PanicOnError ) -// DynFlags manages configuration and parsed values +// DynFlags manages dynamic groups and their flags. type DynFlags struct { - configGroups map[string]*ConfigGroup // Static parent groups - groupOrder []string // Order of group names - SortGroups bool // Sort groups in help message - SortFlags bool // Sort flags in help message - parsedGroups GroupsMap // Parsed child groups organized by parent group - parseBehavior ParseBehavior // Parsing behavior - unparsedArgs []string // Arguments that couldn't be parsed - output io.Writer // Output for usage/help - usage func() // Customizable usage function - title string // Title in the help message - description string // Description after the title in the help message - epilog string // Epilog in the help message + parseBehavior ParseBehavior + + groups map[string]*ConfigGroup // All defined groups + groupOrder []string // Preserve registration order + + parsed GroupsMap // Parsed values + output io.Writer // Help output destination + Usage func() // Usage callback + name string // Optional program name + title string // Optional help title + desc string // Optional help description + epilog string // Optional help footer + + unparsedArgs []string // Unknown args for later inspection + + sortGroups bool + sortFlags bool } -// New initializes a new DynFlags instance -func New(behavior ParseBehavior) *DynFlags { +// New creates a new DynFlags instance. +func New(name string, behavior ParseBehavior) *DynFlags { df := &DynFlags{ - configGroups: make(map[string]*ConfigGroup), - parsedGroups: make(GroupsMap), + name: name, parseBehavior: behavior, + groups: make(map[string]*ConfigGroup), + parsed: make(GroupsMap), output: os.Stdout, } - df.usage = func() { df.Usage() } + df.Usage = func() { + out := df.Output() + df.PrintTitle(out) + df.PrintUsage(out) + df.PrintDescription(out, 80) + df.PrintDefaults() + df.PrintEpilog(out, 80) + } return df } -// Title adds a title to the help message +// Name returns the program name (for usage header). +func (df *DynFlags) Name() string { + return df.name +} + +// Title sets the optional help title. func (df *DynFlags) Title(title string) { df.title = title } -// Description adds a descripton after the Title -func (df *DynFlags) Description(description string) { - df.description = description +// Description sets the optional help description. +func (df *DynFlags) Description(desc string) { + df.desc = desc } -// Epilog adds an epilog after the description of the dynamic flags to the help message +// Epilog sets the help footer. func (df *DynFlags) Epilog(epilog string) { df.epilog = epilog } -// Group defines a new group or retrieves an existing one +// SetOutput overrides the help output destination. +func (df *DynFlags) SetOutput(w io.Writer) { + df.output = w +} + +// Output returns the current help output writer. +func (df *DynFlags) Output() io.Writer { + return df.output +} + +// UnknownArgs returns any unparsed CLI arguments. +func (df *DynFlags) UnknownArgs() []string { + return df.unparsedArgs +} + +// SortGroups enables/disables sorting of group names. +func (df *DynFlags) SortGroups(enable bool) { + df.sortGroups = enable +} + +// SortFlags enables/disables sorting of flags within a group. +func (df *DynFlags) SortFlags(enable bool) { + df.sortFlags = enable +} + +// Group returns an existing group or creates a new one. func (df *DynFlags) Group(name string) *ConfigGroup { - if _, exists := df.configGroups[name]; exists { - return df.configGroups[name] + if g, ok := df.groups[name]; ok { + return g } - - df.groupOrder = append(df.groupOrder, name) group := &ConfigGroup{ Name: name, Flags: make(map[string]*Flag), } - df.configGroups[name] = group + df.groups[name] = group + df.groupOrder = append(df.groupOrder, name) return group } -// UnknownArgs returns the list of unparseable arguments. -func (df *DynFlags) UnknownArgs() []string { - return df.unparsedArgs -} - -// DefaultUsage provides the default usage output -func (df *DynFlags) Usage() { - fmt.Fprintf(df.output, "Usage: [--.. value]\n\n") - df.PrintDefaults() +// Groups returns all parsed values. +func (df *DynFlags) Groups() GroupsMap { + return df.parsed } -// SetOutput sets the output writer -func (df *DynFlags) SetOutput(buf io.Writer) { - df.output = buf +// Parsed returns the parsed result for a specific group and identifier. +func (df *DynFlags) Parsed(groupName, identifier string) *ParsedGroup { + if idMap, ok := df.parsed[groupName]; ok { + return idMap[identifier] + } + return nil } diff --git a/dynflags_test.go b/dynflags_test.go index 8a8e149..f1c618e 100644 --- a/dynflags_test.go +++ b/dynflags_test.go @@ -1,132 +1,182 @@ package dynflags_test import ( - "bytes" + "fmt" + "os" + "strings" "testing" "github.com/containeroo/dynflags" "github.com/stretchr/testify/assert" -) -func TestDynFlagsInitialization(t *testing.T) { - t.Parallel() + flag "github.com/spf13/pflag" +) - t.Run("New initializes correctly", func(t *testing.T) { - t.Parallel() +func TestDynflags(t *testing.T) { + t.Run("Smoke test", func(t *testing.T) { + df := dynflags.New("test.exe", dynflags.ContinueOnError) - df := dynflags.New(dynflags.ContinueOnError) - assert.NotNil(t, df) - assert.NotNil(t, df.Config()) - assert.NotNil(t, df.Parsed()) // df.Parsed() now returns ParsedGroups with GroupsMap internally - }) -} + appGroup := df.Group("app") + appGroup.String("msg", "Hello, World!", "Message to be displayed.") -func TestDynFlagsGroupManagement(t *testing.T) { - t.Parallel() + args := []string{ + "--app.default.msg", "Hello default DynFlags!", + "--app.custom.msg", "Hello custom DynFlags!", + } - t.Run("Create new group", func(t *testing.T) { - t.Parallel() + if err := df.Parse(args); err != nil { + fmt.Fprintf(os.Stderr, "Error parsing flags: %v\n", err) + os.Exit(1) + } - df := dynflags.New(dynflags.ContinueOnError) + defaultGroup := df.Parsed("app", "default") + customGroup := df.Parsed("app", "custom") - // Create Group - group := df.Group("group1") - assert.NotNil(t, group) - assert.Contains(t, df.Config().Groups(), "group1") - assert.Equal(t, group, df.Config().Lookup("group1")) - assert.Equal(t, "group1", group.Name) - assert.NotNil(t, group.Flags) + msg1, _ := dynflags.GetAs[string](defaultGroup, "msg") + msg2, _ := dynflags.GetAs[string](customGroup, "msg") - // Get Group again - group = df.Group("group1") - assert.NotNil(t, group) - assert.Contains(t, df.Config().Groups(), "group1") + fmt.Println("Default:", msg1) + fmt.Println("Custom:", msg2) }) -} -func TestDynFlagsUsageOutput(t *testing.T) { - t.Parallel() + t.Run("extended smoke tests", func(t *testing.T) { + df := dynflags.New("test.exe", dynflags.ContinueOnError) - t.Run("Generate usage with title, description, and epilog", func(t *testing.T) { - t.Parallel() + // HTTP group + http := df.Group("http") + http.String("name", "", "Name of the HTTP checker") + http.String("method", "GET", "HTTP method to use") + http.String("address", "", "HTTP target URL") + http.Bool("allow-duplicate-headers", false, "Allow duplicate HTTP headers") + http.String("expected-status-codes", "200", "Expected HTTP status codes") + http.Bool("skip-tls-verify", false, "Skip TLS verification") - var buf bytes.Buffer - df := dynflags.New(dynflags.ContinueOnError) - df.SetOutput(&buf) - - df.Title("Test Application") - df.Description("This application demonstrates usage of dynamic flags.") - df.Epilog("For more information, visit https://example.com.") - - df.Usage() - - output := buf.String() - assert.Contains(t, output, "Test Application") - assert.Contains(t, output, "This application demonstrates usage of dynamic flags.") - assert.Contains(t, output, "For more information, visit https://example.com.") - }) -} + // ICMP group + icmp := df.Group("icmp") + icmp.String("name", "", "Name of the ICMP checker") + icmp.String("address", "", "ICMP target address") -func TestDynFlagsParsedAndUnknown(t *testing.T) { - t.Parallel() + // TCP group + tcp := df.Group("tcp") + tcp.String("name", "", "Name of the TCP checker") + tcp.String("address", "", "TCP target address") - t.Run("Empty parsed and unknown args", func(t *testing.T) { - t.Parallel() + args := []string{ + "--http.default.name", "HTTP Checker Default", + "--http.default.address", "default.com:80", + "--http.other.name", "HTTP Checker Other", + "--http.other.address", "other.com:443", + "--http.other.method", "POST", + "--icmp.custom.address", "8.8.4.4", + "--tcp.testing.address", "example.com:443", + "--default-interval=5s", // pflag + } - df := dynflags.New(dynflags.ContinueOnError) + err := df.Parse(args) + assert.NoError(t, err) - // With the new GroupsMap approach, this should still be empty initially: - assert.Empty(t, df.Parsed().Groups()) - assert.Empty(t, df.UnknownArgs()) + fmt.Println("\n=== Dynamic Flags ===") + for groupName, identifiers := range df.Groups() { + fmt.Printf("Group: %s\n", groupName) + for identifier, pg := range identifiers { + t.Logf(" Identifier: %s\n", identifier) + for flagKey, value := range pg.Values { + t.Logf(" %s: %v\n", flagKey, value) + } + } + } }) -} -func TestParsedGroupMethods(t *testing.T) { - t.Parallel() + t.Run("help tests", func(t *testing.T) { + buf := strings.Builder{} - t.Run("Retrieve parsed group values", func(t *testing.T) { - t.Parallel() - - df := dynflags.New(dynflags.ContinueOnError) - - // Define a flag in the "testGroup" config - df.Group("testGroup").String("flag1", "defaultValue", "Test flag") - - // Parse actual CLI arguments - args := []string{"--testGroup.identifier1.flag1", "value1"} - err := df.Parse(args) - assert.NoError(t, err) + fs := flag.NewFlagSet("test.exe", flag.ContinueOnError) + fs.SetOutput(&buf) + fs.BoolP("help", "h", false, "Show help.") - // Lookup the parsed data - parsedGroups := df.Parsed() - group := parsedGroups.Lookup("testGroup") - assert.NotNil(t, group) + df := dynflags.New("test.exe", dynflags.ContinueOnError) + df.Title("\nsome dynamic flags:") + df.SetOutput(&buf) - identifier := group.Lookup("identifier1") - assert.NotNil(t, identifier) + fs.Usage = func() { + out := fs.Output() // capture writer ONCE - // The flag should have the value we passed - assert.Equal(t, "value1", identifier.Lookup("flag1")) - }) -} + fmt.Fprintf(out, "Usage: %s [FLAGS] [DYNAMIC FLAGS..]\n", strings.ToLower(fs.Name())) // nolint:errcheck -func TestDynFlagsUnknownArgs(t *testing.T) { - t.Parallel() + fmt.Fprintln(out, "\nGlobal Flags:") // nolint:errcheck + fs.SetOutput(out) + fs.PrintDefaults() - t.Run("Retrieve unparsed arguments", func(t *testing.T) { - t.Parallel() + df.PrintTitle(out) + df.PrintDescription(out, 80) + df.PrintDefaults() + df.PrintEpilog(out, 80) + } - df := dynflags.New(dynflags.ContinueOnError) + // HTTP group + http := df.Group("http") + http.String("name", "", "Name of the HTTP checker").Metavar("TESTVAR") + http.String("method", "GET", "HTTP method to use") + http.String("address", "", "HTTP target URL").Required() + http.Bool("allow-duplicate-headers", false, "Allow duplicate HTTP headers") + http.String("expected-status-codes", "200", "Expected HTTP status codes") + http.Bool("skip-tls-verify", false, "Skip TLS verification").Deprecated("Use --insecure-skip-tls-verify instead") + + // ICMP group + icmp := df.Group("icmp") + icmp.String("name", "", "Name of the ICMP checker") + icmp.String("address", "", "ICMP target address").Required() + + // TCP group + tcp := df.Group("tcp") + tcp.String("name", "", "Name of the TCP checker") + tcp.String("address", "", "TCP target address").Required() - // Passing an argument that won't parse args := []string{ - "--unparsable", "value1", + "--help", } + err := df.Parse(args) assert.NoError(t, err) + unknownArgs := df.UnknownArgs() - // Confirm that the argument ended up in unparsedArgs - unparsedArgs := df.UnknownArgs() - assert.Contains(t, unparsedArgs, "--unparsable") + err = fs.Parse(unknownArgs) + assert.NoError(t, err) + help := fs.Lookup("help") + if help != nil && help.Value.String() == "true" { + buf.Reset() // instead of creating a new one + fs.SetOutput(&buf) + df.SetOutput(&buf) + fs.Usage() + + expected := `Usage: test.exe [FLAGS] [DYNAMIC FLAGS..] + +Global Flags: + -h, --help Show help. + +some dynamic flags: +HTTP + Flag Usage + --http..name TESTVAR Name of the HTTP checker + --http..method METHOD HTTP method to use (default: "GET") + --http..address ADDRESS HTTP target URL [required] + --http..allow-duplicate-headers Allow duplicate HTTP headers (default: false) + --http..expected-status-codes EXPECTED-STATUS-CODES Expected HTTP status codes (default: "200") + --http..skip-tls-verify Skip TLS verification (default: false) [deprecated: Use --insecure-skip-tls-verify instead] + +ICMP + Flag Usage + --icmp..name NAME Name of the ICMP checker + --icmp..address ADDRESS ICMP target address [required] + +TCP + Flag Usage + --tcp..name NAME Name of the TCP checker + --tcp..address ADDRESS TCP target address [required] + +` + + assert.Equal(t, expected, buf.String()) + } }) } diff --git a/examples/advanced/main.go b/examples/advanced/main.go deleted file mode 100644 index 8103312..0000000 --- a/examples/advanced/main.go +++ /dev/null @@ -1,206 +0,0 @@ -package main - -import ( - "bytes" - "errors" - "fmt" - "io" - "os" - "time" - - "github.com/containeroo/dynflags" - flag "github.com/spf13/pflag" -) - -// Example version string -var version = "v1.2.3" - -// HelpRequested is a custom error to indicate the user requested help or version info. -type HelpRequested struct { - Message string -} - -func (e *HelpRequested) Error() string { - return e.Message -} - -// Is checks if the target is a HelpRequested error. -func (e *HelpRequested) Is(target error) bool { - _, ok := target.(*HelpRequested) - return ok -} - -// parseFlags orchestrates parsing both global flags (with pflag) -// and dynamic flags (with dynflags). It returns any error that -// indicates parsing failed, or HelpRequested for help/version output. -func parseFlags(args []string, version string, output io.Writer) (*flag.FlagSet, *dynflags.DynFlags, error) { - // 1. Setup the global pflag FlagSet - flagSet := setupGlobalFlags() - flagSet.SetOutput(output) // direct usage output here if needed - - // 2. Setup dynamic flags - dynFlags := setupDynamicFlags() - dynFlags.SetOutput(output) - dynFlags.SortFlags = true - - // 3. Provide custom usage that prints both global and dynamic flags - setupUsage(flagSet, dynFlags) - - // 4. Parse the dynamic flags first so we can separate known vs. unknown arguments - if err := dynFlags.Parse(args); err != nil { - return nil, nil, fmt.Errorf("error parsing dynamic flags: %w", err) - } - - // Unknown arguments might be pflag or truly unrecognized - unknownArgs := dynFlags.UnknownArgs() - - // 5. Parse pflag (the known global flags) - if err := flagSet.Parse(unknownArgs); err != nil { - return nil, nil, fmt.Errorf("error parsing global flags: %w", err) - } - - // 6. Handle special flags for help and version - if err := handleSpecialFlags(flagSet, version); err != nil { - return nil, nil, err - } - - return flagSet, dynFlags, nil -} - -// setupGlobalFlags defines global flags (pflag) for the application -func setupGlobalFlags() *flag.FlagSet { - flagSet := flag.NewFlagSet("advancedExample", flag.ContinueOnError) - flagSet.SortFlags = false - - // Some generic global flags: - flagSet.Bool("version", false, "Show version and exit.") - flagSet.BoolP("help", "h", false, "Show help.") - flagSet.Duration("default-interval", 2*time.Second, "Default interval between checks.") - return flagSet -} - -// setupDynamicFlags defines dynamic flags for HTTP, ICMP, and TCP as an example -func setupDynamicFlags() *dynflags.DynFlags { - dyn := dynflags.New(dynflags.ContinueOnError) - dyn.Epilog("For more information, see https://github.com/containeroo/dynflags") - dyn.SortGroups = true - dyn.SortFlags = true - - // HTTP group - http := dyn.Group("http") - http.String("name", "", "Name of the HTTP checker") - http.String("method", "GET", "HTTP method to use") - http.String("address", "", "HTTP target URL") - http.Duration("interval", 1*time.Second, "Time between HTTP requests (overrides --default-interval if set)") - http.StringSlices("header", nil, "HTTP headers to send") - http.Bool("allow-duplicate-headers", false, "Allow duplicate HTTP headers") - http.String("expected-status-codes", "200", "Expected HTTP status codes") - http.Bool("skip-tls-verify", false, "Skip TLS verification") - http.Duration("timeout", 2*time.Second, "Timeout for HTTP requests") - - // ICMP group - icmp := dyn.Group("icmp") - icmp.String("name", "", "Name of the ICMP checker") - icmp.String("address", "", "ICMP target address") - icmp.Duration("interval", 1*time.Second, "Time between ICMP requests (overrides --default-interval if set)") - icmp.Duration("read-timeout", 2*time.Second, "Timeout for ICMP read") - icmp.Duration("write-timeout", 2*time.Second, "Timeout for ICMP write") - - // TCP group - tcp := dyn.Group("tcp") - tcp.String("name", "", "Name of the TCP checker") - tcp.String("address", "", "TCP target address") - tcp.Duration("interval", 1*time.Second, "Time between TCP requests (overrides --default-interval if set)") - tcp.Duration("timeout", 2*time.Second, "Timeout for TCP connection") - - return dyn -} - -// setupUsage sets a custom usage function that prints both pflag and dynflags usage. -func setupUsage(flagSet *flag.FlagSet, dynFlags *dynflags.DynFlags) { - flagSet.Usage = func() { - out := flagSet.Output() // capture writer ONCE - - fmt.Fprintf(out, "Usage: %s [GLOBAL FLAGS...] [DYNAMIC FLAGS...]\n", flagSet.Name()) // nolint:errcheck - fmt.Fprintln(out, "\nGlobal Flags:") // nolint:errcheck - flagSet.PrintDefaults() - - fmt.Fprintln(out, "\nDynamic Flags:") // nolint:errcheck - dynFlags.PrintDefaults() - } -} - -// handleSpecialFlags checks if --help or --version was requested. -func handleSpecialFlags(flagSet *flag.FlagSet, versionStr string) error { - helpFlag := flagSet.Lookup("help") - if helpFlag != nil && helpFlag.Value.String() == "true" { - // Capture usage output into a buffer, then return a HelpRequested error. - buffer := &bytes.Buffer{} - flagSet.SetOutput(buffer) - flagSet.Usage() - return &HelpRequested{Message: buffer.String()} - } - - versionFlag := flagSet.Lookup("version") - if versionFlag != nil && versionFlag.Value.String() == "true" { - return &HelpRequested{Message: fmt.Sprintf("%s version %s\n", flagSet.Name(), versionStr)} - } - - return nil -} - -// main is our entry point, showing how to parse and then use the flags. -func main() { - // We'll pretend our arguments are from the CLI; replace os.Args[1:] in real usage. - args := []string{ - "--http.default.name", "HTTP Checker Default", - "--http.default.address", "default.com:80", - "--http.other.name", "HTTP Checker Other", - "--http.other.address", "other.com:443", - "--http.other.method", "POST", - "--icmp.custom.address", "8.8.4.4", - "--tcp.testing.address", "example.com:443", - "--default-interval=5s", // pflag - // "--unknownArg", "someValue", // see how it's handled by dynflags - } - - // Optionally redirect usage/errors to something other than os.Stderr if desired - output := os.Stdout - - // Parse everything - flagSet, dynFlags, err := parseFlags(args, version, output) - if err != nil { - // If the user requested help or version, print the message and exit - var hr *HelpRequested - if errors.As(err, &hr) { - fmt.Fprint(output, hr.Message) //nolint:errcheck - return - } - - fmt.Fprintf(output, "Failed to parse flags: %v\n", err) // nolint:errcheck - os.Exit(1) - } - - // If we got here, parse succeeded. Let's show what we got. - - // 1. Print global flags - fmt.Println("=== Global Flags ===") - defaultInterval, _ := flagSet.GetDuration("default-interval") - fmt.Printf("default-interval: %v\n", defaultInterval) - - // 2. Print dynamic flags - parsedGroups := dynFlags.Parsed() - - fmt.Println("\n=== Dynamic Flags ===") - for groupName, identifiers := range parsedGroups.Groups() { - fmt.Printf("Group: %s\n", groupName) - for identifier, pg := range identifiers { - fmt.Printf(" Identifier: %s\n", identifier) - for flagKey, value := range pg.Values { - fmt.Printf(" %s: %v\n", flagKey, value) - } - } - } - - fmt.Println("\nDone!") -} diff --git a/examples/simple/main.go b/examples/simple/main.go deleted file mode 100644 index 3060581..0000000 --- a/examples/simple/main.go +++ /dev/null @@ -1,71 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "github.com/containeroo/dynflags" -) - -func main() { - // 1. Create a new DynFlags instance. - df := dynflags.New(dynflags.ContinueOnError) - - // Optional metadata for your CLI help text. - df.Title("My Example CLI") - df.Description("This application demonstrates how to use dynflags in a simple program.") - df.Epilog("For more information, visit https://example.com.") - - // 2. Define a group named "app" and add a flag called "msg". - // The third parameter here is the default value, and the fourth is the help text. - appGroup := df.Group("app") - appGroup.String("msg", "Hello, World!", "Message to be displayed.") - - // For demonstration, we hard-code example arguments. - // In a real program, you would typically use: args := os.Args[1:] - args := []string{ - "--app.default.msg", "Hello default DynFlags!", - "--app.custom.msg", "Hello custom DynFlags!", - } - - // 3. Parse the command-line arguments. - if err := df.Parse(args); err != nil { - fmt.Fprintf(os.Stderr, "Error parsing flags: %v\n", err) - os.Exit(1) - } - - // If no arguments were provided, show usage and exit. - if len(args) < 2 { - df.Usage() - os.Exit(0) - } - - // 4. Access the parsed values. - parsedGroups := df.Parsed() - - // Look up the "app" group if it was populated. - appParsed := parsedGroups.Lookup("app") - if appParsed == nil { - fmt.Println("No 'app' flags found.") - return - } - - // Iterate over all groups returned by df.Parsed().Groups() (in this case, only "app") - // and then over each identifier (e.g., "default", "custom") within that group. - for groupName, identifiers := range parsedGroups.Groups() { - for identifierName, parsedGroup := range identifiers { - msg, err := parsedGroup.GetString("msg") // Custom method you may have added - if err != nil { - fmt.Printf("Error getting flag 'msg' in group %q (identifier %q): %v\n", groupName, identifierName, err) - continue - } - fmt.Printf("Group %q, identifier %q => msg: %q\n", groupName, identifierName, msg) - } - } - - // If any arguments were unrecognized or invalid, print them here. - unparsed := df.UnknownArgs() - if len(unparsed) > 0 { - fmt.Println("Unknown arguments:", unparsed) - } -} diff --git a/flag.go b/flag.go index cf126ca..e59f0bc 100644 --- a/flag.go +++ b/flag.go @@ -1,51 +1,35 @@ package dynflags -type FlagType string - -const ( - FlagTypeStringSlice FlagType = "..STRINGs" - FlagTypeString FlagType = "STRING" - FlagTypeInt FlagType = "INT" - FlagTypeIntSlice FlagType = "..INTs" - FlagTypeBool FlagType = "BOOL" - FlagTypeBoolSlice FlagType = "..BOOLs" - FlagTypeDuration FlagType = "DURATION" - FlagTypeDurationSlice FlagType = "..DURATIONs" - FlagTypeFloat FlagType = "FLOAT" - FlagTypeFloatSlice FlagType = "..FLOATs" - FlagTypeIP FlagType = "IP" - FlagTypeIPSlice FlagType = "..IPs" - FlagTypeURL FlagType = "URL" - FlagTypeURLSlice FlagType = "..URLs" -) - -// Flag represents a single configuration flag +// Flag represents a dynamic flag definition (type, value, metadata, etc.) type Flag struct { - Default any // Default value for the flag - Type FlagType // Type of the flag - Usage string // Description for usage - metaVar string // MetaVar for flag - value FlagValue // Encapsulated parsing and value-setting logic + name string + usage string + value Value + required bool + deprecated string + metavar string + defaultSet bool +} + +// Value is implemented by all concrete flag value holders. +type Value interface { + Set(string) error // parses and sets from string + Get() any // returns the parsed value + Default() string // stringified default + IsChanged() bool // whether the value was explicitly set } -func (f *Flag) MetaVar(metaVar string) { - f.metaVar = metaVar +// BoolFlag marks a flag as --flag (true) shorthand. +type BoolFlag interface { + IsBoolFlag() bool } -// FlagValue interface encapsulates parsing and value-setting logic -type FlagValue interface { - // Parse parses the given string value into the flag's value type - Parse(value string) (any, error) - // Set sets the flag's value to the given value - Set(value any) error - // GetBound returns the bound value of the flag. - GetBound() any +// SliceFlag marks slice flags for internal classification. +type SliceFlag interface { + isSlice() } -// Value returns the current value of the flag. -func (f *Flag) GetValue() any { - if f == nil || f.value == nil { - return nil - } - return f.value.GetBound() +// DelimiterSetter is implemented by slice values that support custom delimiters. +type DelimiterSetter interface { + SetDelimiter(string) } diff --git a/flag_test.go b/flag_test.go deleted file mode 100644 index ef7616f..0000000 --- a/flag_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package dynflags_test - -import ( - "testing" - - "github.com/containeroo/dynflags" - "github.com/stretchr/testify/assert" -) - -func TestFlagGetValue(t *testing.T) { - t.Parallel() - - t.Run("Nil Flag - GetValue", func(t *testing.T) { - t.Parallel() - var flag *dynflags.Flag - assert.Nil(t, flag.GetValue(), "Expected nil when flag is nil") - }) - - t.Run("String Flag - GetValue", func(t *testing.T) { - t.Parallel() - - group := &dynflags.ConfigGroup{ - Name: "testGroup", - Flags: make(map[string]*dynflags.Flag), - } - - flag := group.String("example", "default-value", "An example string flag") - assert.Equal(t, "default-value", flag.GetValue(), "Expected GetValue() to return the default value") - }) -} diff --git a/flags_config.go b/flags_config.go deleted file mode 100644 index c1de7e3..0000000 --- a/flags_config.go +++ /dev/null @@ -1,55 +0,0 @@ -package dynflags - -// ConfigGroup represents the static configuration for a group. -type ConfigGroup struct { - Name string // Name of the group. - usage string // Title for usage. If not set it takes the name of the group in Uppercase. - Flags map[string]*Flag // Flags within the group. - flagOrder []string // Order of flags. -} - -// Usage sets the usage for the group. -func (cg *ConfigGroup) Usage(usage string) { - cg.usage = usage -} - -// Lookup retrieves a flag in the group by its name. -func (gc *ConfigGroup) Lookup(flagName string) *Flag { - if gc == nil { - return nil - } - - return gc.Flags[flagName] -} - -// ConfigGroups represents all configuration groups with lookup and iteration support. -type ConfigGroups struct { - groups map[string]*ConfigGroup -} - -// Lookup retrieves a configuration group by its name. -func (cg *ConfigGroups) Lookup(groupName string) *ConfigGroup { - if cg == nil { - return nil - } - - return cg.groups[groupName] -} - -// Groups returns the underlying map for direct iteration. -func (cg *ConfigGroups) Groups() map[string]*ConfigGroup { - if cg == nil { - return nil - } - - return cg.groups -} - -// Config returns a ConfigGroups instance for the dynflags instance. -func (df *DynFlags) Config() *ConfigGroups { - if df == nil { - return nil - } - - return &ConfigGroups{groups: df.configGroups} -} diff --git a/flags_config_test.go b/flags_config_test.go deleted file mode 100644 index 24679dc..0000000 --- a/flags_config_test.go +++ /dev/null @@ -1,135 +0,0 @@ -package dynflags_test - -import ( - "testing" - - "github.com/containeroo/dynflags" - "github.com/stretchr/testify/assert" -) - -func TestConfigGroup(t *testing.T) { - t.Parallel() - - t.Run("Lookup existing flag", func(t *testing.T) { - t.Parallel() - - group := &dynflags.ConfigGroup{ - Name: "testGroup", - Flags: map[string]*dynflags.Flag{"flag1": {Usage: "Test Flag"}}, - } - flag := group.Lookup("flag1") - assert.NotNil(t, flag) - assert.Equal(t, "Test Flag", flag.Usage) - }) - - t.Run("Lookup non-existing flag", func(t *testing.T) { - t.Parallel() - - group := &dynflags.ConfigGroup{ - Name: "testGroup", - Flags: map[string]*dynflags.Flag{}, - } - flag := group.Lookup("flag1") - assert.Nil(t, flag) - }) -} - -func TestConfigGroups(t *testing.T) { - t.Parallel() - - t.Run("Lookup existing group", func(t *testing.T) { - t.Parallel() - - df := dynflags.New(dynflags.ContinueOnError) - df.Group("http") - - groups := df.Config() - group := groups.Lookup("http") - - assert.NotNil(t, group) - assert.Equal(t, "http", group.Name) - }) - - t.Run("Iterate over groups", func(t *testing.T) { - t.Parallel() - - df := dynflags.New(dynflags.ContinueOnError) - df.Group("http") - df.Group("tcp") - - groups := df.Config().Groups() - - assert.Contains(t, groups, "http") - assert.Contains(t, groups, "tcp") - assert.Equal(t, "http", groups["http"].Name) - assert.Equal(t, "tcp", groups["tcp"].Name) - }) -} - -func TestConfigGroup_Lookup_NilHandling(t *testing.T) { - t.Parallel() - - t.Run("Lookup on nil ConfigGroup returns nil", func(t *testing.T) { - t.Parallel() - - var groupConfig *dynflags.ConfigGroup - result := groupConfig.Lookup("flag1") - assert.Nil(t, result, "Expected Lookup on nil ConfigGroup to return nil") - }) - - t.Run("Lookup non-existing flag returns nil", func(t *testing.T) { - t.Parallel() - - groupConfig := &dynflags.ConfigGroup{ - Name: "testGroup", - Flags: map[string]*dynflags.Flag{}, - } - - result := groupConfig.Lookup("nonExistingFlag") - assert.Nil(t, result, "Expected Lookup for non-existing flag to return nil") - }) -} - -func TestConfigGroups_Lookup_NilHandling(t *testing.T) { - t.Parallel() - - t.Run("Lookup on nil ConfigGroups returns nil", func(t *testing.T) { - t.Parallel() - - var configGroups *dynflags.ConfigGroups - result := configGroups.Lookup("group1") - - assert.Nil(t, result, "Expected Lookup on nil ConfigGroups to return nil") - }) - - t.Run("Lookup non-existing group returns nil", func(t *testing.T) { - t.Parallel() - - configGroups := &dynflags.ConfigGroups{} - result := configGroups.Lookup("nonExistingGroup") - - assert.Nil(t, result, "Expected Lookup for non-existing group to return nil") - }) -} - -func TestConfigGroups_Groups_NilHandling(t *testing.T) { - t.Parallel() - - t.Run("Groups on nil ConfigGroups returns nil", func(t *testing.T) { - var configGroups *dynflags.ConfigGroups - result := configGroups.Groups() - - assert.Nil(t, result, "Expected Groups on nil ConfigGroups to return nil") - }) -} - -func TestDynFlags_Config_NilHandling(t *testing.T) { - t.Parallel() - - t.Run("Config on nil DynFlags returns nil", func(t *testing.T) { - var dynFlags *dynflags.DynFlags - result := dynFlags.Config() - - assert.Nil(t, result, "Expected Config on nil DynFlags to return nil") - }) -} diff --git a/flags_parsed.go b/flags_parsed.go deleted file mode 100644 index 6745271..0000000 --- a/flags_parsed.go +++ /dev/null @@ -1,70 +0,0 @@ -package dynflags - -// GroupsMap is a map of group name -> IdentifiersMap. -type GroupsMap map[string]IdentifiersMap - -// IdentifiersMap is a map of identifier -> ParsedGroup pointer. -type IdentifiersMap map[string]*ParsedGroup - -// ParsedGroup represents a runtime group with parsed values. -type ParsedGroup struct { - Parent *ConfigGroup // Reference to the parent static group. - Name string // Identifier for the child group (e.g., "IDENTIFIER1"). - Values map[string]any // Parsed values for the group's flags. -} - -// Lookup retrieves the value of a flag in the parsed group. -func (g *ParsedGroup) Lookup(flagName string) any { - if g == nil { - return nil - } - return g.Values[flagName] -} - -// ParsedGroups represents all parsed groups with lookup and iteration support. -type ParsedGroups struct { - groups GroupsMap // Nested map of group name -> IdentifiersMap. -} - -// Lookup retrieves a group by its name. -func (g *ParsedGroups) Lookup(groupName string) *ParsedIdentifiers { - if g == nil { - return nil - } - if identifiers, exists := g.groups[groupName]; exists { - return &ParsedIdentifiers{Name: groupName, identifiers: identifiers} - } - return nil -} - -// Groups returns the underlying GroupsMap for direct iteration. -func (g *ParsedGroups) Groups() GroupsMap { - return g.groups -} - -// ParsedIdentifiers provides lookup for identifiers within a group. -type ParsedIdentifiers struct { - Name string - identifiers IdentifiersMap -} - -// Lookup retrieves a specific identifier within a group. -func (i *ParsedIdentifiers) Lookup(identifier string) *ParsedGroup { - if i == nil { - return nil - } - return i.identifiers[identifier] -} - -// Parsed returns a ParsedGroups instance for the dynflags instance. -func (f *DynFlags) Parsed() *ParsedGroups { - parsed := make(GroupsMap) - for groupName, groups := range f.parsedGroups { - identifierMap := make(IdentifiersMap) - for _, group := range groups { - identifierMap[group.Name] = group - } - parsed[groupName] = identifierMap - } - return &ParsedGroups{groups: parsed} -} diff --git a/flags_parsed_test.go b/flags_parsed_test.go deleted file mode 100644 index 25f2199..0000000 --- a/flags_parsed_test.go +++ /dev/null @@ -1,167 +0,0 @@ -package dynflags_test - -import ( - "testing" - - "github.com/containeroo/dynflags" - "github.com/stretchr/testify/assert" -) - -func TestParsedGroup(t *testing.T) { - t.Parallel() - - t.Run("Lookup existing parsed flag", func(t *testing.T) { - t.Parallel() - - group := &dynflags.ParsedGroup{ - Name: "testGroup", - Values: map[string]any{"flag1": "value1"}, - } - - value := group.Lookup("flag1") - assert.Equal(t, "value1", value) - }) - - t.Run("Lookup non-existing parsed flag", func(t *testing.T) { - t.Parallel() - - group := &dynflags.ParsedGroup{ - Name: "testGroup", - Values: map[string]any{}, - } - - value := group.Lookup("flag1") - assert.Nil(t, value) - }) -} - -func TestParsedGroups(t *testing.T) { - t.Parallel() - - t.Run("Lookup existing parsed group", func(t *testing.T) { - t.Parallel() - - df := dynflags.New(dynflags.ContinueOnError) - args := []string{"--testgroup.identifier1.flag1", "value1"} - err := df.Parse(args) - assert.NoError(t, err) - - group := df.Group("testGroup") - assert.NotNil(t, group) - assert.Equal(t, "testGroup", group.Name) - }) - - t.Run("Lookup non-existing parsed group", func(t *testing.T) { - t.Parallel() - - parsedGroups := &dynflags.ParsedGroups{} - - group := parsedGroups.Lookup("nonExistentGroup") - assert.Nil(t, group) - }) -} - -func TestDynFlagsParsed(t *testing.T) { - t.Parallel() - - t.Run("Combine parsed groups", func(t *testing.T) { - t.Parallel() - - args := []string{ - "--group1.identifier1.flag1", "value1", - "--group1.identifier2.flag2", "value2", - } - - df := dynflags.New(dynflags.ContinueOnError) - g1 := df.Group("group1") - g1.String("flag1", "", "Description flag1") - g1.String("flag2", "", "Description flag2") - - err := df.Parse(args) - assert.NoError(t, err) - - parsedGroups := df.Parsed() - - group := parsedGroups.Lookup("group1") - assert.NotNil(t, group) - assert.Equal(t, "group1", group.Name) - assert.Equal(t, "value1", group.Lookup("identifier1").Lookup("flag1")) - assert.Equal(t, "value2", group.Lookup("identifier2").Lookup("flag2")) - }) - - t.Run("Handle no parsed groups", func(t *testing.T) { - t.Parallel() - - df := dynflags.New(dynflags.ContinueOnError) - parsedGroups := df.Parsed() - - group := parsedGroups.Lookup("nonExistentGroup") - assert.Nil(t, group) - }) -} - -func TestParsedGroups_Lookup_NilHandling(t *testing.T) { - t.Parallel() - - t.Run("Lookup on nil ParsedGroups returns nil", func(t *testing.T) { - t.Parallel() - - var parsedGroups *dynflags.ParsedGroups - result := parsedGroups.Lookup("http") - assert.Nil(t, result, "Expected Lookup on nil ParsedGroups to return nil") - }) - - t.Run("Lookup non-existing group returns nil", func(t *testing.T) { - t.Parallel() - - parsedGroups := &dynflags.ParsedGroups{} - - result := parsedGroups.Lookup("nonExistingGroup") - assert.Nil(t, result, "Expected Lookup for non-existing group to return nil") - }) -} - -func TestParsedIdentifiers_Lookup_NilHandling(t *testing.T) { - t.Parallel() - - t.Run("Lookup on nil ParsedIdentifiers returns nil", func(t *testing.T) { - t.Parallel() - - var parsedIdentifiers *dynflags.ParsedIdentifiers - result := parsedIdentifiers.Lookup("identifier1") - assert.Nil(t, result, "Expected Lookup on nil ParsedIdentifiers to return nil") - }) - - t.Run("Lookup non-existing identifier returns nil", func(t *testing.T) { - t.Parallel() - - parsedIdentifiers := &dynflags.ParsedIdentifiers{} - - result := parsedIdentifiers.Lookup("nonExistingIdentifier") - assert.Nil(t, result, "Expected Lookup for non-existing identifier to return nil") - }) -} - -func TestParsedGroup_Lookup_NilHandling(t *testing.T) { - t.Parallel() - - t.Run("Lookup on nil ParsedGroup returns nil", func(t *testing.T) { - t.Parallel() - - var parsedGroup *dynflags.ParsedGroup - result := parsedGroup.Lookup("flag1") - assert.Nil(t, result, "Expected Lookup on nil ParsedGroup to return nil") - }) - - t.Run("Lookup non-existing flag returns nil", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Name: "identifier1", - Values: map[string]any{}, - } - - result := parsedGroup.Lookup("nonExistingFlag") - assert.Nil(t, result, "Expected Lookup for non-existing flag to return nil") - }) -} diff --git a/float64.go b/float64.go deleted file mode 100644 index 2b3da36..0000000 --- a/float64.go +++ /dev/null @@ -1,56 +0,0 @@ -package dynflags - -import ( - "fmt" - "strconv" -) - -type Float64Value struct { - Bound *float64 -} - -func (f *Float64Value) GetBound() any { - if f.Bound == nil { - return nil - } - return *f.Bound -} - -func (i *Float64Value) Parse(value string) (any, error) { - return strconv.ParseFloat(value, 64) -} - -func (i *Float64Value) Set(value any) error { - if num, ok := value.(float64); ok { - *i.Bound = num - return nil - } - return fmt.Errorf("invalid value type: expected float64") -} - -// Float64 defines a float64 flag with the specified name, default value, and usage description. -// The flag is added to the group's flag list and returned as a *Flag instance. -func (g *ConfigGroup) Float64(name string, value float64, usage string) *Flag { - bound := &value - flag := &Flag{ - Type: FlagTypeInt, - Default: value, - Usage: usage, - value: &Float64Value{Bound: bound}, - } - g.Flags[name] = flag - g.flagOrder = append(g.flagOrder, name) - return flag -} - -// GetFloat64 returns the float64 value of a flag with the given name -func (pg *ParsedGroup) GetFloat64(flagName string) (float64, error) { - value, exists := pg.Values[flagName] - if !exists { - return 0, fmt.Errorf("flag '%s' not found in group '%s'", flagName, pg.Name) - } - if floatVal, ok := value.(float64); ok { - return floatVal, nil - } - return 0, fmt.Errorf("flag '%s' is not a float64", flagName) -} diff --git a/float64_slice.go b/float64_slice.go deleted file mode 100644 index c1346e3..0000000 --- a/float64_slice.go +++ /dev/null @@ -1,72 +0,0 @@ -package dynflags - -import ( - "fmt" - "strconv" - "strings" -) - -type Float64SlicesValue struct { - Bound *[]float64 -} - -func (f *Float64SlicesValue) GetBound() any { - if f.Bound == nil { - return nil - } - return *f.Bound -} - -func (f *Float64SlicesValue) Parse(value string) (any, error) { - parsed, err := strconv.ParseFloat(value, 64) - if err != nil { - return nil, fmt.Errorf("invalid float64 value: %s, error: %w", value, err) - } - return parsed, nil -} - -func (f *Float64SlicesValue) Set(value any) error { - if parsedFloat, ok := value.(float64); ok { - *f.Bound = append(*f.Bound, parsedFloat) - return nil - } - return fmt.Errorf("invalid value type: expected float64") -} - -// Float64Slices defines a float64 slice flag with the specified name, default value, and usage description. -// The flag is added to the group's flag list and returned as a *Flag instance. -func (g *ConfigGroup) Float64Slices(name string, value []float64, usage string) *Flag { - bound := &value - defaultValue := make([]string, len(value)) - for i, v := range value { - defaultValue[i] = strconv.FormatFloat(v, 'f', -1, 64) - } - - flag := &Flag{ - Type: FlagTypeFloatSlice, - Default: strings.Join(defaultValue, ","), - Usage: usage, - value: &Float64SlicesValue{Bound: bound}, - } - g.Flags[name] = flag - g.flagOrder = append(g.flagOrder, name) - return flag -} - -// GetFloat64Slices returns the []float64 value of a flag with the given name -func (pg *ParsedGroup) GetFloat64Slices(flagName string) ([]float64, error) { - value, exists := pg.Values[flagName] - if !exists { - return nil, fmt.Errorf("flag '%s' not found in group '%s'", flagName, pg.Name) - } - - if slice, ok := value.([]float64); ok { - return slice, nil - } - - if f, ok := value.(float64); ok { - return []float64{f}, nil - } - - return nil, fmt.Errorf("flag '%s' is not a []float64", flagName) -} diff --git a/float64_slice_test.go b/float64_slice_test.go deleted file mode 100644 index 73af06c..0000000 --- a/float64_slice_test.go +++ /dev/null @@ -1,140 +0,0 @@ -package dynflags_test - -import ( - "testing" - - "github.com/containeroo/dynflags" - "github.com/stretchr/testify/assert" -) - -func TestFloat64SlicesValue(t *testing.T) { - t.Parallel() - - t.Run("Parse valid float64 value", func(t *testing.T) { - t.Parallel() - - float64SlicesValue := dynflags.Float64SlicesValue{Bound: &[]float64{}} - parsed, err := float64SlicesValue.Parse("3.14159") - assert.NoError(t, err) - assert.Equal(t, 3.14159, parsed) - }) - - t.Run("Parse invalid float64 value", func(t *testing.T) { - t.Parallel() - - float64SlicesValue := dynflags.Float64SlicesValue{Bound: &[]float64{}} - parsed, err := float64SlicesValue.Parse("invalid") - assert.Error(t, err) - assert.Nil(t, parsed) - }) - - t.Run("Set valid float64 value", func(t *testing.T) { - t.Parallel() - - bound := []float64{1.23} - float64SlicesValue := dynflags.Float64SlicesValue{Bound: &bound} - - err := float64SlicesValue.Set(4.56) - assert.NoError(t, err) - assert.Equal(t, []float64{1.23, 4.56}, bound) - }) - - t.Run("Set invalid type", func(t *testing.T) { - t.Parallel() - - bound := []float64{} - float64SlicesValue := dynflags.Float64SlicesValue{Bound: &bound} - - err := float64SlicesValue.Set("invalid") - assert.Error(t, err) - assert.EqualError(t, err, "invalid value type: expected float64") - }) -} - -func TestGroupConfigFloat64Slices(t *testing.T) { - t.Parallel() - - t.Run("Define float64 slices flag", func(t *testing.T) { - t.Parallel() - - group := &dynflags.ConfigGroup{Flags: make(map[string]*dynflags.Flag)} - defaultValue := []float64{1.23, 4.56} - group.Float64Slices("float64SliceFlag", defaultValue, "A float64 slices flag") - - assert.Contains(t, group.Flags, "float64SliceFlag") - assert.Equal(t, "A float64 slices flag", group.Flags["float64SliceFlag"].Usage) - assert.Equal(t, "1.23,4.56", group.Flags["float64SliceFlag"].Default) - }) -} - -func TestGetFloat64Slices(t *testing.T) { - t.Parallel() - - t.Run("Retrieve []float64 value", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Name: "testGroup", - Values: map[string]any{"flag1": []float64{1.1, 2.2, 3.3}}, - } - - result, err := parsedGroup.GetFloat64Slices("flag1") - assert.NoError(t, err) - assert.Equal(t, []float64{1.1, 2.2, 3.3}, result) - }) - - t.Run("Retrieve single float64 value as []float64", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Name: "testGroup", - Values: map[string]any{"flag1": 42.42}, - } - - result, err := parsedGroup.GetFloat64Slices("flag1") - assert.NoError(t, err) - assert.Equal(t, []float64{42.42}, result) - }) - - t.Run("Flag not found", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Name: "testGroup", - Values: map[string]any{}, - } - - result, err := parsedGroup.GetFloat64Slices("nonExistentFlag") - assert.Error(t, err) - assert.Nil(t, result) - assert.EqualError(t, err, "flag 'nonExistentFlag' not found in group 'testGroup'") - }) - - t.Run("Flag value is invalid type", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Name: "testGroup", - Values: map[string]any{"flag1": "invalid"}, - } - - result, err := parsedGroup.GetFloat64Slices("flag1") - assert.Error(t, err) - assert.Nil(t, result) - assert.EqualError(t, err, "flag 'flag1' is not a []float64") - }) -} - -func TestFloat64SlicesGetBound(t *testing.T) { - t.Run("Float64SlicesValue - GetBound", func(t *testing.T) { - var slices *[]float64 - val := []float64{1.1, 2.2, 3.3} - slices = &val - - floatSlicesValue := dynflags.Float64SlicesValue{Bound: slices} - assert.Equal(t, val, floatSlicesValue.GetBound()) - - floatSlicesValue = dynflags.Float64SlicesValue{Bound: nil} - assert.Nil(t, floatSlicesValue.GetBound()) - }) -} diff --git a/float64_test.go b/float64_test.go deleted file mode 100644 index 647a53a..0000000 --- a/float64_test.go +++ /dev/null @@ -1,130 +0,0 @@ -package dynflags_test - -import ( - "testing" - - "github.com/containeroo/dynflags" - "github.com/stretchr/testify/assert" -) - -func TestFloat64Value_Parse(t *testing.T) { - t.Parallel() - - t.Run("Valid Float64", func(t *testing.T) { - t.Parallel() - - bound := new(float64) - fv := &dynflags.Float64Value{Bound: bound} - - parsedValue, err := fv.Parse("123.456") - assert.NoError(t, err) - assert.Equal(t, 123.456, parsedValue) - }) - - t.Run("Invalid Float64", func(t *testing.T) { - t.Parallel() - - bound := new(float64) - fv := &dynflags.Float64Value{Bound: bound} - - _, err := fv.Parse("invalid") - assert.Error(t, err) - }) -} - -func TestFloat64Value_Set(t *testing.T) { - t.Parallel() - - t.Run("Set Valid Float64", func(t *testing.T) { - t.Parallel() - - bound := new(float64) - fv := &dynflags.Float64Value{Bound: bound} - - err := fv.Set(123.456) - assert.NoError(t, err) - assert.Equal(t, 123.456, *bound) - }) - - t.Run("Set Invalid Float64", func(t *testing.T) { - t.Parallel() - - bound := new(float64) - fv := &dynflags.Float64Value{Bound: bound} - - err := fv.Set("invalid") - assert.Error(t, err) - }) -} - -func TestGroupConfig_Float64(t *testing.T) { - t.Parallel() - - t.Run("Define Float64 Flag", func(t *testing.T) { - t.Parallel() - - group := &dynflags.ConfigGroup{ - Flags: make(map[string]*dynflags.Flag), - } - value := group.Float64("float64-test", 123.456, "test float64 flag") - - assert.NotNil(t, value) - assert.Equal(t, 123.456, value.Default) - assert.Contains(t, group.Flags, "float64-test") - }) -} - -func TestParsedGroup_GetFloat64(t *testing.T) { - t.Parallel() - - t.Run("Get Existing Float64 Value", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Name: "test-group", - Values: map[string]any{"float64-test": 123.456}, - } - - value, err := parsedGroup.GetFloat64("float64-test") - assert.NoError(t, err) - assert.Equal(t, 123.456, value) - }) - - t.Run("Get Non-Existing Float64 Value", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Name: "test-group", - Values: map[string]any{}, - } - - _, err := parsedGroup.GetFloat64("non-existing") - assert.Error(t, err) - }) - - t.Run("Get Invalid Float64 Value", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Name: "test-group", - Values: map[string]any{"invalid-test": "not-a-float"}, - } - - _, err := parsedGroup.GetFloat64("invalid-test") - assert.Error(t, err) - }) -} - -func TestFloat64GetBound(t *testing.T) { - t.Run("Float64Value - GetBound", func(t *testing.T) { - var f *float64 - val := 3.14 - f = &val - - floatValue := dynflags.Float64Value{Bound: f} - assert.Equal(t, 3.14, floatValue.GetBound()) - - floatValue = dynflags.Float64Value{Bound: nil} - assert.Nil(t, floatValue.GetBound()) - }) -} diff --git a/int.go b/int.go deleted file mode 100644 index dcd7e5b..0000000 --- a/int.go +++ /dev/null @@ -1,56 +0,0 @@ -package dynflags - -import ( - "fmt" - "strconv" -) - -type IntValue struct { - Bound *int -} - -func (i *IntValue) GetBound() any { - if i.Bound == nil { - return nil - } - return *i.Bound -} - -func (i *IntValue) Parse(value string) (any, error) { - return strconv.Atoi(value) -} - -func (i *IntValue) Set(value any) error { - if num, ok := value.(int); ok { - *i.Bound = num - return nil - } - return fmt.Errorf("invalid value type: expected int") -} - -// Int defines an integer flag with the specified name, default value, and usage description. -// The flag is added to the group's flag list and returned as a *Flag instance. -func (g *ConfigGroup) Int(name string, value int, usage string) *Flag { - bound := &value - flag := &Flag{ - Type: FlagTypeInt, - Default: value, - Usage: usage, - value: &IntValue{Bound: bound}, - } - g.Flags[name] = flag - g.flagOrder = append(g.flagOrder, name) - return flag -} - -// GetInt returns the int value of a flag with the given name -func (pg *ParsedGroup) GetInt(flagName string) (int, error) { - value, exists := pg.Values[flagName] - if !exists { - return 0, fmt.Errorf("flag '%s' not found in group '%s'", flagName, pg.Name) - } - if intVal, ok := value.(int); ok { - return intVal, nil - } - return 0, fmt.Errorf("flag '%s' is not an int", flagName) -} diff --git a/int_slice.go b/int_slice.go deleted file mode 100644 index c4b0bec..0000000 --- a/int_slice.go +++ /dev/null @@ -1,71 +0,0 @@ -package dynflags - -import ( - "fmt" - "strconv" - "strings" -) - -type IntSlicesValue struct { - Bound *[]int -} - -func (i *IntSlicesValue) GetBound() any { - if i.Bound == nil { - return nil - } - return *i.Bound -} - -func (s *IntSlicesValue) Parse(value string) (any, error) { - parsedValue, err := strconv.Atoi(value) - if err != nil { - return nil, fmt.Errorf("invalid integer value: %s", value) - } - return parsedValue, nil -} - -func (s *IntSlicesValue) Set(value any) error { - if num, ok := value.(int); ok { - *s.Bound = append(*s.Bound, num) - return nil - } - return fmt.Errorf("invalid value type: expected int") -} - -// IntSlices defines an integer slice flag with the specified name, default value, and usage description. -// The flag is added to the group's flag list and returned as a *Flag instance. -func (g *ConfigGroup) IntSlices(name string, value []int, usage string) *Flag { - bound := &value - defaults := make([]string, len(value)) - for i, v := range value { - defaults[i] = strconv.Itoa(v) - } - flag := &Flag{ - Type: FlagTypeIntSlice, - Default: strings.Join(defaults, ","), - Usage: usage, - value: &IntSlicesValue{Bound: bound}, - } - g.Flags[name] = flag - g.flagOrder = append(g.flagOrder, name) - return flag -} - -// GetIntSlices returns the []int value of a flag with the given name -func (pg *ParsedGroup) GetIntSlices(flagName string) ([]int, error) { - value, exists := pg.Values[flagName] - if !exists { - return nil, fmt.Errorf("flag '%s' not found in group '%s'", flagName, pg.Name) - } - - if slice, ok := value.([]int); ok { - return slice, nil - } - - if i, ok := value.(int); ok { - return []int{i}, nil - } - - return nil, fmt.Errorf("flag '%s' is not a []int", flagName) -} diff --git a/int_slice_test.go b/int_slice_test.go deleted file mode 100644 index c23f22f..0000000 --- a/int_slice_test.go +++ /dev/null @@ -1,140 +0,0 @@ -package dynflags_test - -import ( - "testing" - - "github.com/containeroo/dynflags" - "github.com/stretchr/testify/assert" -) - -func TestIntSlicesValue(t *testing.T) { - t.Parallel() - - t.Run("Parse valid int slice value", func(t *testing.T) { - t.Parallel() - - intSlicesValue := dynflags.IntSlicesValue{Bound: &[]int{}} - parsed, err := intSlicesValue.Parse("123") - assert.NoError(t, err) - assert.Equal(t, 123, parsed) - }) - - t.Run("Parse invalid int slice value", func(t *testing.T) { - t.Parallel() - - intSlicesValue := dynflags.IntSlicesValue{Bound: &[]int{}} - parsed, err := intSlicesValue.Parse("invalid") - assert.Error(t, err) - assert.Nil(t, parsed) - }) - - t.Run("Set valid int slice value", func(t *testing.T) { - t.Parallel() - - bound := []int{1} - intSlicesValue := dynflags.IntSlicesValue{Bound: &bound} - - err := intSlicesValue.Set(2) - assert.NoError(t, err) - assert.Equal(t, []int{1, 2}, bound) - }) - - t.Run("Set invalid type", func(t *testing.T) { - t.Parallel() - - bound := []int{1} - intSlicesValue := dynflags.IntSlicesValue{Bound: &bound} - - err := intSlicesValue.Set("invalid") // Invalid type - assert.Error(t, err) - assert.EqualError(t, err, "invalid value type: expected int") - }) -} - -func TestGroupConfigIntSlices(t *testing.T) { - t.Parallel() - - t.Run("Define int slices flag", func(t *testing.T) { - t.Parallel() - - group := &dynflags.ConfigGroup{Flags: make(map[string]*dynflags.Flag)} - defaultValue := []int{1, 2} - group.IntSlices("intSliceFlag", defaultValue, "An int slices flag") - - assert.Contains(t, group.Flags, "intSliceFlag") - assert.Equal(t, "An int slices flag", group.Flags["intSliceFlag"].Usage) - assert.Equal(t, "1,2", group.Flags["intSliceFlag"].Default) - }) -} - -func TestGetIntSlices(t *testing.T) { - t.Parallel() - - t.Run("Retrieve []int value", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Name: "testGroup", - Values: map[string]any{"flag1": []int{1, 2, 3}}, - } - - result, err := parsedGroup.GetIntSlices("flag1") - assert.NoError(t, err) - assert.Equal(t, []int{1, 2, 3}, result) - }) - - t.Run("Retrieve single int value as []int", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Name: "testGroup", - Values: map[string]any{"flag1": 42}, - } - - result, err := parsedGroup.GetIntSlices("flag1") - assert.NoError(t, err) - assert.Equal(t, []int{42}, result) - }) - - t.Run("Flag not found", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Name: "testGroup", - Values: map[string]any{}, - } - - result, err := parsedGroup.GetIntSlices("nonExistentFlag") - assert.Error(t, err) - assert.Nil(t, result) - assert.EqualError(t, err, "flag 'nonExistentFlag' not found in group 'testGroup'") - }) - - t.Run("Flag value is invalid type", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Name: "testGroup", - Values: map[string]any{"flag1": "invalid"}, - } - - result, err := parsedGroup.GetIntSlices("flag1") - assert.Error(t, err) - assert.Nil(t, result) - assert.EqualError(t, err, "flag 'flag1' is not a []int") - }) -} - -func TestIntSlicesGetBound(t *testing.T) { - t.Run("IntSlicesValue - GetBound", func(t *testing.T) { - var slices *[]int - val := []int{1, 2, 3} - slices = &val - - intSlicesValue := dynflags.IntSlicesValue{Bound: slices} - assert.Equal(t, val, intSlicesValue.GetBound()) - - intSlicesValue = dynflags.IntSlicesValue{Bound: nil} - assert.Nil(t, intSlicesValue.GetBound()) - }) -} diff --git a/int_test.go b/int_test.go deleted file mode 100644 index 490d91f..0000000 --- a/int_test.go +++ /dev/null @@ -1,124 +0,0 @@ -package dynflags_test - -import ( - "testing" - - "github.com/containeroo/dynflags" - "github.com/stretchr/testify/assert" -) - -func TestIntValue_Parse(t *testing.T) { - t.Parallel() - - t.Run("ValidInt", func(t *testing.T) { - t.Parallel() - - var bound int - val := &dynflags.IntValue{Bound: &bound} - parsed, err := val.Parse("42") - assert.NoError(t, err) - assert.Equal(t, 42, parsed) - }) - - t.Run("InvalidInt", func(t *testing.T) { - t.Parallel() - - var bound int - val := &dynflags.IntValue{Bound: &bound} - _, err := val.Parse("invalid") - assert.Error(t, err) - }) -} - -func TestIntValue_Set(t *testing.T) { - t.Parallel() - - t.Run("ValidInt", func(t *testing.T) { - t.Parallel() - - var bound int - val := &dynflags.IntValue{Bound: &bound} - assert.NoError(t, val.Set(42)) - assert.Equal(t, 42, bound) - }) - - t.Run("InvalidType", func(t *testing.T) { - t.Parallel() - - var bound int - val := &dynflags.IntValue{Bound: &bound} - assert.Error(t, val.Set("not an int")) - }) -} - -func TestGroupConfig_Int(t *testing.T) { - t.Parallel() - - t.Run("DefineAndRetrieveInt", func(t *testing.T) { - t.Parallel() - - group := &dynflags.ConfigGroup{Flags: make(map[string]*dynflags.Flag)} - bound := group.Int("test-int", 100, "Test integer flag") - assert.NotNil(t, bound) - - flag, exists := group.Flags["test-int"] - assert.True(t, exists) - assert.NotNil(t, flag) - assert.Equal(t, dynflags.FlagTypeInt, flag.Type) - assert.Equal(t, 100, flag.Default) - assert.Equal(t, "Test integer flag", flag.Usage) - }) -} - -func TestParsedGroup_GetInt(t *testing.T) { - t.Parallel() - - t.Run("ValidIntRetrieval", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Values: map[string]any{ - "test-int": 42, - }, - } - val, err := parsedGroup.GetInt("test-int") - assert.NoError(t, err) - assert.Equal(t, 42, val) - }) - - t.Run("FlagNotFound", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Values: make(map[string]any), - } - _, err := parsedGroup.GetInt("non-existent") - assert.Error(t, err) - }) - - t.Run("InvalidType", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Values: map[string]any{ - "test-int": "not an int", - }, - } - _, err := parsedGroup.GetInt("test-int") - assert.Error(t, err) - }) -} - -func TestIntGetBound(t *testing.T) { - t.Run("IntValue - GetBound", func(t *testing.T) { - var i *int - val := 42 - i = &val - - intValue := dynflags.IntValue{Bound: i} - assert.Equal(t, 42, intValue.GetBound()) - - intValue = dynflags.IntValue{Bound: nil} - assert.Nil(t, intValue.GetBound()) - }) -} diff --git a/ip.go b/ip.go deleted file mode 100644 index 8c43dd2..0000000 --- a/ip.go +++ /dev/null @@ -1,68 +0,0 @@ -package dynflags - -import ( - "fmt" - "net" -) - -type IPValue struct { - Bound *net.IP -} - -func (i *IPValue) GetBound() any { - if i.Bound == nil { - return nil - } - return *i.Bound -} - -func (u *IPValue) Parse(value string) (any, error) { - result := net.ParseIP(value) - if result == nil { - return nil, fmt.Errorf("invalid IP address: %s", value) - } - return &result, nil -} - -func (u *IPValue) Set(value any) error { - if parsedIP, ok := value.(*net.IP); ok { - *u.Bound = *parsedIP - return nil - } - return fmt.Errorf("invalid value type: expected IP") -} - -// IP defines an IP flag with the specified name, default value, and usage description. -// The flag is added to the group's flag list and returned as a *Flag instance. -func (g *ConfigGroup) IP(name, value, usage string) *Flag { - bound := new(*net.IP) - if value != "" { - parsed := net.ParseIP(value) - if parsed == nil { - panic(fmt.Sprintf("%s has a invalid default IP flag '%s'", name, value)) - } - *bound = &parsed // Copy the parsed URL into bound - } - flag := &Flag{ - Type: FlagTypeIP, - Default: value, - Usage: usage, - value: &IPValue{Bound: *bound}, - } - g.Flags[name] = flag - g.flagOrder = append(g.flagOrder, name) - return flag -} - -// GetIP returns the net.IP value of a flag with the given name -func (pg *ParsedGroup) GetIP(flagName string) (net.IP, error) { - value, exists := pg.Values[flagName] - if !exists { - return nil, fmt.Errorf("flag '%s' not found in group '%s'", flagName, pg.Name) - } - if ip, ok := value.(net.IP); ok { - return ip, nil - } - - return nil, fmt.Errorf("flag '%s' is not a IP", flagName) -} diff --git a/ip_slice.go b/ip_slice.go deleted file mode 100644 index 264eef8..0000000 --- a/ip_slice.go +++ /dev/null @@ -1,72 +0,0 @@ -package dynflags - -import ( - "fmt" - "net" - "strings" -) - -type IPSlicesValue struct { - Bound *[]net.IP -} - -func (i *IPSlicesValue) GetBound() any { - if i.Bound == nil { - return nil - } - return *i.Bound -} - -func (s *IPSlicesValue) Parse(value string) (any, error) { - ip := net.ParseIP(value) - if ip == nil { - return nil, fmt.Errorf("invalid IP address: %s", value) - } - return ip, nil -} - -func (s *IPSlicesValue) Set(value any) error { - if ip, ok := value.(net.IP); ok { - *s.Bound = append(*s.Bound, ip) - return nil - } - return fmt.Errorf("invalid value type: expected net.IP") -} - -// IPSlices defines an IP slice flag with the specified name, default value, and usage description. -// The flag is added to the group's flag list and returned as a *Flag instance. -func (g *ConfigGroup) IPSlices(name string, value []net.IP, usage string) *Flag { - bound := &value - defaultValue := make([]string, len(value)) - for i, ip := range value { - defaultValue[i] = ip.String() - } - - flag := &Flag{ - Type: FlagTypeIPSlice, - Default: strings.Join(defaultValue, ","), - Usage: usage, - value: &IPSlicesValue{Bound: bound}, - } - g.Flags[name] = flag - g.flagOrder = append(g.flagOrder, name) - return flag -} - -// GetIPSlices returns the []net.IP value of a flag with the given name -func (pg *ParsedGroup) GetIPSlices(flagName string) ([]net.IP, error) { - value, exists := pg.Values[flagName] - if !exists { - return nil, fmt.Errorf("flag '%s' not found in group '%s'", flagName, pg.Name) - } - - if ipSlice, ok := value.([]net.IP); ok { - return ipSlice, nil - } - - if i, ok := value.(net.IP); ok { - return []net.IP{i}, nil - } - - return nil, fmt.Errorf("flag '%s' is not a []net.IP", flagName) -} diff --git a/ip_slice_test.go b/ip_slice_test.go deleted file mode 100644 index 5d913d6..0000000 --- a/ip_slice_test.go +++ /dev/null @@ -1,146 +0,0 @@ -package dynflags_test - -import ( - "net" - "testing" - - "github.com/containeroo/dynflags" - "github.com/stretchr/testify/assert" -) - -func TestIPSlicesValue(t *testing.T) { - t.Parallel() - - t.Run("Parse valid IP", func(t *testing.T) { - t.Parallel() - - ipSlicesValue := dynflags.IPSlicesValue{Bound: &[]net.IP{}} - parsed, err := ipSlicesValue.Parse("192.168.0.1") - assert.NoError(t, err) - assert.Equal(t, net.ParseIP("192.168.0.1"), parsed) - }) - - t.Run("Parse invalid IP", func(t *testing.T) { - t.Parallel() - - ipSlicesValue := dynflags.IPSlicesValue{Bound: &[]net.IP{}} - parsed, err := ipSlicesValue.Parse("invalid-ip") - assert.Error(t, err) - assert.Nil(t, parsed) - }) - - t.Run("Set valid IP", func(t *testing.T) { - t.Parallel() - - bound := []net.IP{net.ParseIP("192.168.0.1")} - ipSlicesValue := dynflags.IPSlicesValue{Bound: &bound} - - err := ipSlicesValue.Set(net.ParseIP("10.0.0.1")) - assert.NoError(t, err) - assert.Equal(t, []net.IP{net.ParseIP("192.168.0.1"), net.ParseIP("10.0.0.1")}, bound) - }) - - t.Run("Set invalid type", func(t *testing.T) { - t.Parallel() - - bound := []net.IP{} - ipSlicesValue := dynflags.IPSlicesValue{Bound: &bound} - - err := ipSlicesValue.Set("invalid-ip-type") - assert.Error(t, err) - assert.EqualError(t, err, "invalid value type: expected net.IP") - }) -} - -func TestGroupConfigIPSlices(t *testing.T) { - t.Parallel() - - t.Run("Define IP slices flag", func(t *testing.T) { - t.Parallel() - - group := &dynflags.ConfigGroup{Flags: make(map[string]*dynflags.Flag)} - defaultValue := []net.IP{net.ParseIP("192.168.0.1"), net.ParseIP("10.0.0.1")} - group.IPSlices("ipSliceFlag", defaultValue, "An IP slices flag") - - assert.Contains(t, group.Flags, "ipSliceFlag") - assert.Equal(t, "An IP slices flag", group.Flags["ipSliceFlag"].Usage) - assert.Equal(t, "192.168.0.1,10.0.0.1", group.Flags["ipSliceFlag"].Default) - }) -} - -func TestGetIPSlices(t *testing.T) { - t.Parallel() - - t.Run("Retrieve []net.IP value", func(t *testing.T) { - t.Parallel() - - ip1 := net.ParseIP("192.168.1.1") - ip2 := net.ParseIP("10.0.0.1") - - parsedGroup := &dynflags.ParsedGroup{ - Name: "testGroup", - Values: map[string]any{"flag1": []net.IP{ip1, ip2}}, - } - - result, err := parsedGroup.GetIPSlices("flag1") - assert.NoError(t, err) - assert.Equal(t, []net.IP{ip1, ip2}, result) - }) - - t.Run("Retrieve single net.IP value as []net.IP", func(t *testing.T) { - t.Parallel() - - ip := net.ParseIP("127.0.0.1") - - parsedGroup := &dynflags.ParsedGroup{ - Name: "testGroup", - Values: map[string]any{"flag1": ip}, - } - - result, err := parsedGroup.GetIPSlices("flag1") - assert.NoError(t, err) - assert.Equal(t, []net.IP{ip}, result) - }) - - t.Run("Flag not found", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Name: "testGroup", - Values: map[string]any{}, - } - - result, err := parsedGroup.GetIPSlices("nonExistentFlag") - assert.Error(t, err) - assert.Nil(t, result) - assert.EqualError(t, err, "flag 'nonExistentFlag' not found in group 'testGroup'") - }) - - t.Run("Flag value is invalid type", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Name: "testGroup", - Values: map[string]any{"flag1": "invalid"}, - } - - result, err := parsedGroup.GetIPSlices("flag1") - assert.Error(t, err) - assert.Nil(t, result) - assert.EqualError(t, err, "flag 'flag1' is not a []net.IP") - }) -} - -func TestIPSlicesGetBound(t *testing.T) { - t.Run("IPSlicesValue - GetBound", func(t *testing.T) { - var slices *[]net.IP - val := []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("10.0.0.1")} - slices = &val - - ipSlicesValue := dynflags.IPSlicesValue{Bound: slices} - assert.Equal(t, val, ipSlicesValue.GetBound()) - - ipSlicesValue = dynflags.IPSlicesValue{Bound: nil} - assert.Nil(t, ipSlicesValue.GetBound()) - }) -} diff --git a/ip_test.go b/ip_test.go deleted file mode 100644 index 14085d8..0000000 --- a/ip_test.go +++ /dev/null @@ -1,140 +0,0 @@ -package dynflags_test - -import ( - "net" - "testing" - - "github.com/containeroo/dynflags" - "github.com/stretchr/testify/assert" -) - -func TestIPValue(t *testing.T) { - t.Parallel() - - t.Run("Parse valid IP address", func(t *testing.T) { - t.Parallel() - - ipValue := dynflags.IPValue{} - parsed, err := ipValue.Parse("192.168.1.1") - assert.NoError(t, err) - assert.NotNil(t, parsed) - assert.Equal(t, "192.168.1.1", parsed.(*net.IP).String()) - }) - - t.Run("Parse invalid IP address", func(t *testing.T) { - t.Parallel() - - ipValue := dynflags.IPValue{} - parsed, err := ipValue.Parse("invalid-ip") - assert.Error(t, err) - assert.Nil(t, parsed) - }) - - t.Run("Set valid IP value", func(t *testing.T) { - t.Parallel() - - bound := net.ParseIP("0.0.0.0") - ipValue := dynflags.IPValue{Bound: &bound} - - parsed := net.ParseIP("192.168.1.1") - err := ipValue.Set(&parsed) - assert.NoError(t, err) - assert.Equal(t, "192.168.1.1", ipValue.Bound.String()) - }) - - t.Run("Set invalid value type", func(t *testing.T) { - t.Parallel() - - bound := net.ParseIP("0.0.0.0") - ipValue := dynflags.IPValue{Bound: &bound} - - err := ipValue.Set("invalid-type") - assert.Error(t, err) - assert.EqualError(t, err, "invalid value type: expected IP") - }) -} - -func TestGroupConfigIP(t *testing.T) { - t.Parallel() - - t.Run("Define IP flag with valid default", func(t *testing.T) { - t.Parallel() - - group := &dynflags.ConfigGroup{Flags: make(map[string]*dynflags.Flag)} - defaultIP := "192.168.1.1" - group.IP("ipFlag", defaultIP, "An example IP flag") - - assert.Contains(t, group.Flags, "ipFlag") - assert.Equal(t, "An example IP flag", group.Flags["ipFlag"].Usage) - assert.Equal(t, defaultIP, group.Flags["ipFlag"].Default) - }) - - t.Run("Define IP flag with invalid default", func(t *testing.T) { - t.Parallel() - - group := &dynflags.ConfigGroup{Flags: make(map[string]*dynflags.Flag)} - - assert.PanicsWithValue(t, - "ipFlag has a invalid default IP flag 'invalid-ip'", - func() { - group.IP("ipFlag", "invalid-ip", "Invalid IP flag") - }) - }) -} - -func TestParsedGroupGetIP(t *testing.T) { - t.Parallel() - - t.Run("Get existing IP flag", func(t *testing.T) { - t.Parallel() - - group := &dynflags.ParsedGroup{ - Values: map[string]any{ - "ipFlag": net.ParseIP("192.168.1.1"), - }, - } - ip, err := group.GetIP("ipFlag") - assert.NoError(t, err) - assert.Equal(t, "192.168.1.1", ip.String()) - }) - - t.Run("Get non-existent IP flag", func(t *testing.T) { - t.Parallel() - - group := &dynflags.ParsedGroup{ - Values: map[string]any{}, - } - ip, err := group.GetIP("ipFlag") - assert.Error(t, err) - assert.Nil(t, ip) - assert.EqualError(t, err, "flag 'ipFlag' not found in group ''") - }) - - t.Run("Get IP flag with invalid type", func(t *testing.T) { - t.Parallel() - - group := &dynflags.ParsedGroup{ - Values: map[string]any{ - "ipFlag": "not-an-ip", - }, - } - ip, err := group.GetIP("ipFlag") - assert.Error(t, err) - assert.Nil(t, ip) - assert.EqualError(t, err, "flag 'ipFlag' is not a IP") - }) -} - -func TestIPGetBound(t *testing.T) { - t.Run("IPValue - GetBound", func(t *testing.T) { - var ip *net.IP - val := net.ParseIP("127.0.0.1") - ip = &val - - ipValue := dynflags.IPValue{Bound: ip} - assert.Equal(t, val, ipValue.GetBound()) - - ipValue = dynflags.IPValue{Bound: nil} - assert.Nil(t, ipValue.GetBound()) - }) -} diff --git a/listen_addr.go b/listen_addr.go deleted file mode 100644 index 4fe80f5..0000000 --- a/listen_addr.go +++ /dev/null @@ -1,65 +0,0 @@ -package dynflags - -import ( - "fmt" - "net" -) - -type ListenAddrValue struct { - Bound *string -} - -func (l *ListenAddrValue) GetBound() any { - if l.Bound == nil { - return nil - } - return *l.Bound -} - -func (l *ListenAddrValue) Parse(value string) (any, error) { - _, err := net.ResolveTCPAddr("tcp", value) - if err != nil { - return nil, fmt.Errorf("invalid listen address: %w", err) - } - return &value, nil -} - -func (l *ListenAddrValue) Set(value any) error { - if str, ok := value.(*string); ok { - *l.Bound = *str - return nil - } - return fmt.Errorf("invalid value type: expected string pointer for listen address") -} - -// ListenAddr defines a flag that validates a TCP listen address (host:port or :port). -func (g *ConfigGroup) ListenAddr(name, defaultValue, usage string) *Flag { - bound := new(string) - if defaultValue != "" { - if _, err := net.ResolveTCPAddr("tcp", defaultValue); err != nil { - panic(fmt.Sprintf("%s has an invalid default listen address '%s': %v", name, defaultValue, err)) - } - *bound = defaultValue // Copy the parsed ListenAddr into bound - } - flag := &Flag{ - Type: FlagTypeString, - Default: defaultValue, - Usage: usage, - value: &ListenAddrValue{Bound: bound}, - } - g.Flags[name] = flag - g.flagOrder = append(g.flagOrder, name) - return flag -} - -// GetListenAddr returns the string value of a validated listen address flag. -func (pg *ParsedGroup) GetListenAddr(flagName string) (string, error) { - value, exists := pg.Values[flagName] - if !exists { - return "", fmt.Errorf("flag '%s' not found in group '%s'", flagName, pg.Name) - } - if str, ok := value.(string); ok { - return str, nil - } - return "", fmt.Errorf("flag '%s' is not a string listen address", flagName) -} diff --git a/listen_addr_slice.go b/listen_addr_slice.go deleted file mode 100644 index 747b7d6..0000000 --- a/listen_addr_slice.go +++ /dev/null @@ -1,75 +0,0 @@ -package dynflags - -import ( - "fmt" - "net" - "strings" -) - -type ListenAddrSlicesValue struct { - Bound *[]string -} - -func (s *ListenAddrSlicesValue) GetBound() any { - if s.Bound == nil { - return nil - } - return *s.Bound -} - -func (s *ListenAddrSlicesValue) Parse(value string) (any, error) { - _, err := net.ResolveTCPAddr("tcp", value) - if err != nil { - return nil, fmt.Errorf("invalid listen address: %w", err) - } - return value, nil -} - -func (s *ListenAddrSlicesValue) Set(value any) error { - if addr, ok := value.(string); ok { - *s.Bound = append(*s.Bound, addr) - return nil - } - return fmt.Errorf("invalid value type: expected string listen address") -} - -// ListenAddrSlices defines a slice-of-listen-address flag with the specified name, default values, and usage. -func (g *ConfigGroup) ListenAddrSlices(name string, value []string, usage string) *Flag { - bound := &value - defaultValue := strings.Join(value, ",") - - // Validate all default addresses - for _, v := range value { - if _, err := net.ResolveTCPAddr("tcp", v); err != nil { - panic(fmt.Sprintf("%s has an invalid default listen address '%s': %v", name, v, err)) - } - } - - flag := &Flag{ - Type: FlagTypeStringSlice, - Default: defaultValue, - Usage: usage, - value: &ListenAddrSlicesValue{Bound: bound}, - } - g.Flags[name] = flag - g.flagOrder = append(g.flagOrder, name) - return flag -} - -// GetListenAddrSlices returns the []string value of a listen address slice flag. -func (pg *ParsedGroup) GetListenAddrSlices(flagName string) ([]string, error) { - value, exists := pg.Values[flagName] - if !exists { - return nil, fmt.Errorf("flag '%s' not found in group '%s'", flagName, pg.Name) - } - - if list, ok := value.([]string); ok { - return list, nil - } - - if str, ok := value.(string); ok { - return []string{str}, nil - } - - return nil, fmt.Errorf("flag '%s' is not a []string listen address slice", flagName) -} diff --git a/listen_addr_slice_test.go b/listen_addr_slice_test.go deleted file mode 100644 index 64b81d8..0000000 --- a/listen_addr_slice_test.go +++ /dev/null @@ -1,151 +0,0 @@ -package dynflags_test - -import ( - "testing" - - "github.com/containeroo/dynflags" - "github.com/stretchr/testify/assert" -) - -func TestListenAddrSlicesValue(t *testing.T) { - t.Parallel() - - t.Run("Parse valid listen address", func(t *testing.T) { - t.Parallel() - - value := dynflags.ListenAddrSlicesValue{Bound: &[]string{}} - parsed, err := value.Parse(":8080") - assert.NoError(t, err) - assert.Equal(t, ":8080", parsed) - }) - - t.Run("Parse invalid listen address", func(t *testing.T) { - t.Parallel() - - value := dynflags.ListenAddrSlicesValue{Bound: &[]string{}} - parsed, err := value.Parse("bad-address") - assert.Error(t, err) - assert.Nil(t, parsed) - }) - - t.Run("Set valid listen address", func(t *testing.T) { - t.Parallel() - - bound := []string{":9090"} - value := dynflags.ListenAddrSlicesValue{Bound: &bound} - - err := value.Set(":8080") - assert.NoError(t, err) - assert.Equal(t, []string{":9090", ":8080"}, bound) - }) - - t.Run("Set invalid type", func(t *testing.T) { - t.Parallel() - - bound := []string{} - value := dynflags.ListenAddrSlicesValue{Bound: &bound} - - err := value.Set(12345) - assert.Error(t, err) - assert.EqualError(t, err, "invalid value type: expected string listen address") - }) -} - -func TestGroupConfigListenAddrSlices(t *testing.T) { - t.Parallel() - - t.Run("Define listen address slices flag", func(t *testing.T) { - t.Parallel() - - group := &dynflags.ConfigGroup{Flags: make(map[string]*dynflags.Flag)} - defaultValue := []string{":9090", "127.0.0.1:9091"} - group.ListenAddrSlices("listenSlice", defaultValue, "Multiple listen addresses") - - assert.Contains(t, group.Flags, "listenSlice") - assert.Equal(t, "Multiple listen addresses", group.Flags["listenSlice"].Usage) - assert.Equal(t, ":9090,127.0.0.1:9091", group.Flags["listenSlice"].Default) - }) - - t.Run("Define listen address slices with invalid default", func(t *testing.T) { - t.Parallel() - - group := &dynflags.ConfigGroup{Flags: make(map[string]*dynflags.Flag)} - - assert.PanicsWithValue(t, - "listenSlice has an invalid default listen address 'bad-address': address bad-address: missing port in address", - func() { - group.ListenAddrSlices("listenSlice", []string{":8080", "bad-address"}, "Invalid default") - }) - }) -} - -func TestGetListenAddrSlices(t *testing.T) { - t.Parallel() - - t.Run("Retrieve []string listen address slice", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Name: "testGroup", - Values: map[string]any{"flag1": []string{":8080", "127.0.0.1:9090"}}, - } - - result, err := parsedGroup.GetListenAddrSlices("flag1") - assert.NoError(t, err) - assert.Equal(t, []string{":8080", "127.0.0.1:9090"}, result) - }) - - t.Run("Retrieve single string as []string", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Name: "testGroup", - Values: map[string]any{"flag1": ":8080"}, - } - - result, err := parsedGroup.GetListenAddrSlices("flag1") - assert.NoError(t, err) - assert.Equal(t, []string{":8080"}, result) - }) - - t.Run("Flag not found", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Name: "testGroup", - Values: map[string]any{}, - } - - result, err := parsedGroup.GetListenAddrSlices("missingFlag") - assert.Error(t, err) - assert.Nil(t, result) - assert.EqualError(t, err, "flag 'missingFlag' not found in group 'testGroup'") - }) - - t.Run("Flag value is invalid type", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Name: "testGroup", - Values: map[string]any{"flag1": 123}, - } - - result, err := parsedGroup.GetListenAddrSlices("flag1") - assert.Error(t, err) - assert.Nil(t, result) - assert.EqualError(t, err, "flag 'flag1' is not a []string listen address slice") - }) -} - -func TestListenAddrSlicesGetBound(t *testing.T) { - t.Run("ListenAddrSlicesValue - GetBound", func(t *testing.T) { - val := []string{":8080", "127.0.0.1:9090"} - bound := &val - - value := dynflags.ListenAddrSlicesValue{Bound: bound} - assert.Equal(t, val, value.GetBound()) - - value = dynflags.ListenAddrSlicesValue{Bound: nil} - assert.Nil(t, value.GetBound()) - }) -} diff --git a/listen_addr_test.go b/listen_addr_test.go deleted file mode 100644 index cc1f162..0000000 --- a/listen_addr_test.go +++ /dev/null @@ -1,136 +0,0 @@ -package dynflags_test - -import ( - "testing" - - "github.com/containeroo/dynflags" - "github.com/stretchr/testify/assert" -) - -func TestListenAddrValue(t *testing.T) { - t.Parallel() - - t.Run("Parse valid listen address", func(t *testing.T) { - t.Parallel() - - value := dynflags.ListenAddrValue{} - parsed, err := value.Parse(":8080") - assert.NoError(t, err) - assert.NotNil(t, parsed) - assert.Equal(t, ":8080", *(parsed.(*string))) - }) - - t.Run("Parse invalid listen address", func(t *testing.T) { - t.Parallel() - - value := dynflags.ListenAddrValue{} - parsed, err := value.Parse("no-port") - assert.Error(t, err) - assert.Nil(t, parsed) - }) - - t.Run("Set valid listen address", func(t *testing.T) { - t.Parallel() - - bound := ":9090" - value := dynflags.ListenAddrValue{Bound: &bound} - - newVal := ":8080" - err := value.Set(&newVal) - assert.NoError(t, err) - assert.Equal(t, ":8080", *value.Bound) - }) - - t.Run("Set invalid value type", func(t *testing.T) { - t.Parallel() - - bound := ":9090" - value := dynflags.ListenAddrValue{Bound: &bound} - - err := value.Set(1234) - assert.Error(t, err) - assert.EqualError(t, err, "invalid value type: expected string pointer for listen address") - }) -} - -func TestGroupConfigListenAddr(t *testing.T) { - t.Parallel() - - t.Run("Define listen address flag with valid default", func(t *testing.T) { - t.Parallel() - - group := &dynflags.ConfigGroup{Flags: make(map[string]*dynflags.Flag)} - defaultAddr := ":9090" - group.ListenAddr("listen", defaultAddr, "Listen address") - - assert.Contains(t, group.Flags, "listen") - assert.Equal(t, "Listen address", group.Flags["listen"].Usage) - assert.Equal(t, defaultAddr, group.Flags["listen"].Default) - }) - - t.Run("Define listen address flag with invalid default", func(t *testing.T) { - t.Parallel() - - group := &dynflags.ConfigGroup{Flags: make(map[string]*dynflags.Flag)} - - assert.PanicsWithValue(t, - "listen has an invalid default listen address 'bad:address': lookup tcp/address: unknown port", - func() { - group.ListenAddr("listen", "bad:address", "Broken default") - }) - }) -} - -func TestParsedGroupGetListenAddr(t *testing.T) { - t.Parallel() - - t.Run("Get existing listen address flag", func(t *testing.T) { - t.Parallel() - - group := &dynflags.ParsedGroup{ - Values: map[string]any{ - "listen": ":9090", - }, - } - addr, err := group.GetListenAddr("listen") - assert.NoError(t, err) - assert.Equal(t, ":9090", addr) - }) - - t.Run("Get non-existent listen address flag", func(t *testing.T) { - t.Parallel() - - group := &dynflags.ParsedGroup{ - Values: map[string]any{}, - } - addr, err := group.GetListenAddr("listen") - assert.Error(t, err) - assert.Equal(t, "", addr) - assert.EqualError(t, err, "flag 'listen' not found in group ''") - }) - - t.Run("Get listen address flag with invalid type", func(t *testing.T) { - t.Parallel() - - group := &dynflags.ParsedGroup{ - Values: map[string]any{ - "listen": 9090, - }, - } - addr, err := group.GetListenAddr("listen") - assert.Error(t, err) - assert.Equal(t, "", addr) - assert.EqualError(t, err, "flag 'listen' is not a string listen address") - }) -} - -func TestListenAddrGetBound(t *testing.T) { - t.Run("ListenAddrValue - GetBound", func(t *testing.T) { - bound := ":9090" - value := dynflags.ListenAddrValue{Bound: &bound} - assert.Equal(t, ":9090", value.GetBound()) - - value = dynflags.ListenAddrValue{Bound: nil} - assert.Nil(t, value.GetBound()) - }) -} diff --git a/parser.go b/parser.go index 9959510..a005ced 100644 --- a/parser.go +++ b/parser.go @@ -5,15 +5,13 @@ import ( "strings" ) -// Parse parses the CLI arguments and populates parsed and unknown groups. +// Parse parses the provided CLI arguments. func (df *DynFlags) Parse(args []string) error { for i := 0; i < len(args); i++ { arg := args[i] - // Extract the key and value - fullKey, value, err := df.extractKeyValue(arg, args, &i) + key, val, err := df.extractKeyValue(arg, args, &i) if err != nil { - // Handle unparseable arguments if df.parseBehavior == ExitOnError { return err } @@ -21,10 +19,8 @@ func (df *DynFlags) Parse(args []string) error { continue } - // Validate and split the key - parentName, identifier, flagName, err := df.splitKey(fullKey) + groupName, ident, flagName, err := df.splitKey(key) if err != nil { - // Handle invalid keys if df.parseBehavior == ExitOnError { return err } @@ -32,8 +28,8 @@ func (df *DynFlags) Parse(args []string) error { continue } - // Handle the flag - if err := df.handleFlag(parentName, identifier, flagName, value); err != nil { + err = df.setFlag(groupName, ident, flagName, val) + if err != nil { if df.parseBehavior == ExitOnError { return err } @@ -43,88 +39,72 @@ func (df *DynFlags) Parse(args []string) error { return nil } -// extractKeyValue extracts the key and value from an argument. -func (df *DynFlags) extractKeyValue(arg string, args []string, index *int) (key, value string, err error) { +// extractKeyValue supports --key=value or --key value syntax. +func (df *DynFlags) extractKeyValue(arg string, args []string, i *int) (key, value string, err error) { if !strings.HasPrefix(arg, "--") { - // Invalid argument format - return "", "", fmt.Errorf("invalid argument format: %s", arg) + return "", "", fmt.Errorf("invalid flag: %s", arg) } arg = strings.TrimPrefix(arg, "--") - - // Handle "--key=value" format if strings.Contains(arg, "=") { parts := strings.SplitN(arg, "=", 2) return parts[0], parts[1], nil } - // Handle "--key value" format - if *index+1 < len(args) && !strings.HasPrefix(args[*index+1], "--") { - *index++ - return arg, args[*index], nil + // try next argument + if *i+1 < len(args) && !strings.HasPrefix(args[*i+1], "--") { + *i++ + return arg, args[*i], nil } - // Missing value for the key - return "", "", fmt.Errorf("missing value for flag: --%s", arg) + return "", "", fmt.Errorf("missing value for --%s", arg) } -// splitKey validates and splits a key into its components. -func (df *DynFlags) splitKey(fullKey string) (group, identifier, flag string, err error) { - parts := strings.Split(fullKey, ".") +// splitKey expects group.identifier.flag pattern. +func (df *DynFlags) splitKey(full string) (group, ident, flag string, err error) { + parts := strings.Split(full, ".") if len(parts) != 3 { - return "", "", "", fmt.Errorf("flag must follow the pattern: --..") + return "", "", "", fmt.Errorf("invalid flag key: --%s (must be --..)", full) } return parts[0], parts[1], parts[2], nil } -// handleFlag processes a known or unknown flag. -func (df *DynFlags) handleFlag(parentName, identifier, flagName, value string) error { - if parentGroup, exists := df.configGroups[parentName]; exists { - if flag := parentGroup.Lookup(flagName); flag != nil { - // Known flag - parsedGroup := df.createOrGetParsedGroup(parentGroup, identifier) - return df.setFlagValue(parsedGroup, flagName, flag, value) - } +// setFlag resolves and sets the value of a known flag. +func (df *DynFlags) setFlag(groupName, ident, flagName, val string) error { + group, ok := df.groups[groupName] + if !ok { + return fmt.Errorf("group %q not defined", groupName) } - // Unknown flag - return fmt.Errorf("unknown flag '%s' in group '%s'", flagName, parentName) -} - -// setFlagValue sets the value of a known flag in the parsed group. -func (df *DynFlags) setFlagValue(parsedGroup *ParsedGroup, flagName string, flag *Flag, value string) error { - parsedValue, err := flag.value.Parse(value) - if err != nil { - return fmt.Errorf("failed to parse value for flag '%s': %v", flagName, err) + flag := group.Lookup(flagName) + if flag == nil { + return fmt.Errorf("flag %q not found in group %q", flagName, groupName) } - if err := flag.value.Set(parsedValue); err != nil { - return fmt.Errorf("failed to set value for flag '%s': %v", flagName, err) + err := flag.value.Set(val) + if err != nil { + return fmt.Errorf("failed to parse --%s.%s.%s: %w", groupName, ident, flagName, err) } - // Store the successfully parsed value - parsedGroup.Values[flagName] = parsedValue + pg := df.getParsedGroup(group, ident) + pg.Values[flagName] = flag.value.Get() return nil } -// createOrGetParsedGroup retrieves or initializes a parsed group using the new GroupsMap/IdentifiersMap. -func (df *DynFlags) createOrGetParsedGroup(parentGroup *ConfigGroup, identifier string) *ParsedGroup { - // Ensure the parent group name has an IdentifiersMap - if _, exists := df.parsedGroups[parentGroup.Name]; !exists { - df.parsedGroups[parentGroup.Name] = make(IdentifiersMap) +// getParsedGroup initializes or retrieves a parsed group for the identifier. +func (df *DynFlags) getParsedGroup(group *ConfigGroup, ident string) *ParsedGroup { + if _, ok := df.parsed[group.Name]; !ok { + df.parsed[group.Name] = make(IdentifiersMap) } - - // Check if we already have a ParsedGroup for this identifier - if existingGroup, ok := df.parsedGroups[parentGroup.Name][identifier]; ok { - return existingGroup + if pg, ok := df.parsed[group.Name][ident]; ok { + return pg } - // Otherwise, create a new ParsedGroup - newGroup := &ParsedGroup{ - Parent: parentGroup, - Name: identifier, + pg := &ParsedGroup{ + Parent: group, + Name: ident, Values: make(map[string]any), } - df.parsedGroups[parentGroup.Name][identifier] = newGroup - return newGroup + df.parsed[group.Name][ident] = pg + return pg } diff --git a/parser_test.go b/parser_test.go deleted file mode 100644 index c043a53..0000000 --- a/parser_test.go +++ /dev/null @@ -1,158 +0,0 @@ -package dynflags_test - -import ( - "testing" - "time" - - "github.com/containeroo/dynflags" - "github.com/stretchr/testify/assert" -) - -func TestDynFlagsParse(t *testing.T) { - t.Parallel() - - t.Run("Parse valid arguments", func(t *testing.T) { - t.Parallel() - - df := dynflags.New(dynflags.ContinueOnError) - group := df.Group("http") - group.String("method", "GET", "HTTP method to use") - group.String("url", "", "Target URL") - - args := []string{ - "--http.identifier1.method", "POST", - "--http.identifier1.url=https://example.com", - } - err := df.Parse(args) - assert.NoError(t, err) - - parsedGroups := df.Parsed() - httpGroup := parsedGroups.Lookup("http") - assert.NotNil(t, httpGroup) - - identifier1 := httpGroup.Lookup("identifier1") - assert.NotNil(t, identifier1) - assert.Equal(t, "POST", identifier1.Lookup("method")) - assert.Equal(t, "https://example.com", identifier1.Lookup("url")) - }) - - t.Run("Exit on missing key", func(t *testing.T) { - df := dynflags.New(dynflags.ExitOnError) - group := df.Group("http") - group.String("method", "GET", "HTTP method to use") - - args := []string{ - "-http.identifier1", "https://example.com", - } - err := df.Parse(args) - assert.Error(t, err) - }) - - t.Run("Parse with missing value", func(t *testing.T) { - t.Parallel() - - df := dynflags.New(dynflags.ContinueOnError) - group := df.Group("http") - group.String("method", "GET", "HTTP method to use") - - args := []string{ - "--http.identifier1.method", - } - err := df.Parse(args) - assert.NoError(t, err) - - unparsedArgs := df.UnknownArgs() - assert.Contains(t, unparsedArgs, "--http.identifier1.method") - }) - - t.Run("Parse with wrong value type and continue", func(t *testing.T) { - t.Parallel() - - df := dynflags.New(dynflags.ContinueOnError) - group := df.Group("http") - group.Duration("timeout", 10*time.Second, "HTTP timeout") - - args := []string{ - "--http.identifier1.timeout", "1", - } - err := df.Parse(args) - assert.NoError(t, err) - - unparsedArgs := df.UnknownArgs() - assert.Contains(t, unparsedArgs, "--http.identifier1.timeout") - }) - - t.Run("Parse with invalid flag format", func(t *testing.T) { - t.Parallel() - - df := dynflags.New(dynflags.ContinueOnError) - - args := []string{ - "-invalidFlag", - } - err := df.Parse(args) - assert.NoError(t, err) - - unparsedArgs := df.UnknownArgs() - assert.Contains(t, unparsedArgs, "-invalidFlag") - }) - - t.Run("Parse with no identifier and exit", func(t *testing.T) { - t.Parallel() - - df := dynflags.New(dynflags.ExitOnError) - group1 := df.Group("http") - group1.Duration("timeout", 10*time.Second, "HTTP timeout") - - args := []string{ - "--http.duration", "10s", - } - err := df.Parse(args) - assert.Error(t, err) - }) - - t.Run("Parse with unknown group and exit on error", func(t *testing.T) { - t.Parallel() - - df := dynflags.New(dynflags.ExitOnError) - - args := []string{ - "--unknown.identifier1.flag1", "value1", - } - err := df.Parse(args) - assert.Error(t, err) - assert.EqualError(t, err, "unknown flag 'flag1' in group 'unknown'") - }) - - t.Run("Handle invalid key format", func(t *testing.T) { - t.Parallel() - - df := dynflags.New(dynflags.ContinueOnError) - - args := []string{ - "--invalidformat", - } - err := df.Parse(args) - assert.NoError(t, err) - - unparsedArgs := df.UnknownArgs() - assert.Contains(t, unparsedArgs, "--invalidformat") - }) - - t.Run("Handle missing flag value", func(t *testing.T) { - t.Parallel() - - df := dynflags.New(dynflags.ContinueOnError) - group := df.Group("http") - group.String("method", "GET", "HTTP method to use") - - args := []string{ - "--http.identifier1.method", - } - err := df.Parse(args) - assert.NoError(t, err) - - unparsedArgs := df.UnknownArgs() - assert.Contains(t, unparsedArgs, "--http.identifier1.method") - }) -} diff --git a/string.go b/string.go deleted file mode 100644 index f2d64f8..0000000 --- a/string.go +++ /dev/null @@ -1,54 +0,0 @@ -package dynflags - -import "fmt" - -type StringValue struct { - Bound *string -} - -func (s *StringValue) GetBound() any { - if s.Bound == nil { - return nil - } - return *s.Bound -} - -func (s *StringValue) Parse(value string) (any, error) { - return value, nil -} - -func (s *StringValue) Set(value any) error { - if str, ok := value.(string); ok { - *s.Bound = str - return nil - } - return fmt.Errorf("invalid value type: expected string") -} - -// String defines a string flag with the specified name, default value, and usage description. -// The flag is added to the group's flag list and returned as a *Flag instance. -func (g *ConfigGroup) String(name, value, usage string) *Flag { - bound := &value - flag := &Flag{ - Type: FlagTypeString, - Default: value, - Usage: usage, - value: &StringValue{Bound: bound}, - } - g.Flags[name] = flag - g.flagOrder = append(g.flagOrder, name) - return flag -} - -// GetString returns the string value of a flag with the given name -func (pg *ParsedGroup) GetString(flagName string) (string, error) { - value, exists := pg.Values[flagName] - if !exists { - return "", fmt.Errorf("flag '%s' not found in group '%s'", flagName, pg.Name) - } - if str, ok := value.(string); ok { - return str, nil - } - - return "", fmt.Errorf("flag '%s' is not a string", flagName) -} diff --git a/string_slice.go b/string_slice.go deleted file mode 100644 index ba1bcdb..0000000 --- a/string_slice.go +++ /dev/null @@ -1,62 +0,0 @@ -package dynflags - -import ( - "fmt" - "strings" -) - -type StringSlicesValue struct { - Bound *[]string -} - -func (s *StringSlicesValue) GetBound() any { - if s.Bound == nil { - return nil - } - return *s.Bound -} - -func (s *StringSlicesValue) Parse(value string) (any, error) { - return value, nil -} - -func (s *StringSlicesValue) Set(value any) error { - if str, ok := value.(string); ok { - *s.Bound = append(*s.Bound, str) - return nil - } - return fmt.Errorf("invalid value type: expected string") -} - -// StringSlices defines a string slice flag with the specified name, default value, and usage description. -// The flag is added to the group's flag list and returned as a *Flag instance. -func (g *ConfigGroup) StringSlices(name string, value []string, usage string) *Flag { - bound := &value - flag := &Flag{ - Type: FlagTypeStringSlice, - Default: strings.Join(value, ","), - Usage: usage, - value: &StringSlicesValue{Bound: bound}, - } - g.Flags[name] = flag - g.flagOrder = append(g.flagOrder, name) - return flag -} - -// GetStringSlices returns the []string value of a flag with the given name -func (pg *ParsedGroup) GetStringSlices(flagName string) ([]string, error) { - value, exists := pg.Values[flagName] - if !exists { - return nil, fmt.Errorf("flag '%s' not found in group '%s'", flagName, pg.Name) - } - - if strSlice, ok := value.([]string); ok { - return strSlice, nil - } - - if str, ok := value.(string); ok { - return []string{str}, nil - } - - return nil, fmt.Errorf("flag '%s' is not a []string", flagName) -} diff --git a/string_slice_test.go b/string_slice_test.go deleted file mode 100644 index edb8db5..0000000 --- a/string_slice_test.go +++ /dev/null @@ -1,169 +0,0 @@ -package dynflags_test - -import ( - "testing" - - "github.com/containeroo/dynflags" - "github.com/stretchr/testify/assert" -) - -func TestStringSlicesValue(t *testing.T) { - t.Parallel() - - t.Run("Parse valid string slice value", func(t *testing.T) { - t.Parallel() - - stringSlicesValue := dynflags.StringSlicesValue{Bound: &[]string{}} - parsed, err := stringSlicesValue.Parse("example") - assert.NoError(t, err) - assert.Equal(t, "example", parsed) - }) - - t.Run("Set valid string slice value", func(t *testing.T) { - t.Parallel() - - bound := []string{"initial"} - stringSlicesValue := dynflags.StringSlicesValue{Bound: &bound} - - err := stringSlicesValue.Set("updated") - assert.NoError(t, err) - assert.Equal(t, []string{"initial", "updated"}, bound) - }) - - t.Run("Set invalid type", func(t *testing.T) { - t.Parallel() - - bound := []string{"initial"} - stringSlicesValue := dynflags.StringSlicesValue{Bound: &bound} - - err := stringSlicesValue.Set(123) // Invalid type - assert.Error(t, err) - assert.EqualError(t, err, "invalid value type: expected string") - }) - - t.Run("Multiple Occurrences Append Correctly", func(t *testing.T) { - t.Parallel() - - var bound []string - value := &dynflags.StringSlicesValue{Bound: &bound} - - assert.NoError(t, value.Set("Content-Type=application/json")) - assert.NoError(t, value.Set("MyHeader=header1")) - assert.NoError(t, value.Set("Header1=value1,Header2=value2")) - - assert.Equal(t, []string{ - "Content-Type=application/json", - "MyHeader=header1", - "Header1=value1,Header2=value2", - }, bound) - }) - - t.Run("Single Value Append", func(t *testing.T) { - t.Parallel() - - var bound []string - value := &dynflags.StringSlicesValue{Bound: &bound} - - assert.NoError(t, value.Set("Content-Type=application/json")) - assert.Equal(t, []string{"Content-Type=application/json"}, bound) - }) - - t.Run("Invalid Value Type", func(t *testing.T) { - t.Parallel() - - var bound []string - value := &dynflags.StringSlicesValue{Bound: &bound} - - err := value.Set(123) // Invalid type - assert.Error(t, err) - assert.Contains(t, err.Error(), "invalid value type") - }) -} - -func TestGroupConfigStringSlices(t *testing.T) { - t.Parallel() - - t.Run("Define string slices flag", func(t *testing.T) { - t.Parallel() - - group := &dynflags.ConfigGroup{Flags: make(map[string]*dynflags.Flag)} - defaultValue := []string{"default1", "default2"} - group.StringSlices("stringSliceFlag", defaultValue, "A string slices flag") - - assert.Contains(t, group.Flags, "stringSliceFlag") - assert.Equal(t, "A string slices flag", group.Flags["stringSliceFlag"].Usage) - assert.Equal(t, "default1,default2", group.Flags["stringSliceFlag"].Default) - }) -} - -func TestGetStringSlices(t *testing.T) { - t.Parallel() - - t.Run("Retrieve []string value", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Name: "testGroup", - Values: map[string]any{"flag1": []string{"value1", "value2"}}, - } - - result, err := parsedGroup.GetStringSlices("flag1") - assert.NoError(t, err) - assert.Equal(t, []string{"value1", "value2"}, result) - }) - - t.Run("Retrieve single string value as []string", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Name: "testGroup", - Values: map[string]any{"flag1": "singleValue"}, - } - - result, err := parsedGroup.GetStringSlices("flag1") - assert.NoError(t, err) - assert.Equal(t, []string{"singleValue"}, result) - }) - - t.Run("Flag not found", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Name: "testGroup", - Values: map[string]any{}, - } - - result, err := parsedGroup.GetStringSlices("nonExistentFlag") - assert.Error(t, err) - assert.Nil(t, result) - assert.EqualError(t, err, "flag 'nonExistentFlag' not found in group 'testGroup'") - }) - - t.Run("Flag value is invalid type", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Name: "testGroup", - Values: map[string]any{"flag1": 123}, // Invalid type (int) - } - - result, err := parsedGroup.GetStringSlices("flag1") - assert.Error(t, err) - assert.Nil(t, result) - assert.EqualError(t, err, "flag 'flag1' is not a []string") - }) -} - -func TestGetStringSlicesGetBound(t *testing.T) { - t.Run("StringSlicesValue - GetBound", func(t *testing.T) { - var slices *[]string - val := []string{"a", "b", "c"} - slices = &val - - stringSlicesValue := dynflags.StringSlicesValue{Bound: slices} - assert.Equal(t, val, stringSlicesValue.GetBound()) - - stringSlicesValue = dynflags.StringSlicesValue{Bound: nil} - assert.Nil(t, stringSlicesValue.GetBound()) - }) -} diff --git a/string_test.go b/string_test.go deleted file mode 100644 index ad8fd12..0000000 --- a/string_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package dynflags_test - -import ( - "testing" - - "github.com/containeroo/dynflags" - "github.com/stretchr/testify/assert" -) - -func TestStringValue(t *testing.T) { - t.Parallel() - - t.Run("Parse valid string", func(t *testing.T) { - t.Parallel() - - stringValue := dynflags.StringValue{} - parsed, err := stringValue.Parse("example") - assert.NoError(t, err) - assert.Equal(t, "example", parsed) - }) - - t.Run("Set valid string", func(t *testing.T) { - t.Parallel() - - bound := "initial" - stringValue := dynflags.StringValue{Bound: &bound} - - err := stringValue.Set("updated") - assert.NoError(t, err) - assert.Equal(t, "updated", bound) - }) - - t.Run("Set invalid type", func(t *testing.T) { - t.Parallel() - - bound := "initial" - stringValue := dynflags.StringValue{Bound: &bound} - - err := stringValue.Set(123) // Invalid type - assert.Error(t, err) - assert.EqualError(t, err, "invalid value type: expected string") - }) -} - -func TestGroupConfigString(t *testing.T) { - t.Parallel() - - t.Run("Define string flag", func(t *testing.T) { - t.Parallel() - - group := &dynflags.ConfigGroup{Flags: make(map[string]*dynflags.Flag)} - defaultValue := "default" - group.String("stringFlag", defaultValue, "A string flag") - - assert.Contains(t, group.Flags, "stringFlag") - assert.Equal(t, "A string flag", group.Flags["stringFlag"].Usage) - assert.Equal(t, defaultValue, group.Flags["stringFlag"].Default) - }) -} - -func TestParsedGroupGetString(t *testing.T) { - t.Parallel() - - t.Run("Get existing string flag", func(t *testing.T) { - t.Parallel() - - group := &dynflags.ParsedGroup{ - Values: map[string]any{ - "stringFlag": "value", - }, - } - str, err := group.GetString("stringFlag") - assert.NoError(t, err) - assert.Equal(t, "value", str) - }) - - t.Run("Get non-existent string flag", func(t *testing.T) { - t.Parallel() - - group := &dynflags.ParsedGroup{ - Values: map[string]any{}, - } - str, err := group.GetString("stringFlag") - assert.Error(t, err) - assert.Equal(t, "", str) - assert.EqualError(t, err, "flag 'stringFlag' not found in group ''") - }) - - t.Run("Get string flag with invalid type", func(t *testing.T) { - t.Parallel() - - group := &dynflags.ParsedGroup{ - Values: map[string]any{ - "stringFlag": 123, // Invalid type - }, - } - str, err := group.GetString("stringFlag") - assert.Error(t, err) - assert.Equal(t, "", str) - assert.EqualError(t, err, "flag 'stringFlag' is not a string") - }) -} - -func TestStringGetBound(t *testing.T) { - t.Run("StringValue - GetBound", func(t *testing.T) { - var str *string - value := "test" - str = &value - - stringValue := dynflags.StringValue{Bound: str} - assert.Equal(t, "test", stringValue.GetBound()) - - stringValue = dynflags.StringValue{Bound: nil} - assert.Nil(t, stringValue.GetBound()) - }) -} diff --git a/types.go b/types.go new file mode 100644 index 0000000..07f79e8 --- /dev/null +++ b/types.go @@ -0,0 +1,44 @@ +package dynflags + +import "fmt" + +// ConfigGroup represents a static group of dynamic flags (e.g., "http", "db", etc.) +type ConfigGroup struct { + Name string + Flags map[string]*Flag + flagOrder []string + usage string +} + +// Lookup returns a flag by name (e.g., "port", "enabled"). +func (g *ConfigGroup) Lookup(name string) *Flag { + return g.Flags[name] +} + +// ParsedGroup holds the resolved values for a specific group + identifier combo. +type ParsedGroup struct { + Parent *ConfigGroup // link to the static definition + Name string // identifier (e.g. "main", "us-west", "1") + Values map[string]any // parsed values keyed by flag name +} + +// GroupsMap maps group names to a set of named identifiers. +type GroupsMap map[string]IdentifiersMap + +// IdentifiersMap maps dynamic identifiers to parsed groups. +type IdentifiersMap map[string]*ParsedGroup + +// Get returns the typed value of a flag. +func GetAs[T any](pg *ParsedGroup, name string) (T, error) { + val, ok := pg.Values[name] + if !ok { + var zero T + return zero, fmt.Errorf("flag %q not found in group %q", name, pg.Name) + } + v, ok := val.(T) + if !ok { + var zero T + return zero, fmt.Errorf("flag %q is not of expected type", name) + } + return v, nil +} diff --git a/url.go b/url.go deleted file mode 100644 index 4fcbb5d..0000000 --- a/url.go +++ /dev/null @@ -1,64 +0,0 @@ -package dynflags - -import ( - "fmt" - "net/url" -) - -type URLValue struct { - Bound *url.URL -} - -func (u *URLValue) GetBound() any { - if u.Bound == nil { - return nil - } - return *u.Bound -} - -func (u *URLValue) Parse(value string) (any, error) { - return url.Parse(value) -} - -func (u *URLValue) Set(value any) error { - if parsedURL, ok := value.(*url.URL); ok { - *u.Bound = *parsedURL - return nil - } - return fmt.Errorf("invalid value type: expected URL") -} - -// URL defines a URL flag with the specified name, default value, and usage description. -// The flag is added to the group's flag list and returned as a *Flag instance. -func (g *ConfigGroup) URL(name, value, usage string) *Flag { - bound := new(url.URL) - if value != "" { - parsed, err := url.Parse(value) - if err != nil { - panic(fmt.Sprintf("invalid default URL for flag '%s': %s", name, err)) - } - *bound = *parsed // Copy the parsed URL into bound - } - flag := &Flag{ - Type: FlagTypeURL, - Default: value, - Usage: usage, - value: &URLValue{Bound: bound}, - } - g.Flags[name] = flag - g.flagOrder = append(g.flagOrder, name) - return flag -} - -// GetURL returns the url.URL value of a flag with the given name -func (pg *ParsedGroup) GetURL(flagName string) (*url.URL, error) { - value, exists := pg.Values[flagName] - if !exists { - return nil, fmt.Errorf("flag '%s' not found in group '%s'", flagName, pg.Name) - } - if url, ok := value.(url.URL); ok { - return &url, nil - } - - return nil, fmt.Errorf("flag '%s' is not a URL", flagName) -} diff --git a/url_slice.go b/url_slice.go deleted file mode 100644 index e80332c..0000000 --- a/url_slice.go +++ /dev/null @@ -1,72 +0,0 @@ -package dynflags - -import ( - "fmt" - "net/url" - "strings" -) - -type URLSlicesValue struct { - Bound *[]*url.URL -} - -func (u *URLSlicesValue) GetBound() any { - if u.Bound == nil { - return nil - } - return *u.Bound -} - -func (u *URLSlicesValue) Parse(value string) (any, error) { - parsedURL, err := url.Parse(value) - if err != nil { - return nil, fmt.Errorf("invalid URL: %s, error: %w", value, err) - } - return parsedURL, nil -} - -func (u *URLSlicesValue) Set(value any) error { - if parsedURL, ok := value.(*url.URL); ok { - *u.Bound = append(*u.Bound, parsedURL) - return nil - } - return fmt.Errorf("invalid value type: expected *url.URL") -} - -// URLSlices defines a URL slice flag with the specified name, default value, and usage description. -// The flag is added to the group's flag list and returned as a *Flag instance. -func (g *ConfigGroup) URLSlices(name string, value []*url.URL, usage string) *Flag { - bound := &value - defaultValue := make([]string, len(value)) - for i, u := range value { - defaultValue[i] = u.String() - } - - flag := &Flag{ - Type: FlagTypeURLSlice, - Default: strings.Join(defaultValue, ","), - Usage: usage, - value: &URLSlicesValue{Bound: bound}, - } - g.Flags[name] = flag - g.flagOrder = append(g.flagOrder, name) - return flag -} - -// GetURLSlices returns the []*url.URL value of a flag with the given name -func (pg *ParsedGroup) GetURLSlices(flagName string) ([]*url.URL, error) { - value, exists := pg.Values[flagName] - if !exists { - return nil, fmt.Errorf("flag '%s' not found in group '%s'", flagName, pg.Name) - } - - if urlSlice, ok := value.([]*url.URL); ok { - return urlSlice, nil - } - - if u, ok := value.(*url.URL); ok { - return []*url.URL{u}, nil - } - - return nil, fmt.Errorf("flag '%s' is not a []*url.URL", flagName) -} diff --git a/url_slice_test.go b/url_slice_test.go deleted file mode 100644 index f53fa2b..0000000 --- a/url_slice_test.go +++ /dev/null @@ -1,154 +0,0 @@ -package dynflags_test - -import ( - "net/url" - "testing" - - "github.com/containeroo/dynflags" - "github.com/stretchr/testify/assert" -) - -func TestURLSlicesValue(t *testing.T) { - t.Parallel() - - t.Run("Parse valid URL", func(t *testing.T) { - t.Parallel() - - urlSlicesValue := dynflags.URLSlicesValue{Bound: &[]*url.URL{}} - parsed, err := urlSlicesValue.Parse("https://example.com") - assert.NoError(t, err) - assert.Equal(t, "https://example.com", parsed.(*url.URL).String()) - }) - - t.Run("Parse invalid URL", func(t *testing.T) { - t.Parallel() - - urlSlicesValue := dynflags.URLSlicesValue{Bound: &[]*url.URL{}} - parsed, err := urlSlicesValue.Parse("://invalid-url") - assert.Error(t, err) - assert.Nil(t, parsed) - }) - - t.Run("Set valid URL", func(t *testing.T) { - t.Parallel() - - bound := []*url.URL{{Scheme: "https", Host: "example.com"}} - urlSlicesValue := dynflags.URLSlicesValue{Bound: &bound} - - err := urlSlicesValue.Set(&url.URL{Scheme: "http", Host: "localhost"}) - assert.NoError(t, err) - assert.Equal(t, []*url.URL{ - {Scheme: "https", Host: "example.com"}, - {Scheme: "http", Host: "localhost"}, - }, bound) - }) - - t.Run("Set invalid type", func(t *testing.T) { - t.Parallel() - - bound := []*url.URL{} - urlSlicesValue := dynflags.URLSlicesValue{Bound: &bound} - - err := urlSlicesValue.Set("invalid-type") - assert.Error(t, err) - assert.EqualError(t, err, "invalid value type: expected *url.URL") - }) -} - -func TestGroupConfigURLSlices(t *testing.T) { - t.Parallel() - - t.Run("Define URL slices flag", func(t *testing.T) { - t.Parallel() - - group := &dynflags.ConfigGroup{Flags: make(map[string]*dynflags.Flag)} - defaultValue := []*url.URL{ - {Scheme: "https", Host: "example.com"}, - {Scheme: "http", Host: "localhost"}, - } - group.URLSlices("urlSliceFlag", defaultValue, "A URL slices flag") - - assert.Contains(t, group.Flags, "urlSliceFlag") - assert.Equal(t, "A URL slices flag", group.Flags["urlSliceFlag"].Usage) - assert.Equal(t, "https://example.com,http://localhost", group.Flags["urlSliceFlag"].Default) - }) -} - -func TestGetURLSlices(t *testing.T) { - t.Parallel() - - t.Run("Retrieve []*url.URL value", func(t *testing.T) { - t.Parallel() - - parsedURL1, _ := url.Parse("https://example.com") - parsedURL2, _ := url.Parse("https://example.org") - - parsedGroup := &dynflags.ParsedGroup{ - Name: "testGroup", - Values: map[string]any{"flag1": []*url.URL{parsedURL1, parsedURL2}}, - } - - result, err := parsedGroup.GetURLSlices("flag1") - assert.NoError(t, err) - assert.Equal(t, []*url.URL{parsedURL1, parsedURL2}, result) - }) - - t.Run("Retrieve single *url.URL value as []*url.URL", func(t *testing.T) { - t.Parallel() - - parsedURL, _ := url.Parse("https://example.com") - - parsedGroup := &dynflags.ParsedGroup{ - Name: "testGroup", - Values: map[string]any{"flag1": parsedURL}, - } - - result, err := parsedGroup.GetURLSlices("flag1") - assert.NoError(t, err) - assert.Equal(t, []*url.URL{parsedURL}, result) - }) - - t.Run("Flag not found", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Name: "testGroup", - Values: map[string]any{}, - } - - result, err := parsedGroup.GetURLSlices("nonExistentFlag") - assert.Error(t, err) - assert.Nil(t, result) - assert.EqualError(t, err, "flag 'nonExistentFlag' not found in group 'testGroup'") - }) - - t.Run("Flag value is invalid type", func(t *testing.T) { - t.Parallel() - - parsedGroup := &dynflags.ParsedGroup{ - Name: "testGroup", - Values: map[string]any{"flag1": 123}, // Invalid type (int) - } - - result, err := parsedGroup.GetURLSlices("flag1") - assert.Error(t, err) - assert.Nil(t, result) - assert.EqualError(t, err, "flag 'flag1' is not a []*url.URL") - }) -} - -func TestURLSlicesGetBound(t *testing.T) { - t.Run("URLSlicesValue - GetBound", func(t *testing.T) { - var slices *[]*url.URL - u1, _ := url.Parse("http://example.com") - u2, _ := url.Parse("http://example.org") - val := []*url.URL{u1, u2} - slices = &val - - urlSlicesValue := dynflags.URLSlicesValue{Bound: slices} - assert.Equal(t, val, urlSlicesValue.GetBound()) - - urlSlicesValue = dynflags.URLSlicesValue{Bound: nil} - assert.Nil(t, urlSlicesValue.GetBound()) - }) -} diff --git a/url_test.go b/url_test.go deleted file mode 100644 index 4080494..0000000 --- a/url_test.go +++ /dev/null @@ -1,146 +0,0 @@ -package dynflags_test - -import ( - "net/url" - "testing" - - "github.com/containeroo/dynflags" - "github.com/stretchr/testify/assert" -) - -func TestURLValue(t *testing.T) { - t.Parallel() - - t.Run("Parse valid URL", func(t *testing.T) { - t.Parallel() - - urlValue := dynflags.URLValue{} - parsed, err := urlValue.Parse("https://example.com") - assert.NoError(t, err) - assert.NotNil(t, parsed) - - parsedURL, ok := parsed.(*url.URL) - assert.True(t, ok) - assert.Equal(t, "https://example.com", parsedURL.String()) - }) - - t.Run("Parse invalid URL", func(t *testing.T) { - t.Parallel() - - urlValue := dynflags.URLValue{} - parsed, err := urlValue.Parse("https://invalid-url^") - assert.Error(t, err) - assert.Nil(t, parsed) - }) - - t.Run("Set valid URL", func(t *testing.T) { - t.Parallel() - - bound := &url.URL{} - urlValue := dynflags.URLValue{Bound: bound} - - parsedURL, _ := url.Parse("https://example.com") - err := urlValue.Set(parsedURL) - assert.NoError(t, err) - assert.Equal(t, "https://example.com", bound.String()) - }) - - t.Run("Set invalid type", func(t *testing.T) { - t.Parallel() - - bound := &url.URL{} - urlValue := dynflags.URLValue{Bound: bound} - - err := urlValue.Set("not-a-url") // Invalid type - assert.Error(t, err) - assert.EqualError(t, err, "invalid value type: expected URL") - }) -} - -func TestGroupConfigURL(t *testing.T) { - t.Parallel() - - t.Run("Define URL flag with default value", func(t *testing.T) { - t.Parallel() - - group := &dynflags.ConfigGroup{Flags: make(map[string]*dynflags.Flag)} - defaultValue := "https://default.com" - urlFlag := group.URL("urlFlag", defaultValue, "A URL flag") - - assert.Equal(t, "https://default.com", urlFlag.Default) - assert.Contains(t, group.Flags, "urlFlag") - assert.Equal(t, "A URL flag", group.Flags["urlFlag"].Usage) - assert.Equal(t, defaultValue, group.Flags["urlFlag"].Default) - }) - - t.Run("Define URL flag with invalid default", func(t *testing.T) { - t.Parallel() - - group := &dynflags.ConfigGroup{Flags: make(map[string]*dynflags.Flag)} - - assert.PanicsWithValue(t, - "invalid default URL for flag 'urlFlag': parse \"http://i nvalid-url\": invalid character \" \" in host name", - func() { - group.URL("urlFlag", "http://i nvalid-url", "Invalid URL flag") - }) - }) -} - -func TestParsedGroupGetURL(t *testing.T) { - t.Parallel() - - t.Run("Get existing URL flag", func(t *testing.T) { - t.Parallel() - - parsedURL, _ := url.Parse("https://example.com") - group := &dynflags.ParsedGroup{ - Values: map[string]any{ - "urlFlag": *parsedURL, - }, - } - retrievedURL, err := group.GetURL("urlFlag") - assert.NoError(t, err) - assert.NotNil(t, retrievedURL) - assert.Equal(t, "https://example.com", retrievedURL.String()) - }) - - t.Run("Get non-existent URL flag", func(t *testing.T) { - t.Parallel() - - group := &dynflags.ParsedGroup{ - Values: map[string]any{}, - } - retrievedURL, err := group.GetURL("urlFlag") - assert.Error(t, err) - assert.Nil(t, retrievedURL) - assert.EqualError(t, err, "flag 'urlFlag' not found in group ''") - }) - - t.Run("Get URL flag with invalid type", func(t *testing.T) { - t.Parallel() - - group := &dynflags.ParsedGroup{ - Values: map[string]any{ - "urlFlag": "not-a-url", // Invalid type - }, - } - retrievedURL, err := group.GetURL("urlFlag") - assert.Error(t, err) - assert.Nil(t, retrievedURL) - assert.EqualError(t, err, "flag 'urlFlag' is not a URL") - }) -} - -func TestURLGetBound(t *testing.T) { - t.Run("URLValue - GetBound", func(t *testing.T) { - var u *url.URL - val, _ := url.Parse("http://example.com") - u = val - - urlValue := dynflags.URLValue{Bound: u} - assert.Equal(t, *val, urlValue.GetBound()) - - urlValue = dynflags.URLValue{Bound: nil} - assert.Nil(t, urlValue.GetBound()) - }) -} diff --git a/usage.go b/usage.go new file mode 100644 index 0000000..90a1d40 --- /dev/null +++ b/usage.go @@ -0,0 +1,138 @@ +package dynflags + +import ( + "fmt" + "io" + "sort" + "strings" + "text/tabwriter" +) + +type FlagPrintMode int + +const ( + PrintShort FlagPrintMode = iota // Only short flags: -p + PrintLong // Only long flags: --port + PrintBoth // Both: -p|--port +) + +// PrintDefaults prints the help text for all dynamic groups and flags. +func (df *DynFlags) PrintDefaults() { + out := df.Output() + w := tabwriter.NewWriter(out, 0, 8, 2, ' ', 0) + + groupOrder := df.groupOrder + if df.sortGroups { + groupOrder = append([]string(nil), groupOrder...) // shallow copy + sort.Strings(groupOrder) + } + + for _, groupName := range groupOrder { + group := df.groups[groupName] + if group == nil { + continue + } + df.printGroup(w, groupName, group) + } + + w.Flush() // nolint:errcheck +} + +func (df *DynFlags) PrintTitle(w io.Writer) { + if df.title != "" { + fmt.Fprintln(w, df.title) // nolint:errcheck + } +} + +func (df *DynFlags) PrintDescription(w io.Writer, width int) { + if df.desc != "" { + fmt.Fprintln(w, wrapText(df.desc, width)) // nolint:errcheck + + fmt.Fprintln(w) // nolint:errcheck + } +} + +func (df *DynFlags) PrintUsage(w io.Writer) { + fmt.Fprint(w, "Usage: "+df.Name()) // nolint:errcheck +} + +func (df *DynFlags) PrintEpilog(w io.Writer, width int) { + if df.epilog != "" { + fmt.Fprintln(w) + fmt.Fprintln(w, wrapText(df.epilog, width)) // nolint:errcheck + + } +} + +func (df *DynFlags) printGroup(w io.Writer, groupName string, group *ConfigGroup) { + // Header + if group.usage != "" { + fmt.Fprintln(w, group.usage) // nolint:errcheck + } else { + fmt.Fprintf(w, "%s\n", strings.ToUpper(groupName)) // nolint:errcheck + } + + flagOrder := group.flagOrder + if df.sortFlags { + flagOrder = append([]string(nil), flagOrder...) + sort.Strings(flagOrder) + } + + if len(flagOrder) == 0 { + fmt.Fprintln(w, " (no flags)") // nolint:errcheck + return + } + + fmt.Fprintln(w, " Flag\tUsage") // nolint:errcheck + + for _, flagName := range flagOrder { + flag := group.Flags[flagName] + if flag == nil { + continue + } + df.printFlag(w, groupName, flag) + } + + fmt.Fprintln(w) // nolint:errcheck +} + +func (df *DynFlags) printFlag(w io.Writer, group string, flag *Flag) { + name := fmt.Sprintf(" --%s..%s", group, flag.name) + if meta := formatMetavar(flag); meta != "" { + name += " " + meta + } + + var usageParts []string + if flag.usage != "" { + usageParts = append(usageParts, flag.usage) + } + + if flag.defaultSet && !(flag.required && flag.value.Default() == "") { + usageParts = append(usageParts, fmt.Sprintf("(default: %s)", flag.value.Default())) + } + + if flag.required { + usageParts = append(usageParts, "[required]") + } + + if flag.deprecated != "" { + usageParts = append(usageParts, "[deprecated: "+flag.deprecated+"]") + } + + fmt.Fprintf(w, "%s\t%s\n", name, strings.Join(usageParts, " ")) // nolint:errcheck +} + +func formatMetavar(flag *Flag) string { + if bf, ok := flag.value.(BoolFlag); ok && bf.IsBoolFlag() { + return "" + } + + meta := flag.metavar + if meta == "" { + meta = strings.ToUpper(flag.name) + } + if _, ok := flag.value.(SliceFlag); ok { + meta += "..." + } + return meta +} diff --git a/value_base.go b/value_base.go new file mode 100644 index 0000000..b22a150 --- /dev/null +++ b/value_base.go @@ -0,0 +1,62 @@ +package dynflags + +// BaseValue is a generic value holder for scalar dynamic flags. +type BaseValue[T any] struct { + ptr *T // target storage + def T // default value + changed bool // whether Set was called + parse func(string) (T, error) // parsing logic + format func(T) string // string formatter + boolMarker bool // identifies bool flags +} + +// NewBaseValue constructs a new BaseValue with parser and formatter. +func NewBaseValue[T any]( + ptr *T, + def T, + parseFn func(string) (T, error), + formatFn func(T) string, +) *BaseValue[T] { + *ptr = def + return &BaseValue[T]{ + ptr: ptr, + def: def, + parse: parseFn, + format: formatFn, + changed: false, + boolMarker: isBoolPointer(ptr), + } +} + +func (v *BaseValue[T]) Set(s string) error { + val, err := v.parse(s) + if err != nil { + return err + } + *v.ptr = val + v.changed = true + return nil +} + +func (v *BaseValue[T]) Get() any { + return *v.ptr +} + +func (v *BaseValue[T]) Default() string { + return v.format(v.def) +} + +func (v *BaseValue[T]) IsChanged() bool { + return v.changed +} + +// IsBoolFlag marks flags as --flag shorthand = true, if type is bool +func (v *BaseValue[T]) IsBoolFlag() bool { + return v.boolMarker +} + +// isBoolPointer determines if the provided pointer is *bool +func isBoolPointer(ptr any) bool { + _, ok := ptr.(*bool) + return ok +} diff --git a/value_base_slice.go b/value_base_slice.go new file mode 100644 index 0000000..2c0a885 --- /dev/null +++ b/value_base_slice.go @@ -0,0 +1,76 @@ +package dynflags + +import ( + "fmt" + "strings" +) + +// BaseSliceValue is a generic value holder for slice flags (e.g., []int, []string). +type BaseSliceValue[T any] struct { + ptr *[]T // target storage + def []T // default slice + changed bool // true if Set was called + parse func(string) (T, error) // element parser + format func(T) string // element formatter + delimiter string // input split separator +} + +// NewBaseSliceValue constructs a new BaseSliceValue. +func NewBaseSliceValue[T any]( + ptr *[]T, + def []T, + parseFn func(string) (T, error), + formatFn func(T) string, + delimiter string, +) *BaseSliceValue[T] { + *ptr = append([]T(nil), def...) // defensive copy + return &BaseSliceValue[T]{ + ptr: ptr, + def: def, + parse: parseFn, + format: formatFn, + delimiter: delimiter, + } +} + +// Set parses and appends new elements from input string. +func (v *BaseSliceValue[T]) Set(s string) error { + if !v.changed { + *v.ptr = nil // clear default only on first use + } + items := strings.Split(s, v.delimiter) + for _, item := range items { + trimmed := strings.TrimSpace(item) + val, err := v.parse(trimmed) + if err != nil { + return fmt.Errorf("invalid value %q: %w", trimmed, err) + } + *v.ptr = append(*v.ptr, val) + } + v.changed = true + return nil +} + +func (v *BaseSliceValue[T]) Get() any { + return *v.ptr +} + +func (v *BaseSliceValue[T]) Default() string { + formatted := make([]string, len(v.def)) + for i, item := range v.def { + formatted[i] = v.format(item) + } + return strings.Join(formatted, v.delimiter) +} + +func (v *BaseSliceValue[T]) IsChanged() bool { + return v.changed +} + +// Implements SliceFlag marker interface. +func (v *BaseSliceValue[T]) isSlice() {} + +// SetDelimiter allows custom input delimiters like ":" or ";". +func (v *BaseSliceValue[T]) SetDelimiter(d string) { + v.delimiter = d +} diff --git a/value_bool.go b/value_bool.go new file mode 100644 index 0000000..333362c --- /dev/null +++ b/value_bool.go @@ -0,0 +1,25 @@ +package dynflags + +import "strconv" + +// Bool defines a dynamic bool flag with a given name, default, and usage. +func (g *ConfigGroup) Bool(name string, def bool, usage string) *FlagBuilder[bool] { + ptr := new(bool) + val := NewBaseValue(ptr, def, strconv.ParseBool, strconv.FormatBool) + + flag := &Flag{ + name: name, + usage: usage, + value: val, + defaultSet: true, // impossible to not set + } + + g.Flags[name] = flag + g.flagOrder = append(g.flagOrder, name) + + return &FlagBuilder[bool]{ + df: nil, // will be injected later via .Group() or .Env() call + bf: flag, + ptr: ptr, + } +} diff --git a/value_string.go b/value_string.go new file mode 100644 index 0000000..aece731 --- /dev/null +++ b/value_string.go @@ -0,0 +1,25 @@ +package dynflags + +import "strconv" + +// String defines a dynamic string flag with default and help text. +func (g *ConfigGroup) String(name string, def string, usage string) *FlagBuilder[string] { + ptr := new(string) + val := NewBaseValue(ptr, def, func(s string) (string, error) { return s, nil }, strconv.Quote) + + flag := &Flag{ + name: name, + usage: usage, + value: val, + defaultSet: def != "", + } + + g.Flags[name] = flag + g.flagOrder = append(g.flagOrder, name) + + return &FlagBuilder[string]{ + df: nil, + bf: flag, + ptr: ptr, + } +} diff --git a/wrap.go b/wrap.go new file mode 100644 index 0000000..428f819 --- /dev/null +++ b/wrap.go @@ -0,0 +1,23 @@ +package dynflags + +import "strings" + +func wrapText(s string, width int) string { + words := strings.Fields(s) + if len(words) == 0 { + return "" + } + + var lines []string + line := words[0] + for _, word := range words[1:] { + if len(line)+len(word)+1 > width { + lines = append(lines, line) + line = word + } else { + line += " " + word + } + } + lines = append(lines, line) + return strings.Join(lines, "\n") +}