Assemble configuration families and inputs
This commit is contained in:
312
integrations/ckconfig/family.go
Normal file
312
integrations/ckconfig/family.go
Normal 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
|
||||
}
|
||||
216
integrations/ckconfig/input.go
Normal file
216
integrations/ckconfig/input.go
Normal 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, ", ")
|
||||
}
|
||||
@@ -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, ", ")
|
||||
}
|
||||
Reference in New Issue
Block a user