diff --git a/config.go b/config.go index 23998a8..53093c7 100644 --- a/config.go +++ b/config.go @@ -97,6 +97,14 @@ type scopesInventory struct { Scopes []configScopeInfo `yaml:"scopes"` } +type preparedConfigSuite[C any] struct { + suite ConfigSuite[C] + ordered []ConfigScope[C] + byName map[string]ConfigScope[C] + installed []ConfigOperation + inventory scopesInventory +} + type configScopesInput struct { Format string } @@ -135,42 +143,27 @@ func RegisterConfig[C any](app *App, suite ConfigSuite[C]) error { if _, exists := app.commands["config"]; exists { return fmt.Errorf("command path %q is already registered", "config") } + prepared := prepareConfigSuite(normalized) commands := make([]Command, 0, 5) - installed := make([]ConfigOperation, 0, 4) - if configOperationDeclared(normalized.Scopes, ConfigValidate) { - validate, err := newConfigValidateCommand(normalized) + for _, operation := range prepared.installed { + var command Command + switch operation { + case ConfigValidate: + command, err = newConfigValidateCommand(prepared) + case ConfigShow: + command, err = newConfigShowCommand(prepared) + case ConfigSources: + command, err = newConfigSourcesCommand(prepared) + case ConfigExplain: + command, err = newConfigExplainCommand(prepared) + } if err != nil { return err } - commands = append(commands, validate) - installed = append(installed, ConfigValidate) + commands = append(commands, command) } - if configOperationDeclared(normalized.Scopes, ConfigShow) { - show, err := newConfigShowCommand(normalized) - if err != nil { - return err - } - commands = append(commands, show) - installed = append(installed, ConfigShow) - } - if configOperationDeclared(normalized.Scopes, ConfigSources) { - sources, err := newConfigSourcesCommand(normalized) - if err != nil { - return err - } - commands = append(commands, sources) - installed = append(installed, ConfigSources) - } - if configOperationDeclared(normalized.Scopes, ConfigExplain) { - explain, err := newConfigExplainCommand(normalized) - if err != nil { - return err - } - commands = append(commands, explain) - installed = append(installed, ConfigExplain) - } - command, err := newConfigScopesCommandWithScopes(normalized.Scopes, installed) + command, err := newConfigScopesCommandWithPrepared(prepared) if err != nil { return err } @@ -290,6 +283,8 @@ func normalizeConfigSuite[C any](suite ConfigSuite[C]) (ConfigSuite[C], error) { scope.Operations = operations normalized.Scopes = append(normalized.Scopes, scope) } + seenNames := make(map[string]struct{}, len(normalized.Selectors)) + seenShorts := make(map[string]struct{}, len(normalized.Selectors)) for index := range normalized.Selectors { selector := &normalized.Selectors[index] binding := selector.Binding.binding @@ -321,16 +316,48 @@ func normalizeConfigSuite[C any](suite ConfigSuite[C]) (ConfigSuite[C], error) { unique = append(unique, scopeName) } selector.Scopes = append([]string(nil), unique...) - for previous := 0; previous < index; previous++ { - other := normalized.Selectors[previous].Binding.binding.meta - if other.Name == binding.meta.Name || binding.meta.Short != "" && other.Short == binding.meta.Short { + if _, exists := seenNames[binding.meta.Name]; exists { + return ConfigSuite[C]{}, fmt.Errorf("duplicate configuration selector flag --%s", binding.meta.Name) + } + if binding.meta.Short != "" { + if _, exists := seenShorts[binding.meta.Short]; exists { return ConfigSuite[C]{}, fmt.Errorf("duplicate configuration selector flag --%s", binding.meta.Name) } + seenShorts[binding.meta.Short] = struct{}{} } + seenNames[binding.meta.Name] = struct{}{} } return normalized, nil } +func prepareConfigSuite[C any](suite ConfigSuite[C]) *preparedConfigSuite[C] { + prepared := &preparedConfigSuite[C]{ + suite: suite, + ordered: orderedConfigScopes(suite.Scopes), + byName: make(map[string]ConfigScope[C], len(suite.Scopes)), + installed: make([]ConfigOperation, 0, 4), + inventory: scopesInventory{Scopes: make([]configScopeInfo, 0, len(suite.Scopes))}, + } + for _, scope := range suite.Scopes { + prepared.byName[scope.Name] = scope + } + for _, operation := range []ConfigOperation{ConfigValidate, ConfigShow, ConfigSources, ConfigExplain} { + if configOperationDeclared(suite.Scopes, operation) { + prepared.installed = append(prepared.installed, operation) + } + } + for _, scope := range prepared.ordered { + operations := make([]ConfigOperation, 0, len(scope.Operations)) + for _, operation := range scope.Operations { + if containsOperation(prepared.installed, operation) { + operations = append(operations, operation) + } + } + prepared.inventory.Scopes = append(prepared.inventory.Scopes, configScopeInfo{Name: scope.Name, Operations: operations}) + } + return prepared +} + func normalizeConfigOperations(operations []ConfigOperation) ([]ConfigOperation, error) { seen := make(map[ConfigOperation]struct{}, len(operations)) for _, operation := range operations { @@ -350,7 +377,7 @@ func normalizeConfigOperations(operations []ConfigOperation) ([]ConfigOperation, return ordered, nil } -func newConfigScopesCommandWithScopes[C any](scopes []ConfigScope[C], installed []ConfigOperation) (Command, error) { +func newConfigScopesCommandWithPrepared[C any](prepared *preparedConfigSuite[C]) (Command, error) { formatDefault := "text" spec := CommandSpec[configScopesInput]{ Name: "scopes", @@ -362,22 +389,12 @@ func newConfigScopesCommandWithScopes[C any](scopes []ConfigScope[C], installed })}, CheckInput: func(input configScopesInput) error { return checkConfigFormat(input.Format) }, Run: func(invocation Invocation, input configScopesInput) Outcome { - inventory := scopesInventory{Scopes: make([]configScopeInfo, 0, len(scopes))} - for _, scope := range orderedConfigScopes(scopes) { - operations := make([]ConfigOperation, 0, len(scope.Operations)) - for _, operation := range scope.Operations { - if containsOperation(installed, operation) { - operations = append(operations, operation) - } - } - inventory.Scopes = append(inventory.Scopes, configScopeInfo{Name: scope.Name, Operations: operations}) - } var output []byte var err error if input.Format == "yaml" { - output, err = yaml.Marshal(inventory) + output, err = yaml.Marshal(prepared.inventory) } else { - output = []byte(formatScopeInventory(inventory)) + output = []byte(formatScopeInventory(prepared.inventory)) } if err != nil { return Failure(err) diff --git a/config_test.go b/config_test.go index 7171943..1025112 100644 --- a/config_test.go +++ b/config_test.go @@ -85,6 +85,77 @@ func TestConfigInputDefaultsToAppAndRetainsAbsentSelections(t *testing.T) { } } +func TestRegisterConfigOwnsPreparedScopeAndSelectorMetadata(t *testing.T) { + type selectorInput struct{ Region Supplied[string] } + var acquired selectorInput + suite := ConfigSuite[selectorInput]{ + Selectors: []Selector[selectorInput]{{ + Binding: ValueFlag[selectorInput](FlagMeta{Name: "region"}, nil, parseString, func(input *selectorInput, value Supplied[string]) { input.Region = value }), + Scopes: []string{"app"}, + }}, + Scopes: []ConfigScope[selectorInput]{{ + Name: "app", + Operations: []ConfigOperation{ConfigValidate}, + Acquire: func(_ context.Context, _ ConfigRequest, input selectorInput) (Acquired[ConfigView], error) { + acquired = input + return Acquired[ConfigView]{}, nil + }, + }}, + } + app, err := New(Identity{Name: "example"}) + if err != nil { + t.Fatal(err) + } + if err := RegisterConfig(app, suite); err != nil { + t.Fatal(err) + } + + suite.Scopes[0].Name = "renamed" + suite.Scopes[0].Operations[0] = ConfigShow + suite.Selectors[0].Scopes[0] = "renamed" + + var inventory bytes.Buffer + if result := app.Execute(context.Background(), []string{"config", "scopes"}, Streams{Out: &inventory}); result.Kind != OutcomeSuccess { + t.Fatalf("scope inventory outcome = %#v", result) + } + if got := inventory.String(); got != "app: validate\n" { + t.Fatalf("scope inventory after caller mutation = %q", got) + } + if result := app.Execute(context.Background(), []string{"config", "validate", "--scope=app", "--region=west"}, Streams{}); result.Kind != OutcomeSuccess { + t.Fatalf("registered scope execution after caller mutation = %#v", result) + } + if acquired.Region != (Supplied[string]{Value: "west", Set: true}) { + t.Fatalf("acquired selector after caller mutation = %#v", acquired) + } +} + +func TestRegisterConfigRejectsDuplicateSelectorNamesAndShorts(t *testing.T) { + type selectorInput struct{ First, Second string } + tests := []struct { + name string + firstMeta FlagMeta + secondMeta FlagMeta + }{ + {name: "name", firstMeta: FlagMeta{Name: "region", Short: "r"}, secondMeta: FlagMeta{Name: "region", Short: "g"}}, + {name: "short", firstMeta: FlagMeta{Name: "region", Short: "r"}, secondMeta: FlagMeta{Name: "group", Short: "r"}}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + app, err := New(Identity{Name: "example"}) + if err != nil { + t.Fatal(err) + } + suite := ConfigSuite[selectorInput]{Selectors: []Selector[selectorInput]{ + {Binding: ValueFlag[selectorInput](test.firstMeta, nil, parseString, func(input *selectorInput, value Supplied[string]) { input.First = value.Value })}, + {Binding: ValueFlag[selectorInput](test.secondMeta, nil, parseString, func(input *selectorInput, value Supplied[string]) { input.Second = value.Value })}, + }} + if err := RegisterConfig(app, suite); err == nil || !strings.Contains(err.Error(), "duplicate configuration selector flag") { + t.Fatalf("RegisterConfig error = %v", err) + } + }) + } +} + func TestRegisterConfigRejectsInvalidSuitesAndCollisions(t *testing.T) { tests := []struct { name string @@ -147,13 +218,14 @@ func TestConfigSelectorPreflightAndScopedContextProjection(t *testing.T) { if err != nil { t.Fatal(err) } + preparedSuite := prepareConfigSuite(normalized) spec := CommandSpec[configInput[selectorInput]]{Flags: configInputFlags(normalized, "text")} prepared, err := prepareInput(spec, [][]string{{"all"}, {"text"}, {""}, nil, {"0"}, {"false"}, nil}, nil, true) if err != nil { t.Fatal(err) } input := prepared.build() - scopeNames, err := preflightConfig(normalized, input, ConfigValidate) + scopeNames, err := preflightConfig(preparedSuite, input, ConfigValidate) if err != nil { t.Fatal(err) } @@ -167,7 +239,7 @@ func TestConfigSelectorPreflightAndScopedContextProjection(t *testing.T) { t.Fatalf("NewContext called during preflight %d times", baseContextCalls) } - contexts := contextsForScopes(normalized, input, scopeNames) + contexts := contextsForScopes(preparedSuite, input, scopeNames) if baseContextCalls != 1 { t.Fatalf("NewContext calls = %d, want one", baseContextCalls) } @@ -194,11 +266,12 @@ func TestConfigPreflightRejectsUnsupportedAggregateAndInapplicableSelector(t *te if err != nil { t.Fatal(err) } + preparedSuite := prepareConfigSuite(normalized) prepared, err := prepareInput(CommandSpec[configInput[selectorInput]]{Flags: configInputFlags(normalized, "text")}, [][]string{{"all"}, {"text"}, nil, nil, nil}, nil, true) if err != nil { t.Fatal(err) } - if _, err := preflightConfig(normalized, prepared.build(), ConfigValidate); err == nil || !strings.Contains(err.Error(), "does not support validate") { + if _, err := preflightConfig(preparedSuite, prepared.build(), ConfigValidate); err == nil || !strings.Contains(err.Error(), "does not support validate") { t.Fatalf("unsupported aggregate error = %v", err) } if newContextCalls != 0 { @@ -209,7 +282,7 @@ func TestConfigPreflightRejectsUnsupportedAggregateAndInapplicableSelector(t *te if err != nil { t.Fatal(err) } - if _, err := preflightConfig(normalized, prepared.build(), ConfigValidate); err == nil || !strings.Contains(err.Error(), "not applicable") { + if _, err := preflightConfig(preparedSuite, prepared.build(), ConfigValidate); err == nil || !strings.Contains(err.Error(), "not applicable") { t.Fatalf("inapplicable selector error = %v", err) } } diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 24bec89..1b512ec 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -779,7 +779,7 @@ allocation costs, with timing variation on repeated success and deep dispatch. ## Stage 10: prepare configuration scope metadata once -**Status:** not started. **Prerequisite:** Stage 9. +**Status:** complete. **Prerequisite:** Stage 9. **Files:** [config.go](../../config.go), [selectors.go](../../selectors.go), configuration handler files, configuration/cancellation tests, scope benchmarks, @@ -821,7 +821,58 @@ Preserve native reporting and per-operation output timing. scope lookup is indexed, and duplicate selector checks are linear in selector count plus declared scope references. -**Evidence:** pending. +**Evidence:** `GOWORK=off GOTOOLCHAIN=local GOPROXY=off GONOPROXY=none +GOSUMDB=off go test -mod=readonly -count=1 .` passed, including existing +configuration, selector, discovery, and cancellation behavior. Added coverage +that caller mutations to scope and selector slices after registration do not +change discovery, capability selection, or projected inputs, and that duplicate +selector names and short names retain their error category. Registration now +normalizes owned metadata, prepares the ordered scopes, name index, installed +operation list, and discovery DTO once, then passes that private value through +validate/show/sources/explain and their preflight/run paths. Aggregate selection +checks every capability using the prepared metadata before any acquisition. +`NewContext`, acquisition, checks, native writers, and encoding are not called +while preparing the suite. Discovery renders the retained DTO at invocation +time. Scope sorting occurs once, named lookup uses the map, and duplicate +selector validation uses seen-name/seen-short maps while preserving validation +order and deduplicating scope references. Public `ConfigSuite` and `ConfigScope` +declarations and signatures are unchanged. + +Measurements used Go 1.26.8, linux/amd64, Linux 6.17.0-41-generic, Intel Core +i5-7260U CPU @ 2.20 GHz, based on source revision `a7960e5` with this stage's +changes in the working tree. The five-sample command passed: +`GOWORK=off GOTOOLCHAIN=local GOPROXY=off GONOPROXY=none GOSUMDB=off go test -mod=readonly -run '^$' -bench '^BenchmarkPerformance(ConfigScopes|ConfigRegistration)$' -benchmem -benchtime=200ms -count=5 .`. +Raw output is in `/tmp/commandkit-performance-config-after-stage10.txt`; +focused repeats for selector-heavy registration and the ordinary two-scope +aggregate are in `/tmp/commandkit-performance-config-registration-repeat.txt` +and `/tmp/commandkit-performance-config-two-scope-repeat.txt`. + +Against Stage 2, two-scope discovery measured 16,788–21,062 ns/op, 18,557 +B/op, and 195 allocations (baseline: 23,840–30,007 ns/op, 21,161–21,162 +B/op, 223 allocations). Named selection measured 18,375–21,756 ns/op, +19,621 B/op, and 207 allocations (baseline: 24,841–26,340 ns/op, 21,817 +B/op, 228 allocations). Two-scope aggregate validation measured 22,977–28,152 +ns/op, 19,637–19,638 B/op, and 208 allocations in the focused repeat (baseline: +25,231–31,126 ns/op, 22,361–22,362 B/op, 237 allocations). + +At 32 scopes, discovery measured 18,253–20,311 ns/op, 21,070 B/op, and 199 +allocations (baseline: 31,439–37,167 ns/op, 28,141–28,142 B/op, 257 +allocations). Named selection measured 18,679–20,529 ns/op, 20,582 B/op, and +207 allocations (baseline: 29,123–74,374 ns/op, 22,775–22,778 B/op, 228 +allocations). Aggregate validation measured 25,036–33,981 ns/op, 22,702 B/op, +and 240 allocations (baseline: 45,548–63,471 ns/op, 29,781–29,782 B/op, 269 +allocations). + +Registration with 4 selectors measured 11,272–14,623 ns/op, 9,310 B/op, and +107 allocations (baseline: 12,573–14,789 ns/op, 8,911 B/op, 104 allocations). +With 64 selectors, the focused repeat measured 72,002–74,712 ns/op, 90,197– +90,200 B/op, and 494 allocations (baseline: 91,675–105,548 ns/op, +82,817–82,818 B/op, 485 allocations). The retained ordered slice, lookup map, +and discovery DTO add registration storage: about 399 bytes and 3 allocations +for 4 selectors, and 7.4 KiB and 9 allocations for 64 selectors. Selector-heavy +registration timing improved in the focused repeat; execution ranges improved +for discovery and named/aggregate selection. These benchmark timings vary and +are observations, not thresholds. ## Stage 11: audit the completed target and reconcile documentation diff --git a/explain.go b/explain.go index 87dcbbd..8820d10 100644 --- a/explain.go +++ b/explain.go @@ -9,8 +9,8 @@ import ( configkit "gitea.maximumdirect.net/eric/ConfigKit" ) -func newConfigExplainCommand[C any](suite ConfigSuite[C]) (Command, error) { - flags := configInputFlags(suite, "text") +func newConfigExplainCommand[C any](prepared *preparedConfigSuite[C]) (Command, error) { + flags := configInputFlags(prepared.suite, "text") flags = append(flags, ValueFlag[configInput[C]](FlagMeta{ Name: "path", Usage: "Configuration path as a JSON Pointer; an empty value selects the root.", @@ -23,7 +23,7 @@ func newConfigExplainCommand[C any](suite ConfigSuite[C]) (Command, error) { Summary: "Explain the effective and historical value at a configuration path.", Flags: flags, CheckInput: func(input configInput[C]) error { - if _, err := preflightConfig(suite, input, ConfigExplain); err != nil { + if _, err := preflightConfig(prepared, input, ConfigExplain); err != nil { return err } if !input.Path.Set { @@ -35,7 +35,7 @@ func newConfigExplainCommand[C any](suite ConfigSuite[C]) (Command, error) { return nil }, Run: func(invocation Invocation, input configInput[C]) Outcome { - return runConfigExplanation(invocation, suite, input) + return runConfigExplanation(invocation, prepared, input) }, } return NewCommand(spec) @@ -82,11 +82,11 @@ type secretReferenceDTO struct { Name string `yaml:"name"` } -func runConfigExplanation[C any](invocation Invocation, suite ConfigSuite[C], input configInput[C]) Outcome { +func runConfigExplanation[C any](invocation Invocation, prepared *preparedConfigSuite[C], input configInput[C]) Outcome { if err := invocation.Context.Err(); err != nil { return Failure(err) } - scopeNames, err := selectConfigScopes(suite, input, ConfigExplain) + scopeNames, err := selectConfigScopes(prepared, input, ConfigExplain) if err != nil { return Failure(err) } @@ -98,8 +98,8 @@ func runConfigExplanation[C any](invocation Invocation, suite ConfigSuite[C], in return Usage(fmt.Errorf("invalid configuration path %q: %w", input.Path.Value, err)) } name := scopeNames[0] - contexts := contextsForScopes(suite, input, scopeNames) - scope, ok := findConfigScope(suite.Scopes, name) + contexts := contextsForScopes(prepared, input, scopeNames) + scope, ok := findConfigScope(prepared, name) if !ok || scope.Acquire == nil { return Failure(fmt.Errorf("configuration scope %q has no acquisition callback", name)) } diff --git a/selectors.go b/selectors.go index 385daad..755e6c1 100644 --- a/selectors.go +++ b/selectors.go @@ -82,20 +82,20 @@ func configurationSelectionFlags[I any](setConfig, setOverlay func(*I, Supplied[ func parseString(value string) (string, error) { return value, nil } -func preflightConfig[C any](suite ConfigSuite[C], input configInput[C], operation ConfigOperation) ([]string, error) { - scopes, err := selectConfigScopes(suite, input, operation) +func preflightConfig[C any](prepared *preparedConfigSuite[C], input configInput[C], operation ConfigOperation) ([]string, error) { + scopes, err := selectConfigScopes(prepared, input, operation) if err != nil { return nil, err } - if suite.CheckInput != nil { - if err := suite.CheckInput(input.Selection, input.SelectorContext); err != nil { + if prepared.suite.CheckInput != nil { + if err := prepared.suite.CheckInput(input.Selection, input.SelectorContext); err != nil { return nil, err } } return scopes, nil } -func selectConfigScopes[C any](suite ConfigSuite[C], input configInput[C], operation ConfigOperation) ([]string, error) { +func selectConfigScopes[C any](prepared *preparedConfigSuite[C], input configInput[C], operation ConfigOperation) ([]string, error) { if err := checkConfigFormat(input.Format); err != nil { return nil, err } @@ -110,16 +110,16 @@ func selectConfigScopes[C any](suite ConfigSuite[C], input configInput[C], opera return nil, fmt.Errorf("configuration scope must not be empty") } - scopes := make([]string, 0, len(suite.Scopes)) + scopes := make([]string, 0, len(prepared.ordered)) if input.Scope == "all" { - for _, scope := range orderedConfigScopes(suite.Scopes) { + for _, scope := range prepared.ordered { scopes = append(scopes, scope.Name) } } else { - scope, ok := findConfigScope(suite.Scopes, input.Scope) + scope, ok := prepared.byName[input.Scope] if !ok { - choices := make([]string, 0, len(suite.Scopes)) - for _, available := range orderedConfigScopes(suite.Scopes) { + choices := make([]string, 0, len(prepared.ordered)) + for _, available := range prepared.ordered { choices = append(choices, available.Name) } return nil, fmt.Errorf("unknown configuration scope %q (available: %s)", input.Scope, joinStrings(choices)) @@ -132,7 +132,7 @@ func selectConfigScopes[C any](suite ConfigSuite[C], input configInput[C], opera if input.Scope == "all" { for _, scopeName := range scopes { - scope, _ := findConfigScope(suite.Scopes, scopeName) + scope := prepared.byName[scopeName] if !containsOperation(scope.Operations, operation) { return nil, unsupportedConfigOperation(scope, operation) } @@ -156,10 +156,10 @@ func selectConfigScopes[C any](suite ConfigSuite[C], input configInput[C], opera return scopes, nil } -func contextsForScopes[C any](suite ConfigSuite[C], input configInput[C], scopeNames []string) map[string]C { +func contextsForScopes[C any](prepared *preparedConfigSuite[C], input configInput[C], scopeNames []string) map[string]C { base := *new(C) - if suite.NewContext != nil { - base = suite.NewContext() + if prepared.suite.NewContext != nil { + base = prepared.suite.NewContext() } contexts := make(map[string]C, len(scopeNames)) for _, name := range scopeNames { @@ -176,13 +176,9 @@ func contextsForScopes[C any](suite ConfigSuite[C], input configInput[C], scopeN return contexts } -func findConfigScope[C any](scopes []ConfigScope[C], name string) (ConfigScope[C], bool) { - for _, scope := range scopes { - if scope.Name == name { - return scope, true - } - } - return ConfigScope[C]{}, false +func findConfigScope[C any](prepared *preparedConfigSuite[C], name string) (ConfigScope[C], bool) { + scope, ok := prepared.byName[name] + return scope, ok } func containsOperation(operations []ConfigOperation, requested ConfigOperation) bool { diff --git a/show.go b/show.go index 15bfff8..1b87d91 100644 --- a/show.go +++ b/show.go @@ -8,17 +8,17 @@ import ( "unicode" ) -func newConfigShowCommand[C any](suite ConfigSuite[C]) (Command, error) { +func newConfigShowCommand[C any](prepared *preparedConfigSuite[C]) (Command, error) { spec := CommandSpec[configInput[C]]{ Name: string(ConfigShow), Summary: "Show effective configuration as YAML.", - Flags: configInputFlags(suite, "yaml"), + Flags: configInputFlags(prepared.suite, "yaml"), CheckInput: func(input configInput[C]) error { - _, err := preflightConfig(suite, input, ConfigShow) + _, err := preflightConfig(prepared, input, ConfigShow) return err }, Run: func(invocation Invocation, input configInput[C]) Outcome { - return runConfigShow(invocation, suite, input) + return runConfigShow(invocation, prepared, input) }, } return NewCommand(spec) @@ -29,19 +29,19 @@ type effectiveDocument struct { view ConfigView } -func runConfigShow[C any](invocation Invocation, suite ConfigSuite[C], input configInput[C]) Outcome { +func runConfigShow[C any](invocation Invocation, prepared *preparedConfigSuite[C], input configInput[C]) Outcome { if err := invocation.Context.Err(); err != nil { return Failure(err) } - scopeNames, err := selectConfigScopes(suite, input, ConfigShow) + scopeNames, err := selectConfigScopes(prepared, input, ConfigShow) if err != nil { return Failure(err) } - contexts := contextsForScopes(suite, input, scopeNames) + contexts := contextsForScopes(prepared, input, scopeNames) documents := make([]effectiveDocument, 0, len(scopeNames)) diagnostics := make([]Diagnostic, 0) for _, name := range scopeNames { - scope, ok := findConfigScope(suite.Scopes, name) + scope, ok := findConfigScope(prepared, name) if !ok || scope.Acquire == nil { return showFailure(fmt.Errorf("configuration scope %q has no acquisition callback", name), diagnostics, false) } diff --git a/sources.go b/sources.go index 73931ab..eca87e1 100644 --- a/sources.go +++ b/sources.go @@ -6,17 +6,17 @@ import ( "strconv" ) -func newConfigSourcesCommand[C any](suite ConfigSuite[C]) (Command, error) { +func newConfigSourcesCommand[C any](prepared *preparedConfigSuite[C]) (Command, error) { spec := CommandSpec[configInput[C]]{ Name: string(ConfigSources), Summary: "List configuration sources used by a scope.", - Flags: configInputFlags(suite, "text"), + Flags: configInputFlags(prepared.suite, "text"), CheckInput: func(input configInput[C]) error { - _, err := preflightConfig(suite, input, ConfigSources) + _, err := preflightConfig(prepared, input, ConfigSources) return err }, Run: func(invocation Invocation, input configInput[C]) Outcome { - return runConfigSources(invocation, suite, input) + return runConfigSources(invocation, prepared, input) }, } return NewCommand(spec) @@ -43,17 +43,17 @@ type locationDTO struct { Column int `yaml:"column"` } -func runConfigSources[C any](invocation Invocation, suite ConfigSuite[C], input configInput[C]) Outcome { +func runConfigSources[C any](invocation Invocation, prepared *preparedConfigSuite[C], input configInput[C]) Outcome { if err := invocation.Context.Err(); err != nil { return Failure(err) } - scopeNames, err := selectConfigScopes(suite, input, ConfigSources) + scopeNames, err := selectConfigScopes(prepared, input, ConfigSources) if err != nil { return Failure(err) } - contexts := contextsForScopes(suite, input, scopeNames) + contexts := contextsForScopes(prepared, input, scopeNames) name := scopeNames[0] - scope, ok := findConfigScope(suite.Scopes, name) + scope, ok := findConfigScope(prepared, name) if !ok || scope.Acquire == nil { return sourceReportFailure(fmt.Errorf("configuration scope %q has no acquisition callback", name), nil, false) } diff --git a/validate.go b/validate.go index ba08773..ad6c802 100644 --- a/validate.go +++ b/validate.go @@ -17,35 +17,35 @@ func configOperationDeclared[C any](scopes []ConfigScope[C], operation ConfigOpe return false } -func newConfigValidateCommand[C any](suite ConfigSuite[C]) (Command, error) { +func newConfigValidateCommand[C any](prepared *preparedConfigSuite[C]) (Command, error) { spec := CommandSpec[configInput[C]]{ Name: string(ConfigValidate), Summary: "Validate selected configuration scopes.", - Flags: configInputFlags(suite, "text"), + Flags: configInputFlags(prepared.suite, "text"), CheckInput: func(input configInput[C]) error { - _, err := preflightConfig(suite, input, ConfigValidate) + _, err := preflightConfig(prepared, input, ConfigValidate) return err }, Run: func(invocation Invocation, input configInput[C]) Outcome { - return runConfigValidation(invocation, suite, input) + return runConfigValidation(invocation, prepared, input) }, } return NewCommand(spec) } -func runConfigValidation[C any](invocation Invocation, suite ConfigSuite[C], input configInput[C]) Outcome { +func runConfigValidation[C any](invocation Invocation, prepared *preparedConfigSuite[C], input configInput[C]) Outcome { if err := invocation.Context.Err(); err != nil { return Failure(err) } - scopeNames, err := selectConfigScopes(suite, input, ConfigValidate) + scopeNames, err := selectConfigScopes(prepared, input, ConfigValidate) if err != nil { return Failure(err) } - contexts := contextsForScopes(suite, input, scopeNames) + contexts := contextsForScopes(prepared, input, scopeNames) diagnostics := make([]Diagnostic, 0) writtenRecord := false for _, name := range scopeNames { - scope, ok := findConfigScope(suite.Scopes, name) + scope, ok := findConfigScope(prepared, name) if !ok || scope.Acquire == nil { return validationFailure(fmt.Errorf("configuration scope %q has no acquisition callback", name), diagnostics, writtenRecord) }