Move integration code into optional modules
This commit is contained in:
461
integrations/ckconfig/config.go
Normal file
461
integrations/ckconfig/config.go
Normal file
@@ -0,0 +1,461 @@
|
||||
package ckconfig
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
// Selection identifies the application configuration inputs supplied to a
|
||||
// configuration scope.
|
||||
type Selection struct {
|
||||
Config Supplied[string]
|
||||
Overlay Supplied[string]
|
||||
}
|
||||
|
||||
// ConfigOperation identifies a supported configuration inspection operation.
|
||||
type ConfigOperation string
|
||||
|
||||
const (
|
||||
ConfigValidate ConfigOperation = "validate"
|
||||
ConfigShow ConfigOperation = "show"
|
||||
ConfigSources ConfigOperation = "sources"
|
||||
ConfigExplain ConfigOperation = "explain"
|
||||
)
|
||||
|
||||
// ConfigRequest describes one configuration operation and its selected inputs.
|
||||
type ConfigRequest struct {
|
||||
Operation ConfigOperation
|
||||
Selection Selection
|
||||
}
|
||||
|
||||
// EffectiveWriter writes an owning library's native effective configuration.
|
||||
type EffectiveWriter interface {
|
||||
WriteEffective(io.Writer) error
|
||||
}
|
||||
|
||||
// SourceReader reports the sources used to construct a configuration.
|
||||
type SourceReader interface {
|
||||
Sources() []configkit.SourceInfo
|
||||
}
|
||||
|
||||
// ExplanationReader writes an owning library's path explanation.
|
||||
type ExplanationReader interface {
|
||||
Explain(configkit.Path) (configkit.Explanation, bool)
|
||||
WriteExplanation(io.Writer, configkit.Path) error
|
||||
}
|
||||
|
||||
// ConfigView contains optional views relevant to supported operations.
|
||||
type ConfigView struct {
|
||||
Effective EffectiveWriter
|
||||
Sources SourceReader
|
||||
Explanation ExplanationReader
|
||||
}
|
||||
|
||||
// Acquired contains a configuration view and diagnostics from its acquisition.
|
||||
type Acquired[T any] struct {
|
||||
Value T
|
||||
Diagnostics []Diagnostic
|
||||
}
|
||||
|
||||
// ConfigScope declares one named configuration and its supported operations.
|
||||
type ConfigScope[C any] struct {
|
||||
Name string
|
||||
Summary string
|
||||
Operations []ConfigOperation
|
||||
Acquire func(context.Context, ConfigRequest, C) (Acquired[ConfigView], error)
|
||||
}
|
||||
|
||||
// Selector binds one typed command flag to selected configuration scopes.
|
||||
// A required binding is required only when a selected scope uses it.
|
||||
type Selector[C any] struct {
|
||||
Binding Flag[C]
|
||||
Scopes []string
|
||||
}
|
||||
|
||||
// ConfigSuite declares selectors and configuration scopes for an App.
|
||||
type ConfigSuite[C any] struct {
|
||||
NewContext func() C
|
||||
Selectors []Selector[C]
|
||||
CheckInput func(Selection, C) error
|
||||
Scopes []ConfigScope[C]
|
||||
}
|
||||
|
||||
type configScopeInfo struct {
|
||||
Name string `yaml:"name"`
|
||||
Operations []ConfigOperation `yaml:"operations"`
|
||||
}
|
||||
|
||||
type scopesInventory struct {
|
||||
Scopes []configScopeInfo `yaml:"scopes"`
|
||||
}
|
||||
|
||||
type preparedConfigSuite[C any] struct {
|
||||
suite ConfigSuite[C]
|
||||
ordered []ConfigScope[C]
|
||||
byName map[string]ConfigScope[C]
|
||||
installed []ConfigOperation
|
||||
inventory scopesInventory
|
||||
}
|
||||
|
||||
type configScopesInput struct {
|
||||
Format string
|
||||
}
|
||||
|
||||
type selectorState[C any] struct {
|
||||
name string
|
||||
required bool
|
||||
scopes []string
|
||||
supplied bool
|
||||
apply func(*C)
|
||||
clear func(*C)
|
||||
}
|
||||
|
||||
type configInput[C any] struct {
|
||||
Selection Selection
|
||||
Scope string
|
||||
Format string
|
||||
Path Supplied[string]
|
||||
SelectorContext C
|
||||
selectors []selectorState[C]
|
||||
}
|
||||
|
||||
// RegisterConfig installs the available config handlers and the scope discovery
|
||||
// command. Its registration and discovery paths do not invoke scope loaders.
|
||||
func RegisterConfig[C any](app *App, suite ConfigSuite[C]) error {
|
||||
if app == nil {
|
||||
return fmt.Errorf("cannot register configuration on a nil app")
|
||||
}
|
||||
if app.configRegistered {
|
||||
return fmt.Errorf("configuration suite is already registered")
|
||||
}
|
||||
normalized, err := normalizeConfigSuite(suite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, exists := app.commands["config"]; exists {
|
||||
return fmt.Errorf("command path %q is already registered", "config")
|
||||
}
|
||||
prepared := prepareConfigSuite(normalized)
|
||||
|
||||
commands := make([]Command, 0, 5)
|
||||
for _, operation := range prepared.installed {
|
||||
var command Command
|
||||
switch operation {
|
||||
case ConfigValidate:
|
||||
command, err = newConfigValidateCommand(prepared)
|
||||
case ConfigShow:
|
||||
command, err = newConfigShowCommand(prepared)
|
||||
case ConfigSources:
|
||||
command, err = newConfigSourcesCommand(prepared)
|
||||
case ConfigExplain:
|
||||
command, err = newConfigExplainCommand(prepared)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
commands = append(commands, command)
|
||||
}
|
||||
command, err := newConfigScopesCommandWithPrepared(prepared)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
commands = append(commands, command)
|
||||
root, err := Group("config", "Inspect configuration.", commands...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := app.Register(root); err != nil {
|
||||
return err
|
||||
}
|
||||
app.configRegistered = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewConfigKitScope adapts a ConfigKit loader into a scope with validation,
|
||||
// effective display, source listing, and path explanation capabilities. The
|
||||
// loader runs once per selected operation. Successful loads expose the result's
|
||||
// public facets directly; ConfigKit LoadError records become CommandKit
|
||||
// diagnostics while the original error is returned.
|
||||
func NewConfigKitScope[C, T any](name, summary string, load func(context.Context, Selection, C) (*configkit.Result[T], error)) (ConfigScope[C], error) {
|
||||
if load == nil {
|
||||
return ConfigScope[C]{}, fmt.Errorf("ConfigKit scope loader must not be nil")
|
||||
}
|
||||
return ConfigScope[C]{
|
||||
Name: name,
|
||||
Summary: summary,
|
||||
Operations: []ConfigOperation{ConfigValidate, ConfigShow, ConfigSources, ConfigExplain},
|
||||
Acquire: func(ctx context.Context, request ConfigRequest, contextValue C) (Acquired[ConfigView], error) {
|
||||
result, err := load(ctx, request.Selection, contextValue)
|
||||
if err != nil {
|
||||
var loadError *configkit.LoadError
|
||||
if errors.As(err, &loadError) {
|
||||
return Acquired[ConfigView]{Diagnostics: ConfigKitDiagnostics(name, loadError.Sources(), loadError.Diagnostics())}, err
|
||||
}
|
||||
return Acquired[ConfigView]{}, err
|
||||
}
|
||||
if result == nil {
|
||||
return Acquired[ConfigView]{}, fmt.Errorf("ConfigKit scope %q returned a nil result", name)
|
||||
}
|
||||
return Acquired[ConfigView]{
|
||||
Value: ConfigView{Effective: result, Sources: result, Explanation: result},
|
||||
Diagnostics: ConfigKitDiagnostics(name, result.Sources(), result.Diagnostics()),
|
||||
}, nil
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ConfigKitDiagnostics groups ConfigKit records with their scope and source legend.
|
||||
func ConfigKitDiagnostics(scope string, sources []configkit.SourceInfo, records []configkit.Diagnostic) []Diagnostic {
|
||||
if len(records) == 0 {
|
||||
return nil
|
||||
}
|
||||
severity := configkit.SeverityWarning
|
||||
for _, record := range records {
|
||||
if record.Severity == configkit.SeverityError {
|
||||
severity = configkit.SeverityError
|
||||
break
|
||||
}
|
||||
}
|
||||
return []Diagnostic{{
|
||||
Severity: severity,
|
||||
Write: func(writer io.Writer) error {
|
||||
if err := writeDiagnosticText(writer, "Configuration scope "+strconv.Quote(scope)+":\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(sources) > 0 {
|
||||
if err := writeDiagnosticText(writer, "Source IDs:\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, source := range sources {
|
||||
line := " " + strconv.Quote(source.ID) + ": label=" + strconv.Quote(source.Label) + ", path=" + strconv.Quote(source.Path) + "\n"
|
||||
if err := writeDiagnosticText(writer, line); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return configkit.WriteDiagnostics(writer, records)
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
func writeDiagnosticText(writer io.Writer, value string) error {
|
||||
written, err := io.WriteString(writer, value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if written != len(value) {
|
||||
return io.ErrShortWrite
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeConfigSuite[C any](suite ConfigSuite[C]) (ConfigSuite[C], error) {
|
||||
normalized := suite
|
||||
normalized.Selectors = append([]Selector[C](nil), suite.Selectors...)
|
||||
normalized.Scopes = make([]ConfigScope[C], 0, len(suite.Scopes))
|
||||
scopeNames := make(map[string]struct{}, len(suite.Scopes))
|
||||
for _, scope := range suite.Scopes {
|
||||
if scope.Name == "" {
|
||||
return ConfigSuite[C]{}, fmt.Errorf("configuration scope name must not be empty")
|
||||
}
|
||||
if scope.Name == "all" {
|
||||
return ConfigSuite[C]{}, fmt.Errorf("configuration scope name %q is reserved", scope.Name)
|
||||
}
|
||||
if _, exists := scopeNames[scope.Name]; exists {
|
||||
return ConfigSuite[C]{}, fmt.Errorf("duplicate configuration scope %q", scope.Name)
|
||||
}
|
||||
scopeNames[scope.Name] = struct{}{}
|
||||
operations, err := normalizeConfigOperations(scope.Operations)
|
||||
if err != nil {
|
||||
return ConfigSuite[C]{}, fmt.Errorf("configuration scope %q: %w", scope.Name, err)
|
||||
}
|
||||
if len(operations) > 0 && scope.Acquire == nil {
|
||||
return ConfigSuite[C]{}, fmt.Errorf("configuration scope %q declares operations without an acquisition callback", scope.Name)
|
||||
}
|
||||
scope.Operations = operations
|
||||
normalized.Scopes = append(normalized.Scopes, scope)
|
||||
}
|
||||
seenNames := make(map[string]struct{}, len(normalized.Selectors))
|
||||
seenShorts := make(map[string]struct{}, len(normalized.Selectors))
|
||||
for index := range normalized.Selectors {
|
||||
selector := &normalized.Selectors[index]
|
||||
binding := selector.Binding.binding
|
||||
if !binding.valid || binding.clear == nil || binding.meta.Name == "" {
|
||||
return ConfigSuite[C]{}, fmt.Errorf("configuration selector %d has an invalid flag binding", index)
|
||||
}
|
||||
if !validFlagName(binding.meta.Name) {
|
||||
return ConfigSuite[C]{}, fmt.Errorf("configuration selector flag name %q is invalid", binding.meta.Name)
|
||||
}
|
||||
if binding.meta.Name == "config" || binding.meta.Name == "overlay" || binding.meta.Name == "scope" || binding.meta.Name == "format" || binding.meta.Name == "help" {
|
||||
return ConfigSuite[C]{}, fmt.Errorf("configuration selector flag --%s conflicts with a standard flag", binding.meta.Name)
|
||||
}
|
||||
if binding.meta.Short == "h" {
|
||||
return ConfigSuite[C]{}, fmt.Errorf("configuration selector short flag -%s conflicts with help", binding.meta.Short)
|
||||
}
|
||||
if binding.meta.Short != "" && (len(binding.meta.Short) != 1 || binding.meta.Short == "-") {
|
||||
return ConfigSuite[C]{}, fmt.Errorf("configuration selector short spelling %q must be one byte", binding.meta.Short)
|
||||
}
|
||||
seen := make(map[string]struct{}, len(selector.Scopes))
|
||||
unique := make([]string, 0, len(selector.Scopes))
|
||||
for _, scopeName := range selector.Scopes {
|
||||
if _, exists := scopeNames[scopeName]; !exists {
|
||||
return ConfigSuite[C]{}, fmt.Errorf("selector --%s references unknown configuration scope %q", binding.meta.Name, scopeName)
|
||||
}
|
||||
if _, exists := seen[scopeName]; exists {
|
||||
continue
|
||||
}
|
||||
seen[scopeName] = struct{}{}
|
||||
unique = append(unique, scopeName)
|
||||
}
|
||||
selector.Scopes = append([]string(nil), unique...)
|
||||
if _, exists := seenNames[binding.meta.Name]; exists {
|
||||
return ConfigSuite[C]{}, fmt.Errorf("duplicate configuration selector flag --%s", binding.meta.Name)
|
||||
}
|
||||
if binding.meta.Short != "" {
|
||||
if _, exists := seenShorts[binding.meta.Short]; exists {
|
||||
return ConfigSuite[C]{}, fmt.Errorf("duplicate configuration selector flag --%s", binding.meta.Name)
|
||||
}
|
||||
seenShorts[binding.meta.Short] = struct{}{}
|
||||
}
|
||||
seenNames[binding.meta.Name] = struct{}{}
|
||||
}
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
func prepareConfigSuite[C any](suite ConfigSuite[C]) *preparedConfigSuite[C] {
|
||||
prepared := &preparedConfigSuite[C]{
|
||||
suite: suite,
|
||||
ordered: orderedConfigScopes(suite.Scopes),
|
||||
byName: make(map[string]ConfigScope[C], len(suite.Scopes)),
|
||||
installed: make([]ConfigOperation, 0, 4),
|
||||
inventory: scopesInventory{Scopes: make([]configScopeInfo, 0, len(suite.Scopes))},
|
||||
}
|
||||
for _, scope := range suite.Scopes {
|
||||
prepared.byName[scope.Name] = scope
|
||||
}
|
||||
for _, operation := range []ConfigOperation{ConfigValidate, ConfigShow, ConfigSources, ConfigExplain} {
|
||||
if configOperationDeclared(suite.Scopes, operation) {
|
||||
prepared.installed = append(prepared.installed, operation)
|
||||
}
|
||||
}
|
||||
for _, scope := range prepared.ordered {
|
||||
operations := make([]ConfigOperation, 0, len(scope.Operations))
|
||||
for _, operation := range scope.Operations {
|
||||
if containsOperation(prepared.installed, operation) {
|
||||
operations = append(operations, operation)
|
||||
}
|
||||
}
|
||||
prepared.inventory.Scopes = append(prepared.inventory.Scopes, configScopeInfo{Name: scope.Name, Operations: operations})
|
||||
}
|
||||
return prepared
|
||||
}
|
||||
|
||||
func normalizeConfigOperations(operations []ConfigOperation) ([]ConfigOperation, error) {
|
||||
seen := make(map[ConfigOperation]struct{}, len(operations))
|
||||
for _, operation := range operations {
|
||||
switch operation {
|
||||
case ConfigValidate, ConfigShow, ConfigSources, ConfigExplain:
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown operation %q", operation)
|
||||
}
|
||||
seen[operation] = struct{}{}
|
||||
}
|
||||
ordered := make([]ConfigOperation, 0, len(seen))
|
||||
for _, operation := range []ConfigOperation{ConfigValidate, ConfigShow, ConfigSources, ConfigExplain} {
|
||||
if _, exists := seen[operation]; exists {
|
||||
ordered = append(ordered, operation)
|
||||
}
|
||||
}
|
||||
return ordered, nil
|
||||
}
|
||||
|
||||
func newConfigScopesCommandWithPrepared[C any](prepared *preparedConfigSuite[C]) (Command, error) {
|
||||
formatDefault := "text"
|
||||
spec := CommandSpec[configScopesInput]{
|
||||
Name: "scopes",
|
||||
Summary: "List configuration scopes and supported operations.",
|
||||
Flags: []Flag[configScopesInput]{ValueFlag[configScopesInput](FlagMeta{
|
||||
Name: "format", Usage: "Output format: text or yaml.",
|
||||
}, &formatDefault, parseConfigFormat, func(input *configScopesInput, value Supplied[string]) {
|
||||
input.Format = value.Value
|
||||
})},
|
||||
CheckInput: func(input configScopesInput) error { return checkConfigFormat(input.Format) },
|
||||
Run: func(invocation Invocation, input configScopesInput) Outcome {
|
||||
var output []byte
|
||||
var err error
|
||||
if input.Format == "yaml" {
|
||||
output, err = yaml.Marshal(prepared.inventory)
|
||||
} else {
|
||||
output = []byte(formatScopeInventory(prepared.inventory))
|
||||
}
|
||||
if err != nil {
|
||||
return Failure(err)
|
||||
}
|
||||
if err := writeReport(invocation.Streams.Out, output); err != nil {
|
||||
return incompleteReport(err)
|
||||
}
|
||||
return Success()
|
||||
},
|
||||
}
|
||||
return NewCommand(spec)
|
||||
}
|
||||
|
||||
func orderedConfigScopes[C any](scopes []ConfigScope[C]) []ConfigScope[C] {
|
||||
ordered := append([]ConfigScope[C](nil), scopes...)
|
||||
sort.SliceStable(ordered, func(i, j int) bool {
|
||||
return configScopeOrder(ordered[i].Name) < configScopeOrder(ordered[j].Name)
|
||||
})
|
||||
return ordered
|
||||
}
|
||||
|
||||
func configScopeOrder(name string) string {
|
||||
switch name {
|
||||
case "app":
|
||||
return "\x00"
|
||||
case "promptkit":
|
||||
return "\x01"
|
||||
default:
|
||||
return "\x02" + name
|
||||
}
|
||||
}
|
||||
|
||||
func formatScopeInventory(inventory scopesInventory) string {
|
||||
var output strings.Builder
|
||||
for _, scope := range inventory.Scopes {
|
||||
output.WriteString(scope.Name)
|
||||
output.WriteString(":")
|
||||
if len(scope.Operations) > 0 {
|
||||
output.WriteString(" ")
|
||||
for index, operation := range scope.Operations {
|
||||
if index > 0 {
|
||||
output.WriteString(", ")
|
||||
}
|
||||
output.WriteString(string(operation))
|
||||
}
|
||||
}
|
||||
output.WriteByte('\n')
|
||||
}
|
||||
return output.String()
|
||||
}
|
||||
|
||||
func parseConfigFormat(value string) (string, error) {
|
||||
if err := checkConfigFormat(value); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func checkConfigFormat(value string) error {
|
||||
if value != "text" && value != "yaml" {
|
||||
return fmt.Errorf("format must be text or yaml")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user