Port native configuration reports
This commit is contained in:
@@ -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)
|
return preparedInput[I]{}, fmt.Errorf("flag --%s: %w", binding.meta.Name, err)
|
||||||
}
|
}
|
||||||
preparedFlags[i] = value
|
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
|
missingRequired = binding.meta.Name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
17
command.go
17
command.go
@@ -67,14 +67,15 @@ type preparedValue[I any] struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type flagBinding[I any] struct {
|
type flagBinding[I any] struct {
|
||||||
identity *bindingIdentity
|
identity *bindingIdentity
|
||||||
meta FlagMeta
|
meta FlagMeta
|
||||||
defaultText string
|
deferredRequired bool
|
||||||
repeated bool
|
defaultText string
|
||||||
boolean bool
|
repeated bool
|
||||||
prepare func([]string) (preparedValue[I], error)
|
boolean bool
|
||||||
clear func(*I)
|
prepare func([]string) (preparedValue[I], error)
|
||||||
valid bool
|
clear func(*I)
|
||||||
|
valid bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Argument represents a typed binding for a positional command argument.
|
// Argument represents a typed binding for a positional command argument.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Implement the optional integration modules and captured registration API
|
# 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
|
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
|
assignment. This document authorizes no publication or changes to sibling
|
||||||
repositories.
|
repositories.
|
||||||
@@ -499,6 +499,20 @@ failure tests, including aggregate preacquisition, marker edge cases, warning-on
|
|||||||
validation, source facet calls, pointer parsing, historical redaction, and
|
validation, source facet calls, pointer parsing, historical redaction, and
|
||||||
cancelled acquisition. Run the complete ckconfig suite when coherent.
|
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
|
## Stage 11: Implement PromptKit engine and scope adapters
|
||||||
|
|
||||||
**Outcome:** ckprompt composes engine configuration through ckconfig and named
|
**Outcome:** ckprompt composes engine configuration through ckconfig and named
|
||||||
|
|||||||
@@ -24,11 +24,12 @@ func CaptureFlags[Outer, I any](flags []Flag[I], target func(*Outer) *FlagValues
|
|||||||
inner := original
|
inner := original
|
||||||
identity := newBindingIdentity()
|
identity := newBindingIdentity()
|
||||||
binding := flagBinding[Outer]{
|
binding := flagBinding[Outer]{
|
||||||
identity: identity,
|
identity: identity,
|
||||||
meta: inner.binding.meta,
|
meta: inner.binding.meta,
|
||||||
defaultText: inner.binding.defaultText,
|
deferredRequired: inner.binding.meta.Required,
|
||||||
repeated: inner.binding.repeated,
|
defaultText: inner.binding.defaultText,
|
||||||
boolean: inner.binding.boolean,
|
repeated: inner.binding.repeated,
|
||||||
|
boolean: inner.binding.boolean,
|
||||||
}
|
}
|
||||||
if target != nil && inner.binding.identity != nil && inner.binding.valid && inner.binding.prepare != nil {
|
if target != nil && inner.binding.identity != nil && inner.binding.valid && inner.binding.prepare != nil {
|
||||||
binding.valid = true
|
binding.valid = true
|
||||||
|
|||||||
@@ -6,41 +6,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
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 {
|
type explanationReport struct {
|
||||||
Scope string `yaml:"scope"`
|
Scope string `yaml:"scope"`
|
||||||
Path string `yaml:"path"`
|
Path string `yaml:"path"`
|
||||||
@@ -82,70 +51,56 @@ type secretReferenceDTO struct {
|
|||||||
Name string `yaml:"name"`
|
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 {
|
if err := invocation.Context.Err(); err != nil {
|
||||||
return Failure(err)
|
return configOperationFailure(err, nil, false)
|
||||||
}
|
}
|
||||||
scopeNames, err := selectConfigScopes(prepared, input, ConfigExplain)
|
if selected.scope.scope.Acquire == nil {
|
||||||
if err != nil {
|
return configOperationFailure(fmt.Errorf("configuration scope %q has no acquisition callback", name), nil, false)
|
||||||
return Failure(err)
|
|
||||||
}
|
}
|
||||||
if !input.Path.Set {
|
acquired, acquireErr := selected.scope.scope.Acquire(invocation.Context, Request{
|
||||||
return Failure(fmt.Errorf("--path is required"))
|
Operation: Explain,
|
||||||
}
|
Selection: plan.selection,
|
||||||
path, err := configkit.ParsePath(input.Path.Value)
|
}, selected.input, state)
|
||||||
if err != nil {
|
diagnostics := append([]commandkit.Diagnostic(nil), acquired.Diagnostics...)
|
||||||
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...)
|
|
||||||
if acquireErr != nil {
|
if acquireErr != nil {
|
||||||
return explanationFailure(acquireErr, diagnostics, false)
|
return configOperationFailure(acquireErr, diagnostics, false)
|
||||||
}
|
}
|
||||||
if err := invocation.Context.Err(); err != nil {
|
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 {
|
explanation, found := acquired.Value.Explanation.Explain(plan.path)
|
||||||
return explanationFailure(fmt.Errorf("configuration scope %q did not provide an explanation reader", name), diagnostics, false)
|
|
||||||
}
|
|
||||||
explanation, found := acquired.Value.Explanation.Explain(path)
|
|
||||||
if !found {
|
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 {
|
if err := invocation.Context.Err(); err != nil {
|
||||||
return explanationFailure(err, diagnostics, false)
|
return configOperationFailure(err, diagnostics, false)
|
||||||
}
|
}
|
||||||
if input.Format != "yaml" {
|
if plan.format != "yaml" {
|
||||||
return writeNativeExplanation(invocation.Streams.Out, acquired.Value.Explanation, path, diagnostics)
|
return writeNativeExplanation(invocation.Streams.Out, acquired.Value.Explanation, plan.path, diagnostics)
|
||||||
}
|
}
|
||||||
var report bytes.Buffer
|
var report bytes.Buffer
|
||||||
if err := encodeYAML(&report, makeExplanationReport(name, explanation)); err != nil {
|
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 {
|
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 {
|
if err := writeConfigReport(invocation.Streams.Out, report.Bytes()); err != nil {
|
||||||
outcome := explanationFailure(err, diagnostics, true)
|
return configDeliveryFailure(err, diagnostics, true)
|
||||||
outcome.DeliveryError = err
|
|
||||||
return outcome
|
|
||||||
}
|
}
|
||||||
outcome := Success()
|
outcome := commandkit.Success()
|
||||||
outcome.Diagnostics = diagnostics
|
outcome.Diagnostics = diagnostics
|
||||||
return outcome
|
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}
|
checked := &checkedWriter{writer: writer}
|
||||||
renderErr := reader.WriteExplanation(checked, path)
|
renderErr := reader.WriteExplanation(checked, path)
|
||||||
if checked.failure != nil {
|
if checked.failure != nil {
|
||||||
@@ -156,11 +111,11 @@ func writeNativeExplanation(writer io.Writer, reader ExplanationReader, path con
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if renderErr != nil {
|
if renderErr != nil {
|
||||||
outcome := explanationFailure(renderErr, diagnostics, true)
|
outcome := configOperationFailure(renderErr, diagnostics, true)
|
||||||
outcome.DeliveryError = checked.failure
|
outcome.DeliveryError = checked.failure
|
||||||
return outcome
|
return outcome
|
||||||
}
|
}
|
||||||
outcome := Success()
|
outcome := commandkit.Success()
|
||||||
outcome.Diagnostics = diagnostics
|
outcome.Diagnostics = diagnostics
|
||||||
return outcome
|
return outcome
|
||||||
}
|
}
|
||||||
@@ -213,18 +168,3 @@ func originReport(origin configkit.Origin) explanationOriginDTO {
|
|||||||
}
|
}
|
||||||
return report
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
353
integrations/ckconfig/family_reports_test.go
Normal file
353
integrations/ckconfig/family_reports_test.go
Normal 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
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package ckconfig
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||||
|
|||||||
47
integrations/ckconfig/operations.go
Normal file
47
integrations/ckconfig/operations.go
Normal 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
|
||||||
|
}
|
||||||
@@ -4,69 +4,48 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"strconv"
|
"strconv"
|
||||||
"unicode"
|
"unicode"
|
||||||
)
|
|
||||||
|
|
||||||
func newConfigShowCommand[C any](prepared *preparedConfigSuite[C]) (Command, error) {
|
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
type effectiveDocument struct {
|
type effectiveDocument struct {
|
||||||
name string
|
name string
|
||||||
view ConfigView
|
view View
|
||||||
}
|
}
|
||||||
|
|
||||||
func runConfigShow[C any](invocation Invocation, prepared *preparedConfigSuite[C], input configInput[C]) Outcome {
|
func runConfigShow[I, S any](invocation commandkit.Invocation, plan *configOperationPlan[I, S], state S) commandkit.Outcome {
|
||||||
if err := invocation.Context.Err(); err != nil {
|
documents := make([]effectiveDocument, 0, len(plan.scopes))
|
||||||
return Failure(err)
|
diagnostics := make([]commandkit.Diagnostic, 0)
|
||||||
}
|
for _, selected := range plan.scopes {
|
||||||
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)
|
|
||||||
}
|
|
||||||
if err := invocation.Context.Err(); err != nil {
|
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...)
|
diagnostics = append(diagnostics, acquired.Diagnostics...)
|
||||||
if acquireErr != nil {
|
if acquireErr != nil {
|
||||||
return showFailure(acquireErr, diagnostics, false)
|
return configOperationFailure(acquireErr, diagnostics, false)
|
||||||
}
|
}
|
||||||
if err := invocation.Context.Err(); err != nil {
|
if err := invocation.Context.Err(); err != nil {
|
||||||
return showFailure(err, diagnostics, false)
|
return configOperationFailure(err, diagnostics, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if acquired.Value.Effective == nil {
|
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 {
|
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}
|
checked := &checkedWriter{writer: invocation.Streams.Out}
|
||||||
renderErr := documents[0].view.Effective.WriteEffective(checked)
|
renderErr := documents[0].view.Effective.WriteEffective(checked)
|
||||||
if checked.failure != nil {
|
if checked.failure != nil {
|
||||||
@@ -77,11 +56,11 @@ func runConfigShow[C any](invocation Invocation, prepared *preparedConfigSuite[C
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if renderErr != nil {
|
if renderErr != nil {
|
||||||
outcome := showFailure(renderErr, diagnostics, true)
|
outcome := configOperationFailure(renderErr, diagnostics, true)
|
||||||
outcome.DeliveryError = checked.failure
|
outcome.DeliveryError = checked.failure
|
||||||
return outcome
|
return outcome
|
||||||
}
|
}
|
||||||
outcome := Success()
|
outcome := commandkit.Success()
|
||||||
outcome.Diagnostics = diagnostics
|
outcome.Diagnostics = diagnostics
|
||||||
return outcome
|
return outcome
|
||||||
}
|
}
|
||||||
@@ -89,38 +68,53 @@ func runConfigShow[C any](invocation Invocation, prepared *preparedConfigSuite[C
|
|||||||
written := false
|
written := false
|
||||||
for index, document := range documents {
|
for index, document := range documents {
|
||||||
if err := invocation.Context.Err(); err != nil {
|
if err := invocation.Context.Err(); err != nil {
|
||||||
return showFailure(err, diagnostics, written)
|
return configOperationFailure(err, diagnostics, written)
|
||||||
}
|
}
|
||||||
var native bytes.Buffer
|
var native bytes.Buffer
|
||||||
if err := document.view.Effective.WriteEffective(&native); err != nil {
|
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 {
|
if err := invocation.Context.Err(); err != nil {
|
||||||
return showFailure(err, diagnostics, written)
|
return configOperationFailure(err, diagnostics, written)
|
||||||
}
|
}
|
||||||
framed, hasDirective := frameEffectiveYAML(document.name, native.Bytes())
|
framed, hasDirective := frameEffectiveYAML(document.name, native.Bytes())
|
||||||
if index > 0 && hasDirective {
|
if index > 0 && hasDirective {
|
||||||
if err := writeReport(invocation.Streams.Out, []byte("...\n")); err != nil {
|
if err := writeConfigReport(invocation.Streams.Out, []byte("...\n")); err != nil {
|
||||||
outcome := showFailure(err, diagnostics, true)
|
return configDeliveryFailure(err, diagnostics, true)
|
||||||
outcome.DeliveryError = err
|
|
||||||
return outcome
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if index < len(documents)-1 && len(framed) > 0 && framed[len(framed)-1] != '\n' {
|
if index < len(documents)-1 && len(framed) > 0 && framed[len(framed)-1] != '\n' {
|
||||||
framed = append(framed, '\n')
|
framed = append(framed, '\n')
|
||||||
}
|
}
|
||||||
if err := writeReport(invocation.Streams.Out, framed); err != nil {
|
if err := writeConfigReport(invocation.Streams.Out, framed); err != nil {
|
||||||
outcome := showFailure(err, diagnostics, true)
|
return configDeliveryFailure(err, diagnostics, true)
|
||||||
outcome.DeliveryError = err
|
|
||||||
return outcome
|
|
||||||
}
|
}
|
||||||
written = true
|
written = true
|
||||||
}
|
}
|
||||||
outcome := Success()
|
outcome := commandkit.Success()
|
||||||
outcome.Diagnostics = diagnostics
|
outcome.Diagnostics = diagnostics
|
||||||
return outcome
|
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 {
|
type yamlPreamble struct {
|
||||||
markerStart int
|
markerStart int
|
||||||
markerEnd int
|
markerEnd int
|
||||||
@@ -166,8 +160,8 @@ func analyzeYAMLPreamble(native []byte) yamlPreamble {
|
|||||||
func frameEffectiveYAML(scope string, native []byte) ([]byte, bool) {
|
func frameEffectiveYAML(scope string, native []byte) ([]byte, bool) {
|
||||||
analysis := analyzeYAMLPreamble(native)
|
analysis := analyzeYAMLPreamble(native)
|
||||||
label := scope
|
label := scope
|
||||||
for _, r := range scope {
|
for _, character := range scope {
|
||||||
if unicode.IsControl(r) {
|
if unicode.IsControl(character) {
|
||||||
label = strconv.Quote(scope)
|
label = strconv.Quote(scope)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -175,8 +169,6 @@ func frameEffectiveYAML(scope string, native []byte) ([]byte, bool) {
|
|||||||
comment := []byte("# scope: " + label + "\n")
|
comment := []byte("# scope: " + label + "\n")
|
||||||
if analysis.markerFound {
|
if analysis.markerFound {
|
||||||
out := make([]byte, 0, len(native)+len(comment))
|
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 {
|
if analysis.inlineMarker {
|
||||||
out = append(out, native[:analysis.markerStart]...)
|
out = append(out, native[:analysis.markerStart]...)
|
||||||
out = append(out, comment...)
|
out = append(out, comment...)
|
||||||
@@ -205,13 +197,6 @@ func frameEffectiveYAML(scope string, native []byte) ([]byte, bool) {
|
|||||||
return result, analysis.hasDirective
|
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 {
|
func isYAMLDocumentStart(line []byte) bool {
|
||||||
return bytes.HasPrefix(line, []byte("---")) && (len(line) == 3 || line[3] == ' ' || line[3] == '\t')
|
return bytes.HasPrefix(line, []byte("---")) && (len(line) == 3 || line[3] == ' ' || line[3] == '\t')
|
||||||
}
|
}
|
||||||
|
|||||||
95
integrations/ckconfig/show_framing_test.go
Normal file
95
integrations/ckconfig/show_framing_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,24 +3,13 @@ package ckconfig
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
|
||||||
|
|
||||||
func newConfigSourcesCommand[C any](prepared *preparedConfigSuite[C]) (Command, error) {
|
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
||||||
spec := CommandSpec[configInput[C]]{
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||||
Name: string(ConfigSources),
|
"go.yaml.in/yaml/v4"
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
type sourceReport struct {
|
type sourceReport struct {
|
||||||
Scope string `yaml:"scope"`
|
Scope string `yaml:"scope"`
|
||||||
@@ -43,35 +32,30 @@ type locationDTO struct {
|
|||||||
Column int `yaml:"column"`
|
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 {
|
if err := invocation.Context.Err(); err != nil {
|
||||||
return Failure(err)
|
return configOperationFailure(err, nil, false)
|
||||||
}
|
}
|
||||||
scopeNames, err := selectConfigScopes(prepared, input, ConfigSources)
|
if selected.scope.scope.Acquire == nil {
|
||||||
if err != nil {
|
return configOperationFailure(fmt.Errorf("configuration scope %q has no acquisition callback", name), nil, false)
|
||||||
return Failure(err)
|
|
||||||
}
|
}
|
||||||
contexts := contextsForScopes(prepared, input, scopeNames)
|
acquired, acquireErr := selected.scope.scope.Acquire(invocation.Context, Request{
|
||||||
name := scopeNames[0]
|
Operation: Sources,
|
||||||
scope, ok := findConfigScope(prepared, name)
|
Selection: plan.selection,
|
||||||
if !ok || scope.Acquire == nil {
|
}, selected.input, state)
|
||||||
return sourceReportFailure(fmt.Errorf("configuration scope %q has no acquisition callback", name), nil, false)
|
diagnostics := append([]commandkit.Diagnostic(nil), acquired.Diagnostics...)
|
||||||
}
|
|
||||||
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...)
|
|
||||||
if acquireErr != nil {
|
if acquireErr != nil {
|
||||||
return sourceReportFailure(acquireErr, diagnostics, false)
|
return configOperationFailure(acquireErr, diagnostics, false)
|
||||||
}
|
}
|
||||||
if err := invocation.Context.Err(); err != nil {
|
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()
|
sources := acquired.Value.Sources.Sources()
|
||||||
document := sourceReport{Scope: name, Sources: make([]sourceDTO, 0, len(sources))}
|
document := sourceReport{Scope: name, Sources: make([]sourceDTO, 0, len(sources))}
|
||||||
for _, source := range sources {
|
for _, source := range sources {
|
||||||
@@ -88,24 +72,21 @@ func runConfigSources[C any](invocation Invocation, prepared *preparedConfigSuit
|
|||||||
}
|
}
|
||||||
document.Sources = append(document.Sources, item)
|
document.Sources = append(document.Sources, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
var report bytes.Buffer
|
var report bytes.Buffer
|
||||||
if input.Format == "yaml" {
|
if plan.format == "yaml" {
|
||||||
if err := encodeYAML(&report, document); err != nil {
|
if err := encodeYAML(&report, document); err != nil {
|
||||||
return sourceReportFailure(err, diagnostics, false)
|
return configOperationFailure(err, diagnostics, false)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
writeSourceReportText(&report, document)
|
writeSourceReportText(&report, document)
|
||||||
}
|
}
|
||||||
if err := invocation.Context.Err(); err != nil {
|
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 {
|
if err := writeConfigReport(invocation.Streams.Out, report.Bytes()); err != nil {
|
||||||
outcome := sourceReportFailure(err, diagnostics, true)
|
return configDeliveryFailure(err, diagnostics, true)
|
||||||
outcome.DeliveryError = err
|
|
||||||
return outcome
|
|
||||||
}
|
}
|
||||||
outcome := Success()
|
outcome := commandkit.Success()
|
||||||
outcome.Diagnostics = diagnostics
|
outcome.Diagnostics = diagnostics
|
||||||
return outcome
|
return outcome
|
||||||
}
|
}
|
||||||
@@ -133,9 +114,16 @@ func writeSourceReportText(writer *bytes.Buffer, document sourceReport) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sourceReportFailure(cause error, diagnostics []Diagnostic, reportStarted bool) Outcome {
|
func encodeYAML(writer io.Writer, document any) error {
|
||||||
outcome := Failure(cause)
|
encoder := yaml.NewEncoder(writer)
|
||||||
outcome.Diagnostics = diagnostics
|
encoder.SetIndent(2)
|
||||||
outcome.ReportIncomplete = reportStarted
|
return encoder.Encode(document)
|
||||||
return outcome
|
}
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,93 +5,51 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
||||||
"go.yaml.in/yaml/v4"
|
"go.yaml.in/yaml/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
func configOperationDeclared[C any](scopes []ConfigScope[C], operation ConfigOperation) bool {
|
func runConfigValidation[I, S any](invocation commandkit.Invocation, plan *configOperationPlan[I, S], state S) commandkit.Outcome {
|
||||||
for _, scope := range scopes {
|
diagnostics := make([]commandkit.Diagnostic, 0)
|
||||||
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)
|
|
||||||
writtenRecord := false
|
writtenRecord := false
|
||||||
for _, name := range scopeNames {
|
for _, selected := range plan.scopes {
|
||||||
scope, ok := findConfigScope(prepared, name)
|
|
||||||
if !ok || scope.Acquire == nil {
|
|
||||||
return validationFailure(fmt.Errorf("configuration scope %q has no acquisition callback", name), diagnostics, writtenRecord)
|
|
||||||
}
|
|
||||||
if err := invocation.Context.Err(); err != nil {
|
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...)
|
diagnostics = append(diagnostics, acquired.Diagnostics...)
|
||||||
if acquireErr != nil {
|
if acquireErr != nil {
|
||||||
return validationFailure(acquireErr, diagnostics, writtenRecord)
|
return configOperationFailure(acquireErr, diagnostics, writtenRecord)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := invocation.Context.Err(); err != nil {
|
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 {
|
if err != nil {
|
||||||
return validationFailure(err, diagnostics, writtenRecord)
|
return configOperationFailure(err, diagnostics, writtenRecord)
|
||||||
}
|
}
|
||||||
if input.Format == "yaml" && !writtenRecord {
|
if plan.format == "yaml" && !writtenRecord {
|
||||||
if err := writeReport(invocation.Streams.Out, []byte("scopes:\n")); err != nil {
|
if err := writeConfigReport(invocation.Streams.Out, []byte("scopes:\n")); err != nil {
|
||||||
outcome := validationFailure(err, diagnostics, true)
|
return configDeliveryFailure(err, diagnostics, true)
|
||||||
outcome.DeliveryError = err
|
|
||||||
return outcome
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := writeReport(invocation.Streams.Out, record); err != nil {
|
if err := writeConfigReport(invocation.Streams.Out, record); err != nil {
|
||||||
outcome := validationFailure(err, diagnostics, true)
|
return configDeliveryFailure(err, diagnostics, true)
|
||||||
outcome.DeliveryError = err
|
|
||||||
return outcome
|
|
||||||
}
|
}
|
||||||
writtenRecord = true
|
writtenRecord = true
|
||||||
}
|
}
|
||||||
outcome := Success()
|
outcome := commandkit.Success()
|
||||||
outcome.Diagnostics = diagnostics
|
outcome.Diagnostics = diagnostics
|
||||||
return outcome
|
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) {
|
func validationSuccessRecord(scope, format string) ([]byte, error) {
|
||||||
if format == "text" {
|
if format == "text" {
|
||||||
return []byte(scope + ": valid\n"), nil
|
return []byte(scope + ": valid\n"), nil
|
||||||
|
|||||||
Reference in New Issue
Block a user