diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index bfece74..ac88033 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # Implement the optional integration modules and captured registration API -Status: implementation in progress. Stages 1 through 8 are complete; subsequent work follows the ordered roadmap. +Status: implementation in progress. Stages 1 through 9 are complete; subsequent work follows the ordered roadmap. 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 repositories. @@ -447,10 +447,20 @@ invocation-local preparation path. needs no placeholder input/state types or forwarding wrapper. A nil loader retains a config/app construction error. -**Completion evidence:** metadata-only suites, no scopes, partial capabilities, -all preflight, unused bindings versus invalid adapters, scope-aware validation, -independently initialized pointer inputs, preserved initialized inapplicable -fields, shared state once, and no callbacks on discovery/help/version. +**Completion evidence — complete:** `Command` eagerly captures the suite, +operation capabilities, scope metadata, selector applicability, callbacks, and +retained scope issues into a root-only `config` declaration. It validates named +scope presence/uniqueness, reserved `all`, operation values, enabled acquisition +wiring, and selector scope references. Generated inspection commands capture +only selector bindings used by scopes that support that operation; metadata-only +suites still provide ordered `config scopes` discovery and do not initialize +suite input/state or call providers. Shared preflight resolves format, scope, +aggregate availability, and explain pointers before per-scope input factories. +Each selected scope receives a fresh input, only its applicable original flags, +and scope-aware validation; successful inputs remain on the invocation-local +plan for the report handlers. `AppCommand` uses the same suite factory and +retains a nil original loader issue. Package execution checks wait for the +operation handlers and replacement of the relocated prototype tests. ## Stage 10: Port configuration reports with operation-specific timing diff --git a/integrations/ckconfig/family.go b/integrations/ckconfig/family.go new file mode 100644 index 0000000..86ff2e2 --- /dev/null +++ b/integrations/ckconfig/family.go @@ -0,0 +1,312 @@ +package ckconfig + +import ( + "context" + "errors" + "fmt" + "io" + "sort" + "strings" + + commandkit "gitea.maximumdirect.net/eric/CommandKit" + configkit "gitea.maximumdirect.net/eric/ConfigKit" + "go.yaml.in/yaml/v4" +) + +type scopeMetadata struct { + Name string `yaml:"name"` + Operations []Operation `yaml:"operations"` +} + +type scopeInventory struct { + Scopes []scopeMetadata `yaml:"scopes"` +} + +type preparedScope[I, S any] struct { + scope Scope[I, S] + selectorIndexes []int +} + +type preparedSuite[I, S any] struct { + newInput func() I + newState func() S + checkInput func(string, Selection, I) error + scopes []preparedScope[I, S] + byName map[string]*preparedScope[I, S] + selectors []Selector[I] + operations []Operation + inventory scopeInventory + issues []error +} + +// Command eagerly captures a configuration suite as a root-only config family. +// Its declaration can be reused in multiple applications; suite factories and +// providers run only during the corresponding invocation path. +func Command[I, S any](suite Suite[I, S]) commandkit.Command { + prepared := captureSuite(suite) + if len(prepared.scopes) == 0 { + prepared.issues = append(prepared.issues, errors.New("configuration suite must declare at least one named scope")) + return commandkit.WithError(commandkit.NoCommand(), errors.Join(prepared.issues...)) + } + + children := make([]commandkit.Command, 0, len(prepared.operations)+1) + for _, operation := range prepared.operations { + children = append(children, newConfigOperationCommand(prepared, operation)) + } + children = append(children, newConfigScopesCommand(prepared)) + declaration := commandkit.RootGroup("config", "Inspect configuration.", children...) + if len(prepared.issues) > 0 { + declaration = commandkit.WithError(declaration, errors.Join(prepared.issues...)) + } + return declaration +} + +// AppCommand builds the state-free convenience suite for application +// configuration. The application consumer supplies only its ConfigKit loader. +func AppCommand[T any](load func(context.Context, Selection) (*configkit.Result[T], error)) commandkit.Command { + var adapterLoad func(context.Context, Selection, struct{}, struct{}) (*configkit.Result[T], error) + if load != nil { + adapterLoad = func(ctx context.Context, selection Selection, _ struct{}, _ struct{}) (*configkit.Result[T], error) { + return load(ctx, selection) + } + } + adapted := FromConfigKit[struct{}, struct{}, T]("app", "Application configuration.", adapterLoad) + return Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{adapted}}) +} + +func captureSuite[I, S any](suite Suite[I, S]) *preparedSuite[I, S] { + prepared := &preparedSuite[I, S]{ + newInput: suite.NewInput, + newState: suite.NewState, + checkInput: suite.CheckInput, + byName: make(map[string]*preparedScope[I, S], len(suite.Scopes)), + selectors: make([]Selector[I], len(suite.Selectors)), + } + for index, selector := range suite.Selectors { + selector.Scopes = append([]string(nil), selector.Scopes...) + prepared.selectors[index] = selector + } + + seenNames := make(map[string]struct{}, len(suite.Scopes)) + for _, original := range suite.Scopes { + scope := original + scope.Operations = normalizeOperations(scope.Operations, &prepared.issues, scope.Name) + scope.issues = append([]error(nil), original.issues...) + if scope.Name == "" { + prepared.issues = append(prepared.issues, errors.New("configuration scope name must not be empty")) + } + if scope.Name == "all" { + prepared.issues = append(prepared.issues, fmt.Errorf("configuration scope name %q is reserved", scope.Name)) + } + if _, exists := seenNames[scope.Name]; exists { + prepared.issues = append(prepared.issues, fmt.Errorf("duplicate configuration scope %q", scope.Name)) + } else { + seenNames[scope.Name] = struct{}{} + } + for _, issue := range scope.issues { + prepared.issues = append(prepared.issues, fmt.Errorf("configuration scope %q: %w", scope.Name, issue)) + } + if len(scope.Operations) > 0 && scope.Acquire == nil { + prepared.issues = append(prepared.issues, fmt.Errorf("configuration scope %q declares operations without an acquisition callback", scope.Name)) + } + prepared.scopes = append(prepared.scopes, preparedScope[I, S]{scope: scope}) + } + // Appending above may move the backing array. Rebuild the lookup only after + // the ordered scope slice is complete. + prepared.byName = make(map[string]*preparedScope[I, S], len(prepared.scopes)) + for index := range prepared.scopes { + prepared.byName[prepared.scopes[index].scope.Name] = &prepared.scopes[index] + } + + for index, selector := range prepared.selectors { + seen := make(map[string]struct{}, len(selector.Scopes)) + uniqueScopes := make([]string, 0, len(selector.Scopes)) + for _, name := range selector.Scopes { + if _, exists := seenNames[name]; !exists { + prepared.issues = append(prepared.issues, fmt.Errorf("configuration selector %d references unknown scope %q", index, name)) + } + if _, exists := seen[name]; exists { + continue + } + seen[name] = struct{}{} + uniqueScopes = append(uniqueScopes, name) + } + prepared.selectors[index].Scopes = uniqueScopes + if len(selector.Scopes) == 0 { + for scopeIndex := range prepared.scopes { + if len(prepared.scopes[scopeIndex].scope.Operations) > 0 { + prepared.scopes[scopeIndex].selectorIndexes = append(prepared.scopes[scopeIndex].selectorIndexes, index) + } + } + } else { + for _, name := range uniqueScopes { + if scope, exists := prepared.byName[name]; exists && len(scope.scope.Operations) > 0 { + scope.selectorIndexes = append(scope.selectorIndexes, index) + } + } + } + } + + for _, operation := range []Operation{Validate, Show, Sources, Explain} { + for _, scope := range prepared.scopes { + if containsOperation(scope.scope.Operations, operation) { + prepared.operations = append(prepared.operations, operation) + break + } + } + } + for _, scope := range orderedScopeRecords(prepared.scopes) { + prepared.inventory.Scopes = append(prepared.inventory.Scopes, scopeMetadata{ + Name: scope.scope.Name, + Operations: append([]Operation(nil), scope.scope.Operations...), + }) + } + return prepared +} + +func normalizeOperations(operations []Operation, issues *[]error, scope string) []Operation { + seen := make(map[Operation]struct{}, len(operations)) + for _, operation := range operations { + switch operation { + case Validate, Show, Sources, Explain: + default: + *issues = append(*issues, fmt.Errorf("configuration scope %q declares unknown operation %q", scope, operation)) + } + seen[operation] = struct{}{} + } + ordered := make([]Operation, 0, len(seen)) + for _, operation := range []Operation{Validate, Show, Sources, Explain} { + if _, exists := seen[operation]; exists { + ordered = append(ordered, operation) + } + } + return ordered +} + +func orderedScopeRecords[I, S any](scopes []preparedScope[I, S]) []preparedScope[I, S] { + ordered := append([]preparedScope[I, S](nil), scopes...) + sort.SliceStable(ordered, func(i, j int) bool { + return scopeOrder(ordered[i].scope.Name) < scopeOrder(ordered[j].scope.Name) + }) + return ordered +} + +func scopeOrder(name string) string { + switch name { + case "app": + return "\x00" + case "promptkit": + return "\x01" + default: + return "\x02" + name + } +} + +func containsOperation(operations []Operation, requested Operation) bool { + for _, operation := range operations { + if operation == requested { + return true + } + } + return false +} + +func selectorApplies(scopes []string, name string) bool { + if len(scopes) == 0 { + return true + } + for _, scope := range scopes { + if scope == name { + return true + } + } + return false +} + +func joinNames(names []string) string { + ordered := append([]string(nil), names...) + sort.Strings(ordered) + if len(ordered) == 0 { + return "none" + } + return strings.Join(ordered, ", ") +} + +func newConfigScopesCommand[I, S any](prepared *preparedSuite[I, S]) commandkit.Command { + formatDefault := "text" + spec := commandkit.CommandSpec[scopeInventoryInput]{ + Name: "scopes", + Summary: "List configuration scopes and supported operations.", + Flags: []commandkit.Flag[scopeInventoryInput]{commandkit.ValueFlag( + commandkit.FlagMeta{Name: "format", Usage: "Output format: text or yaml."}, &formatDefault, parseConfigString, + func(input *scopeInventoryInput, value commandkit.Supplied[string]) { input.Format = value.Value }, + )}, + CheckInput: func(input scopeInventoryInput) error { return checkConfigFormat(input.Format) }, + Run: func(invocation commandkit.Invocation, input scopeInventoryInput) commandkit.Outcome { + var output []byte + var err error + if input.Format == "yaml" { + output, err = yaml.Marshal(prepared.inventory) + } else { + output = []byte(formatScopeInventory(prepared.inventory)) + } + if err != nil { + return commandkit.Failure(err) + } + if err := writeConfigReport(invocation.Streams.Out, output); err != nil { + return incompleteConfigReport(err) + } + return commandkit.Success() + }, + } + return commandkit.NewCommand(spec) +} + +type scopeInventoryInput struct{ Format string } + +type scopeInventoryYAML struct { + Scopes []scopeMetadata `yaml:"scopes"` +} + +func formatScopeInventory(inventory scopeInventory) string { + var output strings.Builder + for _, scope := range inventory.Scopes { + output.WriteString(scope.Name) + output.WriteString(": ") + for index, operation := range scope.Operations { + if index > 0 { + output.WriteString(", ") + } + output.WriteString(string(operation)) + } + output.WriteByte('\n') + } + return output.String() +} + +func checkConfigFormat(format string) error { + if format != "text" && format != "yaml" { + return fmt.Errorf("unsupported configuration format %q", format) + } + return nil +} + +func parseConfigString(value string) (string, error) { return value, nil } + +func writeConfigReport(writer io.Writer, content []byte) error { + written, err := writer.Write(content) + if err != nil { + return err + } + if written != len(content) { + return io.ErrShortWrite + } + return nil +} + +func incompleteConfigReport(err error) commandkit.Outcome { + outcome := commandkit.Failure(err) + outcome.ReportIncomplete = true + outcome.DeliveryError = err + return outcome +} diff --git a/integrations/ckconfig/input.go b/integrations/ckconfig/input.go new file mode 100644 index 0000000..2900b69 --- /dev/null +++ b/integrations/ckconfig/input.go @@ -0,0 +1,216 @@ +package ckconfig + +import ( + "fmt" + + commandkit "gitea.maximumdirect.net/eric/CommandKit" + configkit "gitea.maximumdirect.net/eric/ConfigKit" +) + +type configCommandInput[I, S any] struct { + selection Selection + scope commandkit.Supplied[string] + format commandkit.Supplied[string] + path commandkit.Supplied[string] + values commandkit.FlagValues[I] + plan *configOperationPlan[I, S] +} + +type selectedScopeInput[I, S any] struct { + scope *preparedScope[I, S] + input I +} + +type configOperationPlan[I, S any] struct { + prepared *preparedSuite[I, S] + operation Operation + selection Selection + scopeName string + format string + path configkit.Path + scopes []selectedScopeInput[I, S] +} + +func newConfigOperationCommand[I, S any](prepared *preparedSuite[I, S], operation Operation) commandkit.Command { + defaultScope := "app" + defaultFormat := "text" + if operation == Show { + defaultFormat = "yaml" + } + flags := []commandkit.Flag[*configCommandInput[I, S]]{ + commandkit.ValueFlag(commandkit.FlagMeta{Name: "scope", Usage: "Configuration scope name or all where supported."}, &defaultScope, parseConfigString, + func(input **configCommandInput[I, S], value commandkit.Supplied[string]) { (*input).scope = value }), + commandkit.ValueFlag(commandkit.FlagMeta{Name: "format", Usage: "Output format."}, &defaultFormat, parseConfigString, + func(input **configCommandInput[I, S], value commandkit.Supplied[string]) { (*input).format = value }), + commandkit.ValueFlag(commandkit.FlagMeta{Name: "config", Usage: "Configuration source."}, (*string)(nil), parseConfigString, + func(input **configCommandInput[I, S], value commandkit.Supplied[string]) { + (*input).selection.Config = value + }), + commandkit.ValueFlag(commandkit.FlagMeta{Name: "overlay", Usage: "Overlay source."}, (*string)(nil), parseConfigString, + func(input **configCommandInput[I, S], value commandkit.Supplied[string]) { + (*input).selection.Overlay = value + }), + } + if operation == Explain { + flags = append(flags, commandkit.ValueFlag(commandkit.FlagMeta{Name: "path", Usage: "JSON Pointer path to explain.", Required: true}, (*string)(nil), parseConfigString, + func(input **configCommandInput[I, S], value commandkit.Supplied[string]) { (*input).path = value })) + } + selectorIndexes := selectorIndexesForOperation(prepared, operation) + selectorBindings := make([]commandkit.Flag[I], 0, len(selectorIndexes)) + for _, index := range selectorIndexes { + selectorBindings = append(selectorBindings, prepared.selectors[index].Binding) + } + flags = append(flags, commandkit.CaptureFlags(selectorBindings, func(input **configCommandInput[I, S]) *commandkit.FlagValues[I] { + if input == nil || *input == nil { + return nil + } + return &(*input).values + })...) + + name, summary := operationDeclaration(operation) + return commandkit.NewCommand(commandkit.CommandSpec[*configCommandInput[I, S]]{ + Name: name, Summary: summary, + NewInput: func() *configCommandInput[I, S] { return &configCommandInput[I, S]{} }, + Flags: flags, + CheckInput: func(input *configCommandInput[I, S]) error { + if input == nil { + return fmt.Errorf("configuration input must not be nil") + } + return prepareConfigOperation(prepared, operation, input) + }, + Run: func(invocation commandkit.Invocation, input *configCommandInput[I, S]) commandkit.Outcome { + return runConfigOperation(invocation, operation, input) + }, + }) +} + +func selectorIndexesForOperation[I, S any](prepared *preparedSuite[I, S], operation Operation) []int { + used := make([]int, 0, len(prepared.selectors)) + for index := range prepared.selectors { + for _, scope := range prepared.scopes { + if containsOperation(scope.scope.Operations, operation) && selectorApplies(prepared.selectors[index].Scopes, scope.scope.Name) { + used = append(used, index) + break + } + } + } + return used +} + +func operationDeclaration(operation Operation) (string, string) { + switch operation { + case Validate: + return "validate", "Validate configuration." + case Show: + return "show", "Show effective configuration." + case Sources: + return "sources", "List configuration sources." + case Explain: + return "explain", "Explain a configuration path." + default: + return string(operation), "Inspect configuration." + } +} + +func prepareConfigOperation[I, S any](prepared *preparedSuite[I, S], operation Operation, input *configCommandInput[I, S]) error { + format := input.format.Value + if err := checkConfigFormat(format); err != nil { + return err + } + if operation == Show && format != "yaml" { + return fmt.Errorf("unsupported configuration show format %q", format) + } + + scopeName := "app" + if input.scope.Set { + scopeName = input.scope.Value + } + if scopeName == "" { + return fmt.Errorf("configuration scope must not be empty") + } + if scopeName == "all" && operation != Validate && operation != Show { + return fmt.Errorf("scope all is supported only for validate and show") + } + + selected := make([]*preparedScope[I, S], 0, len(prepared.scopes)) + if scopeName == "all" { + for _, scope := range orderedScopeRecords(prepared.scopes) { + selected = append(selected, prepared.byName[scope.scope.Name]) + } + } else if scope, exists := prepared.byName[scopeName]; exists { + selected = append(selected, scope) + } else { + choices := make([]string, 0, len(prepared.scopes)) + for _, scope := range orderedScopeRecords(prepared.scopes) { + choices = append(choices, scope.scope.Name) + } + return fmt.Errorf("unknown configuration scope %q (available: %s)", scopeName, joinNames(choices)) + } + + for _, scope := range selected { + if !containsOperation(scope.scope.Operations, operation) { + return fmt.Errorf("configuration scope %q does not support %s (available: %s)", scope.scope.Name, operation, joinOperations(scope.scope.Operations)) + } + } + + var path configkit.Path + if operation == Explain { + if !input.path.Set { + return fmt.Errorf("configuration explanation requires --path") + } + parsed, err := configkit.ParsePath(input.path.Value) + if err != nil { + return fmt.Errorf("invalid configuration path: %w", err) + } + path = parsed + } + + plan := &configOperationPlan[I, S]{ + prepared: prepared, + operation: operation, + selection: input.selection, + scopeName: scopeName, + format: format, + path: path, + scopes: make([]selectedScopeInput[I, S], 0, len(selected)), + } + operationSelectors := selectorIndexesForOperation(prepared, operation) + capturedSelectors := make(map[int]struct{}, len(operationSelectors)) + for _, index := range operationSelectors { + capturedSelectors[index] = struct{}{} + } + for _, scope := range selected { + var value I + if prepared.newInput != nil { + value = prepared.newInput() + } + bindings := make([]commandkit.Flag[I], 0, len(scope.selectorIndexes)) + for _, index := range scope.selectorIndexes { + if _, ok := capturedSelectors[index]; ok { + bindings = append(bindings, prepared.selectors[index].Binding) + } + } + if err := input.values.ApplyTo(&value, bindings...); err != nil { + return fmt.Errorf("configuration scope %q: %w", scope.scope.Name, err) + } + if prepared.checkInput != nil { + if err := prepared.checkInput(scope.scope.Name, input.selection, value); err != nil { + return fmt.Errorf("configuration scope %q: %w", scope.scope.Name, err) + } + } + plan.scopes = append(plan.scopes, selectedScopeInput[I, S]{scope: scope, input: value}) + } + input.plan = plan + return nil +} + +func joinOperations(operations []Operation) string { + values := make([]string, len(operations)) + for index, operation := range operations { + values[index] = string(operation) + } + if len(values) == 0 { + return "none" + } + return strings.Join(values, ", ") +} diff --git a/integrations/ckconfig/selectors.go b/integrations/ckconfig/selectors.go deleted file mode 100644 index 7d4d93b..0000000 --- a/integrations/ckconfig/selectors.go +++ /dev/null @@ -1,228 +0,0 @@ -package ckconfig - -import ( - "fmt" - "sort" - "strings" -) - -func configInputFlags[C any](suite ConfigSuite[C], formatDefault string) []Flag[configInput[C]] { - scopeDefault := "app" - flags := []Flag[configInput[C]]{ - ValueFlag[configInput[C]](FlagMeta{Name: "scope", Usage: "Configuration scope name or all where supported."}, &scopeDefault, parseString, func(input *configInput[C], value Supplied[string]) { - input.Scope = value.Value - }), - ValueFlag[configInput[C]](FlagMeta{Name: "format", Usage: "Output format."}, &formatDefault, parseString, func(input *configInput[C], value Supplied[string]) { - input.Format = value.Value - }), - } - flags = append(flags, configurationSelectionFlags( - func(input *configInput[C], value Supplied[string]) { input.Selection.Config = value }, - func(input *configInput[C], value Supplied[string]) { input.Selection.Overlay = value }, - )...) - for _, selector := range suite.Selectors { - flags = append(flags, adaptSelector(selector)) - } - return flags -} - -func adaptSelector[C any](selector Selector[C]) Flag[configInput[C]] { - flag := adaptContextSelector(selector, - func(input *configInput[C]) *C { return &input.SelectorContext }, - func(input *configInput[C], state selectorState[C]) { input.selectors = append(input.selectors, state) }, - ) - if flag.binding.meta.Required { - flag.binding.meta.Required = false // Scope-aware preflight enforces this requirement. - flag.binding.meta.Usage += " (required where applicable)" - } - return flag -} - -func adaptContextSelector[C any, I any](selector Selector[C], selectContext func(*I) *C, record func(*I, selectorState[C])) Flag[I] { - binding := selector.Binding.binding - meta := binding.meta - if len(selector.Scopes) == 0 { - meta.Usage += " (applies to all scopes)" - } else { - meta.Usage += " (applies to: " + joinNames(selector.Scopes) + ")" - } - return Flag[I]{binding: flagBinding[I]{ - meta: meta, - defaultText: binding.defaultText, - repeated: binding.repeated, - boolean: binding.boolean, - valid: binding.valid, - prepare: func(values []string) (func(*I), error) { - apply, err := binding.prepare(values) - if err != nil { - return nil, err - } - state := selectorState[C]{ - name: binding.meta.Name, - required: binding.meta.Required, - scopes: append([]string(nil), selector.Scopes...), - supplied: len(values) > 0, - apply: apply, - clear: binding.clear, - } - return func(input *I) { - apply(selectContext(input)) - record(input, state) - }, nil - }, - }} -} - -func configurationSelectionFlags[I any](setConfig, setOverlay func(*I, Supplied[string])) []Flag[I] { - return []Flag[I]{ - ValueFlag[I](FlagMeta{Name: "config", Usage: "Configuration source."}, nil, parseString, setConfig), - ValueFlag[I](FlagMeta{Name: "overlay", Usage: "Overlay source."}, nil, parseString, setOverlay), - } -} - -func parseString(value string) (string, error) { return value, nil } - -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 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](prepared *preparedConfigSuite[C], input configInput[C], operation ConfigOperation) ([]string, error) { - if err := checkConfigFormat(input.Format); err != nil { - return nil, err - } - if operation == ConfigShow && input.Format != "yaml" { - return nil, fmt.Errorf("unsupported configuration show format %q", input.Format) - } - if input.Scope == "all" { - if operation != ConfigValidate && operation != ConfigShow { - return nil, fmt.Errorf("scope all is supported only for validate and show") - } - } else if input.Scope == "" { - return nil, fmt.Errorf("configuration scope must not be empty") - } - - scopes := make([]string, 0, len(prepared.ordered)) - if input.Scope == "all" { - for _, scope := range prepared.ordered { - scopes = append(scopes, scope.Name) - } - } else { - scope, ok := prepared.byName[input.Scope] - if !ok { - 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)) - } - if !containsOperation(scope.Operations, operation) { - return nil, unsupportedConfigOperation(scope, operation) - } - scopes = append(scopes, scope.Name) - } - - if input.Scope == "all" { - for _, scopeName := range scopes { - scope := prepared.byName[scopeName] - if !containsOperation(scope.Operations, operation) { - return nil, unsupportedConfigOperation(scope, operation) - } - } - } - for _, selector := range input.selectors { - applies := false - for _, scopeName := range scopes { - if selectorApplies(selector.scopes, scopeName) { - applies = true - break - } - } - if applies && selector.required && !selector.supplied { - return nil, fmt.Errorf("required flag --%s was not supplied", selector.name) - } - if selector.supplied && !applies { - return nil, fmt.Errorf("selector is not applicable to configuration scope %q", input.Scope) - } - } - return scopes, nil -} - -func contextsForScopes[C any](prepared *preparedConfigSuite[C], input configInput[C], scopeNames []string) map[string]C { - base := *new(C) - if prepared.suite.NewContext != nil { - base = prepared.suite.NewContext() - } - contexts := make(map[string]C, len(scopeNames)) - for _, name := range scopeNames { - context := base - for _, selector := range input.selectors { - if selectorApplies(selector.scopes, name) { - selector.apply(&context) - } else { - selector.clear(&context) - } - } - contexts[name] = context - } - return contexts -} - -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 { - for _, operation := range operations { - if operation == requested { - return true - } - } - return false -} - -func unsupportedConfigOperation[C any](scope ConfigScope[C], operation ConfigOperation) error { - return fmt.Errorf("configuration scope %q does not support %s (available: %s)", scope.Name, operation, joinOperations(scope.Operations)) -} - -func selectorApplies(scopes []string, name string) bool { - if len(scopes) == 0 { - return true - } - for _, scope := range scopes { - if scope == name { - return true - } - } - return false -} - -func joinNames(names []string) string { - ordered := append([]string(nil), names...) - sort.Strings(ordered) - return joinStrings(ordered) -} - -func joinOperations(operations []ConfigOperation) string { - values := make([]string, len(operations)) - for index, operation := range operations { - values[index] = string(operation) - } - return joinStrings(values) -} - -func joinStrings(values []string) string { - if len(values) == 0 { - return "none" - } - return strings.Join(values, ", ") -}