Simplify typed input preparation
This commit is contained in:
63
binding.go
63
binding.go
@@ -239,17 +239,6 @@ func (p preparedInput[I]) build() I {
|
|||||||
return 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) {
|
|
||||||
return prepareInputWithSpellings(spec, flagValues, nil, operands, enforceRequired)
|
|
||||||
}
|
|
||||||
|
|
||||||
func prepareInputWithSpellings[I any](spec CommandSpec[I], flagValues, flagSpellings [][]string, operands []string, enforceRequired bool) (preparedInput[I], error) {
|
|
||||||
return prepareInputWithOccurrences(spec, flagValues, flagSpellings, nil, operands, enforceRequired)
|
|
||||||
}
|
|
||||||
|
|
||||||
func prepareInputWithOccurrences[I any](spec CommandSpec[I], flagValues, flagSpellings [][]string, occurrences []commandmeta.FlagOccurrence, operands []string, enforceRequired bool) (preparedInput[I], error) {
|
func prepareInputWithOccurrences[I any](spec CommandSpec[I], flagValues, flagSpellings [][]string, occurrences []commandmeta.FlagOccurrence, operands []string, enforceRequired bool) (preparedInput[I], error) {
|
||||||
if len(flagValues) != len(spec.Flags) {
|
if len(flagValues) != len(spec.Flags) {
|
||||||
return preparedInput[I]{}, fmt.Errorf("received %d flag value sets for %d bindings", len(flagValues), len(spec.Flags))
|
return preparedInput[I]{}, fmt.Errorf("received %d flag value sets for %d bindings", len(flagValues), len(spec.Flags))
|
||||||
@@ -272,18 +261,7 @@ func prepareInputWithOccurrences[I any](spec CommandSpec[I], flagValues, flagSpe
|
|||||||
binding := flag.binding
|
binding := flag.binding
|
||||||
value, err := binding.prepare(flagValues[i])
|
value, err := binding.prepare(flagValues[i])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var occurrenceErr *flagOccurrenceError
|
return preparedInput[I]{}, attributeFlagConversionError(binding.meta.Name, flagSpellings[i], err)
|
||||||
if errors.As(err, &occurrenceErr) {
|
|
||||||
spelling := "--" + binding.meta.Name
|
|
||||||
if occurrenceErr.index >= 0 && occurrenceErr.index < len(flagSpellings[i]) && flagSpellings[i][occurrenceErr.index] != "" {
|
|
||||||
spelling = flagSpellings[i][occurrenceErr.index]
|
|
||||||
}
|
|
||||||
return preparedInput[I]{}, &flagConversionError{
|
|
||||||
binding: binding.meta.Name, spelling: spelling,
|
|
||||||
occurrence: occurrenceErr.index + 1, cause: occurrenceErr.cause,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return preparedInput[I]{}, fmt.Errorf("flag --%s: %w", binding.meta.Name, err)
|
|
||||||
}
|
}
|
||||||
preparedFlags[i] = value
|
preparedFlags[i] = value
|
||||||
for index, occurrence := range value.ordered {
|
for index, occurrence := range value.ordered {
|
||||||
@@ -315,7 +293,7 @@ func prepareInputWithOccurrences[I any](spec CommandSpec[I], flagValues, flagSpe
|
|||||||
if missingRequired != "" {
|
if missingRequired != "" {
|
||||||
return preparedInput[I]{}, fmt.Errorf("required flag --%s was not supplied", missingRequired)
|
return preparedInput[I]{}, fmt.Errorf("required flag --%s was not supplied", missingRequired)
|
||||||
}
|
}
|
||||||
argumentValues, err := allocateOperands(operands, spec.Arguments, enforceRequired)
|
arguments, err := prepareOperands(operands, spec.Arguments, enforceRequired)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return preparedInput[I]{}, err
|
return preparedInput[I]{}, err
|
||||||
}
|
}
|
||||||
@@ -327,17 +305,38 @@ func prepareInputWithOccurrences[I any](spec CommandSpec[I], flagValues, flagSpe
|
|||||||
preparedValues = append(preparedValues, preparedValue[I]{apply: occurrence.apply})
|
preparedValues = append(preparedValues, preparedValue[I]{apply: occurrence.apply})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for i, argument := range spec.Arguments {
|
preparedValues = append(preparedValues, arguments...)
|
||||||
binding := argument.binding
|
|
||||||
value, err := binding.prepare(argumentValues[i])
|
|
||||||
if err != nil {
|
|
||||||
return preparedInput[I]{}, fmt.Errorf("argument %q: %w", binding.meta.Name, err)
|
|
||||||
}
|
|
||||||
preparedValues = append(preparedValues, value)
|
|
||||||
}
|
|
||||||
return preparedInput[I]{newInput: spec.NewInput, values: preparedValues}, nil
|
return preparedInput[I]{newInput: spec.NewInput, values: preparedValues}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func attributeFlagConversionError(name string, spellings []string, err error) error {
|
||||||
|
var occurrenceErr *flagOccurrenceError
|
||||||
|
if errors.As(err, &occurrenceErr) {
|
||||||
|
spelling := "--" + name
|
||||||
|
if occurrenceErr.index >= 0 && occurrenceErr.index < len(spellings) && spellings[occurrenceErr.index] != "" {
|
||||||
|
spelling = spellings[occurrenceErr.index]
|
||||||
|
}
|
||||||
|
return &flagConversionError{binding: name, spelling: spelling, occurrence: occurrenceErr.index + 1, cause: occurrenceErr.cause}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("flag --%s: %w", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareOperands[I any](operands []string, definitions []Argument[I], enforceRequired bool) ([]preparedValue[I], error) {
|
||||||
|
values, err := allocateOperands(operands, definitions, enforceRequired)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
prepared := make([]preparedValue[I], 0, len(definitions))
|
||||||
|
for i, argument := range definitions {
|
||||||
|
value, err := argument.binding.prepare(values[i])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("argument %q: %w", argument.binding.meta.Name, err)
|
||||||
|
}
|
||||||
|
prepared = append(prepared, value)
|
||||||
|
}
|
||||||
|
return prepared, nil
|
||||||
|
}
|
||||||
|
|
||||||
func newBindingIdentity() *bindingIdentity { return &bindingIdentity{marker: 1} }
|
func newBindingIdentity() *bindingIdentity { return &bindingIdentity{marker: 1} }
|
||||||
|
|
||||||
func allocateOperands[I any](operands []string, definitions []Argument[I], enforceRequired bool) ([][]string, error) {
|
func allocateOperands[I any](operands []string, definitions []Argument[I], enforceRequired bool) ([][]string, error) {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func TestScalarBindingsKeepDefaultsSeparateFromPresence(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defaultMessage = "changed after declaration"
|
defaultMessage = "changed after declaration"
|
||||||
prepared, err := prepareInput(spec, [][]string{nil, nil, nil}, nil, true)
|
prepared, err := prepareInputWithOccurrences(spec, [][]string{nil, nil, nil}, nil, nil, nil, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -60,7 +60,7 @@ func TestScalarBindingsKeepDefaultsSeparateFromPresence(t *testing.T) {
|
|||||||
t.Fatalf("NewInput runtime value = %q, want caller value", input.runtime)
|
t.Fatalf("NewInput runtime value = %q, want caller value", input.runtime)
|
||||||
}
|
}
|
||||||
|
|
||||||
prepared, err = prepareInput(spec, [][]string{{"", "last"}, {"0"}, {"true", "false"}}, nil, true)
|
prepared, err = prepareInputWithOccurrences(spec, [][]string{{"", "last"}, {"0"}, {"true", "false"}}, nil, nil, nil, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@ func TestScalarBindingsKeepDefaultsSeparateFromPresence(t *testing.T) {
|
|||||||
t.Fatalf("enabled = %#v, want explicit false", input.enabled)
|
t.Fatalf("enabled = %#v, want explicit false", input.enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
prepared, err = prepareInput(spec, [][]string{{""}, nil, nil}, nil, true)
|
prepared, err = prepareInputWithOccurrences(spec, [][]string{{""}, nil, nil}, nil, nil, nil, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -83,7 +83,7 @@ func TestScalarBindingsKeepDefaultsSeparateFromPresence(t *testing.T) {
|
|||||||
if input.message.Value != "" || !input.message.Set {
|
if input.message.Value != "" || !input.message.Set {
|
||||||
t.Fatalf("message = %#v, want explicitly supplied empty string", input.message)
|
t.Fatalf("message = %#v, want explicitly supplied empty string", input.message)
|
||||||
}
|
}
|
||||||
prepared, err = prepareInput(spec, [][]string{nil, nil, nil}, nil, true)
|
prepared, err = prepareInputWithOccurrences(spec, [][]string{nil, nil, nil}, nil, nil, nil, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -207,7 +207,7 @@ func TestRepeatArgumentAllocationReservesLaterMinimums(t *testing.T) {
|
|||||||
if err := validateCommandDeclaration(NewCommand(spec)); err != nil {
|
if err := validateCommandDeclaration(NewCommand(spec)); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
prepared, err := prepareInput(spec, nil, []string{"a", "b", "id-1", "x", "y"}, true)
|
prepared, err := prepareInputWithOccurrences(spec, nil, nil, nil, []string{"a", "b", "id-1", "x", "y"}, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -237,10 +237,10 @@ func TestIncompletePreparationSkipsMissingRequirementsAndInputFactory(t *testing
|
|||||||
if err := validateCommandDeclaration(NewCommand(spec)); err != nil {
|
if err := validateCommandDeclaration(NewCommand(spec)); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if _, err := prepareInput(spec, [][]string{nil}, nil, true); err == nil {
|
if _, err := prepareInputWithOccurrences(spec, [][]string{nil}, nil, nil, nil, true); err == nil {
|
||||||
t.Fatal("strict preparation accepted missing requirements")
|
t.Fatal("strict preparation accepted missing requirements")
|
||||||
}
|
}
|
||||||
if _, err := prepareInput(spec, [][]string{nil}, nil, false); err != nil {
|
if _, err := prepareInputWithOccurrences(spec, [][]string{nil}, nil, nil, nil, false); err != nil {
|
||||||
t.Fatalf("help preparation rejected omitted execution inputs: %v", err)
|
t.Fatalf("help preparation rejected omitted execution inputs: %v", err)
|
||||||
}
|
}
|
||||||
if factoryCalls != 0 {
|
if factoryCalls != 0 {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Consolidated audit catalog
|
# Consolidated audit catalog
|
||||||
|
|
||||||
Status: **6 distinct open findings: 6 P3**, consolidated from 30
|
Status: **4 distinct open findings: 4 P3**, consolidated from 30
|
||||||
numbered entries in seven audit documents. Reviewed audit state:
|
numbered entries in seven audit documents. Reviewed audit state:
|
||||||
`db732e3a1de55c844f77870e1c6db683c2685d5d`.
|
`db732e3a1de55c844f77870e1c6db683c2685d5d`.
|
||||||
|
|
||||||
@@ -412,6 +412,10 @@ their purpose; no generic operation engine or rule registry is selected.
|
|||||||
|
|
||||||
## F19 — P3: Typed input preparation mixes conversion, provenance, and replay assembly
|
## F19 — P3: Typed input preparation mixes conversion, provenance, and replay assembly
|
||||||
|
|
||||||
|
**Complete.** Conversion-error attribution and operand preparation are extracted
|
||||||
|
without replacing flag ordering or replay. Focused conversion, occurrence,
|
||||||
|
capture and replay tests pass.
|
||||||
|
|
||||||
`prepareInputWithOccurrences` handles shape validation, conversion-error
|
`prepareInputWithOccurrences` handles shape validation, conversion-error
|
||||||
spelling, occurrence identities, application order, required flags, operands,
|
spelling, occurrence identities, application order, required flags, operands,
|
||||||
and prepared callbacks. Captured binding replay depends on those shared
|
and prepared callbacks. Captured binding replay depends on those shared
|
||||||
@@ -515,6 +519,10 @@ snapshot errors and operator-input validation.
|
|||||||
|
|
||||||
## F24 — P3: Obsolete private production entry points remain for tests alone
|
## F24 — P3: Obsolete private production entry points remain for tests alone
|
||||||
|
|
||||||
|
**Complete.** Reporter selection/finalization, routed parsing and occurrence-aware
|
||||||
|
preparation replace the obsolete wrappers. Graph and text checks confirm their
|
||||||
|
removal; useful lifecycle and replay tests pass.
|
||||||
|
|
||||||
Legacy reporter setters/close, `App.parse`, and typed-preparation wrappers have
|
Legacy reporter setters/close, `App.parse`, and typed-preparation wrappers have
|
||||||
only test callers, while production uses current selection, finalization,
|
only test callers, while production uses current selection, finalization,
|
||||||
routed-session, and occurrence-aware paths. Some tests therefore exercise
|
routed-session, and occurrence-aware paths. Some tests therefore exercise
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Audit remediation implementation plan
|
# Audit remediation implementation plan
|
||||||
|
|
||||||
Status: in progress; Stages 1–19 complete.
|
Status: in progress; Stages 1–20 complete.
|
||||||
|
|
||||||
Baseline: `d492c7747175c01a53261206c2b4356e53453621`. Before this sprint,
|
Baseline: `d492c7747175c01a53261206c2b4356e53453621`. Before this sprint,
|
||||||
intervening changes recorded decisions and retired the completed older
|
intervening changes recorded decisions and retired the completed older
|
||||||
@@ -629,6 +629,13 @@ Complete when each capture responsibility has a clear place and behavior holds.
|
|||||||
|
|
||||||
## Stage 20 — Simplify typed preparation and retire remaining test wrappers
|
## Stage 20 — Simplify typed preparation and retire remaining test wrappers
|
||||||
|
|
||||||
|
**Complete.** Conversion-error attribution and operand preparation are separate
|
||||||
|
private responsibilities (F19); obsolete reporter, parser and binding wrappers
|
||||||
|
are removed and test-only observation lives in tests (F24). Graph discovery and
|
||||||
|
text references confirmed the current occurrence-aware callers and absence of
|
||||||
|
the retired entry points. Focused binding, replay, occurrence, conversion, routed
|
||||||
|
parsing and reporter tests passed offline with Go 1.27.0 and `-mod=readonly`.
|
||||||
|
|
||||||
Findings: F19 and completion of F24. Sources: `binding.go`, `flag_values.go`,
|
Findings: F19 and completion of F24. Sources: `binding.go`, `flag_values.go`,
|
||||||
`parser.go` and related tests; review reporter cleanup from stage 3 as well.
|
`parser.go` and related tests; review reporter cleanup from stage 3 as well.
|
||||||
|
|
||||||
|
|||||||
@@ -639,12 +639,6 @@ func (state *reportingState) presentationLocked() PresentationResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (state *reportingState) presentation() PresentationResult {
|
|
||||||
state.mu.Lock()
|
|
||||||
defer state.mu.Unlock()
|
|
||||||
return state.presentationLocked()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (state *reportingState) deliveryResultLocked(destination reportingDestination) DeliveryResult {
|
func (state *reportingState) deliveryResultLocked(destination reportingDestination) DeliveryResult {
|
||||||
current := state.destination(destination)
|
current := state.destination(destination)
|
||||||
result := DeliveryResult{RenderError: current.renderError, WriteError: current.writeError}
|
result := DeliveryResult{RenderError: current.renderError, WriteError: current.writeError}
|
||||||
|
|||||||
@@ -747,3 +747,9 @@ func TestReporterFinalizationReleasesBorrowedDestinations(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (state *reportingState) presentation() PresentationResult {
|
||||||
|
state.mu.Lock()
|
||||||
|
defer state.mu.Unlock()
|
||||||
|
return state.presentationLocked()
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user