diff --git a/internal/requestflag/innerflag.go b/internal/requestflag/innerflag.go index 528915f..866ce27 100644 --- a/internal/requestflag/innerflag.go +++ b/internal/requestflag/innerflag.go @@ -32,6 +32,8 @@ type InnerFlag[ // map[string]any before SetInnerField runs. The hint is ignored for typed outer // flags whose zero value already carries a dispatchable reflect.Kind. OuterIsArrayOfObjects bool + + hasBeenSet bool } // GetDataAliases returns the aliases recognized when parsing inner field keys from piped or flag YAML. @@ -89,6 +91,7 @@ func (f *InnerFlag[T]) Set(name string, rawVal string) error { if settableInnerField, ok := f.OuterFlag.(SettableInnerField); ok { settableInnerField.SetInnerField(f.InnerField, parsedValue) + f.hasBeenSet = true } else { return fmt.Errorf("Cannot set inner field on %v", f.OuterFlag) } @@ -106,7 +109,7 @@ func (f *InnerFlag[T]) String() string { } func (f *InnerFlag[T]) IsSet() bool { - return false + return f.hasBeenSet } func (f *InnerFlag[T]) Names() []string { diff --git a/internal/requestflag/innerflag_precedence_test.go b/internal/requestflag/innerflag_precedence_test.go new file mode 100644 index 0000000..ad63103 --- /dev/null +++ b/internal/requestflag/innerflag_precedence_test.go @@ -0,0 +1,36 @@ +package requestflag + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/urfave/cli/v3" +) + +func TestInnerFlagCLIValueBeatsPipedData(t *testing.T) { + t.Parallel() + + outer := &Flag[map[string]any]{ + Name: "address", + BodyPath: "address", + } + assert.NoError(t, outer.PreParse()) + + cityInner := &InnerFlag[string]{ + Name: "address.city", + InnerField: "city", + OuterFlag: outer, + } + assert.NoError(t, cityInner.Set("address.city", "cli-value")) + assert.True(t, cityInner.IsSet()) + + data := map[string]any{ + "address": map[string]any{"city": "piped-value"}, + } + cmd := &cli.Command{Flags: []cli.Flag{outer, cityInner}} + assert.NoError(t, ApplyStdinDataToFlags(cmd, data)) + + outerVal, ok := outer.Get().(map[string]any) + assert.True(t, ok) + assert.Equal(t, "cli-value", outerVal["city"]) +} diff --git a/internal/requestflag/requestflag.go b/internal/requestflag/requestflag.go index 60736b4..37f485f 100644 --- a/internal/requestflag/requestflag.go +++ b/internal/requestflag/requestflag.go @@ -137,11 +137,9 @@ type RequestContents struct { func applyStdinDataToFlags(cmd *cli.Command, data map[string]any, onSet func(cli.Flag)) error { for _, flag := range cmd.Flags { - if flag.IsSet() { - continue - } - - // Handle inner flags: look for their value nested under the outer flag's body path. + // Handle inner flags before the generic IsSet check. InnerFlag tracks whether + // it has ever been set, while array-of-object precedence is scoped to the + // trailing element of the outer value. if inner, ok := flag.(HasOuterFlag); ok { outer, outerOk := inner.GetOuterFlag().(InRequest) if !outerOk || outer.GetBodyPath() == "" { @@ -179,6 +177,10 @@ func applyStdinDataToFlags(cmd *cli.Command, data map[string]any, onSet func(cli continue } + if flag.IsSet() { + continue + } + inReq, ok := flag.(InRequest) if !ok { continue diff --git a/internal/requestflag/stdinprovenance_test.go b/internal/requestflag/stdinprovenance_test.go index bffd0ab..27e966f 100644 --- a/internal/requestflag/stdinprovenance_test.go +++ b/internal/requestflag/stdinprovenance_test.go @@ -158,3 +158,36 @@ func TestApplyStdinDataToFlagsWithProvenancePreservesExplicitEmptyCollections(t }) } } + +func TestApplyStdinDataToFlagsFillsUnsetFieldOnTrailingArrayElement(t *testing.T) { + t.Parallel() + + outer := &Flag[[]map[string]any]{Name: "entries", BodyPath: "entries"} + typeFlag := &InnerFlag[string]{ + Name: "entries.type", + InnerField: "type", + OuterFlag: outer, + OuterIsArrayOfObjects: true, + } + thresholdFlag := &InnerFlag[int64]{ + Name: "entries.compact-threshold", + InnerField: "compact_threshold", + OuterFlag: outer, + OuterIsArrayOfObjects: true, + } + require.NoError(t, outer.PreParse()) + require.NoError(t, typeFlag.Set(typeFlag.Name, "first")) + require.NoError(t, thresholdFlag.Set(thresholdFlag.Name, "10")) + require.NoError(t, typeFlag.Set(typeFlag.Name, "second")) + + command := &cli.Command{Flags: []cli.Flag{outer, typeFlag, thresholdFlag}} + err := ApplyStdinDataToFlags(command, map[string]any{ + "entries": map[string]any{"compact_threshold": 20}, + }) + + require.NoError(t, err) + require.Equal(t, []map[string]any{ + {"type": "first", "compact_threshold": int64(10)}, + {"type": "second", "compact_threshold": int64(20)}, + }, outer.Get()) +}