Prepare configuration scope metadata
This commit is contained in:
109
config.go
109
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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
16
explain.go
16
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))
|
||||
}
|
||||
|
||||
38
selectors.go
38
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 {
|
||||
|
||||
16
show.go
16
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)
|
||||
}
|
||||
|
||||
16
sources.go
16
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)
|
||||
}
|
||||
|
||||
16
validate.go
16
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user