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 preparedComparison[I, S any] struct { scope *preparedScope[I, S] request ComparisonRequest[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] comparison *preparedComparison[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) scope := selected[0] left, err := prepareScopeInput(prepared, input.values, scope, input.selection) if err != nil { return fmt.Errorf("configuration scope %q left input: %w", scope.scope.Name, err) } right, err := prepareScopeInput(prepared, input.values, scope, input.against) if err != nil { return fmt.Errorf("configuration scope %q right input: %w", scope.scope.Name, err) } plan.comparison = &preparedComparison[I, S]{ scope: scope, request: ComparisonRequest[I]{ Left: ComparisonSide[I]{Selection: input.selection, Input: left}, Right: ComparisonSide[I]{Selection: input.against, Input: right}, }, } input.plan = plan return nil } for _, scope := range selected { value, err := prepareScopeInput(prepared, input.values, scope, input.selection) if 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 prepareScopeInput[I, S any](prepared *preparedSuite[I, S], values commandkit.FlagValues[I], scope *preparedScope[I, S], selection Selection) (I, error) { var value I if prepared.newInput != nil { value = prepared.newInput() } // Availability was checked before this helper, so every binding for the // selected scope belongs to the operation's captured union. if err := values.ApplyTo(&value, scope.bindings...); err != nil { return value, err } if prepared.checkInput != nil { if err := prepared.checkInput(scope.scope.Name, selection, value); err != nil { return value, err } } return value, 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, ", ") }