Add captured flag value application
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Implement the optional integration modules and captured registration API
|
# Implement the optional integration modules and captured registration API
|
||||||
|
|
||||||
Status: implementation in progress. Stages 1 through 3 are complete; subsequent work follows the ordered roadmap.
|
Status: implementation in progress. Stages 1 through 4 are complete; subsequent work follows the ordered roadmap.
|
||||||
Execute the stages in order. Each stage is sized as a separate Luna coding-agent
|
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
|
assignment. This document authorizes no publication or changes to sibling
|
||||||
repositories.
|
repositories.
|
||||||
@@ -254,10 +254,14 @@ checks. No new grammar restriction was added.
|
|||||||
materialize or apply the target's input. Scope inapplicability never skips
|
materialize or apply the target's input. Scope inapplicability never skips
|
||||||
supplied-value conversion.
|
supplied-value conversion.
|
||||||
|
|
||||||
**Completion evidence:** original/copied/lookalike identity, defaults and presence,
|
**Completion evidence — complete:** `CaptureFlags` stores invocation-local
|
||||||
selected required flags, wrapped errors, nil destination, empty selection,
|
prepared values by original binding identity, and `ApplyTo` accepts copied handles
|
||||||
repeatable independence, and no second conversion. Check both ordinary and
|
while rejecting lookalikes, uncaptured handles, and nil destinations with
|
||||||
captured bindings through the same preparation path.
|
`ErrFlagWiring`. Required checks apply only to selected bindings and match
|
||||||
|
`ErrFlagInput`; defaults and explicit presence are retained. Nil accessors and
|
||||||
|
invalid inner bindings are rejected at installation. Repeat applications copy
|
||||||
|
core-owned slices without reconversion. Wrapped wiring/input errors route to
|
||||||
|
operational/usage outcomes respectively. Root tests pass.
|
||||||
|
|
||||||
## Stage 5: Compile typed runners and real built-in declarations
|
## Stage 5: Compile typed runners and real built-in declarations
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func (a *App) Execute(ctx context.Context, args []string, streams Streams) Outco
|
|||||||
}
|
}
|
||||||
input := buildInput()
|
input := buildInput()
|
||||||
if err := command.record.payload.checkInput(input); err != nil {
|
if err := command.record.payload.checkInput(input); err != nil {
|
||||||
return current.present(Usage(err), parsed.Path)
|
return current.present(classifyInputError(err), parsed.Path)
|
||||||
}
|
}
|
||||||
if len(parsed.Path) == 1 && parsed.Path[0] == "help" {
|
if len(parsed.Path) == 1 && parsed.Path[0] == "help" {
|
||||||
help, ok := input.(*helpInput)
|
help, ok := input.(*helpInput)
|
||||||
|
|||||||
89
flag_values.go
Normal file
89
flag_values.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package commandkit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrFlagInput identifies a missing required captured flag selected for an input.
|
||||||
|
var ErrFlagInput = errors.New("required flag input is missing")
|
||||||
|
|
||||||
|
// ErrFlagWiring identifies an invalid captured binding or destination.
|
||||||
|
var ErrFlagWiring = errors.New("flag wiring is invalid")
|
||||||
|
|
||||||
|
// FlagValues stores invocation-local values prepared for captured flags.
|
||||||
|
type FlagValues[I any] struct {
|
||||||
|
values map[*bindingIdentity]preparedValue[I]
|
||||||
|
}
|
||||||
|
|
||||||
|
// CaptureFlags adapts flags declared for I into flags on Outer. The returned
|
||||||
|
// flags preserve the original identities and store prepared values in target.
|
||||||
|
func CaptureFlags[Outer, I any](flags []Flag[I], target func(*Outer) *FlagValues[I]) []Flag[Outer] {
|
||||||
|
captured := make([]Flag[Outer], len(flags))
|
||||||
|
for index, original := range flags {
|
||||||
|
inner := original
|
||||||
|
identity := newBindingIdentity()
|
||||||
|
binding := flagBinding[Outer]{
|
||||||
|
identity: identity,
|
||||||
|
meta: inner.binding.meta,
|
||||||
|
defaultText: inner.binding.defaultText,
|
||||||
|
repeated: inner.binding.repeated,
|
||||||
|
boolean: inner.binding.boolean,
|
||||||
|
}
|
||||||
|
if target != nil && inner.binding.identity != nil && inner.binding.valid && inner.binding.prepare != nil {
|
||||||
|
binding.valid = true
|
||||||
|
binding.prepare = func(raw []string) (preparedValue[Outer], error) {
|
||||||
|
prepared, err := inner.binding.prepare(raw)
|
||||||
|
if err != nil {
|
||||||
|
return preparedValue[Outer]{}, err
|
||||||
|
}
|
||||||
|
return preparedValue[Outer]{
|
||||||
|
identity: identity,
|
||||||
|
supplied: prepared.supplied,
|
||||||
|
apply: func(outer *Outer) {
|
||||||
|
store := target(outer)
|
||||||
|
if store == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if store.values == nil {
|
||||||
|
store.values = make(map[*bindingIdentity]preparedValue[I])
|
||||||
|
}
|
||||||
|
store.values[inner.binding.identity] = prepared
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
captured[index] = Flag[Outer]{binding: binding}
|
||||||
|
}
|
||||||
|
return captured
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyTo applies selected captured flags to an initialized destination.
|
||||||
|
// Repeated core-owned values are copied for each application.
|
||||||
|
func (values FlagValues[I]) ApplyTo(target *I, selected ...Flag[I]) error {
|
||||||
|
if target == nil {
|
||||||
|
return fmt.Errorf("%w: destination is nil", ErrFlagWiring)
|
||||||
|
}
|
||||||
|
for _, flag := range selected {
|
||||||
|
binding := flag.binding
|
||||||
|
if binding.identity == nil {
|
||||||
|
return fmt.Errorf("%w: selected flag has no identity", ErrFlagWiring)
|
||||||
|
}
|
||||||
|
prepared, exists := values.values[binding.identity]
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("%w: flag --%s was not captured", ErrFlagWiring, binding.meta.Name)
|
||||||
|
}
|
||||||
|
if binding.meta.Required && !prepared.supplied {
|
||||||
|
return fmt.Errorf("flag --%s: %w", binding.meta.Name, ErrFlagInput)
|
||||||
|
}
|
||||||
|
prepared.apply(target)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func classifyInputError(err error) Outcome {
|
||||||
|
if errors.Is(err, ErrFlagWiring) {
|
||||||
|
return Failure(err)
|
||||||
|
}
|
||||||
|
return Usage(err)
|
||||||
|
}
|
||||||
144
flag_values_test.go
Normal file
144
flag_values_test.go
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
package commandkit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type capturedInput struct {
|
||||||
|
name Supplied[string]
|
||||||
|
tags Supplied[[]string]
|
||||||
|
}
|
||||||
|
|
||||||
|
type capturedOuter struct {
|
||||||
|
flags FlagValues[capturedInput]
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCaptureFlagsAppliesOnlyOriginalBindingWithoutReconversion(t *testing.T) {
|
||||||
|
parseCalls := 0
|
||||||
|
original := ValueFlag(FlagMeta{Name: "name"}, nil, func(value string) (string, error) {
|
||||||
|
parseCalls++
|
||||||
|
return value, nil
|
||||||
|
}, func(input *capturedInput, value Supplied[string]) { input.name = value })
|
||||||
|
copied := original
|
||||||
|
captured := CaptureFlags([]Flag[capturedInput]{original}, func(input *capturedOuter) *FlagValues[capturedInput] { return &input.flags })
|
||||||
|
prepared, err := captured[0].binding.prepare([]string{"chosen"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
outer := capturedOuter{}
|
||||||
|
prepared.apply(&outer)
|
||||||
|
var target capturedInput
|
||||||
|
if err := outer.flags.ApplyTo(&target, copied); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if target.name.Value != "chosen" || !target.name.Set || parseCalls != 1 {
|
||||||
|
t.Fatalf("target=%#v parse calls=%d", target, parseCalls)
|
||||||
|
}
|
||||||
|
lookalike := ValueFlag(FlagMeta{Name: "name"}, nil, func(value string) (string, error) { return value, nil }, func(*capturedInput, Supplied[string]) {})
|
||||||
|
if err := outer.flags.ApplyTo(&target, lookalike); !errors.Is(err, ErrFlagWiring) {
|
||||||
|
t.Fatalf("lookalike binding error = %v", err)
|
||||||
|
}
|
||||||
|
if err := outer.flags.ApplyTo(nil, copied); !errors.Is(err, ErrFlagWiring) {
|
||||||
|
t.Fatalf("nil target error = %v", err)
|
||||||
|
}
|
||||||
|
if err := (FlagValues[capturedInput]{}).ApplyTo(&target); err != nil {
|
||||||
|
t.Fatalf("empty selection from zero store: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCaptureFlagsDefersRequiredCheckToSelectedBindings(t *testing.T) {
|
||||||
|
required := ValueFlag(FlagMeta{Name: "name", Required: true}, strptr("default"), func(value string) (string, error) { return value, nil }, func(input *capturedInput, value Supplied[string]) { input.name = value })
|
||||||
|
captured := CaptureFlags([]Flag[capturedInput]{required}, func(input *capturedOuter) *FlagValues[capturedInput] { return &input.flags })
|
||||||
|
prepared, err := captured[0].binding.prepare(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
outer := capturedOuter{}
|
||||||
|
prepared.apply(&outer)
|
||||||
|
var target capturedInput
|
||||||
|
if err := outer.flags.ApplyTo(&target); err != nil {
|
||||||
|
t.Fatalf("unselected required flag failed: %v", err)
|
||||||
|
}
|
||||||
|
if err := outer.flags.ApplyTo(&target, required); !errors.Is(err, ErrFlagInput) {
|
||||||
|
t.Fatalf("selected missing required flag error = %v", err)
|
||||||
|
}
|
||||||
|
optional := ValueFlag(FlagMeta{Name: "name"}, strptr("default"), func(value string) (string, error) { return value, nil }, func(input *capturedInput, value Supplied[string]) { input.name = value })
|
||||||
|
optionalCapture := CaptureFlags([]Flag[capturedInput]{optional}, func(input *capturedOuter) *FlagValues[capturedInput] { return &input.flags })
|
||||||
|
optionalPrepared, err := optionalCapture[0].binding.prepare(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
outer = capturedOuter{}
|
||||||
|
optionalPrepared.apply(&outer)
|
||||||
|
target = capturedInput{}
|
||||||
|
if err := outer.flags.ApplyTo(&target, optional); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if target.name.Value != "default" || target.name.Set {
|
||||||
|
t.Fatalf("captured default value = %#v", target.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
badCapture := CaptureFlags[capturedOuter]([]Flag[capturedInput]{required}, nil)
|
||||||
|
command := NewCommand(CommandSpec[capturedOuter]{Name: "broken", Flags: badCapture})
|
||||||
|
if _, err := New(Identity{Name: "example"}, command); err == nil {
|
||||||
|
t.Fatal("installation accepted a nil captured-flag accessor")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCaptureFlagsCopiesRepeatedValuesOnEachApplication(t *testing.T) {
|
||||||
|
original := RepeatFlag(FlagMeta{Name: "tag"}, nil, func(value string) (string, error) { return value, nil }, func(input *capturedInput, value Supplied[[]string]) { input.tags = value })
|
||||||
|
captured := CaptureFlags([]Flag[capturedInput]{original}, func(input *capturedOuter) *FlagValues[capturedInput] { return &input.flags })
|
||||||
|
prepared, err := captured[0].binding.prepare([]string{"a,b", "c"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
outer := capturedOuter{}
|
||||||
|
prepared.apply(&outer)
|
||||||
|
var first, second capturedInput
|
||||||
|
if err := outer.flags.ApplyTo(&first, original); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := outer.flags.ApplyTo(&second, original); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
first.tags.Value[0] = "changed"
|
||||||
|
if second.tags.Value[0] != "a,b" || second.tags.Value[1] != "c" || !second.tags.Set {
|
||||||
|
t.Fatalf("second application shares repeat storage: %#v", second.tags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCapturedFlagErrorClassificationPreservesWrappedCategories(t *testing.T) {
|
||||||
|
if got := classifyInputError(fmt.Errorf("scope input: %w", ErrFlagWiring)); got.Kind != OutcomeOperational {
|
||||||
|
t.Fatalf("wiring outcome = %#v", got)
|
||||||
|
}
|
||||||
|
if got := classifyInputError(fmt.Errorf("scope input: %w", ErrFlagInput)); got.Kind != OutcomeUsage {
|
||||||
|
t.Fatalf("input outcome = %#v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckInputClassifiesWrappedFlagWiringAsOperational(t *testing.T) {
|
||||||
|
command := NewCommand(CommandSpec[struct{}]{
|
||||||
|
Name: "run",
|
||||||
|
CheckInput: func(struct{}) error { return fmt.Errorf("scope selector: %w", ErrFlagWiring) },
|
||||||
|
Run: func(Invocation, struct{}) Outcome { t.Fatal("run called after wiring error"); return Success() },
|
||||||
|
})
|
||||||
|
app, err := New(Identity{Name: "example"}, command)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if outcome := app.Execute(t.Context(), []string{"run"}, Streams{}); outcome.Kind != OutcomeOperational || !errors.Is(outcome.Cause, ErrFlagWiring) {
|
||||||
|
t.Fatalf("wrapped wiring outcome = %#v", outcome)
|
||||||
|
}
|
||||||
|
usage := NewCommand(CommandSpec[struct{}]{Name: "usage", CheckInput: func(struct{}) error { return fmt.Errorf("selected value: %w", ErrFlagInput) }, Run: func(Invocation, struct{}) Outcome { return Success() }})
|
||||||
|
app, err = New(Identity{Name: "example"}, usage)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if outcome := app.Execute(t.Context(), []string{"usage"}, Streams{}); outcome.Kind != OutcomeUsage || !errors.Is(outcome.Cause, ErrFlagInput) {
|
||||||
|
t.Fatalf("wrapped input outcome = %#v", outcome)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func strptr(value string) *string { return &value }
|
||||||
Reference in New Issue
Block a user