Add typed command input bindings
This commit is contained in:
257
binding.go
Normal file
257
binding.go
Normal file
@@ -0,0 +1,257 @@
|
||||
package commandkit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// ValueFlag declares a singleton flag with a typed parser, optional default,
|
||||
// and setter. Every supplied occurrence is parsed; the last parsed value is
|
||||
// applied with Set=true.
|
||||
func ValueFlag[I, V any](meta FlagMeta, defaultValue *V, parse func(string) (V, error), set func(*I, Supplied[V])) Flag[I] {
|
||||
var fallback V
|
||||
if defaultValue != nil {
|
||||
fallback = *defaultValue
|
||||
}
|
||||
binding := flagBinding[I]{meta: meta, defaultText: fmt.Sprint(fallback)}
|
||||
if parse == nil || set == nil {
|
||||
return Flag[I]{binding: binding}
|
||||
}
|
||||
binding.valid = true
|
||||
binding.prepare = func(values []string) (func(*I), error) {
|
||||
value := fallback
|
||||
supplied := len(values) > 0
|
||||
for _, raw := range values {
|
||||
parsed, err := parse(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
value = parsed
|
||||
}
|
||||
result := Supplied[V]{Value: value, Set: supplied}
|
||||
return func(input *I) { set(input, result) }, nil
|
||||
}
|
||||
return Flag[I]{binding: binding}
|
||||
}
|
||||
|
||||
// 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}
|
||||
if set == nil {
|
||||
return Flag[I]{binding: binding}
|
||||
}
|
||||
binding.valid = true
|
||||
binding.prepare = func(values []string) (func(*I), error) {
|
||||
value := defaultValue
|
||||
supplied := len(values) > 0
|
||||
for _, raw := range values {
|
||||
parsed, err := strconv.ParseBool(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
value = parsed
|
||||
}
|
||||
result := Supplied[bool]{Value: value, Set: supplied}
|
||||
return func(input *I) { set(input, result) }, nil
|
||||
}
|
||||
return Flag[I]{binding: binding}
|
||||
}
|
||||
|
||||
// RepeatFlag declares a repeatable flag. Supplied occurrences replace defaults
|
||||
// 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}
|
||||
if parse == nil || set == nil {
|
||||
return Flag[I]{binding: binding}
|
||||
}
|
||||
binding.valid = true
|
||||
binding.prepare = func(values []string) (func(*I), error) {
|
||||
parsedValues := append([]V(nil), fallback...)
|
||||
supplied := len(values) > 0
|
||||
if supplied {
|
||||
parsedValues = make([]V, 0, len(values))
|
||||
for _, raw := range values {
|
||||
parsed, err := parse(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parsedValues = append(parsedValues, parsed)
|
||||
}
|
||||
}
|
||||
return func(input *I) {
|
||||
result := Supplied[[]V]{Value: append([]V(nil), parsedValues...), Set: supplied}
|
||||
set(input, result)
|
||||
}, nil
|
||||
}
|
||||
return Flag[I]{binding: binding}
|
||||
}
|
||||
|
||||
// ValueArgument declares an optional or required singleton positional
|
||||
// argument.
|
||||
func ValueArgument[I, V any](meta ArgumentMeta, required bool, parse func(string) (V, error), set func(*I, Supplied[V])) Argument[I] {
|
||||
min := 0
|
||||
if required {
|
||||
min = 1
|
||||
}
|
||||
binding := argumentBinding[I]{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) {
|
||||
if len(values) > 1 {
|
||||
return nil, 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
|
||||
}
|
||||
value = parsed
|
||||
}
|
||||
result := Supplied[V]{Value: value, Set: supplied}
|
||||
return func(input *I) { set(input, result) }, nil
|
||||
}
|
||||
return Argument[I]{binding: binding}
|
||||
}
|
||||
|
||||
// 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}
|
||||
if parse == nil || set == nil {
|
||||
return Argument[I]{binding: binding}
|
||||
}
|
||||
binding.valid = true
|
||||
binding.prepare = func(values []string) (func(*I), error) {
|
||||
if max >= 0 && len(values) > max {
|
||||
return nil, 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
|
||||
}
|
||||
parsedValues = append(parsedValues, parsed)
|
||||
}
|
||||
return func(input *I) {
|
||||
result := Supplied[[]V]{Value: append([]V(nil), parsedValues...), Set: len(values) > 0}
|
||||
set(input, result)
|
||||
}, nil
|
||||
}
|
||||
return Argument[I]{binding: binding}
|
||||
}
|
||||
|
||||
type preparedInput[I any] struct {
|
||||
newInput func() I
|
||||
setters []func(*I)
|
||||
}
|
||||
|
||||
func (p preparedInput[I]) build() I {
|
||||
var input I
|
||||
if p.newInput != nil {
|
||||
input = p.newInput()
|
||||
}
|
||||
for _, setter := range p.setters {
|
||||
setter(&input)
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
// prepareInput parses all supplied bindings into typed closures before it
|
||||
// creates an input value. When enforceRequired is false, missing required flags
|
||||
// and operands are allowed so metadata help can validate supplied syntax.
|
||||
func prepareInput[I any](spec CommandSpec[I], flagValues [][]string, operands []string, enforceRequired bool) (preparedInput[I], error) {
|
||||
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))
|
||||
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])
|
||||
if err != nil {
|
||||
return preparedInput[I]{}, fmt.Errorf("flag --%s: %w", binding.meta.Name, err)
|
||||
}
|
||||
setters = append(setters, setter)
|
||||
}
|
||||
argumentValues, err := allocateOperands(operands, spec.Arguments, enforceRequired)
|
||||
if err != nil {
|
||||
return preparedInput[I]{}, err
|
||||
}
|
||||
for i, argument := range spec.Arguments {
|
||||
binding := argument.binding
|
||||
setter, err := binding.prepare(argumentValues[i])
|
||||
if err != nil {
|
||||
return preparedInput[I]{}, fmt.Errorf("argument %q: %w", binding.meta.Name, err)
|
||||
}
|
||||
setters = append(setters, setter)
|
||||
}
|
||||
return preparedInput[I]{newInput: spec.NewInput, setters: setters}, nil
|
||||
}
|
||||
|
||||
func allocateOperands[I any](operands []string, definitions []Argument[I], enforceRequired bool) ([][]string, error) {
|
||||
for _, argument := range definitions {
|
||||
binding := argument.binding
|
||||
if binding.min < 0 || binding.max < -1 || (binding.max >= 0 && binding.min > binding.max) {
|
||||
return nil, fmt.Errorf("argument %q has invalid bounds %d..%d", binding.meta.Name, binding.min, binding.max)
|
||||
}
|
||||
}
|
||||
if enforceRequired {
|
||||
minimum := 0
|
||||
for _, argument := range definitions {
|
||||
if argument.binding.min > len(operands)-minimum {
|
||||
return nil, fmt.Errorf("not enough positional arguments")
|
||||
}
|
||||
minimum += argument.binding.min
|
||||
}
|
||||
}
|
||||
|
||||
suffixMinimum := make([]int, len(definitions)+1)
|
||||
for i := len(definitions) - 1; i >= 0; i-- {
|
||||
minimum := definitions[i].binding.min
|
||||
remaining := len(operands) - suffixMinimum[i+1]
|
||||
if minimum > remaining {
|
||||
suffixMinimum[i] = len(operands)
|
||||
} else {
|
||||
suffixMinimum[i] = suffixMinimum[i+1] + minimum
|
||||
}
|
||||
}
|
||||
|
||||
allocated := make([][]string, len(definitions))
|
||||
position := 0
|
||||
for i, argument := range definitions {
|
||||
binding := argument.binding
|
||||
remaining := len(operands) - position
|
||||
available := remaining - suffixMinimum[i+1]
|
||||
if available < 0 {
|
||||
available = 0
|
||||
}
|
||||
count := available
|
||||
if binding.max >= 0 && count > binding.max {
|
||||
count = binding.max
|
||||
}
|
||||
if !enforceRequired && count < binding.min && remaining > 0 {
|
||||
count = remaining
|
||||
if binding.max >= 0 && count > binding.max {
|
||||
count = binding.max
|
||||
}
|
||||
}
|
||||
if enforceRequired && count < binding.min {
|
||||
return nil, fmt.Errorf("not enough values for argument %q", binding.meta.Name)
|
||||
}
|
||||
allocated[i] = operands[position : position+count]
|
||||
position += count
|
||||
}
|
||||
if position < len(operands) {
|
||||
return nil, fmt.Errorf("received %d extra positional arguments", len(operands)-position)
|
||||
}
|
||||
return allocated, nil
|
||||
}
|
||||
208
binding_test.go
Normal file
208
binding_test.go
Normal file
@@ -0,0 +1,208 @@
|
||||
package commandkit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type bindingInput struct {
|
||||
message Supplied[string]
|
||||
count Supplied[int]
|
||||
enabled Supplied[bool]
|
||||
tags Supplied[[]string]
|
||||
runtime string
|
||||
}
|
||||
|
||||
func TestScalarBindingsKeepDefaultsSeparateFromPresence(t *testing.T) {
|
||||
defaultMessage := "fallback"
|
||||
factoryCalls := 0
|
||||
spec := CommandSpec[bindingInput]{
|
||||
Name: "run",
|
||||
NewInput: func() bindingInput {
|
||||
factoryCalls++
|
||||
return bindingInput{runtime: "caller state"}
|
||||
},
|
||||
Flags: []Flag[bindingInput]{
|
||||
ValueFlag(FlagMeta{Name: "message"}, &defaultMessage, func(value string) (string, error) { return value, nil }, func(input *bindingInput, value Supplied[string]) { input.message = value }),
|
||||
ValueFlag(FlagMeta{Name: "count"}, (*int)(nil), strconv.Atoi, func(input *bindingInput, value Supplied[int]) { input.count = value }),
|
||||
BoolFlag(FlagMeta{Name: "enabled"}, true, func(input *bindingInput, value Supplied[bool]) { input.enabled = value }),
|
||||
},
|
||||
}
|
||||
if _, err := NewCommand(spec); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defaultMessage = "changed after declaration"
|
||||
prepared, err := prepareInput(spec, [][]string{nil, nil, nil}, nil, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if factoryCalls != 0 {
|
||||
t.Fatal("preparing typed values invoked NewInput")
|
||||
}
|
||||
input := prepared.build()
|
||||
if factoryCalls != 1 {
|
||||
t.Fatalf("NewInput calls = %d, want 1", factoryCalls)
|
||||
}
|
||||
if input.message.Value != "fallback" || input.message.Set {
|
||||
t.Fatalf("omitted message = %#v, want fallback with Set=false", input.message)
|
||||
}
|
||||
if input.count.Value != 0 || input.count.Set {
|
||||
t.Fatalf("omitted count = %#v, want zero with Set=false", input.count)
|
||||
}
|
||||
if !input.enabled.Value || input.enabled.Set {
|
||||
t.Fatalf("omitted enabled = %#v, want true default with Set=false", input.enabled)
|
||||
}
|
||||
if input.runtime != "caller state" {
|
||||
t.Fatalf("NewInput runtime value = %q, want caller value", input.runtime)
|
||||
}
|
||||
|
||||
prepared, err = prepareInput(spec, [][]string{{"", "last"}, {"0"}, {"true", "false"}}, nil, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
input = prepared.build()
|
||||
if input.message.Value != "last" || !input.message.Set {
|
||||
t.Fatalf("message = %#v, want last supplied value", input.message)
|
||||
}
|
||||
if input.count.Value != 0 || !input.count.Set {
|
||||
t.Fatalf("count = %#v, want explicit zero", input.count)
|
||||
}
|
||||
if input.enabled.Value || !input.enabled.Set {
|
||||
t.Fatalf("enabled = %#v, want explicit false", input.enabled)
|
||||
}
|
||||
|
||||
prepared, err = prepareInput(spec, [][]string{{""}, nil, nil}, nil, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
input = prepared.build()
|
||||
if input.message.Value != "" || !input.message.Set {
|
||||
t.Fatalf("message = %#v, want explicitly supplied empty string", input.message)
|
||||
}
|
||||
prepared, err = prepareInput(spec, [][]string{nil, nil, nil}, nil, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
input = prepared.build()
|
||||
if input.message.Value != "fallback" || input.message.Set || input.count.Set || input.enabled.Set {
|
||||
t.Fatalf("omitted values after a supplied invocation = %#v", input)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBoolFlagRejectsExplicitEmptyValue(t *testing.T) {
|
||||
flag := BoolFlag(FlagMeta{Name: "enabled"}, false, func(*bool, Supplied[bool]) {})
|
||||
_, err := flag.binding.prepare([]string{""})
|
||||
if err == nil {
|
||||
t.Fatal("BoolFlag accepted an explicitly empty value")
|
||||
}
|
||||
if _, err := flag.binding.prepare([]string{"true"}); err != nil {
|
||||
t.Fatalf("BoolFlag rejected the presence value: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
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 })
|
||||
defaults[0] = "changed after declaration"
|
||||
|
||||
applyDefault, err := flag.binding.prepare(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
first := &bindingInput{}
|
||||
applyDefault(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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSingletonParsesEveryOccurrenceAndPreservesCallerCause(t *testing.T) {
|
||||
parseFailure := errors.New("bad number")
|
||||
flag := ValueFlag[struct{}, int](FlagMeta{Name: "count"}, nil, func(value string) (int, error) {
|
||||
if value == "bad" {
|
||||
return 0, parseFailure
|
||||
}
|
||||
return strconv.Atoi(value)
|
||||
}, func(*struct{}, Supplied[int]) {})
|
||||
_, err := flag.binding.prepare([]string{"bad", "9"})
|
||||
if !errors.Is(err, parseFailure) {
|
||||
t.Fatalf("parse error = %v, want caller cause", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepeatArgumentAllocationReservesLaterMinimums(t *testing.T) {
|
||||
type operandsInput struct {
|
||||
prefix Supplied[[]string]
|
||||
id Supplied[string]
|
||||
suffix Supplied[[]string]
|
||||
}
|
||||
spec := CommandSpec[operandsInput]{
|
||||
Name: "restore",
|
||||
Arguments: []Argument[operandsInput]{
|
||||
RepeatArgument(ArgumentMeta{Name: "prefix"}, 0, 2, func(value string) (string, error) { return value, nil }, func(input *operandsInput, value Supplied[[]string]) { input.prefix = value }),
|
||||
ValueArgument(ArgumentMeta{Name: "id"}, true, func(value string) (string, error) { return value, nil }, func(input *operandsInput, value Supplied[string]) { input.id = value }),
|
||||
RepeatArgument(ArgumentMeta{Name: "suffix"}, 1, -1, func(value string) (string, error) { return value, nil }, func(input *operandsInput, value Supplied[[]string]) { input.suffix = value }),
|
||||
},
|
||||
}
|
||||
if _, err := NewCommand(spec); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
prepared, err := prepareInput(spec, nil, []string{"a", "b", "id-1", "x", "y"}, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
input := prepared.build()
|
||||
if !reflect.DeepEqual(input.prefix.Value, []string{"a", "b"}) || !input.prefix.Set {
|
||||
t.Fatalf("prefix = %#v", input.prefix)
|
||||
}
|
||||
if input.id.Value != "id-1" || !input.id.Set {
|
||||
t.Fatalf("id = %#v", input.id)
|
||||
}
|
||||
if !reflect.DeepEqual(input.suffix.Value, []string{"x", "y"}) || !input.suffix.Set {
|
||||
t.Fatalf("suffix = %#v", input.suffix)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncompletePreparationSkipsMissingRequirementsAndInputFactory(t *testing.T) {
|
||||
factoryCalls := 0
|
||||
spec := CommandSpec[struct{}]{
|
||||
Name: "show",
|
||||
NewInput: func() struct{} {
|
||||
factoryCalls++
|
||||
return struct{}{}
|
||||
},
|
||||
Flags: []Flag[struct{}]{ValueFlag(FlagMeta{Name: "required", Required: true}, (*string)(nil), func(value string) (string, error) { return value, nil }, func(*struct{}, Supplied[string]) {})},
|
||||
Arguments: []Argument[struct{}]{ValueArgument(ArgumentMeta{Name: "id"}, true, func(value string) (string, error) { return value, nil }, func(*struct{}, Supplied[string]) {})},
|
||||
}
|
||||
if _, err := NewCommand(spec); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := prepareInput(spec, [][]string{nil}, nil, true); err == nil {
|
||||
t.Fatal("strict preparation accepted missing requirements")
|
||||
}
|
||||
if _, err := prepareInput(spec, [][]string{nil}, nil, false); err != nil {
|
||||
t.Fatalf("help preparation rejected omitted execution inputs: %v", err)
|
||||
}
|
||||
if factoryCalls != 0 {
|
||||
t.Fatalf("preparation invoked NewInput %d times", factoryCalls)
|
||||
}
|
||||
}
|
||||
50
command.go
50
command.go
@@ -3,6 +3,7 @@ package commandkit
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// Command is an opaque command declaration. Use NewCommand or Group to create
|
||||
@@ -49,9 +50,12 @@ type Flag[I any] struct {
|
||||
}
|
||||
|
||||
type flagBinding[I any] struct {
|
||||
meta FlagMeta
|
||||
prepare func([]string) (func(*I), error)
|
||||
valid bool
|
||||
meta FlagMeta
|
||||
defaultText string
|
||||
repeated bool
|
||||
boolean bool
|
||||
prepare func([]string) (func(*I), error)
|
||||
valid bool
|
||||
}
|
||||
|
||||
// Argument represents a typed binding for a positional command argument.
|
||||
@@ -91,7 +95,7 @@ type CommandSpec[I any] struct {
|
||||
type commandPayload interface {
|
||||
validate() error
|
||||
hasRun() bool
|
||||
newInput() any
|
||||
prepare([][]string, []string, bool) (func() any, error)
|
||||
run(Invocation, any) Outcome
|
||||
}
|
||||
|
||||
@@ -107,12 +111,15 @@ func (c typedCommand[I]) hasRun() bool {
|
||||
return c.spec.Run != nil
|
||||
}
|
||||
|
||||
func (c typedCommand[I]) newInput() any {
|
||||
var input I
|
||||
if c.spec.NewInput != nil {
|
||||
input = c.spec.NewInput()
|
||||
func (c typedCommand[I]) prepare(flagValues [][]string, operands []string, enforceRequired bool) (func() any, error) {
|
||||
prepared, err := prepareInput(c.spec, flagValues, operands, enforceRequired)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &input
|
||||
return func() any {
|
||||
input := prepared.build()
|
||||
return &input
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c typedCommand[I]) run(invocation Invocation, input any) Outcome {
|
||||
@@ -222,8 +229,8 @@ func validateBindings[I any](flags []Flag[I], arguments []Argument[I]) error {
|
||||
shortNames := make(map[string]struct{}, len(flags))
|
||||
for _, flag := range flags {
|
||||
binding := flag.binding
|
||||
if binding.meta.Name == "" || strings.ContainsAny(binding.meta.Name, " \t\r\n") || strings.HasPrefix(binding.meta.Name, "-") {
|
||||
return fmt.Errorf("flag name %q must be a nonempty token without leading dashes", binding.meta.Name)
|
||||
if !validFlagName(binding.meta.Name) {
|
||||
return fmt.Errorf("flag name %q must use lowercase hyphen-separated words", binding.meta.Name)
|
||||
}
|
||||
if binding.meta.Name == "help" {
|
||||
return fmt.Errorf("flag name %q is reserved for help", binding.meta.Name)
|
||||
@@ -263,3 +270,24 @@ func validateBindings[I any](flags []Flag[I], arguments []Argument[I]) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validFlagName(name string) bool {
|
||||
if name == "" || name[0] == '-' || name[len(name)-1] == '-' {
|
||||
return false
|
||||
}
|
||||
segmentHasCharacter := false
|
||||
for _, character := range name {
|
||||
if character == '-' {
|
||||
if !segmentHasCharacter {
|
||||
return false
|
||||
}
|
||||
segmentHasCharacter = false
|
||||
continue
|
||||
}
|
||||
if !unicode.IsLower(character) && !unicode.IsDigit(character) {
|
||||
return false
|
||||
}
|
||||
segmentHasCharacter = true
|
||||
}
|
||||
return segmentHasCharacter
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Initial CommandKit implementation plan
|
||||
|
||||
Status: implementation underway; Stages 1-2 complete and Stages 3-14 pending.
|
||||
Status: implementation underway; Stages 1-3 complete and Stages 4-14 pending.
|
||||
|
||||
Date: 2026-10-07.
|
||||
|
||||
@@ -239,7 +239,7 @@ version reservations, and atomic per-App registration. Verified with `go test ./
|
||||
|
||||
## Stage 3: implement typed bindings and input assembly
|
||||
|
||||
**Status:** pending. **Prerequisite:** Stage 2.
|
||||
**Status:** complete. **Prerequisite:** Stage 2.
|
||||
|
||||
Add root factories in `binding.go` using these signatures:
|
||||
|
||||
@@ -288,6 +288,10 @@ last-wins, repeated order and comma-containing strings, defaults, positional
|
||||
allocation, caller parse errors, and fresh collections/input state. Adapter-level
|
||||
integration of these cases belongs to Stage 4/5; avoid duplicating every case.
|
||||
|
||||
**Completion evidence:** binding behavior tests and `go test ./...` pass. Input
|
||||
factories remain deferred until callers build a prepared input; help-mode
|
||||
preparation parses supplied values without enforcing omitted required values.
|
||||
|
||||
## Stage 4: implement the internal Cobra adapter
|
||||
|
||||
**Status:** pending. **Prerequisite:** Stage 3.
|
||||
|
||||
@@ -186,6 +186,9 @@ func TestNewRejectsEmptyIdentityAndInvalidBindingMetadata(t *testing.T) {
|
||||
{validFlag("one", "x"), validFlag("one", "y")},
|
||||
{validFlag("one", "x"), validFlag("two", "x")},
|
||||
{validFlag("help", "")},
|
||||
{validFlag("SomeFlag", "")},
|
||||
{validFlag("some_flag", "")},
|
||||
{validFlag("some--flag", "")},
|
||||
{{binding: flagBinding[registryInput]{meta: FlagMeta{Name: "missing"}}}},
|
||||
} {
|
||||
if _, err := NewCommand(CommandSpec[registryInput]{Name: "flags", Flags: flags}); err == nil {
|
||||
|
||||
Reference in New Issue
Block a user