Add ConfigKit validation and diagnostics

This commit is contained in:
2026-10-07 13:50:48 +00:00
parent cc9b0eddab
commit 3945f1c33a
9 changed files with 490 additions and 14 deletions

View File

@@ -2,9 +2,11 @@ package commandkit
import (
"context"
"errors"
"fmt"
"io"
"sort"
"strconv"
"strings"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
@@ -129,11 +131,22 @@ func RegisterConfig[C any](app *App, suite ConfigSuite[C]) error {
return fmt.Errorf("command path %q is already registered", "config")
}
command, err := newConfigScopesCommandWithScopes(normalized.Scopes, nil)
commands := make([]Command, 0, 2)
installed := make([]ConfigOperation, 0, 1)
if configOperationDeclared(normalized.Scopes, ConfigValidate) {
validate, err := newConfigValidateCommand(normalized)
if err != nil {
return err
}
commands = append(commands, validate)
installed = append(installed, ConfigValidate)
}
command, err := newConfigScopesCommandWithScopes(normalized.Scopes, installed)
if err != nil {
return err
}
root, err := Group("config", "Inspect configuration.", command)
commands = append(commands, command)
root, err := Group("config", "Inspect configuration.", commands...)
if err != nil {
return err
}
@@ -144,6 +157,80 @@ func RegisterConfig[C any](app *App, suite ConfigSuite[C]) error {
return nil
}
// NewConfigKitScope adapts a ConfigKit loader into a configuration scope.
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...)