Add typed command input bindings
This commit is contained in:
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user