2026-10-11 01:34:25 +00:00
package ckconfig
import (
"fmt"
2026-10-11 01:39:23 +00:00
"strings"
2026-10-11 01:34:25 +00:00
2026-11-04 03:52:46 +00:00
commandkit "go.maximumdirect.net/commandkit"
2026-11-04 14:58:20 +00:00
configkit "go.maximumdirect.net/configkit"
2026-10-11 01:34:25 +00:00
)
type configCommandInput [ I , S any ] struct {
2026-11-07 04:36:05 +00:00
selection Selection
against Selection
scope commandkit . Supplied [ string ]
path commandkit . Supplied [ string ]
2026-11-07 04:44:01 +00:00
list commandkit . Supplied [ bool ]
historical commandkit . Supplied [ bool ]
under commandkit . Supplied [ string ]
explicit commandkit . Supplied [ bool ]
2026-11-07 04:47:23 +00:00
sourceID commandkit . Supplied [ string ]
2026-11-07 04:49:40 +00:00
details commandkit . Supplied [ bool ]
check commandkit . Supplied [ bool ]
2026-11-07 04:52:58 +00:00
severities [ ] configkit . Severity
codes [ ] string
2026-11-07 04:36:05 +00:00
values commandkit . FlagValues [ I ]
againstValues commandkit . FlagValues [ I ]
plan * configOperationPlan [ I , S ]
2026-10-11 01:34:25 +00:00
}
type selectedScopeInput [ I , S any ] struct {
2026-11-07 04:39:34 +00:00
scope * preparedScope [ I , S ]
input I
selection Selection
2026-10-11 01:34:25 +00:00
}
2026-10-11 21:43:30 +00:00
type preparedComparison [ I , S any ] struct {
scope * preparedScope [ I , S ]
request ComparisonRequest [ I ]
}
2026-10-11 01:34:25 +00:00
type configOperationPlan [ I , S any ] struct {
2026-10-11 21:43:30 +00:00
prepared * preparedSuite [ I , S ]
operation Operation
scopeName string
path configkit . Path
2026-11-07 04:47:23 +00:00
pathSet bool
sourceID commandkit . Supplied [ string ]
2026-11-07 04:49:40 +00:00
details bool
check bool
2026-11-07 04:52:58 +00:00
severities [ ] configkit . Severity
codes [ ] string
2026-11-07 04:44:01 +00:00
list bool
help bool
historical bool
under * configkit . Path
explicit commandkit . Supplied [ bool ]
2026-10-11 21:43:30 +00:00
scopes [ ] selectedScopeInput [ I , S ]
comparison * preparedComparison [ I , S ]
2026-10-11 01:34:25 +00:00
}
func newConfigOperationCommand [ I , S any ] ( prepared * preparedSuite [ I , S ] , operation Operation ) commandkit . Command {
defaultScope := "app"
2026-11-03 04:09:27 +00:00
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" } )
}
2026-10-11 01:34:25 +00:00
flags := [ ] commandkit . Flag [ * configCommandInput [ I , S ] ] {
2026-11-03 04:09:27 +00:00
commandkit . ValueFlag ( commandkit . FlagMeta { Name : "scope" , Usage : "Configuration scope name or all where supported." , Completion : commandkit . CompleteChoices ( scopeChoices ... ) } , & defaultScope , parseConfigString ,
2026-10-11 01:34:25 +00:00
func ( input * * configCommandInput [ I , S ] , value commandkit . Supplied [ string ] ) { ( * input ) . scope = value } ) ,
2026-11-03 04:09:27 +00:00
commandkit . ValueFlag ( commandkit . FlagMeta { Name : "config" , Usage : "Configuration source." , Completion : commandkit . CompleteFiles ( ) } , ( * string ) ( nil ) , parseConfigString ,
2026-10-11 01:34:25 +00:00
func ( input * * configCommandInput [ I , S ] , value commandkit . Supplied [ string ] ) {
( * input ) . selection . Config = value
} ) ,
2026-11-03 04:09:27 +00:00
commandkit . ValueFlag ( commandkit . FlagMeta { Name : "overlay" , Usage : "Overlay source." , Completion : commandkit . CompleteFiles ( ) } , ( * string ) ( nil ) , parseConfigString ,
2026-10-11 01:34:25 +00:00
func ( input * * configCommandInput [ I , S ] , value commandkit . Supplied [ string ] ) {
( * input ) . selection . Overlay = value
} ) ,
}
2026-10-11 21:39:19 +00:00
if operation == Diff {
flags = append ( flags ,
2026-11-07 04:49:40 +00:00
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 } ) ,
2026-11-03 04:09:27 +00:00
commandkit . ValueFlag ( commandkit . FlagMeta { Name : "against-config" , Usage : "Configuration source for the right side." , Completion : commandkit . CompleteFiles ( ) } , ( * string ) ( nil ) , parseConfigString ,
2026-10-11 21:39:19 +00:00
func ( input * * configCommandInput [ I , S ] , value commandkit . Supplied [ string ] ) {
( * input ) . against . Config = value
} ) ,
2026-11-03 04:09:27 +00:00
commandkit . ValueFlag ( commandkit . FlagMeta { Name : "against-overlay" , Usage : "Overlay source for the right side." , Completion : commandkit . CompleteFiles ( ) } , ( * string ) ( nil ) , parseConfigString ,
2026-10-11 21:39:19 +00:00
func ( input * * configCommandInput [ I , S ] , value commandkit . Supplied [ string ] ) {
( * input ) . against . Overlay = value
} ) ,
)
}
2026-11-07 04:52:58 +00:00
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
} ) ,
)
}
2026-11-07 04:39:34 +00:00
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
} ) )
}
}
2026-10-11 01:34:25 +00:00
if operation == Explain {
2026-11-07 04:44:01 +00:00
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 } ) ,
)
2026-10-11 01:34:25 +00:00
}
2026-11-07 04:47:23 +00:00
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 } ) )
}
2026-10-11 13:05:22 +00:00
flags = append ( flags , commandkit . CaptureFlags ( prepared . bindingsByOperation [ operation ] , func ( input * * configCommandInput [ I , S ] ) * commandkit . FlagValues [ I ] {
2026-10-11 01:34:25 +00:00
if input == nil || * input == nil {
return nil
}
return & ( * input ) . values
} ) ... )
2026-11-07 04:36:05 +00:00
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
} ) ... )
}
2026-10-11 01:34:25 +00:00
name , summary := operationDeclaration ( operation )
return commandkit . NewCommand ( commandkit . CommandSpec [ * configCommandInput [ I , S ] ] {
Name : name , Summary : summary ,
2026-11-07 04:44:01 +00:00
Description : operationDescription ( operation ) ,
NewInput : func ( ) * configCommandInput [ I , S ] { return & configCommandInput [ I , S ] { } } ,
Flags : flags ,
2026-10-11 01:34:25 +00:00
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 )
} ,
} )
}
2026-11-07 04:44:01 +00:00
func operationDescription ( operation Operation ) string {
if operation == Explain {
return "Use --list to discover paths or --path POINTER to explain one path."
}
return ""
}
2026-10-11 01:34:25 +00:00
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."
2026-10-11 21:39:19 +00:00
case Diff :
return "diff" , "Compare two configuration selections."
2026-10-11 01:34:25 +00:00
default :
return string ( operation ) , "Inspect configuration."
}
}
func prepareConfigOperation [ I , S any ] ( prepared * preparedSuite [ I , S ] , operation Operation , input * configCommandInput [ I , S ] ) error {
2026-11-07 04:52:58 +00:00
if operation == Validate && ! input . details . Value && ( len ( input . severities ) > 0 || len ( input . codes ) > 0 ) {
return fmt . Errorf ( "--severity and --code require --details" )
}
2026-11-07 04:44:01 +00:00
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
}
}
2026-10-11 01:34:25 +00:00
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" )
}
2026-11-07 04:47:23 +00:00
if scopeName == "all" && operation == Show && input . path . Set {
return fmt . Errorf ( "path-selected show requires one scope" )
}
2026-10-11 01:34:25 +00:00
2026-10-11 13:05:22 +00:00
var selected [ ] * preparedScope [ I , S ]
2026-10-11 01:34:25 +00:00
if scopeName == "all" {
2026-10-11 13:05:22 +00:00
selected = prepared . scopes
2026-10-11 01:34:25 +00:00
} else if scope , exists := prepared . byName [ scopeName ] ; exists {
2026-10-11 13:05:22 +00:00
selected = [ ] * preparedScope [ I , S ] { scope }
2026-10-11 01:34:25 +00:00
} else {
2026-10-11 13:05:22 +00:00
return fmt . Errorf ( "unknown configuration scope %q (available: %s)" , scopeName , prepared . scopeChoices )
2026-10-11 01:34:25 +00:00
}
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 ) )
}
2026-11-07 04:39:34 +00:00
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 )
}
2026-11-07 04:44:01 +00:00
if operation == Explain && input . list . Value && ! scope . scope . Features . PathDiscovery {
return fmt . Errorf ( "configuration scope %q does not support path discovery" , scope . scope . Name )
}
2026-11-07 04:47:23 +00:00
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 )
}
2026-10-11 01:34:25 +00:00
}
var path configkit . Path
2026-11-07 04:47:23 +00:00
if ( operation == Explain || operation == Show ) && input . path . Set {
2026-10-11 01:34:25 +00:00
parsed , err := configkit . ParsePath ( input . path . Value )
if err != nil {
return fmt . Errorf ( "invalid configuration path: %w" , err )
}
path = parsed
}
2026-11-07 04:44:01 +00:00
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
}
2026-10-11 01:34:25 +00:00
plan := & configOperationPlan [ I , S ] {
2026-11-07 04:44:01 +00:00
prepared : prepared ,
operation : operation ,
scopeName : scopeName ,
path : path ,
2026-11-07 04:47:23 +00:00
pathSet : input . path . Set ,
sourceID : input . sourceID ,
2026-11-07 04:49:40 +00:00
details : input . details . Value ,
check : input . check . Value ,
2026-11-07 04:52:58 +00:00
severities : append ( [ ] configkit . Severity ( nil ) , input . severities ... ) ,
codes : append ( [ ] string ( nil ) , input . codes ... ) ,
2026-11-07 04:44:01 +00:00
list : input . list . Value ,
historical : input . historical . Value ,
under : under ,
explicit : input . explicit ,
scopes : make ( [ ] selectedScopeInput [ I , S ] , 0 , len ( selected ) ) ,
2026-10-11 01:34:25 +00:00
}
2026-10-11 21:39:19 +00:00
if operation == Diff {
2026-10-11 21:43:30 +00:00
scope := selected [ 0 ]
2026-11-07 04:39:34 +00:00
input . selection = cloneSelection ( input . selection )
input . against = cloneSelection ( input . against )
2026-11-07 04:36:05 +00:00
left , err := prepareScopeInput ( prepared , input . values , scope , scope . bindings , input . selection , true )
2026-10-11 21:43:30 +00:00
if err != nil {
return fmt . Errorf ( "configuration scope %q left input: %w" , scope . scope . Name , err )
}
2026-11-07 04:36:05 +00:00
right , err := prepareScopeInput ( prepared , input . againstValues , scope , scope . againstBindings , input . against , true )
2026-10-11 21:43:30 +00:00
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 } ,
} ,
}
2026-10-11 21:39:19 +00:00
input . plan = plan
return nil
}
2026-10-11 01:34:25 +00:00
for _ , scope := range selected {
2026-11-07 04:39:34 +00:00
selection := cloneSelection ( input . selection )
value , err := prepareScopeInput ( prepared , input . values , scope , scope . bindings , selection , false )
2026-10-11 21:43:30 +00:00
if err != nil {
2026-10-11 01:34:25 +00:00
return fmt . Errorf ( "configuration scope %q: %w" , scope . scope . Name , err )
}
2026-11-07 04:39:34 +00:00
plan . scopes = append ( plan . scopes , selectedScopeInput [ I , S ] { scope : scope , input : value , selection : selection } )
2026-10-11 01:34:25 +00:00
}
input . plan = plan
return nil
}
2026-11-07 04:36:05 +00:00
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 ) {
2026-10-11 21:43:30 +00:00
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.
2026-11-07 04:36:05 +00:00
apply := values . ApplyTo
if suppliedOnly {
apply = values . ApplySuppliedTo
}
if err := apply ( & value , bindings ... ) ; err != nil {
2026-10-11 21:43:30 +00:00
return value , err
}
if prepared . checkInput != nil {
if err := prepared . checkInput ( scope . scope . Name , selection , value ) ; err != nil {
return value , err
}
}
return value , nil
}
2026-10-11 01:34:25 +00:00
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 , ", " )
}