Unify typed binding preparation

This commit is contained in:
2026-10-11 01:18:07 +00:00
parent de9bfdcc4c
commit f63b096525
5 changed files with 130 additions and 65 deletions

View File

@@ -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