Files
commandkit/integrations/ckconfig/input.go

217 lines
7.5 KiB
Go

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