Assemble configuration families and inputs

This commit is contained in:
2026-10-11 01:34:25 +00:00
parent 1e1eca4a88
commit 809a36bc46
4 changed files with 543 additions and 233 deletions

View File

@@ -1,6 +1,6 @@
# Implement the optional integration modules and captured registration API # Implement the optional integration modules and captured registration API
Status: implementation in progress. Stages 1 through 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 Execute the stages in order. Each stage is sized as a separate Luna coding-agent
assignment. This document authorizes no publication or changes to sibling assignment. This document authorizes no publication or changes to sibling
repositories. repositories.
@@ -447,10 +447,20 @@ invocation-local preparation path.
needs no placeholder input/state types or forwarding wrapper. A nil loader needs no placeholder input/state types or forwarding wrapper. A nil loader
retains a config/app construction error. retains a config/app construction error.
**Completion evidence:** metadata-only suites, no scopes, partial capabilities, **Completion evidence — complete:** `Command` eagerly captures the suite,
all preflight, unused bindings versus invalid adapters, scope-aware validation, operation capabilities, scope metadata, selector applicability, callbacks, and
independently initialized pointer inputs, preserved initialized inapplicable retained scope issues into a root-only `config` declaration. It validates named
fields, shared state once, and no callbacks on discovery/help/version. 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 ## Stage 10: Port configuration reports with operation-specific timing

View File

@@ -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
}

View File

@@ -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, ", ")
}

View File

@@ -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, ", ")
}