Unify typed binding preparation
This commit is contained in:
79
binding.go
79
binding.go
@@ -13,24 +13,24 @@ func ValueFlag[I, V any](meta FlagMeta, defaultValue *V, parse func(string) (V,
|
|||||||
if defaultValue != nil {
|
if defaultValue != nil {
|
||||||
fallback = *defaultValue
|
fallback = *defaultValue
|
||||||
}
|
}
|
||||||
binding := flagBinding[I]{meta: meta, defaultText: fmt.Sprint(fallback)}
|
binding := flagBinding[I]{identity: newBindingIdentity(), meta: meta, defaultText: fmt.Sprint(fallback)}
|
||||||
if parse == nil || set == nil {
|
if parse == nil || set == nil {
|
||||||
return Flag[I]{binding: binding}
|
return Flag[I]{binding: binding}
|
||||||
}
|
}
|
||||||
binding.valid = true
|
binding.valid = true
|
||||||
binding.clear = func(input *I) { set(input, Supplied[V]{}) }
|
binding.clear = func(input *I) { set(input, Supplied[V]{}) }
|
||||||
binding.prepare = func(values []string) (func(*I), error) {
|
binding.prepare = func(values []string) (preparedValue[I], error) {
|
||||||
value := fallback
|
value := fallback
|
||||||
supplied := len(values) > 0
|
supplied := len(values) > 0
|
||||||
for _, raw := range values {
|
for _, raw := range values {
|
||||||
parsed, err := parse(raw)
|
parsed, err := parse(raw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return preparedValue[I]{}, err
|
||||||
}
|
}
|
||||||
value = parsed
|
value = parsed
|
||||||
}
|
}
|
||||||
result := Supplied[V]{Value: value, Set: supplied}
|
result := Supplied[V]{Value: value, Set: supplied}
|
||||||
return func(input *I) { set(input, result) }, nil
|
return preparedValue[I]{identity: binding.identity, supplied: supplied, apply: func(input *I) { set(input, result) }}, nil
|
||||||
}
|
}
|
||||||
return Flag[I]{binding: binding}
|
return Flag[I]{binding: binding}
|
||||||
}
|
}
|
||||||
@@ -38,24 +38,24 @@ func ValueFlag[I, V any](meta FlagMeta, defaultValue *V, parse func(string) (V,
|
|||||||
// BoolFlag declares a boolean flag. A bare flag is supplied by the parser as
|
// BoolFlag declares a boolean flag. A bare flag is supplied by the parser as
|
||||||
// "true"; explicit values are converted with strconv.ParseBool.
|
// "true"; explicit values are converted with strconv.ParseBool.
|
||||||
func BoolFlag[I any](meta FlagMeta, defaultValue bool, set func(*I, Supplied[bool])) Flag[I] {
|
func BoolFlag[I any](meta FlagMeta, defaultValue bool, set func(*I, Supplied[bool])) Flag[I] {
|
||||||
binding := flagBinding[I]{meta: meta, defaultText: fmt.Sprint(defaultValue), boolean: true}
|
binding := flagBinding[I]{identity: newBindingIdentity(), meta: meta, defaultText: fmt.Sprint(defaultValue), boolean: true}
|
||||||
if set == nil {
|
if set == nil {
|
||||||
return Flag[I]{binding: binding}
|
return Flag[I]{binding: binding}
|
||||||
}
|
}
|
||||||
binding.valid = true
|
binding.valid = true
|
||||||
binding.clear = func(input *I) { set(input, Supplied[bool]{}) }
|
binding.clear = func(input *I) { set(input, Supplied[bool]{}) }
|
||||||
binding.prepare = func(values []string) (func(*I), error) {
|
binding.prepare = func(values []string) (preparedValue[I], error) {
|
||||||
value := defaultValue
|
value := defaultValue
|
||||||
supplied := len(values) > 0
|
supplied := len(values) > 0
|
||||||
for _, raw := range values {
|
for _, raw := range values {
|
||||||
parsed, err := strconv.ParseBool(raw)
|
parsed, err := strconv.ParseBool(raw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return preparedValue[I]{}, err
|
||||||
}
|
}
|
||||||
value = parsed
|
value = parsed
|
||||||
}
|
}
|
||||||
result := Supplied[bool]{Value: value, Set: supplied}
|
result := Supplied[bool]{Value: value, Set: supplied}
|
||||||
return func(input *I) { set(input, result) }, nil
|
return preparedValue[I]{identity: binding.identity, supplied: supplied, apply: func(input *I) { set(input, result) }}, nil
|
||||||
}
|
}
|
||||||
return Flag[I]{binding: binding}
|
return Flag[I]{binding: binding}
|
||||||
}
|
}
|
||||||
@@ -64,13 +64,13 @@ func BoolFlag[I any](meta FlagMeta, defaultValue bool, set func(*I, Supplied[boo
|
|||||||
// and are parsed in argv order, with each occurrence treated as one value.
|
// and are parsed in argv order, with each occurrence treated as one value.
|
||||||
func RepeatFlag[I, V any](meta FlagMeta, defaults []V, parse func(string) (V, error), set func(*I, Supplied[[]V])) Flag[I] {
|
func RepeatFlag[I, V any](meta FlagMeta, defaults []V, parse func(string) (V, error), set func(*I, Supplied[[]V])) Flag[I] {
|
||||||
fallback := append([]V(nil), defaults...)
|
fallback := append([]V(nil), defaults...)
|
||||||
binding := flagBinding[I]{meta: meta, defaultText: fmt.Sprint(fallback), repeated: true}
|
binding := flagBinding[I]{identity: newBindingIdentity(), meta: meta, defaultText: fmt.Sprint(fallback), repeated: true}
|
||||||
if parse == nil || set == nil {
|
if parse == nil || set == nil {
|
||||||
return Flag[I]{binding: binding}
|
return Flag[I]{binding: binding}
|
||||||
}
|
}
|
||||||
binding.valid = true
|
binding.valid = true
|
||||||
binding.clear = func(input *I) { set(input, Supplied[[]V]{}) }
|
binding.clear = func(input *I) { set(input, Supplied[[]V]{}) }
|
||||||
binding.prepare = func(values []string) (func(*I), error) {
|
binding.prepare = func(values []string) (preparedValue[I], error) {
|
||||||
parsedValues := append([]V(nil), fallback...)
|
parsedValues := append([]V(nil), fallback...)
|
||||||
supplied := len(values) > 0
|
supplied := len(values) > 0
|
||||||
if supplied {
|
if supplied {
|
||||||
@@ -78,15 +78,15 @@ func RepeatFlag[I, V any](meta FlagMeta, defaults []V, parse func(string) (V, er
|
|||||||
for _, raw := range values {
|
for _, raw := range values {
|
||||||
parsed, err := parse(raw)
|
parsed, err := parse(raw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return preparedValue[I]{}, err
|
||||||
}
|
}
|
||||||
parsedValues = append(parsedValues, parsed)
|
parsedValues = append(parsedValues, parsed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return func(input *I) {
|
return preparedValue[I]{identity: binding.identity, supplied: supplied, apply: func(input *I) {
|
||||||
result := Supplied[[]V]{Value: append([]V(nil), parsedValues...), Set: supplied}
|
result := Supplied[[]V]{Value: append([]V(nil), parsedValues...), Set: supplied}
|
||||||
set(input, result)
|
set(input, result)
|
||||||
}, nil
|
}}, nil
|
||||||
}
|
}
|
||||||
return Flag[I]{binding: binding}
|
return Flag[I]{binding: binding}
|
||||||
}
|
}
|
||||||
@@ -98,26 +98,26 @@ func ValueArgument[I, V any](meta ArgumentMeta, required bool, parse func(string
|
|||||||
if required {
|
if required {
|
||||||
min = 1
|
min = 1
|
||||||
}
|
}
|
||||||
binding := argumentBinding[I]{meta: meta, min: min, max: 1}
|
binding := argumentBinding[I]{identity: newBindingIdentity(), meta: meta, min: min, max: 1}
|
||||||
if parse == nil || set == nil {
|
if parse == nil || set == nil {
|
||||||
return Argument[I]{binding: binding}
|
return Argument[I]{binding: binding}
|
||||||
}
|
}
|
||||||
binding.valid = true
|
binding.valid = true
|
||||||
binding.prepare = func(values []string) (func(*I), error) {
|
binding.prepare = func(values []string) (preparedValue[I], error) {
|
||||||
if len(values) > 1 {
|
if len(values) > 1 {
|
||||||
return nil, fmt.Errorf("received %d values; maximum is 1", len(values))
|
return preparedValue[I]{}, fmt.Errorf("received %d values; maximum is 1", len(values))
|
||||||
}
|
}
|
||||||
var value V
|
var value V
|
||||||
supplied := len(values) == 1
|
supplied := len(values) == 1
|
||||||
if supplied {
|
if supplied {
|
||||||
parsed, err := parse(values[0])
|
parsed, err := parse(values[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return preparedValue[I]{}, err
|
||||||
}
|
}
|
||||||
value = parsed
|
value = parsed
|
||||||
}
|
}
|
||||||
result := Supplied[V]{Value: value, Set: supplied}
|
result := Supplied[V]{Value: value, Set: supplied}
|
||||||
return func(input *I) { set(input, result) }, nil
|
return preparedValue[I]{identity: binding.identity, supplied: supplied, apply: func(input *I) { set(input, result) }}, nil
|
||||||
}
|
}
|
||||||
return Argument[I]{binding: binding}
|
return Argument[I]{binding: binding}
|
||||||
}
|
}
|
||||||
@@ -125,34 +125,34 @@ func ValueArgument[I, V any](meta ArgumentMeta, required bool, parse func(string
|
|||||||
// RepeatArgument declares a positional argument that accepts a bounded number
|
// RepeatArgument declares a positional argument that accepts a bounded number
|
||||||
// of values. A maximum of -1 means unbounded.
|
// of values. A maximum of -1 means unbounded.
|
||||||
func RepeatArgument[I, V any](meta ArgumentMeta, min, max int, parse func(string) (V, error), set func(*I, Supplied[[]V])) Argument[I] {
|
func RepeatArgument[I, V any](meta ArgumentMeta, min, max int, parse func(string) (V, error), set func(*I, Supplied[[]V])) Argument[I] {
|
||||||
binding := argumentBinding[I]{meta: meta, min: min, max: max}
|
binding := argumentBinding[I]{identity: newBindingIdentity(), meta: meta, min: min, max: max}
|
||||||
if parse == nil || set == nil {
|
if parse == nil || set == nil {
|
||||||
return Argument[I]{binding: binding}
|
return Argument[I]{binding: binding}
|
||||||
}
|
}
|
||||||
binding.valid = true
|
binding.valid = true
|
||||||
binding.prepare = func(values []string) (func(*I), error) {
|
binding.prepare = func(values []string) (preparedValue[I], error) {
|
||||||
if max >= 0 && len(values) > max {
|
if max >= 0 && len(values) > max {
|
||||||
return nil, fmt.Errorf("received %d values; maximum is %d", len(values), max)
|
return preparedValue[I]{}, fmt.Errorf("received %d values; maximum is %d", len(values), max)
|
||||||
}
|
}
|
||||||
parsedValues := make([]V, 0, len(values))
|
parsedValues := make([]V, 0, len(values))
|
||||||
for _, raw := range values {
|
for _, raw := range values {
|
||||||
parsed, err := parse(raw)
|
parsed, err := parse(raw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return preparedValue[I]{}, err
|
||||||
}
|
}
|
||||||
parsedValues = append(parsedValues, parsed)
|
parsedValues = append(parsedValues, parsed)
|
||||||
}
|
}
|
||||||
return func(input *I) {
|
return preparedValue[I]{identity: binding.identity, supplied: len(values) > 0, apply: func(input *I) {
|
||||||
result := Supplied[[]V]{Value: append([]V(nil), parsedValues...), Set: len(values) > 0}
|
result := Supplied[[]V]{Value: append([]V(nil), parsedValues...), Set: len(values) > 0}
|
||||||
set(input, result)
|
set(input, result)
|
||||||
}, nil
|
}}, nil
|
||||||
}
|
}
|
||||||
return Argument[I]{binding: binding}
|
return Argument[I]{binding: binding}
|
||||||
}
|
}
|
||||||
|
|
||||||
type preparedInput[I any] struct {
|
type preparedInput[I any] struct {
|
||||||
newInput func() I
|
newInput func() I
|
||||||
setters []func(*I)
|
values []preparedValue[I]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p preparedInput[I]) build() I {
|
func (p preparedInput[I]) build() I {
|
||||||
@@ -160,8 +160,8 @@ func (p preparedInput[I]) build() I {
|
|||||||
if p.newInput != nil {
|
if p.newInput != nil {
|
||||||
input = p.newInput()
|
input = p.newInput()
|
||||||
}
|
}
|
||||||
for _, setter := range p.setters {
|
for _, value := range p.values {
|
||||||
setter(&input)
|
value.apply(&input)
|
||||||
}
|
}
|
||||||
return input
|
return input
|
||||||
}
|
}
|
||||||
@@ -173,33 +173,40 @@ func prepareInput[I any](spec CommandSpec[I], flagValues [][]string, operands []
|
|||||||
if len(flagValues) != len(spec.Flags) {
|
if len(flagValues) != len(spec.Flags) {
|
||||||
return preparedInput[I]{}, fmt.Errorf("received %d flag value sets for %d bindings", len(flagValues), len(spec.Flags))
|
return preparedInput[I]{}, fmt.Errorf("received %d flag value sets for %d bindings", len(flagValues), len(spec.Flags))
|
||||||
}
|
}
|
||||||
setters := make([]func(*I), 0, len(spec.Flags)+len(spec.Arguments))
|
preparedFlags := make([]preparedValue[I], len(spec.Flags))
|
||||||
|
missingRequired := ""
|
||||||
for i, flag := range spec.Flags {
|
for i, flag := range spec.Flags {
|
||||||
binding := flag.binding
|
binding := flag.binding
|
||||||
if enforceRequired && binding.meta.Required && len(flagValues[i]) == 0 {
|
value, err := binding.prepare(flagValues[i])
|
||||||
return preparedInput[I]{}, fmt.Errorf("required flag --%s was not supplied", binding.meta.Name)
|
|
||||||
}
|
|
||||||
setter, err := binding.prepare(flagValues[i])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return preparedInput[I]{}, fmt.Errorf("flag --%s: %w", binding.meta.Name, err)
|
return preparedInput[I]{}, fmt.Errorf("flag --%s: %w", binding.meta.Name, err)
|
||||||
}
|
}
|
||||||
setters = append(setters, setter)
|
preparedFlags[i] = value
|
||||||
|
if enforceRequired && binding.meta.Required && !value.supplied && missingRequired == "" {
|
||||||
|
missingRequired = binding.meta.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if missingRequired != "" {
|
||||||
|
return preparedInput[I]{}, fmt.Errorf("required flag --%s was not supplied", missingRequired)
|
||||||
}
|
}
|
||||||
argumentValues, err := allocateOperands(operands, spec.Arguments, enforceRequired)
|
argumentValues, err := allocateOperands(operands, spec.Arguments, enforceRequired)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return preparedInput[I]{}, err
|
return preparedInput[I]{}, err
|
||||||
}
|
}
|
||||||
|
preparedValues := append([]preparedValue[I](nil), preparedFlags...)
|
||||||
for i, argument := range spec.Arguments {
|
for i, argument := range spec.Arguments {
|
||||||
binding := argument.binding
|
binding := argument.binding
|
||||||
setter, err := binding.prepare(argumentValues[i])
|
value, err := binding.prepare(argumentValues[i])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return preparedInput[I]{}, fmt.Errorf("argument %q: %w", binding.meta.Name, err)
|
return preparedInput[I]{}, fmt.Errorf("argument %q: %w", binding.meta.Name, err)
|
||||||
}
|
}
|
||||||
setters = append(setters, setter)
|
preparedValues = append(preparedValues, value)
|
||||||
}
|
}
|
||||||
return preparedInput[I]{newInput: spec.NewInput, setters: setters}, nil
|
return preparedInput[I]{newInput: spec.NewInput, values: preparedValues}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newBindingIdentity() *bindingIdentity { return &bindingIdentity{marker: 1} }
|
||||||
|
|
||||||
func allocateOperands[I any](operands []string, definitions []Argument[I], enforceRequired bool) ([][]string, error) {
|
func allocateOperands[I any](operands []string, definitions []Argument[I], enforceRequired bool) ([][]string, error) {
|
||||||
for _, argument := range definitions {
|
for _, argument := range definitions {
|
||||||
binding := argument.binding
|
binding := argument.binding
|
||||||
|
|||||||
@@ -104,8 +104,7 @@ func TestBoolFlagRejectsExplicitEmptyValue(t *testing.T) {
|
|||||||
|
|
||||||
func TestRepeatFlagUsesFreshDefaultsAndWholeOccurrenceValues(t *testing.T) {
|
func TestRepeatFlagUsesFreshDefaultsAndWholeOccurrenceValues(t *testing.T) {
|
||||||
defaults := []string{"base"}
|
defaults := []string{"base"}
|
||||||
var captured Supplied[[]string]
|
flag := RepeatFlag(FlagMeta{Name: "tag"}, defaults, func(value string) (string, error) { return value, nil }, func(input *bindingInput, value Supplied[[]string]) { input.tags = value })
|
||||||
flag := RepeatFlag(FlagMeta{Name: "tag"}, defaults, func(value string) (string, error) { return value, nil }, func(_ *bindingInput, value Supplied[[]string]) { captured = value })
|
|
||||||
defaults[0] = "changed after declaration"
|
defaults[0] = "changed after declaration"
|
||||||
|
|
||||||
applyDefault, err := flag.binding.prepare(nil)
|
applyDefault, err := flag.binding.prepare(nil)
|
||||||
@@ -113,25 +112,25 @@ func TestRepeatFlagUsesFreshDefaultsAndWholeOccurrenceValues(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
first := &bindingInput{}
|
first := &bindingInput{}
|
||||||
applyDefault(first)
|
applyDefault.apply(first)
|
||||||
second := &bindingInput{}
|
second := &bindingInput{}
|
||||||
applyDefault(second)
|
applyDefault.apply(second)
|
||||||
if !reflect.DeepEqual(captured.Value, []string{"base"}) || captured.Set {
|
if !reflect.DeepEqual(first.tags.Value, []string{"base"}) || first.tags.Set {
|
||||||
t.Fatalf("omitted repeat value = %#v, want copied default with Set=false", captured)
|
t.Fatalf("omitted repeat value = %#v, want copied default with Set=false", first.tags)
|
||||||
}
|
}
|
||||||
captured.Value[0] = "changed one input"
|
first.tags.Value[0] = "changed one input"
|
||||||
applyDefault(second)
|
if !reflect.DeepEqual(second.tags.Value, []string{"base"}) {
|
||||||
if !reflect.DeepEqual(captured.Value, []string{"base"}) {
|
t.Fatalf("repeat default collection was reused: %#v", second.tags.Value)
|
||||||
t.Fatalf("repeat default collection was reused: %#v", captured.Value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
applyValues, err := flag.binding.prepare([]string{"a,b", "c"})
|
applyValues, err := flag.binding.prepare([]string{"a,b", "c"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
applyValues(&bindingInput{})
|
third := &bindingInput{}
|
||||||
if !reflect.DeepEqual(captured.Value, []string{"a,b", "c"}) || !captured.Set {
|
applyValues.apply(third)
|
||||||
t.Fatalf("supplied repeat values = %#v, want two ordered occurrences", captured)
|
if !reflect.DeepEqual(third.tags.Value, []string{"a,b", "c"}) || !third.tags.Set {
|
||||||
|
t.Fatalf("supplied repeat values = %#v, want two ordered occurrences", third.tags)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,6 +148,46 @@ func TestSingletonParsesEveryOccurrenceAndPreservesCallerCause(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPreparedScalarConvertsEveryOccurrenceOnceBeforeInputCreation(t *testing.T) {
|
||||||
|
parseCalls := 0
|
||||||
|
var received Supplied[string]
|
||||||
|
flag := ValueFlag(FlagMeta{Name: "value"}, (*string)(nil), func(value string) (string, error) {
|
||||||
|
parseCalls++
|
||||||
|
return value, nil
|
||||||
|
}, func(input *bindingInput, value Supplied[string]) { input.message = value })
|
||||||
|
prepared, err := flag.binding.prepare([]string{"earlier", "last"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if parseCalls != 2 || prepared.identity != flag.binding.identity || !prepared.supplied {
|
||||||
|
t.Fatalf("prepared scalar = %#v, parse calls = %d", prepared, parseCalls)
|
||||||
|
}
|
||||||
|
copy := flag
|
||||||
|
lookalike := ValueFlag(FlagMeta{Name: "value"}, (*string)(nil), func(value string) (string, error) { return value, nil }, func(*bindingInput, Supplied[string]) {})
|
||||||
|
if copy.binding.identity != flag.binding.identity || lookalike.binding.identity == flag.binding.identity {
|
||||||
|
t.Fatal("binding identity was not preserved by copying or distinct across constructors")
|
||||||
|
}
|
||||||
|
input := &bindingInput{}
|
||||||
|
prepared.apply(input)
|
||||||
|
received = input.message
|
||||||
|
if parseCalls != 2 || received.Value != "last" || !received.Set {
|
||||||
|
t.Fatalf("applied value = %#v, parse calls = %d", received, parseCalls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInvalidBindingConstructionIsReportedAtInstallation(t *testing.T) {
|
||||||
|
badFlag := ValueFlag[struct{}, string](FlagMeta{Name: "broken"}, nil, nil, func(*struct{}, Supplied[string]) {})
|
||||||
|
badArgument := RepeatArgument[struct{}, string](ArgumentMeta{Name: "items"}, 2, 1, func(value string) (string, error) { return value, nil }, func(*struct{}, Supplied[[]string]) {})
|
||||||
|
command := NewCommand(CommandSpec[struct{}]{Name: "run", Flags: []Flag[struct{}]{badFlag}, Arguments: []Argument[struct{}]{badArgument}})
|
||||||
|
app, err := New(Identity{Name: "example"}, command)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("installation accepted nil parser wiring and invalid argument cardinality")
|
||||||
|
}
|
||||||
|
if app != nil {
|
||||||
|
t.Fatal("failed installation returned a usable app")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRepeatArgumentAllocationReservesLaterMinimums(t *testing.T) {
|
func TestRepeatArgumentAllocationReservesLaterMinimums(t *testing.T) {
|
||||||
type operandsInput struct {
|
type operandsInput struct {
|
||||||
prefix Supplied[[]string]
|
prefix Supplied[[]string]
|
||||||
|
|||||||
26
command.go
26
command.go
@@ -58,12 +58,21 @@ type Flag[I any] struct {
|
|||||||
binding flagBinding[I]
|
binding flagBinding[I]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type bindingIdentity struct{ marker byte }
|
||||||
|
|
||||||
|
type preparedValue[I any] struct {
|
||||||
|
identity *bindingIdentity
|
||||||
|
supplied bool
|
||||||
|
apply func(*I)
|
||||||
|
}
|
||||||
|
|
||||||
type flagBinding[I any] struct {
|
type flagBinding[I any] struct {
|
||||||
|
identity *bindingIdentity
|
||||||
meta FlagMeta
|
meta FlagMeta
|
||||||
defaultText string
|
defaultText string
|
||||||
repeated bool
|
repeated bool
|
||||||
boolean bool
|
boolean bool
|
||||||
prepare func([]string) (func(*I), error)
|
prepare func([]string) (preparedValue[I], error)
|
||||||
clear func(*I)
|
clear func(*I)
|
||||||
valid bool
|
valid bool
|
||||||
}
|
}
|
||||||
@@ -74,11 +83,12 @@ type Argument[I any] struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type argumentBinding[I any] struct {
|
type argumentBinding[I any] struct {
|
||||||
meta ArgumentMeta
|
identity *bindingIdentity
|
||||||
min int
|
meta ArgumentMeta
|
||||||
max int
|
min int
|
||||||
prepare func([]string) (func(*I), error)
|
max int
|
||||||
valid bool
|
prepare func([]string) (preparedValue[I], error)
|
||||||
|
valid bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommandSpec declares one command using typed input and handler callbacks.
|
// CommandSpec declares one command using typed input and handler callbacks.
|
||||||
@@ -319,7 +329,7 @@ func validateBindings[I any](flags []Flag[I], arguments []Argument[I]) error {
|
|||||||
}
|
}
|
||||||
shortNames[binding.meta.Short] = struct{}{}
|
shortNames[binding.meta.Short] = struct{}{}
|
||||||
}
|
}
|
||||||
if !binding.valid || binding.prepare == nil {
|
if binding.identity == nil || !binding.valid || binding.prepare == nil {
|
||||||
return fmt.Errorf("flag %q is missing parser or setter wiring", binding.meta.Name)
|
return fmt.Errorf("flag %q is missing parser or setter wiring", binding.meta.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -332,7 +342,7 @@ func validateBindings[I any](flags []Flag[I], arguments []Argument[I]) error {
|
|||||||
if binding.min < 0 || binding.max < -1 || (binding.max >= 0 && binding.min > binding.max) {
|
if binding.min < 0 || binding.max < -1 || (binding.max >= 0 && binding.min > binding.max) {
|
||||||
return fmt.Errorf("argument %q has invalid bounds %d..%d", binding.meta.Name, binding.min, binding.max)
|
return fmt.Errorf("argument %q has invalid bounds %d..%d", binding.meta.Name, binding.min, binding.max)
|
||||||
}
|
}
|
||||||
if !binding.valid || binding.prepare == nil {
|
if binding.identity == nil || !binding.valid || binding.prepare == nil {
|
||||||
return fmt.Errorf("argument %q is missing parser or setter wiring", binding.meta.Name)
|
return fmt.Errorf("argument %q is missing parser or setter wiring", binding.meta.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Implement the optional integration modules and captured registration API
|
# Implement the optional integration modules and captured registration API
|
||||||
|
|
||||||
Status: implementation in progress. Stages 1 and 2 are complete; subsequent work follows the ordered roadmap.
|
Status: implementation in progress. Stages 1 through 3 are complete; subsequent work follows the ordered roadmap.
|
||||||
Execute the stages in order. Each stage is sized as a separate Luna coding-agent
|
Execute the stages in order. Each stage is sized as a separate Luna coding-agent
|
||||||
assignment. This document authorizes no publication or changes to sibling
|
assignment. This document authorizes no publication or changes to sibling
|
||||||
repositories.
|
repositories.
|
||||||
@@ -222,9 +222,14 @@ object deep copies.
|
|||||||
combined names, short names, duplicate bindings, and help conflicts. Avoid
|
combined names, short names, duplicate bindings, and help conflicts. Avoid
|
||||||
integration-specific reserved-flag lists.
|
integration-specific reserved-flag lists.
|
||||||
|
|
||||||
**Completion evidence:** existing presence/repetition/help tests, adjusted to the
|
**Completion evidence — complete:** flags and operands now prepare the same
|
||||||
selected constructors, plus focused converter call counts and repeated-slice
|
identity-bearing typed application value. Binding constructors retain invalid
|
||||||
independence. Do not add a new grammar restriction.
|
wiring/cardinality for installation; copies preserve identity and independent
|
||||||
|
lookalikes do not. Conversion remains before input creation, every singleton
|
||||||
|
occurrence is parsed once, and repeated core slices are copied per application.
|
||||||
|
Existing presence, repetition, help, and operand tests pass with the captured
|
||||||
|
constructors, along with focused identity, converter-count, and independence
|
||||||
|
checks. No new grammar restriction was added.
|
||||||
|
|
||||||
## Stage 4: Implement captured flag application
|
## Stage 4: Implement captured flag application
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,12 @@ type registryInput struct{ value string }
|
|||||||
|
|
||||||
func TestCapturedDeclarationsRetainCallbacksWithoutCallingThem(t *testing.T) {
|
func TestCapturedDeclarationsRetainCallbacksWithoutCallingThem(t *testing.T) {
|
||||||
var factoryCalls, prepareCalls, setterCalls, checkCalls, runCalls int
|
var factoryCalls, prepareCalls, setterCalls, checkCalls, runCalls int
|
||||||
|
identity := newBindingIdentity()
|
||||||
flag := Flag[registryInput]{binding: flagBinding[registryInput]{
|
flag := Flag[registryInput]{binding: flagBinding[registryInput]{
|
||||||
meta: FlagMeta{Name: "mode"}, valid: true,
|
identity: identity, meta: FlagMeta{Name: "mode"}, valid: true,
|
||||||
prepare: func([]string) (func(*registryInput), error) {
|
prepare: func([]string) (preparedValue[registryInput], error) {
|
||||||
prepareCalls++
|
prepareCalls++
|
||||||
return func(*registryInput) { setterCalls++ }, nil
|
return preparedValue[registryInput]{identity: identity, apply: func(*registryInput) { setterCalls++ }}, nil
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
command := NewCommand(CommandSpec[registryInput]{
|
command := NewCommand(CommandSpec[registryInput]{
|
||||||
@@ -175,7 +176,10 @@ func TestNewRejectsEmptyIdentity(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestNewCommandCopiesBindingMetadata(t *testing.T) {
|
func TestNewCommandCopiesBindingMetadata(t *testing.T) {
|
||||||
flags := []Flag[registryInput]{{binding: flagBinding[registryInput]{meta: FlagMeta{Name: "mode"}, valid: true, prepare: func([]string) (func(*registryInput), error) { return func(*registryInput) {}, nil }}}}
|
identity := newBindingIdentity()
|
||||||
|
flags := []Flag[registryInput]{{binding: flagBinding[registryInput]{identity: identity, meta: FlagMeta{Name: "mode"}, valid: true, prepare: func([]string) (preparedValue[registryInput], error) {
|
||||||
|
return preparedValue[registryInput]{identity: identity, apply: func(*registryInput) {}}, nil
|
||||||
|
}}}}
|
||||||
command := NewCommand(CommandSpec[registryInput]{Name: "run", Flags: flags})
|
command := NewCommand(CommandSpec[registryInput]{Name: "run", Flags: flags})
|
||||||
flags[0].binding.meta.Name = "changed"
|
flags[0].binding.meta.Name = "changed"
|
||||||
payload := command.record.payload.(typedCommand[registryInput])
|
payload := command.record.payload.(typedCommand[registryInput])
|
||||||
|
|||||||
Reference in New Issue
Block a user