Files
commandkit/selectors.go

222 lines
6.4 KiB
Go

package commandkit
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]] {
return adaptContextSelector(selector,
func(input *configInput[C]) *C { return &input.SelectorContext },
func(input *configInput[C], state selectorState[C]) { input.selectors = append(input.selectors, state) },
)
}
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]{
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](suite ConfigSuite[C], input configInput[C], operation ConfigOperation) ([]string, error) {
scopes, err := selectConfigScopes(suite, input, operation)
if err != nil {
return nil, err
}
if suite.CheckInput != nil {
if err := suite.CheckInput(input.Selection, input.SelectorContext); err != nil {
return nil, err
}
}
return scopes, nil
}
func selectConfigScopes[C any](suite ConfigSuite[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(suite.Scopes))
if input.Scope == "all" {
for _, scope := range orderedConfigScopes(suite.Scopes) {
scopes = append(scopes, scope.Name)
}
} else {
scope, ok := findConfigScope(suite.Scopes, input.Scope)
if !ok {
return nil, fmt.Errorf("unknown configuration scope %q", input.Scope)
}
if !containsOperation(scope.Operations, operation) {
return nil, unsupportedConfigOperation(scope, operation)
}
scopes = append(scopes, scope.Name)
}
if input.Scope == "all" {
for _, scopeName := range scopes {
scope, _ := findConfigScope(suite.Scopes, scopeName)
if !containsOperation(scope.Operations, operation) {
return nil, unsupportedConfigOperation(scope, operation)
}
}
}
for _, selector := range input.selectors {
if !selector.supplied {
continue
}
applies := false
for _, scopeName := range scopes {
if selectorApplies(selector.scopes, scopeName) {
applies = true
break
}
}
if !applies {
return nil, fmt.Errorf("selector is not applicable to configuration scope %q", input.Scope)
}
}
return scopes, nil
}
func contextsForScopes[C any](suite ConfigSuite[C], input configInput[C], scopeNames []string) map[string]C {
base := *new(C)
if suite.NewContext != nil {
base = 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](scopes []ConfigScope[C], name string) (ConfigScope[C], bool) {
for _, scope := range scopes {
if scope.Name == name {
return scope, true
}
}
return ConfigScope[C]{}, false
}
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, ", ")
}