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

@@ -5,8 +5,9 @@ and shared command infrastructure across related applications.
The current Go library provides an instance-owned command registry, typed flag The current Go library provides an instance-owned command registry, typed flag
and operand bindings, Cobra-backed routing, invocation outcomes, generated help, and operand bindings, Cobra-backed routing, invocation outcomes, generated help,
and application or CommandKit version reporting. Standard configuration and application or CommandKit version reporting, configuration scope discovery, and
PromptKit command families remain planned. ConfigKit-backed validation. Effective display, source and explanation reports,
and PromptKit commands remain planned.
## Documentation ## Documentation

View File

@@ -2,9 +2,11 @@ package commandkit
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"sort" "sort"
"strconv"
"strings" "strings"
configkit "gitea.maximumdirect.net/eric/ConfigKit" 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") 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 { if err != nil {
return err return err
} }
root, err := Group("config", "Inspect configuration.", command) commands = append(commands, command)
root, err := Group("config", "Inspect configuration.", commands...)
if err != nil { if err != nil {
return err return err
} }
@@ -144,6 +157,80 @@ func RegisterConfig[C any](app *App, suite ConfigSuite[C]) error {
return nil 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) { func normalizeConfigSuite[C any](suite ConfigSuite[C]) (ConfigSuite[C], error) {
normalized := suite normalized := suite
normalized.Selectors = append([]Selector[C](nil), suite.Selectors...) normalized.Selectors = append([]Selector[C](nil), suite.Selectors...)

View File

@@ -47,7 +47,7 @@ func TestConfigScopeDiscoveryOrdersAndDeduplicatesCapabilities(t *testing.T) {
if outcome.Kind != OutcomeSuccess { if outcome.Kind != OutcomeSuccess {
t.Fatalf("text inventory outcome = %#v", outcome) t.Fatalf("text inventory outcome = %#v", outcome)
} }
wantText := "app:\npromptkit:\nmetadata:\nzeta:\n" wantText := "app: validate\npromptkit:\nmetadata:\nzeta:\n"
if textOutput.String() != wantText { if textOutput.String() != wantText {
t.Fatalf("text inventory = %q, want %q", textOutput.String(), wantText) t.Fatalf("text inventory = %q, want %q", textOutput.String(), wantText)
} }

242
configkit_test.go Normal file
View File

@@ -0,0 +1,242 @@
package commandkit_test
import (
"bytes"
"context"
"errors"
"io"
"reflect"
"strings"
"testing"
"testing/fstest"
commandkit "gitea.maximumdirect.net/eric/CommandKit"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
)
func TestConfigKitScopeUsesDefaultsOverlayAndRecordedResolution(t *testing.T) {
type configuration struct {
Mode string `yaml:"mode"`
Defaulted string `yaml:"defaulted"`
}
filesystem := configkit.NewFilesystem("fixture", fstest.MapFS{
"config/main.yml": &fstest.MapFile{Data: []byte("mode: base\n")},
"config/overlays/dev.yml": &fstest.MapFile{Data: []byte("mode: overlay\n")},
})
definition := configkit.Definition[configuration]{
Defaults: func() configuration { return configuration{Defaulted: "from default"} },
Resolve: func(input *configuration, resolution *configkit.Resolution) error {
resolution.Record(configkit.Keys("mode"), configkit.ChangeNormalization, "normalized for use")
input.Mode = strings.ToUpper(input.Mode)
return nil
},
}
var received commandkit.Selection
scope, err := commandkit.NewConfigKitScope[struct{}, configuration]("app", "Application settings.", func(_ context.Context, selection commandkit.Selection, _ struct{}) (*configkit.Result[configuration], error) {
received = selection
return configkit.LoadLayered(configkit.Input{Source: filesystem.File(selection.Config.Value)}, configkit.LayeredOptions[configuration]{
Definition: definition,
Overlay: selection.Overlay.Value,
OverlayFilesystem: &configkit.FilesystemContext{Root: "config", BaseDir: "config", FS: &filesystem},
})
})
if err != nil {
t.Fatal(err)
}
selection := commandkit.Selection{
Config: commandkit.Supplied[string]{Value: "config/main.yml", Set: true},
Overlay: commandkit.Supplied[string]{Value: "dev", Set: true},
}
acquired, err := scope.Acquire(context.Background(), commandkit.ConfigRequest{Operation: commandkit.ConfigValidate, Selection: selection}, struct{}{})
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(received, selection) {
t.Fatalf("loader selection = %#v, want %#v", received, selection)
}
if acquired.Value.Effective == nil || acquired.Value.Sources == nil || acquired.Value.Explanation == nil {
t.Fatalf("ConfigKit result facets = %#v", acquired.Value)
}
if got := acquired.Value.Sources.Sources(); len(got) < 2 {
t.Fatalf("sources = %#v, want base and overlay", got)
}
var effective bytes.Buffer
if err := acquired.Value.Effective.WriteEffective(&effective); err != nil {
t.Fatal(err)
}
if !strings.Contains(effective.String(), "mode: OVERLAY") || !strings.Contains(effective.String(), "defaulted: from default") {
t.Fatalf("effective configuration = %q", effective.String())
}
if explanation, ok := acquired.Value.Explanation.Explain(configkit.Keys("mode")); !ok || len(explanation.History) == 0 {
t.Fatalf("resolution explanation = %#v, present=%t", explanation, ok)
}
}
func TestConfigKitScopePreservesAbsentAndEmptySelections(t *testing.T) {
type configuration struct {
Value string `yaml:"value"`
}
var seen []commandkit.Selection
scope, err := commandkit.NewConfigKitScope[struct{}, configuration]("app", "", func(_ context.Context, selection commandkit.Selection, _ struct{}) (*configkit.Result[configuration], error) {
seen = append(seen, selection)
return configkit.Load(configkit.NoSource(), configkit.Options[configuration]{Definition: configkit.Definition[configuration]{Defaults: func() configuration { return configuration{Value: "default"} }}})
})
if err != nil {
t.Fatal(err)
}
_, err = scope.Acquire(context.Background(), commandkit.ConfigRequest{}, struct{}{})
if err != nil {
t.Fatal(err)
}
empty := commandkit.Selection{Config: commandkit.Supplied[string]{Set: true}, Overlay: commandkit.Supplied[string]{Set: true}}
_, err = scope.Acquire(context.Background(), commandkit.ConfigRequest{Selection: empty}, struct{}{})
if err != nil {
t.Fatal(err)
}
want := []commandkit.Selection{{}, empty}
if !reflect.DeepEqual(seen, want) {
t.Fatalf("loader selections = %#v, want %#v", seen, want)
}
}
func TestConfigKitScopeRequiresLoaderAndNonNilResult(t *testing.T) {
if _, err := commandkit.NewConfigKitScope[struct{}, struct{}]("app", "", nil); err == nil {
t.Fatal("accepted a nil ConfigKit loader")
}
scope, err := commandkit.NewConfigKitScope[struct{}, struct{}]("app", "", func(context.Context, commandkit.Selection, struct{}) (*configkit.Result[struct{}], error) {
return nil, nil
})
if err != nil {
t.Fatal(err)
}
if _, err := scope.Acquire(context.Background(), commandkit.ConfigRequest{}, struct{}{}); err == nil || !strings.Contains(err.Error(), "nil result") {
t.Fatalf("nil result error = %v", err)
}
}
func TestConfigKitLoadErrorRetainsIdentityAndStructuredDiagnostics(t *testing.T) {
type configuration struct {
Name string `yaml:"name"`
}
var loadError error
scope, err := commandkit.NewConfigKitScope[struct{}, configuration]("app", "", func(context.Context, commandkit.Selection, struct{}) (*configkit.Result[configuration], error) {
_, loadError = configkit.Load(configkit.Mapping("bad source", map[string]any{"unknown": true}), configkit.Options[configuration]{Definition: configkit.Definition[configuration]{}})
return nil, loadError
})
if err != nil {
t.Fatal(err)
}
acquired, acquireError := scope.Acquire(context.Background(), commandkit.ConfigRequest{}, struct{}{})
var publicLoadError *configkit.LoadError
if acquireError != loadError || !errors.As(acquireError, &publicLoadError) {
t.Fatalf("load error identity = %v (original %v), public type=%T", acquireError, loadError, acquireError)
}
if len(acquired.Diagnostics) != 1 || acquired.Diagnostics[0].Severity != configkit.SeverityError {
t.Fatalf("structured diagnostics = %#v", acquired.Diagnostics)
}
var rendered bytes.Buffer
if err := acquired.Diagnostics[0].Write(&rendered); err != nil {
t.Fatal(err)
}
if !strings.Contains(rendered.String(), `Configuration scope "app":`) || !strings.Contains(rendered.String(), `"bad source"`) {
t.Fatalf("grouped diagnostics = %q", rendered.String())
}
app, _ := commandkit.New(commandkit.Identity{Name: "example"})
if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{scope}}); err != nil {
t.Fatal(err)
}
var output, diagnostic bytes.Buffer
outcome := app.Execute(context.Background(), []string{"config", "validate"}, commandkit.Streams{Out: &output, Err: &diagnostic})
if outcome.Cause != loadError || len(outcome.Diagnostics) != 1 || strings.Count(diagnostic.String(), `Configuration scope "app":`) != 1 || output.Len() != 0 {
t.Fatalf("presented load failure outcome=%#v output=%q diagnostics=%q", outcome, output.String(), diagnostic.String())
}
}
func TestConfigValidationEmitsSuccessAsScopesCompleteAndStopsAtFailure(t *testing.T) {
app, err := commandkit.New(commandkit.Identity{Name: "example"})
if err != nil {
t.Fatal(err)
}
var calls []string
newContextCalls := 0
failure := errors.New("promptkit could not load")
suite := commandkit.ConfigSuite[string]{
NewContext: func() string { newContextCalls++; return "runtime" },
Scopes: []commandkit.ConfigScope[string]{
{Name: "zeta", Operations: []commandkit.ConfigOperation{commandkit.ConfigValidate}, Acquire: func(context.Context, commandkit.ConfigRequest, string) (commandkit.Acquired[commandkit.ConfigView], error) {
calls = append(calls, "zeta")
return commandkit.Acquired[commandkit.ConfigView]{}, nil
}},
{Name: "promptkit", Operations: []commandkit.ConfigOperation{commandkit.ConfigValidate}, Acquire: func(context.Context, commandkit.ConfigRequest, string) (commandkit.Acquired[commandkit.ConfigView], error) {
calls = append(calls, "promptkit")
return commandkit.Acquired[commandkit.ConfigView]{}, failure
}},
{Name: "app", Operations: []commandkit.ConfigOperation{commandkit.ConfigValidate}, Acquire: func(context.Context, commandkit.ConfigRequest, string) (commandkit.Acquired[commandkit.ConfigView], error) {
calls = append(calls, "app")
return commandkit.Acquired[commandkit.ConfigView]{}, nil
}},
},
}
if err := commandkit.RegisterConfig(app, suite); err != nil {
t.Fatal(err)
}
var output, diagnostic bytes.Buffer
outcome := app.Execute(context.Background(), []string{"config", "validate", "--scope", "all"}, commandkit.Streams{Out: &output, Err: &diagnostic})
if outcome.Kind != commandkit.OutcomeOperational || !errors.Is(outcome.Cause, failure) || !outcome.ReportIncomplete {
t.Fatalf("validation outcome = %#v", outcome)
}
if !reflect.DeepEqual(calls, []string{"app", "promptkit"}) || newContextCalls != 1 {
t.Fatalf("acquisitions = %v, NewContext calls=%d", calls, newContextCalls)
}
if output.String() != "app: valid\n" || !strings.Contains(diagnostic.String(), failure.Error()) {
t.Fatalf("output=%q diagnostics=%q", output.String(), diagnostic.String())
}
}
func TestConfigValidationYAMLWarningsAndShortWrites(t *testing.T) {
type configuration struct {
Name string `yaml:"name"`
}
warningScope, err := commandkit.NewConfigKitScope[struct{}, configuration]("app", "", func(context.Context, commandkit.Selection, struct{}) (*configkit.Result[configuration], error) {
return configkit.Load(configkit.NoSource(), configkit.Options[configuration]{Definition: configkit.Definition[configuration]{
Validate: func(_ *configuration, validation *configkit.Validation) {
validation.Warning(configkit.Keys("name"), "uses a demonstration name")
},
}})
})
if err != nil {
t.Fatal(err)
}
app, _ := commandkit.New(commandkit.Identity{Name: "example"})
if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{warningScope}}); err != nil {
t.Fatal(err)
}
var output, diagnostic bytes.Buffer
outcome := app.Execute(context.Background(), []string{"config", "validate", "--format", "yaml"}, commandkit.Streams{Out: &output, Err: &diagnostic})
if outcome.Kind != commandkit.OutcomeSuccess || outcome.Status != 0 || outcome.DiagnosticDelivery != commandkit.DeliveryComplete {
t.Fatalf("warning validation outcome = %#v", outcome)
}
if output.String() != "scopes:\n - name: app\n valid: true\n" || !strings.Contains(diagnostic.String(), "uses a demonstration name") {
t.Fatalf("output=%q diagnostics=%q", output.String(), diagnostic.String())
}
outcome = app.Execute(context.Background(), []string{"config", "validate"}, commandkit.Streams{Out: configKitPartialWriter{}, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeOperational || !outcome.ReportIncomplete || !errors.Is(outcome.Cause, io.ErrShortWrite) {
t.Fatalf("report short write outcome = %#v", outcome)
}
var delivered bytes.Buffer
outcome = app.Execute(context.Background(), []string{"config", "validate"}, commandkit.Streams{Out: &delivered, Err: configKitPartialWriter{}})
if outcome.DiagnosticDelivery != commandkit.DeliveryIncomplete || !errors.Is(outcome.DeliveryError, io.ErrShortWrite) {
t.Fatalf("diagnostic short write outcome = %#v", outcome)
}
}
type configKitPartialWriter struct{}
func (configKitPartialWriter) Write(data []byte) (int, error) {
if len(data) == 0 {
return 0, nil
}
return len(data) - 1, nil
}

View File

@@ -102,6 +102,29 @@ runtime context or acquire configuration. It reports the intersection between
declared scope capabilities and installed handlers, so an operation is listed declared scope capabilities and installed handlers, so an operation is listed
only when the corresponding command is available. only when the corresponding command is available.
### Validate configuration
`config validate` loads and validates the selected scope. The default scope is
`app`; `--scope all` validates every registered scope and is available when each
scope declares validation. The command checks all selected capabilities before
calling scope providers.
Its default format is text; `--format yaml` emits a `scopes` sequence with
`name` and `valid: true` for each scope that completed successfully.
```text
example config validate
example config validate --scope promptkit --format yaml
example config validate --scope all
```
Each successful scope is reported as `SCOPE: valid`. Aggregate validation stops
at the first load or output failure; records from earlier successful scopes may
already have been written. Provider warnings go to the diagnostic stream and do
not alone make validation fail. `--config` and `--overlay` preserve whether an
empty value was explicitly supplied, so a scope callback can map those inputs to
its own ConfigKit load policy. Help and invalid syntax do not call scope
providers.
## Outcome status ## Outcome status
CommandKit usage failures use status `2`; operational failures use status `1`. CommandKit usage failures use status `2`; operational failures use status `1`.

View File

@@ -4,8 +4,9 @@ CommandKit currently contains adopted repository policies, accepted architectura
decisions, implementation plans, identity and outcome primitives, typed command decisions, implementation plans, identity and outcome primitives, typed command
declarations, an instance-owned registry, a Cobra-backed routing and parsing declarations, an instance-owned registry, a Cobra-backed routing and parsing
adapter, public `App.Execute` coordination, and generated help/version commands. adapter, public `App.Execute` coordination, and generated help/version commands.
Configuration suite registration and scope discovery are implemented; the Configuration suite registration, scope discovery, ConfigKit integration, and
configuration operations remain pending. The validation are implemented; other configuration and PromptKit operations remain
pending. The
[README](../README.md) provides project orientation; the [README](../README.md) provides project orientation; the
[documentation policy](policy/documentation.md#canonical-ownership) assigns [documentation policy](policy/documentation.md#canonical-ownership) assigns
canonical documentation owners. canonical documentation owners.

View File

@@ -1,6 +1,6 @@
# Initial CommandKit implementation plan # Initial CommandKit implementation plan
Status: implementation underway; Stages 1-7 complete and Stages 8-14 pending. Status: implementation underway; Stages 1-8 complete and Stages 9-14 pending.
Date: 2026-10-07. Date: 2026-10-07.
@@ -551,7 +551,7 @@ sequenced in Stages 8-11.
## Stage 8: integrate ConfigKit and implement validation ## Stage 8: integrate ConfigKit and implement validation
**Status:** pending. **Prerequisite:** Stage 7. **Status:** complete. **Prerequisite:** Stage 7.
```go ```go
func NewConfigKitScope[C, T any](name, summary string, func NewConfigKitScope[C, T any](name, summary string,
@@ -594,6 +594,16 @@ empty overlay callback input; warnings permit validation success; structured loa
errors retain public identity and render once; all validates in order and stops errors retain public identity and render once; all validates in order and stops
on failure. Report/diagnostic short writes retain their writer errors. on failure. Report/diagnostic short writes retain their writer errors.
**Completion evidence:** added the ConfigKit scope adapter and grouped diagnostic
writer. The adapter exposes native effective/source/explanation facets, captures
success and `LoadError` diagnostics, and preserves loader errors. `config validate`
now installs for declared scopes, preflights all requested capabilities, acquires
in stable order, and emits success records incrementally in text or YAML. Checks
cover ConfigKit defaults, overlay, recorded resolution, absent/empty selections,
warning success, load-error identity and single presentation, aggregate stop
behavior, and report/diagnostic short writes. `go test ./...` passes; README,
development orientation, and CLI reference describe the implemented boundary.
## Stage 9: implement native effective YAML display ## Stage 9: implement native effective YAML display
**Status:** pending. **Prerequisite:** Stage 8. **Status:** pending. **Prerequisite:** Stage 8.

View File

@@ -64,6 +64,19 @@ func adaptSelector[C any](selector Selector[C]) Flag[configInput[C]] {
func parseString(value string) (string, error) { return value, nil } func parseString(value string) (string, error) { return value, nil }
func preflightConfig[C any](suite ConfigSuite[C], input configInput[C], operation ConfigOperation) ([]string, error) { 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 { if err := checkConfigFormat(input.Format); err != nil {
return nil, err return nil, err
} }
@@ -114,11 +127,6 @@ func preflightConfig[C any](suite ConfigSuite[C], input configInput[C], operatio
return nil, fmt.Errorf("selector is not applicable to configuration scope %q", input.Scope) return nil, fmt.Errorf("selector is not applicable to configuration scope %q", input.Scope)
} }
} }
if suite.CheckInput != nil {
if err := suite.CheckInput(input.Selection, input.SelectorContext); err != nil {
return nil, err
}
}
return scopes, nil return scopes, nil
} }

104
validate.go Normal file
View File

@@ -0,0 +1,104 @@
package commandkit
import (
"bytes"
"fmt"
"strings"
"go.yaml.in/yaml/v4"
)
func configOperationDeclared[C any](scopes []ConfigScope[C], operation ConfigOperation) bool {
for _, scope := range scopes {
if containsOperation(scope.Operations, operation) {
return true
}
}
return false
}
func newConfigValidateCommand[C any](suite ConfigSuite[C]) (Command, error) {
spec := CommandSpec[configInput[C]]{
Name: string(ConfigValidate),
Summary: "Validate selected configuration scopes.",
Flags: configInputFlags(suite, "text"),
CheckInput: func(input configInput[C]) error {
_, err := preflightConfig(suite, input, ConfigValidate)
return err
},
Run: func(invocation Invocation, input configInput[C]) Outcome {
return runConfigValidation(invocation, suite, input)
},
}
return NewCommand(spec)
}
func runConfigValidation[C any](invocation Invocation, suite ConfigSuite[C], input configInput[C]) Outcome {
scopeNames, err := selectConfigScopes(suite, input, ConfigValidate)
if err != nil {
return Failure(err)
}
contexts := contextsForScopes(suite, input, scopeNames)
diagnostics := make([]Diagnostic, 0)
writtenRecord := false
for _, name := range scopeNames {
scope, ok := findConfigScope(suite.Scopes, name)
if !ok || scope.Acquire == nil {
return validationFailure(fmt.Errorf("configuration scope %q has no acquisition callback", name), diagnostics, writtenRecord)
}
acquired, acquireErr := scope.Acquire(invocation.Context, ConfigRequest{Operation: ConfigValidate, Selection: input.Selection}, contexts[name])
diagnostics = append(diagnostics, acquired.Diagnostics...)
if acquireErr != nil {
return validationFailure(acquireErr, diagnostics, writtenRecord)
}
record, err := validationSuccessRecord(name, input.Format)
if err != nil {
return validationFailure(err, diagnostics, writtenRecord)
}
if input.Format == "yaml" && !writtenRecord {
if err := writeReport(invocation.Streams.Out, []byte("scopes:\n")); err != nil {
return validationFailure(err, diagnostics, true)
}
}
if err := writeReport(invocation.Streams.Out, record); err != nil {
return validationFailure(err, diagnostics, true)
}
writtenRecord = true
}
outcome := Success()
outcome.Diagnostics = diagnostics
return outcome
}
func validationFailure(cause error, diagnostics []Diagnostic, reportStarted bool) Outcome {
outcome := Failure(cause)
outcome.Diagnostics = diagnostics
outcome.ReportIncomplete = reportStarted
return outcome
}
func validationSuccessRecord(scope, format string) ([]byte, error) {
if format == "text" {
return []byte(scope + ": valid\n"), nil
}
encoded, err := yaml.Marshal(struct {
Name string `yaml:"name"`
Valid bool `yaml:"valid"`
}{Name: scope, Valid: true})
if err != nil {
return nil, err
}
lines := strings.Split(strings.TrimSuffix(string(encoded), "\n"), "\n")
var record bytes.Buffer
for index, line := range lines {
if index == 0 {
record.WriteString(" - ")
} else {
record.WriteString(" ")
}
record.WriteString(line)
record.WriteByte('\n')
}
return record.Bytes(), nil
}