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 {
|
||||
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 {
|
||||
return Flag[I]{binding: binding}
|
||||
}
|
||||
binding.valid = true
|
||||
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
|
||||
supplied := len(values) > 0
|
||||
for _, raw := range values {
|
||||
parsed, err := parse(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return preparedValue[I]{}, err
|
||||
}
|
||||
value = parsed
|
||||
}
|
||||
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}
|
||||
}
|
||||
@@ -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
|
||||
// "true"; explicit values are converted with strconv.ParseBool.
|
||||
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 {
|
||||
return Flag[I]{binding: binding}
|
||||
}
|
||||
binding.valid = true
|
||||
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
|
||||
supplied := len(values) > 0
|
||||
for _, raw := range values {
|
||||
parsed, err := strconv.ParseBool(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return preparedValue[I]{}, err
|
||||
}
|
||||
value = parsed
|
||||
}
|
||||
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}
|
||||
}
|
||||
@@ -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.
|
||||
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...)
|
||||
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 {
|
||||
return Flag[I]{binding: binding}
|
||||
}
|
||||
binding.valid = true
|
||||
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...)
|
||||
supplied := len(values) > 0
|
||||
if supplied {
|
||||
@@ -78,15 +78,15 @@ func RepeatFlag[I, V any](meta FlagMeta, defaults []V, parse func(string) (V, er
|
||||
for _, raw := range values {
|
||||
parsed, err := parse(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return preparedValue[I]{}, err
|
||||
}
|
||||
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}
|
||||
set(input, result)
|
||||
}, nil
|
||||
}}, nil
|
||||
}
|
||||
return Flag[I]{binding: binding}
|
||||
}
|
||||
@@ -98,26 +98,26 @@ func ValueArgument[I, V any](meta ArgumentMeta, required bool, parse func(string
|
||||
if required {
|
||||
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 {
|
||||
return Argument[I]{binding: binding}
|
||||
}
|
||||
binding.valid = true
|
||||
binding.prepare = func(values []string) (func(*I), error) {
|
||||
binding.prepare = func(values []string) (preparedValue[I], error) {
|
||||
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
|
||||
supplied := len(values) == 1
|
||||
if supplied {
|
||||
parsed, err := parse(values[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return preparedValue[I]{}, err
|
||||
}
|
||||
value = parsed
|
||||
}
|
||||
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}
|
||||
}
|
||||
@@ -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
|
||||
// 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] {
|
||||
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 {
|
||||
return Argument[I]{binding: binding}
|
||||
}
|
||||
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 {
|
||||
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))
|
||||
for _, raw := range values {
|
||||
parsed, err := parse(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return preparedValue[I]{}, err
|
||||
}
|
||||
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}
|
||||
set(input, result)
|
||||
}, nil
|
||||
}}, nil
|
||||
}
|
||||
return Argument[I]{binding: binding}
|
||||
}
|
||||
|
||||
type preparedInput[I any] struct {
|
||||
newInput func() I
|
||||
setters []func(*I)
|
||||
values []preparedValue[I]
|
||||
}
|
||||
|
||||
func (p preparedInput[I]) build() I {
|
||||
@@ -160,8 +160,8 @@ func (p preparedInput[I]) build() I {
|
||||
if p.newInput != nil {
|
||||
input = p.newInput()
|
||||
}
|
||||
for _, setter := range p.setters {
|
||||
setter(&input)
|
||||
for _, value := range p.values {
|
||||
value.apply(&input)
|
||||
}
|
||||
return input
|
||||
}
|
||||
@@ -173,33 +173,40 @@ func prepareInput[I any](spec CommandSpec[I], flagValues [][]string, operands []
|
||||
if 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 {
|
||||
binding := flag.binding
|
||||
if enforceRequired && binding.meta.Required && len(flagValues[i]) == 0 {
|
||||
return preparedInput[I]{}, fmt.Errorf("required flag --%s was not supplied", binding.meta.Name)
|
||||
}
|
||||
setter, err := binding.prepare(flagValues[i])
|
||||
value, err := binding.prepare(flagValues[i])
|
||||
if err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
return preparedInput[I]{}, err
|
||||
}
|
||||
preparedValues := append([]preparedValue[I](nil), preparedFlags...)
|
||||
for i, argument := range spec.Arguments {
|
||||
binding := argument.binding
|
||||
setter, err := binding.prepare(argumentValues[i])
|
||||
value, err := binding.prepare(argumentValues[i])
|
||||
if err != nil {
|
||||
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) {
|
||||
for _, argument := range definitions {
|
||||
binding := argument.binding
|
||||
|
||||
@@ -104,8 +104,7 @@ func TestBoolFlagRejectsExplicitEmptyValue(t *testing.T) {
|
||||
|
||||
func TestRepeatFlagUsesFreshDefaultsAndWholeOccurrenceValues(t *testing.T) {
|
||||
defaults := []string{"base"}
|
||||
var captured Supplied[[]string]
|
||||
flag := RepeatFlag(FlagMeta{Name: "tag"}, defaults, func(value string) (string, error) { return value, nil }, func(_ *bindingInput, value Supplied[[]string]) { captured = value })
|
||||
flag := RepeatFlag(FlagMeta{Name: "tag"}, defaults, func(value string) (string, error) { return value, nil }, func(input *bindingInput, value Supplied[[]string]) { input.tags = value })
|
||||
defaults[0] = "changed after declaration"
|
||||
|
||||
applyDefault, err := flag.binding.prepare(nil)
|
||||
@@ -113,25 +112,25 @@ func TestRepeatFlagUsesFreshDefaultsAndWholeOccurrenceValues(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
first := &bindingInput{}
|
||||
applyDefault(first)
|
||||
applyDefault.apply(first)
|
||||
second := &bindingInput{}
|
||||
applyDefault(second)
|
||||
if !reflect.DeepEqual(captured.Value, []string{"base"}) || captured.Set {
|
||||
t.Fatalf("omitted repeat value = %#v, want copied default with Set=false", captured)
|
||||
applyDefault.apply(second)
|
||||
if !reflect.DeepEqual(first.tags.Value, []string{"base"}) || first.tags.Set {
|
||||
t.Fatalf("omitted repeat value = %#v, want copied default with Set=false", first.tags)
|
||||
}
|
||||
captured.Value[0] = "changed one input"
|
||||
applyDefault(second)
|
||||
if !reflect.DeepEqual(captured.Value, []string{"base"}) {
|
||||
t.Fatalf("repeat default collection was reused: %#v", captured.Value)
|
||||
first.tags.Value[0] = "changed one input"
|
||||
if !reflect.DeepEqual(second.tags.Value, []string{"base"}) {
|
||||
t.Fatalf("repeat default collection was reused: %#v", second.tags.Value)
|
||||
}
|
||||
|
||||
applyValues, err := flag.binding.prepare([]string{"a,b", "c"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
applyValues(&bindingInput{})
|
||||
if !reflect.DeepEqual(captured.Value, []string{"a,b", "c"}) || !captured.Set {
|
||||
t.Fatalf("supplied repeat values = %#v, want two ordered occurrences", captured)
|
||||
third := &bindingInput{}
|
||||
applyValues.apply(third)
|
||||
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) {
|
||||
type operandsInput struct {
|
||||
prefix Supplied[[]string]
|
||||
|
||||
26
command.go
26
command.go
@@ -58,12 +58,21 @@ type Flag[I any] struct {
|
||||
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 {
|
||||
identity *bindingIdentity
|
||||
meta FlagMeta
|
||||
defaultText string
|
||||
repeated bool
|
||||
boolean bool
|
||||
prepare func([]string) (func(*I), error)
|
||||
prepare func([]string) (preparedValue[I], error)
|
||||
clear func(*I)
|
||||
valid bool
|
||||
}
|
||||
@@ -74,11 +83,12 @@ type Argument[I any] struct {
|
||||
}
|
||||
|
||||
type argumentBinding[I any] struct {
|
||||
meta ArgumentMeta
|
||||
min int
|
||||
max int
|
||||
prepare func([]string) (func(*I), error)
|
||||
valid bool
|
||||
identity *bindingIdentity
|
||||
meta ArgumentMeta
|
||||
min int
|
||||
max int
|
||||
prepare func([]string) (preparedValue[I], error)
|
||||
valid bool
|
||||
}
|
||||
|
||||
// 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{}{}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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
|
||||
assignment. This document authorizes no publication or changes to sibling
|
||||
repositories.
|
||||
@@ -222,9 +222,14 @@ object deep copies.
|
||||
combined names, short names, duplicate bindings, and help conflicts. Avoid
|
||||
integration-specific reserved-flag lists.
|
||||
|
||||
**Completion evidence:** existing presence/repetition/help tests, adjusted to the
|
||||
selected constructors, plus focused converter call counts and repeated-slice
|
||||
independence. Do not add a new grammar restriction.
|
||||
**Completion evidence — complete:** flags and operands now prepare the same
|
||||
identity-bearing typed application value. Binding constructors retain invalid
|
||||
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
|
||||
|
||||
|
||||
@@ -9,11 +9,12 @@ type registryInput struct{ value string }
|
||||
|
||||
func TestCapturedDeclarationsRetainCallbacksWithoutCallingThem(t *testing.T) {
|
||||
var factoryCalls, prepareCalls, setterCalls, checkCalls, runCalls int
|
||||
identity := newBindingIdentity()
|
||||
flag := Flag[registryInput]{binding: flagBinding[registryInput]{
|
||||
meta: FlagMeta{Name: "mode"}, valid: true,
|
||||
prepare: func([]string) (func(*registryInput), error) {
|
||||
identity: identity, meta: FlagMeta{Name: "mode"}, valid: true,
|
||||
prepare: func([]string) (preparedValue[registryInput], error) {
|
||||
prepareCalls++
|
||||
return func(*registryInput) { setterCalls++ }, nil
|
||||
return preparedValue[registryInput]{identity: identity, apply: func(*registryInput) { setterCalls++ }}, nil
|
||||
},
|
||||
}}
|
||||
command := NewCommand(CommandSpec[registryInput]{
|
||||
@@ -175,7 +176,10 @@ func TestNewRejectsEmptyIdentity(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})
|
||||
flags[0].binding.meta.Name = "changed"
|
||||
payload := command.record.payload.(typedCommand[registryInput])
|
||||
|
||||
Reference in New Issue
Block a user