package ckconfig import ( "fmt" "sort" "strings" ) func configInputFlags[C any](suite ConfigSuite[C], formatDefault string) []Flag[configInput[C]] { scopeDefault := "app" flags := []Flag[configInput[C]]{ ValueFlag[configInput[C]](FlagMeta{Name: "scope", Usage: "Configuration scope name or all where supported."}, &scopeDefault, parseString, func(input *configInput[C], value Supplied[string]) { input.Scope = value.Value }), ValueFlag[configInput[C]](FlagMeta{Name: "format", Usage: "Output format."}, &formatDefault, parseString, func(input *configInput[C], value Supplied[string]) { input.Format = value.Value }), } flags = append(flags, configurationSelectionFlags( func(input *configInput[C], value Supplied[string]) { input.Selection.Config = value }, func(input *configInput[C], value Supplied[string]) { input.Selection.Overlay = value }, )...) for _, selector := range suite.Selectors { flags = append(flags, adaptSelector(selector)) } return flags } func adaptSelector[C any](selector Selector[C]) Flag[configInput[C]] { flag := adaptContextSelector(selector, func(input *configInput[C]) *C { return &input.SelectorContext }, func(input *configInput[C], state selectorState[C]) { input.selectors = append(input.selectors, state) }, ) if flag.binding.meta.Required { flag.binding.meta.Required = false // Scope-aware preflight enforces this requirement. flag.binding.meta.Usage += " (required where applicable)" } return flag } func adaptContextSelector[C any, I any](selector Selector[C], selectContext func(*I) *C, record func(*I, selectorState[C])) Flag[I] { binding := selector.Binding.binding meta := binding.meta if len(selector.Scopes) == 0 { meta.Usage += " (applies to all scopes)" } else { meta.Usage += " (applies to: " + joinNames(selector.Scopes) + ")" } return Flag[I]{binding: flagBinding[I]{ meta: meta, defaultText: binding.defaultText, repeated: binding.repeated, boolean: binding.boolean, valid: binding.valid, prepare: func(values []string) (func(*I), error) { apply, err := binding.prepare(values) if err != nil { return nil, err } state := selectorState[C]{ name: binding.meta.Name, required: binding.meta.Required, scopes: append([]string(nil), selector.Scopes...), supplied: len(values) > 0, apply: apply, clear: binding.clear, } return func(input *I) { apply(selectContext(input)) record(input, state) }, nil }, }} } func configurationSelectionFlags[I any](setConfig, setOverlay func(*I, Supplied[string])) []Flag[I] { return []Flag[I]{ ValueFlag[I](FlagMeta{Name: "config", Usage: "Configuration source."}, nil, parseString, setConfig), ValueFlag[I](FlagMeta{Name: "overlay", Usage: "Overlay source."}, nil, parseString, setOverlay), } } func parseString(value string) (string, error) { return value, nil } func preflightConfig[C any](prepared *preparedConfigSuite[C], input configInput[C], operation ConfigOperation) ([]string, error) { scopes, err := selectConfigScopes(prepared, input, operation) if err != nil { return nil, err } if prepared.suite.CheckInput != nil { if err := prepared.suite.CheckInput(input.Selection, input.SelectorContext); err != nil { return nil, err } } return scopes, nil } func selectConfigScopes[C any](prepared *preparedConfigSuite[C], input configInput[C], operation ConfigOperation) ([]string, error) { if err := checkConfigFormat(input.Format); err != nil { return nil, err } if operation == ConfigShow && input.Format != "yaml" { return nil, fmt.Errorf("unsupported configuration show format %q", input.Format) } if input.Scope == "all" { if operation != ConfigValidate && operation != ConfigShow { return nil, fmt.Errorf("scope all is supported only for validate and show") } } else if input.Scope == "" { return nil, fmt.Errorf("configuration scope must not be empty") } scopes := make([]string, 0, len(prepared.ordered)) if input.Scope == "all" { for _, scope := range prepared.ordered { scopes = append(scopes, scope.Name) } } else { scope, ok := prepared.byName[input.Scope] if !ok { choices := make([]string, 0, len(prepared.ordered)) for _, available := range prepared.ordered { choices = append(choices, available.Name) } return nil, fmt.Errorf("unknown configuration scope %q (available: %s)", input.Scope, joinStrings(choices)) } if !containsOperation(scope.Operations, operation) { return nil, unsupportedConfigOperation(scope, operation) } scopes = append(scopes, scope.Name) } if input.Scope == "all" { for _, scopeName := range scopes { scope := prepared.byName[scopeName] if !containsOperation(scope.Operations, operation) { return nil, unsupportedConfigOperation(scope, operation) } } } for _, selector := range input.selectors { applies := false for _, scopeName := range scopes { if selectorApplies(selector.scopes, scopeName) { applies = true break } } if applies && selector.required && !selector.supplied { return nil, fmt.Errorf("required flag --%s was not supplied", selector.name) } if selector.supplied && !applies { return nil, fmt.Errorf("selector is not applicable to configuration scope %q", input.Scope) } } return scopes, nil } func contextsForScopes[C any](prepared *preparedConfigSuite[C], input configInput[C], scopeNames []string) map[string]C { base := *new(C) if prepared.suite.NewContext != nil { base = prepared.suite.NewContext() } contexts := make(map[string]C, len(scopeNames)) for _, name := range scopeNames { context := base for _, selector := range input.selectors { if selectorApplies(selector.scopes, name) { selector.apply(&context) } else { selector.clear(&context) } } contexts[name] = context } return contexts } func findConfigScope[C any](prepared *preparedConfigSuite[C], name string) (ConfigScope[C], bool) { scope, ok := prepared.byName[name] return scope, ok } func containsOperation(operations []ConfigOperation, requested ConfigOperation) bool { for _, operation := range operations { if operation == requested { return true } } return false } func unsupportedConfigOperation[C any](scope ConfigScope[C], operation ConfigOperation) error { return fmt.Errorf("configuration scope %q does not support %s (available: %s)", scope.Name, operation, joinOperations(scope.Operations)) } func selectorApplies(scopes []string, name string) bool { if len(scopes) == 0 { return true } for _, scope := range scopes { if scope == name { return true } } return false } func joinNames(names []string) string { ordered := append([]string(nil), names...) sort.Strings(ordered) return joinStrings(ordered) } func joinOperations(operations []ConfigOperation) string { values := make([]string, len(operations)) for index, operation := range operations { values[index] = string(operation) } return joinStrings(values) } func joinStrings(values []string) string { if len(values) == 0 { return "none" } return strings.Join(values, ", ") }