Port native configuration reports

This commit is contained in:
2026-10-11 01:39:23 +00:00
parent 809a36bc46
commit c0f3a21fc7
12 changed files with 671 additions and 288 deletions

View File

@@ -182,7 +182,7 @@ func prepareInput[I any](spec CommandSpec[I], flagValues [][]string, operands []
return preparedInput[I]{}, fmt.Errorf("flag --%s: %w", binding.meta.Name, err)
}
preparedFlags[i] = value
if enforceRequired && binding.meta.Required && !value.supplied && missingRequired == "" {
if enforceRequired && binding.meta.Required && !binding.deferredRequired && !value.supplied && missingRequired == "" {
missingRequired = binding.meta.Name
}
}

View File

@@ -67,14 +67,15 @@ type preparedValue[I any] struct {
}
type flagBinding[I any] struct {
identity *bindingIdentity
meta FlagMeta
defaultText string
repeated bool
boolean bool
prepare func([]string) (preparedValue[I], error)
clear func(*I)
valid bool
identity *bindingIdentity
meta FlagMeta
deferredRequired bool
defaultText string
repeated bool
boolean bool
prepare func([]string) (preparedValue[I], error)
clear func(*I)
valid bool
}
// Argument represents a typed binding for a positional command argument.

View File

@@ -1,6 +1,6 @@
# Implement the optional integration modules and captured registration API
Status: implementation in progress. Stages 1 through 9 are complete; subsequent work follows the ordered roadmap.
Status: implementation in progress. Stages 1 through 10 are complete; subsequent work follows the ordered roadmap.
Execute the stages in order. Each stage is sized as a separate Luna coding-agent
assignment. This document authorizes no publication or changes to sibling
repositories.
@@ -499,6 +499,20 @@ failure tests, including aggregate preacquisition, marker edge cases, warning-on
validation, source facet calls, pointer parsing, historical redaction, and
cancelled acquisition. Run the complete ckconfig suite when coherent.
**Completion evidence — complete:** validate/show/sources/explain now execute from
the prepared scope records and one shared state. Validation emits ordered
successes and warnings do not prevent success; show preacquires aggregate views,
keeps single-scope native output intact, and its YAML framing preserves native
content, markers, directive preambles, comments, CRLF and document boundaries.
Sources reads the native inventory once and retains empty and zero-valued fields.
Explain parses pointers before acquisition, supports the root pointer, and keeps
safe history, provenance, secret references and empty lists. Provider and
cancellation causes are retained; report delivery failures are marked separately
from diagnostics. Captured required flags now retain help metadata while deferring
enforcement to the selected original binding. The focused ckconfig adapter and
report tests pass from an explicit file list; root `go test .` passes. The full
ckconfig package suite remains deferred until Stage 13 replaces prototype tests.
## Stage 11: Implement PromptKit engine and scope adapters
**Outcome:** ckprompt composes engine configuration through ckconfig and named

View File

@@ -24,11 +24,12 @@ func CaptureFlags[Outer, I any](flags []Flag[I], target func(*Outer) *FlagValues
inner := original
identity := newBindingIdentity()
binding := flagBinding[Outer]{
identity: identity,
meta: inner.binding.meta,
defaultText: inner.binding.defaultText,
repeated: inner.binding.repeated,
boolean: inner.binding.boolean,
identity: identity,
meta: inner.binding.meta,
deferredRequired: inner.binding.meta.Required,
defaultText: inner.binding.defaultText,
repeated: inner.binding.repeated,
boolean: inner.binding.boolean,
}
if target != nil && inner.binding.identity != nil && inner.binding.valid && inner.binding.prepare != nil {
binding.valid = true

View File

@@ -6,41 +6,10 @@ import (
"fmt"
"io"
commandkit "gitea.maximumdirect.net/eric/CommandKit"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
)
func newConfigExplainCommand[C any](prepared *preparedConfigSuite[C]) (Command, error) {
flags := configInputFlags(prepared.suite, "text")
flags = append(flags, ValueFlag[configInput[C]](FlagMeta{
Name: "path",
Usage: "Configuration path as a JSON Pointer; an empty value selects the root.",
Required: true,
}, nil, parseString, func(input *configInput[C], value Supplied[string]) {
input.Path = value
}))
spec := CommandSpec[configInput[C]]{
Name: string(ConfigExplain),
Summary: "Explain the effective and historical value at a configuration path.",
Flags: flags,
CheckInput: func(input configInput[C]) error {
if _, err := preflightConfig(prepared, input, ConfigExplain); err != nil {
return err
}
if !input.Path.Set {
return fmt.Errorf("--path is required")
}
if _, err := configkit.ParsePath(input.Path.Value); err != nil {
return fmt.Errorf("invalid configuration path %q: %w", input.Path.Value, err)
}
return nil
},
Run: func(invocation Invocation, input configInput[C]) Outcome {
return runConfigExplanation(invocation, prepared, input)
},
}
return NewCommand(spec)
}
type explanationReport struct {
Scope string `yaml:"scope"`
Path string `yaml:"path"`
@@ -82,70 +51,56 @@ type secretReferenceDTO struct {
Name string `yaml:"name"`
}
func runConfigExplanation[C any](invocation Invocation, prepared *preparedConfigSuite[C], input configInput[C]) Outcome {
func runConfigExplanation[I, S any](invocation commandkit.Invocation, plan *configOperationPlan[I, S], state S) commandkit.Outcome {
selected := plan.scopes[0]
name := selected.scope.scope.Name
if err := invocation.Context.Err(); err != nil {
return Failure(err)
return configOperationFailure(err, nil, false)
}
scopeNames, err := selectConfigScopes(prepared, input, ConfigExplain)
if err != nil {
return Failure(err)
if selected.scope.scope.Acquire == nil {
return configOperationFailure(fmt.Errorf("configuration scope %q has no acquisition callback", name), nil, false)
}
if !input.Path.Set {
return Failure(fmt.Errorf("--path is required"))
}
path, err := configkit.ParsePath(input.Path.Value)
if err != nil {
return Usage(fmt.Errorf("invalid configuration path %q: %w", input.Path.Value, err))
}
name := scopeNames[0]
contexts := contextsForScopes(prepared, input, scopeNames)
scope, ok := findConfigScope(prepared, name)
if !ok || scope.Acquire == nil {
return Failure(fmt.Errorf("configuration scope %q has no acquisition callback", name))
}
if err := invocation.Context.Err(); err != nil {
return explanationFailure(err, nil, false)
}
acquired, acquireErr := scope.Acquire(invocation.Context, ConfigRequest{Operation: ConfigExplain, Selection: input.Selection}, contexts[name])
diagnostics := append([]Diagnostic(nil), acquired.Diagnostics...)
acquired, acquireErr := selected.scope.scope.Acquire(invocation.Context, Request{
Operation: Explain,
Selection: plan.selection,
}, selected.input, state)
diagnostics := append([]commandkit.Diagnostic(nil), acquired.Diagnostics...)
if acquireErr != nil {
return explanationFailure(acquireErr, diagnostics, false)
return configOperationFailure(acquireErr, diagnostics, false)
}
if err := invocation.Context.Err(); err != nil {
return explanationFailure(err, diagnostics, false)
return configOperationFailure(err, diagnostics, false)
}
if acquired.Value.Explanation == nil {
return configOperationFailure(fmt.Errorf("configuration scope %q did not provide an explanation reader", name), diagnostics, false)
}
if acquired.Value.Explanation == nil {
return explanationFailure(fmt.Errorf("configuration scope %q did not provide an explanation reader", name), diagnostics, false)
}
explanation, found := acquired.Value.Explanation.Explain(path)
explanation, found := acquired.Value.Explanation.Explain(plan.path)
if !found {
return explanationFailure(fmt.Errorf("configuration path %q is not known in scope %q", path.String(), name), diagnostics, false)
return configOperationFailure(fmt.Errorf("configuration path %q is not known in scope %q", plan.path.String(), name), diagnostics, false)
}
if err := invocation.Context.Err(); err != nil {
return explanationFailure(err, diagnostics, false)
return configOperationFailure(err, diagnostics, false)
}
if input.Format != "yaml" {
return writeNativeExplanation(invocation.Streams.Out, acquired.Value.Explanation, path, diagnostics)
if plan.format != "yaml" {
return writeNativeExplanation(invocation.Streams.Out, acquired.Value.Explanation, plan.path, diagnostics)
}
var report bytes.Buffer
if err := encodeYAML(&report, makeExplanationReport(name, explanation)); err != nil {
return explanationFailure(err, diagnostics, false)
return configOperationFailure(err, diagnostics, false)
}
if err := invocation.Context.Err(); err != nil {
return explanationFailure(err, diagnostics, false)
return configOperationFailure(err, diagnostics, false)
}
if err := writeReport(invocation.Streams.Out, report.Bytes()); err != nil {
outcome := explanationFailure(err, diagnostics, true)
outcome.DeliveryError = err
return outcome
if err := writeConfigReport(invocation.Streams.Out, report.Bytes()); err != nil {
return configDeliveryFailure(err, diagnostics, true)
}
outcome := Success()
outcome := commandkit.Success()
outcome.Diagnostics = diagnostics
return outcome
}
func writeNativeExplanation(writer io.Writer, reader ExplanationReader, path configkit.Path, diagnostics []Diagnostic) Outcome {
func writeNativeExplanation(writer io.Writer, reader ExplanationReader, path configkit.Path, diagnostics []commandkit.Diagnostic) commandkit.Outcome {
checked := &checkedWriter{writer: writer}
renderErr := reader.WriteExplanation(checked, path)
if checked.failure != nil {
@@ -156,11 +111,11 @@ func writeNativeExplanation(writer io.Writer, reader ExplanationReader, path con
}
}
if renderErr != nil {
outcome := explanationFailure(renderErr, diagnostics, true)
outcome := configOperationFailure(renderErr, diagnostics, true)
outcome.DeliveryError = checked.failure
return outcome
}
outcome := Success()
outcome := commandkit.Success()
outcome.Diagnostics = diagnostics
return outcome
}
@@ -213,18 +168,3 @@ func originReport(origin configkit.Origin) explanationOriginDTO {
}
return report
}
func locationReports(locations []configkit.Location) []locationDTO {
report := make([]locationDTO, 0, len(locations))
for _, location := range locations {
report = append(report, locationDTO{SourceID: location.SourceID, Line: location.Line, Column: location.Column})
}
return report
}
func explanationFailure(cause error, diagnostics []Diagnostic, reportStarted bool) Outcome {
outcome := Failure(cause)
outcome.Diagnostics = diagnostics
outcome.ReportIncomplete = reportStarted
return outcome
}

View File

@@ -0,0 +1,353 @@
package ckconfig
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"strings"
"testing"
commandkit "gitea.maximumdirect.net/eric/CommandKit"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
)
func TestScopeDiscoveryIsOrderedAndDoesNotUseSuiteFactories(t *testing.T) {
inputCalls, stateCalls, acquireCalls := 0, 0, 0
suite := Suite[struct{}, struct{}]{
NewInput: func() struct{} { inputCalls++; return struct{}{} },
NewState: func() struct{} { stateCalls++; return struct{}{} },
Scopes: []Scope[struct{}, struct{}]{
{Name: "zeta"},
{Name: "promptkit", Operations: []Operation{}},
{Name: "app", Summary: "Application settings."},
{Name: "alpha"},
},
}
for index := range suite.Scopes {
suite.Scopes[index].Acquire = func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) {
acquireCalls++
return commandkit.Acquired[View]{}, nil
}
}
// An unused malformed binding has no generated command to validate.
suite.Selectors = []Selector[struct{}]{{Binding: commandkit.Flag[struct{}]{}, Scopes: []string{"app"}}}
app := newConfigTestApp(t, Command(suite))
var output, diagnostic bytes.Buffer
outcome := app.Execute(context.Background(), []string{"config", "scopes"}, commandkit.Streams{Out: &output, Err: &diagnostic})
if outcome.Kind != commandkit.OutcomeSuccess {
t.Fatalf("discovery outcome = %#v; diagnostics %q", outcome, diagnostic.String())
}
if got, want := output.String(), "app: \npromptkit: \nalpha: \nzeta: \n"; got != want {
t.Fatalf("discovery output = %q, want %q", got, want)
}
if inputCalls != 0 || stateCalls != 0 || acquireCalls != 0 {
t.Fatalf("discovery invoked suite work: input=%d state=%d acquire=%d", inputCalls, stateCalls, acquireCalls)
}
}
func TestValidationPreparesIndependentScopeInputsAndOneSharedState(t *testing.T) {
type inspectInput struct {
Campaign commandkit.Supplied[string]
Retained string
}
type runState struct{ ID int }
var inputCalls, stateCalls, checkCalls int
var checked []*inspectInput
var acquired []*inspectInput
var states []*runState
var checkErr error
commandkitFlag := commandkit.ValueFlag[*inspectInput, string](
commandkit.FlagMeta{Name: "campaign", Usage: "Campaign selector.", Required: true},
(*string)(nil), func(value string) (string, error) { return value, nil },
func(input **inspectInput, value commandkit.Supplied[string]) { (*input).Campaign = value },
)
makeScope := func(name string) Scope[*inspectInput, *runState] {
return Scope[*inspectInput, *runState]{Name: name, Operations: []Operation{Validate}, Acquire: func(_ context.Context, request Request, input *inspectInput, state *runState) (commandkit.Acquired[View], error) {
if request.Operation != Validate || !request.Selection.Config.Set || request.Selection.Config.Value != "chosen.yaml" {
t.Errorf("request = %#v", request)
}
acquired = append(acquired, input)
states = append(states, state)
return commandkit.Acquired[View]{Diagnostics: []commandkit.Diagnostic{commandkit.MessageDiagnostic(commandkit.SeverityWarning, name+" warning")}}, nil
}}
}
suite := Suite[*inspectInput, *runState]{
NewInput: func() *inspectInput {
inputCalls++
return &inspectInput{Retained: fmt.Sprintf("initial-%d", inputCalls)}
},
NewState: func() *runState {
stateCalls++
if checkCalls != 2 {
t.Errorf("shared state created after %d input checks", checkCalls)
}
return &runState{ID: stateCalls}
},
Selectors: []Selector[*inspectInput]{{Binding: commandkitFlag, Scopes: []string{"promptkit"}}},
CheckInput: func(name string, selection Selection, input *inspectInput) error {
checkCalls++
checked = append(checked, input)
if !selection.Config.Set || input.Retained == "" {
return fmt.Errorf("missing initialized input for %s", name)
}
if name == "app" && input.Campaign.Set {
return fmt.Errorf("inapplicable selector was applied to app")
}
if name == "promptkit" && (!input.Campaign.Set || input.Campaign.Value != "spring") {
return fmt.Errorf("promptkit campaign = %#v", input.Campaign)
}
if checkErr != nil {
return checkErr
}
return nil
},
Scopes: []Scope[*inspectInput, *runState]{makeScope("promptkit"), makeScope("app")},
}
app := newConfigTestApp(t, Command(suite))
var output, diagnostic bytes.Buffer
outcome := app.Execute(context.Background(), []string{"config", "validate", "--scope", "all", "--config", "chosen.yaml", "--campaign", "spring"}, commandkit.Streams{Out: &output, Err: &diagnostic})
if outcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(diagnostic.String(), "app warning") || !strings.Contains(diagnostic.String(), "promptkit warning") {
t.Fatalf("validation outcome=%#v diagnostics=%q", outcome, diagnostic.String())
}
if got, want := output.String(), "app: valid\npromptkit: valid\n"; got != want {
t.Fatalf("validation output = %q, want %q", got, want)
}
if inputCalls != 2 || checkCalls != 2 || stateCalls != 1 || len(acquired) != 2 {
t.Fatalf("factory/callback counts input=%d check=%d state=%d acquire=%d", inputCalls, checkCalls, stateCalls, len(acquired))
}
if checked[0] != acquired[0] || checked[1] != acquired[1] || checked[0] == checked[1] {
t.Fatalf("scope inputs were not retained independently: checked=%p,%p acquired=%p,%p", checked[0], checked[1], acquired[0], acquired[1])
}
if states[0] == nil || states[0] != states[1] || states[0].ID != 1 {
t.Fatalf("shared state values = %#v", states)
}
}
func TestValidationChecksAggregateCoverageBeforeFactories(t *testing.T) {
newInputCalls, newStateCalls, acquireCalls := 0, 0, 0
makeScope := func(name string, operations []Operation) Scope[struct{}, struct{}] {
return Scope[struct{}, struct{}]{Name: name, Operations: operations, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) {
acquireCalls++
return commandkit.Acquired[View]{}, nil
}}
}
app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{
NewInput: func() struct{} { newInputCalls++; return struct{}{} },
NewState: func() struct{} { newStateCalls++; return struct{}{} },
Scopes: []Scope[struct{}, struct{}]{makeScope("app", []Operation{Validate}), makeScope("promptkit", []Operation{Show})},
}))
var output bytes.Buffer
outcome := app.Execute(context.Background(), []string{"config", "validate", "--scope", "all"}, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeUsage || newInputCalls != 0 || newStateCalls != 0 || acquireCalls != 0 || output.Len() != 0 {
t.Fatalf("unsupported aggregate outcome=%#v counts=%d/%d/%d output=%q", outcome, newInputCalls, newStateCalls, acquireCalls, output.String())
}
}
func TestRequiredSelectorAppliesOnlyToItsSelectedScope(t *testing.T) {
type input struct{ Value commandkit.Supplied[string] }
var calls []string
flag := commandkit.ValueFlag(commandkit.FlagMeta{Name: "tenant", Required: true}, (*string)(nil), func(value string) (string, error) { return value, nil }, func(target *input, value commandkit.Supplied[string]) { target.Value = value })
makeScope := func(name string) Scope[input, struct{}] {
return Scope[input, struct{}]{Name: name, Operations: []Operation{Validate}, Acquire: func(_ context.Context, _ Request, value input, _ struct{}) (commandkit.Acquired[View], error) {
calls = append(calls, name)
if name == "app" && value.Value.Set {
t.Errorf("tenant selector applied to app: %#v", value)
}
return commandkit.Acquired[View]{}, nil
}}
}
app := newConfigTestApp(t, Command(Suite[input, struct{}]{Selectors: []Selector[input]{{Binding: flag, Scopes: []string{"promptkit"}}}, Scopes: []Scope[input, struct{}]{makeScope("app"), makeScope("promptkit")}}))
var output bytes.Buffer
outcome := app.Execute(context.Background(), []string{"config", "validate", "--scope", "app"}, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess || len(calls) != 1 || calls[0] != "app" {
t.Fatalf("unselected required selector outcome=%#v calls=%v", outcome, calls)
}
calls = nil
outcome = app.Execute(context.Background(), []string{"config", "validate", "--scope", "promptkit"}, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeUsage || len(calls) != 0 || !errors.Is(outcome.Cause, commandkit.ErrFlagInput) {
t.Fatalf("selected required selector outcome=%#v calls=%v", outcome, calls)
}
}
func TestShowAcquiresAllViewsBeforeRenderingAndFramesNativeYAML(t *testing.T) {
var output bytes.Buffer
var acquisitionOutputLengths []int
makeScope := func(name, native string, acquireErr error) Scope[struct{}, *int] {
return Scope[struct{}, *int]{Name: name, Operations: []Operation{Show}, Acquire: func(_ context.Context, _ Request, _ struct{}, state *int) (commandkit.Acquired[View], error) {
*state++
acquisitionOutputLengths = append(acquisitionOutputLengths, output.Len())
if acquireErr != nil {
return commandkit.Acquired[View]{}, acquireErr
}
return commandkit.Acquired[View]{Value: View{Effective: effectiveValue(native)}}, nil
}}
}
providerErr := errors.New("second provider failed")
app := newConfigTestApp(t, Command(Suite[struct{}, *int]{NewState: func() *int { return new(int) }, Scopes: []Scope[struct{}, *int]{
makeScope("app", "---\nname: app\n", nil),
makeScope("promptkit", "---\nname: promptkit\n", providerErr),
}}))
outcome := app.Execute(context.Background(), []string{"config", "show", "--scope", "all"}, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeOperational || outcome.Cause != providerErr || output.Len() != 0 {
t.Fatalf("aggregate provider failure outcome=%#v output=%q", outcome, output.String())
}
if len(acquisitionOutputLengths) != 2 || acquisitionOutputLengths[0] != 0 || acquisitionOutputLengths[1] != 0 {
t.Fatalf("show rendered before acquisition completed: %v", acquisitionOutputLengths)
}
output.Reset()
acquisitionOutputLengths = nil
app = newConfigTestApp(t, Command(Suite[struct{}, *int]{NewState: func() *int { return new(int) }, Scopes: []Scope[struct{}, *int]{
makeScope("app", "---\nname: app\n", nil),
makeScope("promptkit", "name: promptkit\n", nil),
}}))
outcome = app.Execute(context.Background(), []string{"config", "show", "--scope", "all"}, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess {
t.Fatalf("aggregate show outcome = %#v", outcome)
}
want := "---\n# scope: app\nname: app\n---\n# scope: promptkit\nname: promptkit\n"
if output.String() != want {
t.Fatalf("aggregate show output = %q, want %q", output.String(), want)
}
}
func TestSourcesReadsNativeInventoryOnceAndPreservesZeroValues(t *testing.T) {
reader := &sourceInventory{values: []configkit.SourceInfo{{
ID: "synthetic", Label: "", Path: "", Layer: configkit.Layer("base"), Kind: configkit.SourceKind("mapping"), UnderlayIndex: 0,
ImportedFrom: &configkit.Location{SourceID: "", Line: 0, Column: 0},
}}}
app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{{Name: "app", Operations: []Operation{Sources}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) {
return commandkit.Acquired[View]{Value: View{Sources: reader}}, nil
}}}}))
var output bytes.Buffer
outcome := app.Execute(context.Background(), []string{"config", "sources", "--format", "yaml"}, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess || reader.calls != 1 {
t.Fatalf("sources outcome=%#v source calls=%d", outcome, reader.calls)
}
for _, fragment := range []string{"sources:", "id: synthetic", "label: \"\"", "path: \"\"", "underlay_index: 0", "imported_from:", "line: 0", "column: 0"} {
if !strings.Contains(output.String(), fragment) {
t.Errorf("source report missing %q: %s", fragment, output.String())
}
}
}
func TestExplainParsesBeforeAcquisitionAndPreservesNativeSafeHistory(t *testing.T) {
reader := &nativeExplanation{
path: configkit.Path{},
value: configkit.Explanation{
Path: configkit.Path{},
Effective: configkit.ValueSnapshot{Present: true, Redacted: true},
History: []configkit.ValueChange{{
Before: configkit.ValueSnapshot{Present: true, Redacted: true},
After: configkit.ValueSnapshot{Present: true, Redacted: true},
Reasons: []string{"override"},
Dependencies: []configkit.Path{},
}},
SecretReference: &configkit.SecretReferenceInfo{Name: "APP_TOKEN"},
}, found: true,
}
acquireCalls := 0
app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{{Name: "app", Operations: []Operation{Explain}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) {
acquireCalls++
return commandkit.Acquired[View]{Value: View{Explanation: reader}}, nil
}}}}))
var output bytes.Buffer
outcome := app.Execute(context.Background(), []string{"config", "explain", "--path=", "--format", "yaml"}, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess || acquireCalls != 1 || reader.path.String() != "" {
t.Fatalf("root explanation outcome=%#v calls=%d path=%q", outcome, acquireCalls, reader.path.String())
}
if strings.Contains(output.String(), "APP_TOKEN_VALUE") || !strings.Contains(output.String(), "redacted: true") || !strings.Contains(output.String(), "history:") || !strings.Contains(output.String(), "dependencies: []") || !strings.Contains(output.String(), "name: APP_TOKEN") {
t.Fatalf("safe explanation report = %s", output.String())
}
output.Reset()
acquireCalls = 0
outcome = app.Execute(context.Background(), []string{"config", "explain", "--path", "/bad~2"}, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeUsage || acquireCalls != 0 || output.Len() != 0 {
t.Fatalf("malformed pointer was not rejected before acquisition: outcome=%#v calls=%d", outcome, acquireCalls)
}
}
func TestReportWriteFailureTracksIncompleteDeliveryAndCancellation(t *testing.T) {
writeErr := errors.New("output failed")
app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{{Name: "app", Operations: []Operation{Sources}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) {
return commandkit.Acquired[View]{Value: View{Sources: &sourceInventory{values: []configkit.SourceInfo{}}}}, nil
}}}}))
outcome := app.Execute(context.Background(), []string{"config", "sources"}, commandkit.Streams{Out: failingOutput{err: writeErr}, Err: io.Discard})
if !errors.Is(outcome.Cause, writeErr) || outcome.DeliveryError != writeErr || !outcome.ReportIncomplete {
t.Fatalf("source delivery failure outcome = %#v", outcome)
}
ctx, cancel := context.WithCancel(context.Background())
calls := 0
app = newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{
{Name: "app", Operations: []Operation{Validate}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) {
calls++
return commandkit.Acquired[View]{}, nil
}},
{Name: "promptkit", Operations: []Operation{Validate}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) {
calls++
cancel()
return commandkit.Acquired[View]{}, nil
}},
}}))
var output bytes.Buffer
outcome = app.Execute(ctx, []string{"config", "validate", "--scope", "all"}, commandkit.Streams{Out: &output, Err: io.Discard})
if !errors.Is(outcome.Cause, context.Canceled) || !outcome.ReportIncomplete || calls != 2 || output.String() != "app: valid\n" {
t.Fatalf("cancelled aggregate validation outcome=%#v calls=%d output=%q", outcome, calls, output.String())
}
}
type effectiveValue string
func (value effectiveValue) WriteEffective(writer io.Writer) error {
_, err := io.WriteString(writer, string(value))
return err
}
type sourceInventory struct {
values []configkit.SourceInfo
calls int
}
func (reader *sourceInventory) Sources() []configkit.SourceInfo {
reader.calls++
return reader.values
}
type nativeExplanation struct {
path configkit.Path
value configkit.Explanation
found bool
}
func (reader *nativeExplanation) Explain(path configkit.Path) (configkit.Explanation, bool) {
reader.path = path
return reader.value, reader.found
}
func (reader *nativeExplanation) WriteExplanation(writer io.Writer, path configkit.Path) error {
reader.path = path
_, err := fmt.Fprintf(writer, "Explanation for %q\n", path.String())
return err
}
type failingOutput struct{ err error }
func (writer failingOutput) Write(value []byte) (int, error) {
if len(value) == 0 {
return 0, writer.err
}
return 1, writer.err
}
func newConfigTestApp(t *testing.T, command commandkit.Command) *commandkit.App {
t.Helper()
app, err := commandkit.New(commandkit.Identity{Name: "example"}, command)
if err != nil {
t.Fatal(err)
}
return app
}

View File

@@ -2,6 +2,7 @@ package ckconfig
import (
"fmt"
"strings"
commandkit "gitea.maximumdirect.net/eric/CommandKit"
configkit "gitea.maximumdirect.net/eric/ConfigKit"

View File

@@ -0,0 +1,47 @@
package ckconfig
import (
"errors"
"fmt"
commandkit "gitea.maximumdirect.net/eric/CommandKit"
)
func runConfigOperation[I, S any](invocation commandkit.Invocation, operation Operation, input *configCommandInput[I, S]) commandkit.Outcome {
if input == nil || input.plan == nil {
return commandkit.Failure(errors.New("configuration operation was not prepared"))
}
if err := invocation.Context.Err(); err != nil {
return commandkit.Failure(err)
}
plan := input.plan
var state S
if plan.prepared.newState != nil {
state = plan.prepared.newState()
}
switch operation {
case Validate:
return runConfigValidation(invocation, plan, state)
case Show:
return runConfigShow(invocation, plan, state)
case Sources:
return runConfigSources(invocation, plan, state)
case Explain:
return runConfigExplanation(invocation, plan, state)
default:
return commandkit.Failure(fmt.Errorf("unsupported configuration operation %q", operation))
}
}
func configOperationFailure(cause error, diagnostics []commandkit.Diagnostic, reportStarted bool) commandkit.Outcome {
outcome := commandkit.Failure(cause)
outcome.Diagnostics = diagnostics
outcome.ReportIncomplete = reportStarted
return outcome
}
func configDeliveryFailure(cause error, diagnostics []commandkit.Diagnostic, reportStarted bool) commandkit.Outcome {
outcome := configOperationFailure(cause, diagnostics, reportStarted)
outcome.DeliveryError = cause
return outcome
}

View File

@@ -4,69 +4,48 @@ import (
"bytes"
"errors"
"fmt"
"io"
"strconv"
"unicode"
)
func newConfigShowCommand[C any](prepared *preparedConfigSuite[C]) (Command, error) {
spec := CommandSpec[configInput[C]]{
Name: string(ConfigShow),
Summary: "Show effective configuration as YAML.",
Flags: configInputFlags(prepared.suite, "yaml"),
CheckInput: func(input configInput[C]) error {
_, err := preflightConfig(prepared, input, ConfigShow)
return err
},
Run: func(invocation Invocation, input configInput[C]) Outcome {
return runConfigShow(invocation, prepared, input)
},
}
return NewCommand(spec)
}
commandkit "gitea.maximumdirect.net/eric/CommandKit"
)
type effectiveDocument struct {
name string
view ConfigView
view View
}
func runConfigShow[C any](invocation Invocation, prepared *preparedConfigSuite[C], input configInput[C]) Outcome {
if err := invocation.Context.Err(); err != nil {
return Failure(err)
}
scopeNames, err := selectConfigScopes(prepared, input, ConfigShow)
if err != nil {
return Failure(err)
}
contexts := contextsForScopes(prepared, input, scopeNames)
documents := make([]effectiveDocument, 0, len(scopeNames))
diagnostics := make([]Diagnostic, 0)
for _, name := range scopeNames {
scope, ok := findConfigScope(prepared, name)
if !ok || scope.Acquire == nil {
return showFailure(fmt.Errorf("configuration scope %q has no acquisition callback", name), diagnostics, false)
}
func runConfigShow[I, S any](invocation commandkit.Invocation, plan *configOperationPlan[I, S], state S) commandkit.Outcome {
documents := make([]effectiveDocument, 0, len(plan.scopes))
diagnostics := make([]commandkit.Diagnostic, 0)
for _, selected := range plan.scopes {
if err := invocation.Context.Err(); err != nil {
return showFailure(err, diagnostics, false)
return configOperationFailure(err, diagnostics, false)
}
acquired, acquireErr := scope.Acquire(invocation.Context, ConfigRequest{Operation: ConfigShow, Selection: input.Selection}, contexts[name])
if selected.scope.scope.Acquire == nil {
return configOperationFailure(fmt.Errorf("configuration scope %q has no acquisition callback", selected.scope.scope.Name), diagnostics, false)
}
acquired, acquireErr := selected.scope.scope.Acquire(invocation.Context, Request{
Operation: Show,
Selection: plan.selection,
}, selected.input, state)
diagnostics = append(diagnostics, acquired.Diagnostics...)
if acquireErr != nil {
return showFailure(acquireErr, diagnostics, false)
return configOperationFailure(acquireErr, diagnostics, false)
}
if err := invocation.Context.Err(); err != nil {
return showFailure(err, diagnostics, false)
return configOperationFailure(err, diagnostics, false)
}
if acquired.Value.Effective == nil {
return showFailure(fmt.Errorf("configuration scope %q did not provide an effective configuration writer", name), diagnostics, false)
return configOperationFailure(fmt.Errorf("configuration scope %q did not provide an effective configuration writer", selected.scope.scope.Name), diagnostics, false)
}
documents = append(documents, effectiveDocument{name: name, view: acquired.Value})
documents = append(documents, effectiveDocument{name: selected.scope.scope.Name, view: acquired.Value})
}
if err := invocation.Context.Err(); err != nil {
return showFailure(err, diagnostics, false)
return configOperationFailure(err, diagnostics, false)
}
if len(documents) == 1 && input.Scope != "all" {
if len(documents) == 1 && plan.scopeName != "all" {
checked := &checkedWriter{writer: invocation.Streams.Out}
renderErr := documents[0].view.Effective.WriteEffective(checked)
if checked.failure != nil {
@@ -77,11 +56,11 @@ func runConfigShow[C any](invocation Invocation, prepared *preparedConfigSuite[C
}
}
if renderErr != nil {
outcome := showFailure(renderErr, diagnostics, true)
outcome := configOperationFailure(renderErr, diagnostics, true)
outcome.DeliveryError = checked.failure
return outcome
}
outcome := Success()
outcome := commandkit.Success()
outcome.Diagnostics = diagnostics
return outcome
}
@@ -89,38 +68,53 @@ func runConfigShow[C any](invocation Invocation, prepared *preparedConfigSuite[C
written := false
for index, document := range documents {
if err := invocation.Context.Err(); err != nil {
return showFailure(err, diagnostics, written)
return configOperationFailure(err, diagnostics, written)
}
var native bytes.Buffer
if err := document.view.Effective.WriteEffective(&native); err != nil {
return showFailure(err, diagnostics, written)
return configOperationFailure(err, diagnostics, written)
}
if err := invocation.Context.Err(); err != nil {
return showFailure(err, diagnostics, written)
return configOperationFailure(err, diagnostics, written)
}
framed, hasDirective := frameEffectiveYAML(document.name, native.Bytes())
if index > 0 && hasDirective {
if err := writeReport(invocation.Streams.Out, []byte("...\n")); err != nil {
outcome := showFailure(err, diagnostics, true)
outcome.DeliveryError = err
return outcome
if err := writeConfigReport(invocation.Streams.Out, []byte("...\n")); err != nil {
return configDeliveryFailure(err, diagnostics, true)
}
}
if index < len(documents)-1 && len(framed) > 0 && framed[len(framed)-1] != '\n' {
framed = append(framed, '\n')
}
if err := writeReport(invocation.Streams.Out, framed); err != nil {
outcome := showFailure(err, diagnostics, true)
outcome.DeliveryError = err
return outcome
if err := writeConfigReport(invocation.Streams.Out, framed); err != nil {
return configDeliveryFailure(err, diagnostics, true)
}
written = true
}
outcome := Success()
outcome := commandkit.Success()
outcome.Diagnostics = diagnostics
return outcome
}
type checkedWriter struct {
writer io.Writer
failure error
}
func (writer *checkedWriter) Write(content []byte) (int, error) {
if writer.failure != nil {
return 0, writer.failure
}
written, err := writer.writer.Write(content)
if err == nil && written != len(content) {
err = io.ErrShortWrite
}
if err != nil {
writer.failure = err
}
return written, err
}
type yamlPreamble struct {
markerStart int
markerEnd int
@@ -166,8 +160,8 @@ func analyzeYAMLPreamble(native []byte) yamlPreamble {
func frameEffectiveYAML(scope string, native []byte) ([]byte, bool) {
analysis := analyzeYAMLPreamble(native)
label := scope
for _, r := range scope {
if unicode.IsControl(r) {
for _, character := range scope {
if unicode.IsControl(character) {
label = strconv.Quote(scope)
break
}
@@ -175,8 +169,6 @@ func frameEffectiveYAML(scope string, native []byte) ([]byte, bool) {
comment := []byte("# scope: " + label + "\n")
if analysis.markerFound {
out := make([]byte, 0, len(native)+len(comment))
// Inline content may be a block scalar; place the label before
// its marker rather than interrupting the native value.
if analysis.inlineMarker {
out = append(out, native[:analysis.markerStart]...)
out = append(out, comment...)
@@ -205,13 +197,6 @@ func frameEffectiveYAML(scope string, native []byte) ([]byte, bool) {
return result, analysis.hasDirective
}
func showFailure(cause error, diagnostics []Diagnostic, reportStarted bool) Outcome {
outcome := Failure(cause)
outcome.Diagnostics = diagnostics
outcome.ReportIncomplete = reportStarted
return outcome
}
func isYAMLDocumentStart(line []byte) bool {
return bytes.HasPrefix(line, []byte("---")) && (len(line) == 3 || line[3] == ' ' || line[3] == '\t')
}

View File

@@ -0,0 +1,95 @@
package ckconfig
import (
"bytes"
"io"
"reflect"
"strings"
"testing"
"go.yaml.in/yaml/v4"
)
func TestEffectiveYAMLFramingPreservesOneNativeDocument(t *testing.T) {
for _, native := range []string{
"--- {setting: value}\n", "---\t{setting: value}\n", "--- value\n",
"--- |\n first\n second\n", "--- # authored\nsetting: value\n",
"setting: value\n", "%YAML 1.1\n--- {setting: value}\n",
} {
t.Run(native, func(t *testing.T) {
var want any
if err := yaml.Unmarshal([]byte(native), &want); err != nil {
t.Fatal(err)
}
framed, _ := frameEffectiveYAML("app", []byte(native))
withoutLabel := bytes.Replace(framed, []byte("# scope: app\n"), nil, 1)
if !strings.HasPrefix(native, "---") && !strings.HasPrefix(native, "%") {
withoutLabel = bytes.TrimPrefix(withoutLabel, []byte("---\n"))
}
if !bytes.Equal(withoutLabel, []byte(native)) {
t.Fatalf("native content changed: %q", framed)
}
if !bytes.Contains(framed, []byte("# scope: app\n")) {
t.Fatalf("missing label: %q", framed)
}
decoder := yaml.NewDecoder(bytes.NewReader(framed))
var got any
if err := decoder.Decode(&got); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("got=%#v want=%#v", got, want)
}
if err := decoder.Decode(new(any)); err != io.EOF {
t.Fatalf("extra document: %v", err)
}
})
}
}
func TestEffectiveYAMLFramingPreservesBoundariesAndDirectives(t *testing.T) {
for _, test := range []struct {
name string
scope string
native string
directive bool
label string
}{
{name: "no trailing newline", scope: "app", native: "value: plain", label: "# scope: app\n"},
{name: "CRLF", scope: "app", native: "# authored\r\n---\r\nvalue: yes\r\n", label: "# scope: app\n"},
{name: "directive preamble", scope: "app", native: "%YAML 1.1\n# authored\n---\nvalue: yes", directive: true, label: "# scope: app\n"},
{name: "control character label", scope: "line\nbreak", native: "value: yes", label: "# scope: \"line\\nbreak\"\n"},
} {
t.Run(test.name, func(t *testing.T) {
var want any
if err := yaml.Unmarshal([]byte(test.native), &want); err != nil {
t.Fatal(err)
}
framed, hasDirective := frameEffectiveYAML(test.scope, []byte(test.native))
if hasDirective != test.directive {
t.Fatalf("hasDirective = %t, want %t", hasDirective, test.directive)
}
withoutLabel := bytes.Replace(framed, []byte(test.label), nil, 1)
if !strings.HasPrefix(test.native, "---") && !strings.HasPrefix(test.native, "%") {
withoutLabel = bytes.TrimPrefix(withoutLabel, []byte("---\n"))
}
if !bytes.Equal(withoutLabel, []byte(test.native)) {
t.Fatalf("native content changed: got %q want %q", withoutLabel, test.native)
}
if !bytes.Contains(framed, []byte(test.label)) {
t.Fatalf("missing scope label %q in %q", test.label, framed)
}
decoder := yaml.NewDecoder(bytes.NewReader(framed))
var first any
if err := decoder.Decode(&first); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(first, want) {
t.Fatalf("decoded value = %#v, want %#v", first, want)
}
if err := decoder.Decode(new(any)); err != io.EOF {
t.Fatalf("unexpected extra YAML document: %v", err)
}
})
}
}

View File

@@ -3,24 +3,13 @@ package ckconfig
import (
"bytes"
"fmt"
"io"
"strconv"
)
func newConfigSourcesCommand[C any](prepared *preparedConfigSuite[C]) (Command, error) {
spec := CommandSpec[configInput[C]]{
Name: string(ConfigSources),
Summary: "List configuration sources used by a scope.",
Flags: configInputFlags(prepared.suite, "text"),
CheckInput: func(input configInput[C]) error {
_, err := preflightConfig(prepared, input, ConfigSources)
return err
},
Run: func(invocation Invocation, input configInput[C]) Outcome {
return runConfigSources(invocation, prepared, input)
},
}
return NewCommand(spec)
}
commandkit "gitea.maximumdirect.net/eric/CommandKit"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
"go.yaml.in/yaml/v4"
)
type sourceReport struct {
Scope string `yaml:"scope"`
@@ -43,35 +32,30 @@ type locationDTO struct {
Column int `yaml:"column"`
}
func runConfigSources[C any](invocation Invocation, prepared *preparedConfigSuite[C], input configInput[C]) Outcome {
func runConfigSources[I, S any](invocation commandkit.Invocation, plan *configOperationPlan[I, S], state S) commandkit.Outcome {
selected := plan.scopes[0]
name := selected.scope.scope.Name
if err := invocation.Context.Err(); err != nil {
return Failure(err)
return configOperationFailure(err, nil, false)
}
scopeNames, err := selectConfigScopes(prepared, input, ConfigSources)
if err != nil {
return Failure(err)
if selected.scope.scope.Acquire == nil {
return configOperationFailure(fmt.Errorf("configuration scope %q has no acquisition callback", name), nil, false)
}
contexts := contextsForScopes(prepared, input, scopeNames)
name := scopeNames[0]
scope, ok := findConfigScope(prepared, name)
if !ok || scope.Acquire == nil {
return sourceReportFailure(fmt.Errorf("configuration scope %q has no acquisition callback", name), nil, false)
}
if err := invocation.Context.Err(); err != nil {
return sourceReportFailure(err, nil, false)
}
acquired, acquireErr := scope.Acquire(invocation.Context, ConfigRequest{Operation: ConfigSources, Selection: input.Selection}, contexts[name])
diagnostics := append([]Diagnostic(nil), acquired.Diagnostics...)
acquired, acquireErr := selected.scope.scope.Acquire(invocation.Context, Request{
Operation: Sources,
Selection: plan.selection,
}, selected.input, state)
diagnostics := append([]commandkit.Diagnostic(nil), acquired.Diagnostics...)
if acquireErr != nil {
return sourceReportFailure(acquireErr, diagnostics, false)
return configOperationFailure(acquireErr, diagnostics, false)
}
if err := invocation.Context.Err(); err != nil {
return sourceReportFailure(err, diagnostics, false)
return configOperationFailure(err, diagnostics, false)
}
if acquired.Value.Sources == nil {
return configOperationFailure(fmt.Errorf("configuration scope %q did not provide a source reader", name), diagnostics, false)
}
if acquired.Value.Sources == nil {
return sourceReportFailure(fmt.Errorf("configuration scope %q did not provide a source reader", name), diagnostics, false)
}
sources := acquired.Value.Sources.Sources()
document := sourceReport{Scope: name, Sources: make([]sourceDTO, 0, len(sources))}
for _, source := range sources {
@@ -88,24 +72,21 @@ func runConfigSources[C any](invocation Invocation, prepared *preparedConfigSuit
}
document.Sources = append(document.Sources, item)
}
var report bytes.Buffer
if input.Format == "yaml" {
if plan.format == "yaml" {
if err := encodeYAML(&report, document); err != nil {
return sourceReportFailure(err, diagnostics, false)
return configOperationFailure(err, diagnostics, false)
}
} else {
writeSourceReportText(&report, document)
}
if err := invocation.Context.Err(); err != nil {
return sourceReportFailure(err, diagnostics, false)
return configOperationFailure(err, diagnostics, false)
}
if err := writeReport(invocation.Streams.Out, report.Bytes()); err != nil {
outcome := sourceReportFailure(err, diagnostics, true)
outcome.DeliveryError = err
return outcome
if err := writeConfigReport(invocation.Streams.Out, report.Bytes()); err != nil {
return configDeliveryFailure(err, diagnostics, true)
}
outcome := Success()
outcome := commandkit.Success()
outcome.Diagnostics = diagnostics
return outcome
}
@@ -133,9 +114,16 @@ func writeSourceReportText(writer *bytes.Buffer, document sourceReport) {
}
}
func sourceReportFailure(cause error, diagnostics []Diagnostic, reportStarted bool) Outcome {
outcome := Failure(cause)
outcome.Diagnostics = diagnostics
outcome.ReportIncomplete = reportStarted
return outcome
func encodeYAML(writer io.Writer, document any) error {
encoder := yaml.NewEncoder(writer)
encoder.SetIndent(2)
return encoder.Encode(document)
}
func locationReports(locations []configkit.Location) []locationDTO {
report := make([]locationDTO, 0, len(locations))
for _, location := range locations {
report = append(report, locationDTO{SourceID: location.SourceID, Line: location.Line, Column: location.Column})
}
return report
}

View File

@@ -5,93 +5,51 @@ import (
"fmt"
"strings"
commandkit "gitea.maximumdirect.net/eric/CommandKit"
"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](prepared *preparedConfigSuite[C]) (Command, error) {
spec := CommandSpec[configInput[C]]{
Name: string(ConfigValidate),
Summary: "Validate selected configuration scopes.",
Flags: configInputFlags(prepared.suite, "text"),
CheckInput: func(input configInput[C]) error {
_, err := preflightConfig(prepared, input, ConfigValidate)
return err
},
Run: func(invocation Invocation, input configInput[C]) Outcome {
return runConfigValidation(invocation, prepared, input)
},
}
return NewCommand(spec)
}
func runConfigValidation[C any](invocation Invocation, prepared *preparedConfigSuite[C], input configInput[C]) Outcome {
if err := invocation.Context.Err(); err != nil {
return Failure(err)
}
scopeNames, err := selectConfigScopes(prepared, input, ConfigValidate)
if err != nil {
return Failure(err)
}
contexts := contextsForScopes(prepared, input, scopeNames)
diagnostics := make([]Diagnostic, 0)
func runConfigValidation[I, S any](invocation commandkit.Invocation, plan *configOperationPlan[I, S], state S) commandkit.Outcome {
diagnostics := make([]commandkit.Diagnostic, 0)
writtenRecord := false
for _, name := range scopeNames {
scope, ok := findConfigScope(prepared, name)
if !ok || scope.Acquire == nil {
return validationFailure(fmt.Errorf("configuration scope %q has no acquisition callback", name), diagnostics, writtenRecord)
}
for _, selected := range plan.scopes {
if err := invocation.Context.Err(); err != nil {
return validationFailure(err, diagnostics, writtenRecord)
return configOperationFailure(err, diagnostics, writtenRecord)
}
acquired, acquireErr := scope.Acquire(invocation.Context, ConfigRequest{Operation: ConfigValidate, Selection: input.Selection}, contexts[name])
if selected.scope.scope.Acquire == nil {
return configOperationFailure(fmt.Errorf("configuration scope %q has no acquisition callback", selected.scope.scope.Name), diagnostics, writtenRecord)
}
acquired, acquireErr := selected.scope.scope.Acquire(invocation.Context, Request{
Operation: Validate,
Selection: plan.selection,
}, selected.input, state)
diagnostics = append(diagnostics, acquired.Diagnostics...)
if acquireErr != nil {
return validationFailure(acquireErr, diagnostics, writtenRecord)
return configOperationFailure(acquireErr, diagnostics, writtenRecord)
}
if err := invocation.Context.Err(); err != nil {
return validationFailure(err, diagnostics, writtenRecord)
return configOperationFailure(err, diagnostics, writtenRecord)
}
record, err := validationSuccessRecord(name, input.Format)
record, err := validationSuccessRecord(selected.scope.scope.Name, plan.format)
if err != nil {
return validationFailure(err, diagnostics, writtenRecord)
return configOperationFailure(err, diagnostics, writtenRecord)
}
if input.Format == "yaml" && !writtenRecord {
if err := writeReport(invocation.Streams.Out, []byte("scopes:\n")); err != nil {
outcome := validationFailure(err, diagnostics, true)
outcome.DeliveryError = err
return outcome
if plan.format == "yaml" && !writtenRecord {
if err := writeConfigReport(invocation.Streams.Out, []byte("scopes:\n")); err != nil {
return configDeliveryFailure(err, diagnostics, true)
}
}
if err := writeReport(invocation.Streams.Out, record); err != nil {
outcome := validationFailure(err, diagnostics, true)
outcome.DeliveryError = err
return outcome
if err := writeConfigReport(invocation.Streams.Out, record); err != nil {
return configDeliveryFailure(err, diagnostics, true)
}
writtenRecord = true
}
outcome := Success()
outcome := commandkit.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