217 lines
7.5 KiB
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, ", ")
|
||
|
|
}
|