package ckconfig import ( "fmt" "strings" commandkit "go.maximumdirect.net/commandkit" configkit "go.maximumdirect.net/configkit" ) type configCommandInput[I, S any] struct { selection Selection against Selection scope commandkit.Supplied[string] path commandkit.Supplied[string] list commandkit.Supplied[bool] historical commandkit.Supplied[bool] under commandkit.Supplied[string] explicit commandkit.Supplied[bool] sourceID commandkit.Supplied[string] details commandkit.Supplied[bool] check commandkit.Supplied[bool] severities []configkit.Severity codes []string values commandkit.FlagValues[I] againstValues commandkit.FlagValues[I] plan *configOperationPlan[I, S] } type selectedScopeInput[I, S any] struct { scope *preparedScope[I, S] input I selection Selection } 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 scopeName string path configkit.Path pathSet bool sourceID commandkit.Supplied[string] details bool check bool severities []configkit.Severity codes []string list bool help bool historical bool under *configkit.Path explicit commandkit.Supplied[bool] scopes []selectedScopeInput[I, S] comparison *preparedComparison[I, S] } func newConfigOperationCommand[I, S any](prepared *preparedSuite[I, S], operation Operation) commandkit.Command { defaultScope := "app" var scopeChoices []commandkit.CompletionChoice allSupported := len(prepared.scopes) > 0 for _, scope := range prepared.scopes { if containsOperation(scope.scope.Operations, operation) { scopeChoices = append(scopeChoices, commandkit.CompletionChoice{Value: scope.scope.Name}) } else { allSupported = false } } if allSupported && (operation == Validate || operation == Show) { scopeChoices = append(scopeChoices, commandkit.CompletionChoice{Value: "all"}) } flags := []commandkit.Flag[*configCommandInput[I, S]]{ commandkit.ValueFlag(commandkit.FlagMeta{Name: "scope", Usage: "Configuration scope name or all where supported.", Completion: commandkit.CompleteChoices(scopeChoices...)}, &defaultScope, parseConfigString, func(input **configCommandInput[I, S], value commandkit.Supplied[string]) { (*input).scope = value }), commandkit.ValueFlag(commandkit.FlagMeta{Name: "config", Usage: "Configuration source.", Completion: commandkit.CompleteFiles()}, (*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.", Completion: commandkit.CompleteFiles()}, (*string)(nil), parseConfigString, func(input **configCommandInput[I, S], value commandkit.Supplied[string]) { (*input).selection.Overlay = value }), } if operation == Diff { flags = append(flags, commandkit.BoolFlag(commandkit.FlagMeta{Name: "details", Usage: "Show native before and after difference evidence."}, false, func(input **configCommandInput[I, S], value commandkit.Supplied[bool]) { (*input).details = value }), commandkit.BoolFlag(commandkit.FlagMeta{Name: "check", Usage: "Return status 3 when differences are found."}, false, func(input **configCommandInput[I, S], value commandkit.Supplied[bool]) { (*input).check = value }), commandkit.ValueFlag(commandkit.FlagMeta{Name: "against-config", Usage: "Configuration source for the right side.", Completion: commandkit.CompleteFiles()}, (*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.", Completion: commandkit.CompleteFiles()}, (*string)(nil), parseConfigString, func(input **configCommandInput[I, S], value commandkit.Supplied[string]) { (*input).against.Overlay = value }), ) } if operation == Validate { flags = append(flags, commandkit.BoolFlag(commandkit.FlagMeta{Name: "details", Usage: "Show native validation findings."}, false, func(input **configCommandInput[I, S], value commandkit.Supplied[bool]) { (*input).details = value }), commandkit.RepeatFlag(commandkit.FlagMeta{Name: "severity", Usage: "Filter detailed findings by error or warning."}, []configkit.Severity(nil), parseValidationSeverity, func(input **configCommandInput[I, S], value commandkit.Supplied[[]configkit.Severity]) { (*input).severities = value.Value }), commandkit.RepeatFlag(commandkit.FlagMeta{Name: "code", Usage: "Filter detailed findings by exact native code."}, []string(nil), parseConfigString, func(input **configCommandInput[I, S], value commandkit.Supplied[[]string]) { (*input).codes = value.Value }), ) } if supportsOverrides(prepared, operation) { flags = append(flags, commandkit.RepeatFlag(commandkit.FlagMeta{Name: "set", Usage: "Override POINTER=JSON (pointer keys cannot contain =)."}, []configkit.Override(nil), overrideParser("--set"), func(input **configCommandInput[I, S], value commandkit.Supplied[[]configkit.Override]) { (*input).selection.Overrides = value.Value })) if operation == Diff { flags = append(flags, commandkit.RepeatFlag(commandkit.FlagMeta{Name: "against-set", Usage: "Right-side override POINTER=JSON (pointer keys cannot contain =)."}, []configkit.Override(nil), overrideParser("--against-set"), func(input **configCommandInput[I, S], value commandkit.Supplied[[]configkit.Override]) { (*input).against.Overrides = value.Value })) } } if operation == Explain { flags = append(flags, commandkit.ValueFlag(commandkit.FlagMeta{Name: "path", Usage: "JSON Pointer path to explain."}, (*string)(nil), parseConfigString, func(input **configCommandInput[I, S], value commandkit.Supplied[string]) { (*input).path = value }), commandkit.BoolFlag(commandkit.FlagMeta{Name: "list", Usage: "List configuration paths."}, false, func(input **configCommandInput[I, S], value commandkit.Supplied[bool]) { (*input).list = value }), commandkit.BoolFlag(commandkit.FlagMeta{Name: "historical", Usage: "Include historical-only paths when listing."}, false, func(input **configCommandInput[I, S], value commandkit.Supplied[bool]) { (*input).historical = value }), commandkit.ValueFlag(commandkit.FlagMeta{Name: "under", Usage: "List paths under a JSON Pointer."}, (*string)(nil), parseConfigString, func(input **configCommandInput[I, S], value commandkit.Supplied[string]) { (*input).under = value }), commandkit.BoolFlag(commandkit.FlagMeta{Name: "explicit", Usage: "Filter listed current paths by explicitness."}, false, func(input **configCommandInput[I, S], value commandkit.Supplied[bool]) { (*input).explicit = value }), ) } if operation == Show { flags = append(flags, commandkit.ValueFlag(commandkit.FlagMeta{Name: "path", Usage: "JSON Pointer to one effective value."}, (*string)(nil), parseConfigString, func(input **configCommandInput[I, S], value commandkit.Supplied[string]) { (*input).path = value })) } if operation == Sources { flags = append(flags, commandkit.ValueFlag(commandkit.FlagMeta{Name: "source-id", Usage: "Select one source ID from this acquisition."}, (*string)(nil), parseConfigString, func(input **configCommandInput[I, S], value commandkit.Supplied[string]) { (*input).sourceID = 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 })...) if operation == Diff { flags = append(flags, commandkit.CaptureFlags(prepared.againstBindings, func(input **configCommandInput[I, S]) *commandkit.FlagValues[I] { if input == nil || *input == nil { return nil } return &(*input).againstValues })...) } name, summary := operationDeclaration(operation) return commandkit.NewCommand(commandkit.CommandSpec[*configCommandInput[I, S]]{ Name: name, Summary: summary, Description: operationDescription(operation), 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 operationDescription(operation Operation) string { if operation == Explain { return "Use --list to discover paths or --path POINTER to explain one path." } return "" } 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 { if operation == Validate && !input.details.Value && (len(input.severities) > 0 || len(input.codes) > 0) { return fmt.Errorf("--severity and --code require --details") } if operation == Explain { if input.list.Value && input.path.Set { return fmt.Errorf("--list and --path cannot be combined") } if !input.list.Value && (input.historical.Set || input.under.Set || input.explicit.Set) { return fmt.Errorf("--historical, --under, and --explicit require --list") } if !input.list.Value && !input.path.Set { input.plan = &configOperationPlan[I, S]{prepared: prepared, operation: operation, help: true} return nil } } 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") } if scopeName == "all" && operation == Show && input.path.Set { return fmt.Errorf("path-selected show requires one scope") } 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)) } if (len(input.selection.Overrides) > 0 || len(input.against.Overrides) > 0) && !scope.scope.Features.Overrides { return fmt.Errorf("configuration scope %q does not support overrides", scope.scope.Name) } if operation == Explain && input.list.Value && !scope.scope.Features.PathDiscovery { return fmt.Errorf("configuration scope %q does not support path discovery", scope.scope.Name) } if operation == Show && input.path.Set && !scope.scope.Features.PathShow { return fmt.Errorf("configuration scope %q does not support path-selected show", scope.scope.Name) } } var path configkit.Path if (operation == Explain || operation == Show) && input.path.Set { parsed, err := configkit.ParsePath(input.path.Value) if err != nil { return fmt.Errorf("invalid configuration path: %w", err) } path = parsed } var under *configkit.Path if input.under.Set { parsed, err := configkit.ParsePath(input.under.Value) if err != nil { return fmt.Errorf("invalid subtree path: %w", err) } under = &parsed } plan := &configOperationPlan[I, S]{ prepared: prepared, operation: operation, scopeName: scopeName, path: path, pathSet: input.path.Set, sourceID: input.sourceID, details: input.details.Value, check: input.check.Value, severities: append([]configkit.Severity(nil), input.severities...), codes: append([]string(nil), input.codes...), list: input.list.Value, historical: input.historical.Value, under: under, explicit: input.explicit, scopes: make([]selectedScopeInput[I, S], 0, len(selected)), } if operation == Diff { scope := selected[0] input.selection = cloneSelection(input.selection) input.against = cloneSelection(input.against) left, err := prepareScopeInput(prepared, input.values, scope, scope.bindings, input.selection, true) if err != nil { return fmt.Errorf("configuration scope %q left input: %w", scope.scope.Name, err) } right, err := prepareScopeInput(prepared, input.againstValues, scope, scope.againstBindings, input.against, true) 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 { selection := cloneSelection(input.selection) value, err := prepareScopeInput(prepared, input.values, scope, scope.bindings, selection, false) 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, selection: selection}) } input.plan = plan return nil } func prepareScopeInput[I, S any](prepared *preparedSuite[I, S], values commandkit.FlagValues[I], scope *preparedScope[I, S], bindings []commandkit.Flag[I], selection Selection, suppliedOnly bool) (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. apply := values.ApplyTo if suppliedOnly { apply = values.ApplySuppliedTo } if err := apply(&value, 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 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, ", ") }