Add typed command input bindings

This commit is contained in:
2026-10-07 13:22:57 +00:00
parent f99be3257c
commit 82128463cd
5 changed files with 513 additions and 13 deletions

View File

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