Files
commandkit/integrations/ckconfig/input.go

218 lines
7.3 KiB
Go

package ckconfig
import (
"fmt"
"strings"
commandkit "gitea.maximumdirect.net/eric/CommandKit"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
)
type configCommandInput[I, S any] struct {
selection Selection
against 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 || operation == Diff {
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 == Diff {
flags = append(flags,
commandkit.ValueFlag(commandkit.FlagMeta{Name: "against-config", Usage: "Configuration source for the right side."}, (*string)(nil), parseConfigString,
func(input **configCommandInput[I, S], value commandkit.Supplied[string]) {
(*input).against.Config = value
}),
commandkit.ValueFlag(commandkit.FlagMeta{Name: "against-overlay", Usage: "Overlay source for the right side."}, (*string)(nil), parseConfigString,
func(input **configCommandInput[I, S], value commandkit.Supplied[string]) {
(*input).against.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 }))
}
flags = append(flags, commandkit.CaptureFlags(prepared.bindingsByOperation[operation], 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 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."
case Diff:
return "diff", "Compare two configuration selections."
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)
}
if operation == Diff && format != "yaml" {
return fmt.Errorf("unsupported configuration diff 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")
}
var selected []*preparedScope[I, S]
if scopeName == "all" {
selected = prepared.scopes
} else if scope, exists := prepared.byName[scopeName]; exists {
selected = []*preparedScope[I, S]{scope}
} else {
return fmt.Errorf("unknown configuration scope %q (available: %s)", scopeName, prepared.scopeChoices)
}
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)),
}
if operation == Diff {
input.against = inheritComparisonSelection(input.selection, input.against)
input.plan = plan
return nil
}
for _, scope := range selected {
var value I
if prepared.newInput != nil {
value = prepared.newInput()
}
// Availability was checked above, so every binding for this scope
// belongs to the operation's captured union.
if err := input.values.ApplyTo(&value, scope.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 inheritComparisonSelection(left, right Selection) Selection {
if !right.Config.Set {
right.Config = left.Config
}
if !right.Overlay.Set {
right.Overlay = left.Overlay
}
return right
}
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, ", ")
}