Render ConfigKit show and validate through reporter
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Invocation reporting implementation plan
|
# Invocation reporting implementation plan
|
||||||
|
|
||||||
Status: Stages 1–10 complete. Stage 11 is next.
|
Status: Stages 1–11 complete. Stage 12 is next.
|
||||||
|
|
||||||
## Objective and authority
|
## Objective and authority
|
||||||
|
|
||||||
@@ -418,7 +418,7 @@ migration; Stage 20 will validate the assembled modules.
|
|||||||
|
|
||||||
## Stage 11: Migrate configuration show and validate
|
## Stage 11: Migrate configuration show and validate
|
||||||
|
|
||||||
Status: pending.
|
Status: complete.
|
||||||
|
|
||||||
Primary files: [show.go](../../integrations/ckconfig/show.go),
|
Primary files: [show.go](../../integrations/ckconfig/show.go),
|
||||||
[validate.go](../../integrations/ckconfig/validate.go), and
|
[validate.go](../../integrations/ckconfig/validate.go), and
|
||||||
@@ -449,6 +449,15 @@ deferred context ordering, rendering failures, and report write failures.
|
|||||||
Update tests of superseded outcome fields to observable reports and
|
Update tests of superseded outcome fields to observable reports and
|
||||||
`Presentation`, rather than dropping their original failure protection.
|
`Presentation`, rather than dropping their original failure protection.
|
||||||
|
|
||||||
|
Configuration show and validate now submit provider messages through the shared
|
||||||
|
reporter and render results through managed output. Tests retain acquisition
|
||||||
|
ordering, shared state, YAML framing, cancellation with partial validation
|
||||||
|
output, and cover warning visibility, terminal context, quiet rendering, and
|
||||||
|
destination failures through `Presentation`. The ckconfig tests remain
|
||||||
|
unrunnable because `sources.go`, `explain.go`, `diff.go`, and family wiring and
|
||||||
|
tests still reference APIs scheduled for later migrations; Stage 20 will run the
|
||||||
|
assembled suites.
|
||||||
|
|
||||||
## Stage 12: Migrate configuration sources, explain, and diff
|
## Stage 12: Migrate configuration sources, explain, and diff
|
||||||
|
|
||||||
Status: pending.
|
Status: pending.
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ func TestValidationPreparesIndependentScopeInputsAndOneSharedState(t *testing.T)
|
|||||||
}
|
}
|
||||||
app := newConfigTestApp(t, Command(suite))
|
app := newConfigTestApp(t, Command(suite))
|
||||||
var output, diagnostic bytes.Buffer
|
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})
|
outcome := app.Execute(context.Background(), []string{"config", "validate", "--scope", "all", "--config", "chosen.yaml", "--campaign", "spring", "--verbose"}, commandkit.Streams{Out: &output, Err: &diagnostic})
|
||||||
if outcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(diagnostic.String(), "app warning") || !strings.Contains(diagnostic.String(), "promptkit warning") {
|
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())
|
t.Fatalf("validation outcome=%#v diagnostics=%q", outcome, diagnostic.String())
|
||||||
}
|
}
|
||||||
@@ -289,7 +289,7 @@ func TestShowAcquiresAllViewsBeforeRenderingAndFramesNativeYAML(t *testing.T) {
|
|||||||
makeScope("promptkit", "---\nname: promptkit\n", providerErr),
|
makeScope("promptkit", "---\nname: promptkit\n", providerErr),
|
||||||
}}))
|
}}))
|
||||||
outcome := app.Execute(context.Background(), []string{"config", "show", "--scope", "all"}, commandkit.Streams{Out: &output, Err: io.Discard})
|
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 {
|
if outcome.Kind != commandkit.OutcomeOperational || outcome.Cause != providerErr || output.Len() != 0 || outcome.Presentation.Output.State != commandkit.DeliveryNone {
|
||||||
t.Fatalf("aggregate provider failure outcome=%#v output=%q", outcome, output.String())
|
t.Fatalf("aggregate provider failure outcome=%#v output=%q", outcome, output.String())
|
||||||
}
|
}
|
||||||
if len(acquisitionOutputLengths) != 2 || acquisitionOutputLengths[0] != 0 || acquisitionOutputLengths[1] != 0 {
|
if len(acquisitionOutputLengths) != 2 || acquisitionOutputLengths[0] != 0 || acquisitionOutputLengths[1] != 0 {
|
||||||
@@ -303,7 +303,7 @@ func TestShowAcquiresAllViewsBeforeRenderingAndFramesNativeYAML(t *testing.T) {
|
|||||||
makeScope("promptkit", "name: promptkit\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})
|
outcome = app.Execute(context.Background(), []string{"config", "show", "--scope", "all"}, commandkit.Streams{Out: &output, Err: io.Discard})
|
||||||
if outcome.Kind != commandkit.OutcomeSuccess {
|
if outcome.Kind != commandkit.OutcomeSuccess || outcome.Presentation.Output.State != commandkit.DeliveryComplete {
|
||||||
t.Fatalf("aggregate show outcome = %#v", outcome)
|
t.Fatalf("aggregate show outcome = %#v", outcome)
|
||||||
}
|
}
|
||||||
want := "---\n# scope: app\nname: app\n---\n# scope: promptkit\nname: promptkit\n"
|
want := "---\n# scope: app\nname: app\n---\n# scope: promptkit\nname: promptkit\n"
|
||||||
@@ -312,6 +312,109 @@ func TestShowAcquiresAllViewsBeforeRenderingAndFramesNativeYAML(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfigShowSeparatesWarningsFromTerminalContextAcrossModes(t *testing.T) {
|
||||||
|
providerErr := errors.New("private provider cause")
|
||||||
|
app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{{
|
||||||
|
Name: "app", Operations: []Operation{Show}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) {
|
||||||
|
return commandkit.Acquired[View]{
|
||||||
|
Diagnostics: []commandkit.Diagnostic{commandkit.MessageDiagnostic(commandkit.SeverityWarning, "independent warning")},
|
||||||
|
Explanation: commandkit.Explanation{Details: []commandkit.Detail{func(writer io.Writer) error {
|
||||||
|
_, err := io.WriteString(writer, "native terminal context\n")
|
||||||
|
return err
|
||||||
|
}}},
|
||||||
|
}, providerErr
|
||||||
|
},
|
||||||
|
}}}))
|
||||||
|
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
flags []string
|
||||||
|
wantWarning bool
|
||||||
|
wantTerminal bool
|
||||||
|
}{
|
||||||
|
{name: "ordinary", wantTerminal: true},
|
||||||
|
{name: "verbose", flags: []string{"--verbose"}, wantWarning: true, wantTerminal: true},
|
||||||
|
{name: "quiet", flags: []string{"--quiet"}, wantTerminal: true},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
args := append([]string{"config", "show"}, test.flags...)
|
||||||
|
var output, diagnostic bytes.Buffer
|
||||||
|
outcome := app.Execute(context.Background(), args, commandkit.Streams{Out: &output, Err: &diagnostic})
|
||||||
|
if outcome.Kind != commandkit.OutcomeOperational || outcome.Cause != providerErr {
|
||||||
|
t.Fatalf("provider failure outcome = %#v", outcome)
|
||||||
|
}
|
||||||
|
if got := strings.Contains(diagnostic.String(), "independent warning"); got != test.wantWarning {
|
||||||
|
t.Fatalf("warning visibility = %v, output %q", got, diagnostic.String())
|
||||||
|
}
|
||||||
|
if got := strings.Contains(diagnostic.String(), "native terminal context"); got != test.wantTerminal {
|
||||||
|
t.Fatalf("terminal context visibility = %v, output %q", got, diagnostic.String())
|
||||||
|
}
|
||||||
|
if strings.Contains(diagnostic.String(), providerErr.Error()) {
|
||||||
|
t.Fatalf("cause text was duplicated into presentation: %q", diagnostic.String())
|
||||||
|
}
|
||||||
|
if test.wantWarning && strings.Index(diagnostic.String(), "independent warning") > strings.Index(diagnostic.String(), "native terminal context") {
|
||||||
|
t.Fatalf("deferred warning did not precede terminal context: %q", diagnostic.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigShowAndValidationUseManagedRendering(t *testing.T) {
|
||||||
|
t.Run("quiet show still records native render errors", func(t *testing.T) {
|
||||||
|
renderErr := errors.New("native renderer failed")
|
||||||
|
app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{{
|
||||||
|
Name: "app", Operations: []Operation{Show}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) {
|
||||||
|
return commandkit.Acquired[View]{Value: View{Effective: failedEffective{err: renderErr}}}, nil
|
||||||
|
},
|
||||||
|
}}}))
|
||||||
|
var output, diagnostic bytes.Buffer
|
||||||
|
outcome := app.Execute(context.Background(), []string{"config", "show", "--quiet"}, commandkit.Streams{Out: &output, Err: &diagnostic})
|
||||||
|
if outcome.Kind != commandkit.OutcomeOperational || !errors.Is(outcome.Presentation.Output.RenderError, renderErr) || outcome.Cause != nil || output.Len() != 0 {
|
||||||
|
t.Fatalf("quiet render failure outcome=%#v output=%q", outcome, output.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, operation := range []Operation{Show, Validate} {
|
||||||
|
t.Run(string(operation)+" write failure", func(t *testing.T) {
|
||||||
|
writeErr := errors.New("report destination failed")
|
||||||
|
app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{{
|
||||||
|
Name: "app",
|
||||||
|
Operations: []Operation{operation},
|
||||||
|
Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) {
|
||||||
|
return commandkit.Acquired[View]{Value: View{Effective: effectiveValue("value: ready\n")}}, nil
|
||||||
|
},
|
||||||
|
}}}))
|
||||||
|
commandName := "show"
|
||||||
|
if operation == Validate {
|
||||||
|
commandName = "validate"
|
||||||
|
}
|
||||||
|
outcome := app.Execute(context.Background(), []string{"config", commandName}, commandkit.Streams{Out: failingOutput{err: writeErr}, Err: io.Discard})
|
||||||
|
if outcome.Kind != commandkit.OutcomeOperational || !errors.Is(outcome.Presentation.Output.WriteError, writeErr) || outcome.Cause != nil {
|
||||||
|
t.Fatalf("report write failure outcome = %#v", outcome)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run(string(operation)+" quiet suppression", func(t *testing.T) {
|
||||||
|
writeErr := errors.New("quiet destination should not be used")
|
||||||
|
app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{{
|
||||||
|
Name: "app",
|
||||||
|
Operations: []Operation{operation},
|
||||||
|
Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) {
|
||||||
|
return commandkit.Acquired[View]{Value: View{Effective: effectiveValue("value: ready\n")}}, nil
|
||||||
|
},
|
||||||
|
}}}))
|
||||||
|
commandName := "show"
|
||||||
|
if operation == Validate {
|
||||||
|
commandName = "validate"
|
||||||
|
}
|
||||||
|
outcome := app.Execute(context.Background(), []string{"config", commandName, "--quiet"}, commandkit.Streams{Out: failingOutput{err: writeErr}, Err: io.Discard})
|
||||||
|
if outcome.Kind != commandkit.OutcomeSuccess || outcome.Presentation.Output.State != commandkit.DeliverySuppressed || outcome.Presentation.Output.WriteError != nil || outcome.Cause != nil {
|
||||||
|
t.Fatalf("quiet report outcome = %#v", outcome)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSourcesReadsNativeInventoryOnceAndPreservesZeroValues(t *testing.T) {
|
func TestSourcesReadsNativeInventoryOnceAndPreservesZeroValues(t *testing.T) {
|
||||||
reader := &sourceInventory{values: []configkit.SourceInfo{{
|
reader := &sourceInventory{values: []configkit.SourceInfo{{
|
||||||
ID: "synthetic", Label: "", Path: "", Layer: configkit.Layer("base"), Kind: configkit.SourceKind("mapping"), UnderlayIndex: 0,
|
ID: "synthetic", Label: "", Path: "", Layer: configkit.Layer("base"), Kind: configkit.SourceKind("mapping"), UnderlayIndex: 0,
|
||||||
@@ -394,7 +497,7 @@ func TestReportWriteFailureTracksIncompleteDeliveryAndCancellation(t *testing.T)
|
|||||||
}}))
|
}}))
|
||||||
var output bytes.Buffer
|
var output bytes.Buffer
|
||||||
outcome = app.Execute(ctx, []string{"config", "validate", "--scope", "all"}, commandkit.Streams{Out: &output, Err: io.Discard})
|
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" {
|
if !errors.Is(outcome.Cause, context.Canceled) || outcome.Presentation.Output.State != commandkit.DeliveryComplete || calls != 2 || output.String() != "app: valid\n" {
|
||||||
t.Fatalf("cancelled aggregate validation outcome=%#v calls=%d output=%q", outcome, calls, output.String())
|
t.Fatalf("cancelled aggregate validation outcome=%#v calls=%d output=%q", outcome, calls, output.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -406,6 +509,12 @@ func (value effectiveValue) WriteEffective(writer io.Writer) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type failedEffective struct{ err error }
|
||||||
|
|
||||||
|
func (value failedEffective) WriteEffective(io.Writer) error {
|
||||||
|
return value.err
|
||||||
|
}
|
||||||
|
|
||||||
type sourceInventory struct {
|
type sourceInventory struct {
|
||||||
values []configkit.SourceInfo
|
values []configkit.SourceInfo
|
||||||
calls int
|
calls int
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package ckconfig
|
package ckconfig
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
||||||
@@ -9,10 +8,10 @@ import (
|
|||||||
|
|
||||||
func runConfigOperation[I, S any](invocation commandkit.Invocation, operation Operation, input *configCommandInput[I, S]) commandkit.Outcome {
|
func runConfigOperation[I, S any](invocation commandkit.Invocation, operation Operation, input *configCommandInput[I, S]) commandkit.Outcome {
|
||||||
if input == nil || input.plan == nil {
|
if input == nil || input.plan == nil {
|
||||||
return commandkit.Failure(errors.New("configuration operation was not prepared"))
|
return commandkit.Failure("Configuration operation was not prepared.")
|
||||||
}
|
}
|
||||||
if err := invocation.Context.Err(); err != nil {
|
if err := invocation.Context.Err(); err != nil {
|
||||||
return commandkit.Failure(err)
|
return configFailure("Configuration operation was canceled.", commandkit.Explanation{}, err)
|
||||||
}
|
}
|
||||||
plan := input.plan
|
plan := input.plan
|
||||||
var state S
|
var state S
|
||||||
@@ -31,19 +30,47 @@ func runConfigOperation[I, S any](invocation commandkit.Invocation, operation Op
|
|||||||
case Diff:
|
case Diff:
|
||||||
return runConfigDiff(invocation, plan, state)
|
return runConfigDiff(invocation, plan, state)
|
||||||
default:
|
default:
|
||||||
return commandkit.Failure(fmt.Errorf("unsupported configuration operation %q", operation))
|
return commandkit.Failure(fmt.Sprintf("Unsupported configuration operation %q.", operation))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func configOperationFailure(cause error, diagnostics []commandkit.Diagnostic, reportStarted bool) commandkit.Outcome {
|
func queueConfigDiagnostics(reporter *commandkit.Reporter, diagnostics []commandkit.Diagnostic) {
|
||||||
outcome := commandkit.Failure(cause)
|
deferred := reporter.AtEnd()
|
||||||
outcome.Diagnostics = diagnostics
|
for _, diagnostic := range diagnostics {
|
||||||
outcome.ReportIncomplete = reportStarted
|
if diagnostic.Write == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch diagnostic.Severity {
|
||||||
|
case commandkit.SeverityInfo:
|
||||||
|
_ = deferred.Info("", diagnostic.Write)
|
||||||
|
case commandkit.SeverityDebug:
|
||||||
|
_ = deferred.Debug("", diagnostic.Write)
|
||||||
|
case commandkit.SeverityProgress:
|
||||||
|
_ = reporter.Progress("", diagnostic.Write)
|
||||||
|
default:
|
||||||
|
_ = deferred.Warn("", diagnostic.Write)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func configFailure(message string, explanation commandkit.Explanation, cause error) commandkit.Outcome {
|
||||||
|
if explanation.Message != "" || len(explanation.Details) > 0 {
|
||||||
|
message = explanation.Message
|
||||||
|
}
|
||||||
|
outcome := commandkit.Failure(message, explanation.Details...)
|
||||||
|
if cause != nil {
|
||||||
|
outcome = outcome.WithCause(cause)
|
||||||
|
}
|
||||||
return outcome
|
return outcome
|
||||||
}
|
}
|
||||||
|
|
||||||
func configDeliveryFailure(cause error, diagnostics []commandkit.Diagnostic, reportStarted bool) commandkit.Outcome {
|
func configAcquisitionFailure(
|
||||||
outcome := configOperationFailure(cause, diagnostics, reportStarted)
|
reporter *commandkit.Reporter,
|
||||||
outcome.DeliveryError = cause
|
operation, scope string,
|
||||||
return outcome
|
acquired commandkit.Acquired[View],
|
||||||
|
cause error,
|
||||||
|
) commandkit.Outcome {
|
||||||
|
queueConfigDiagnostics(reporter, acquired.Diagnostics)
|
||||||
|
message := fmt.Sprintf("Unable to %s configuration scope %q.", operation, scope)
|
||||||
|
return configFailure(message, acquired.Explanation, cause)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package ckconfig
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -18,101 +17,76 @@ type effectiveDocument struct {
|
|||||||
|
|
||||||
func runConfigShow[I, S any](invocation commandkit.Invocation, plan *configOperationPlan[I, S], state S) commandkit.Outcome {
|
func runConfigShow[I, S any](invocation commandkit.Invocation, plan *configOperationPlan[I, S], state S) commandkit.Outcome {
|
||||||
documents := make([]effectiveDocument, 0, len(plan.scopes))
|
documents := make([]effectiveDocument, 0, len(plan.scopes))
|
||||||
diagnostics := make([]commandkit.Diagnostic, 0)
|
reporter := invocation.Reporter()
|
||||||
for _, selected := range plan.scopes {
|
for _, selected := range plan.scopes {
|
||||||
if err := invocation.Context.Err(); err != nil {
|
if err := invocation.Context.Err(); err != nil {
|
||||||
return configOperationFailure(err, diagnostics, false)
|
return configFailure("Configuration show was canceled.", commandkit.Explanation{}, err)
|
||||||
}
|
}
|
||||||
if selected.scope.scope.Acquire == nil {
|
if selected.scope.scope.Acquire == nil {
|
||||||
return configOperationFailure(fmt.Errorf("configuration scope %q has no acquisition callback", selected.scope.scope.Name), diagnostics, false)
|
message := fmt.Sprintf("Configuration scope %q has no acquisition callback.", selected.scope.scope.Name)
|
||||||
|
return commandkit.Failure(message)
|
||||||
}
|
}
|
||||||
acquired, acquireErr := selected.scope.scope.Acquire(invocation.Context, Request{
|
acquired, acquireErr := selected.scope.scope.Acquire(invocation.Context, Request{
|
||||||
Operation: Show,
|
Operation: Show,
|
||||||
Selection: plan.selection,
|
Selection: plan.selection,
|
||||||
}, selected.input, state)
|
}, selected.input, state)
|
||||||
diagnostics = append(diagnostics, acquired.Diagnostics...)
|
|
||||||
if acquireErr != nil {
|
if acquireErr != nil {
|
||||||
return configOperationFailure(acquireErr, diagnostics, false)
|
return configAcquisitionFailure(reporter, "show", selected.scope.scope.Name, acquired, acquireErr)
|
||||||
}
|
}
|
||||||
|
queueConfigDiagnostics(reporter, acquired.Diagnostics)
|
||||||
if err := invocation.Context.Err(); err != nil {
|
if err := invocation.Context.Err(); err != nil {
|
||||||
return configOperationFailure(err, diagnostics, false)
|
return configFailure("Configuration show was canceled.", acquired.Explanation, err)
|
||||||
}
|
}
|
||||||
if acquired.Value.Effective == nil {
|
if acquired.Value.Effective == nil {
|
||||||
return configOperationFailure(fmt.Errorf("configuration scope %q did not provide an effective configuration writer", selected.scope.scope.Name), diagnostics, false)
|
message := fmt.Sprintf("Configuration scope %q did not provide an effective configuration writer.", selected.scope.scope.Name)
|
||||||
|
return commandkit.Failure(message)
|
||||||
}
|
}
|
||||||
documents = append(documents, effectiveDocument{name: selected.scope.scope.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 configOperationFailure(err, diagnostics, false)
|
return configFailure("Configuration show was canceled.", commandkit.Explanation{}, err)
|
||||||
}
|
}
|
||||||
if len(documents) == 1 && plan.scopeName != "all" {
|
if len(documents) == 1 && plan.scopeName != "all" {
|
||||||
checked := &checkedWriter{writer: invocation.Streams.Out}
|
err := reporter.RenderOutput(func(writer io.Writer) error {
|
||||||
renderErr := documents[0].view.Effective.WriteEffective(checked)
|
return documents[0].view.Effective.WriteEffective(writer)
|
||||||
if checked.failure != nil {
|
})
|
||||||
if renderErr == nil {
|
if err != nil && invocation.Context.Err() != nil {
|
||||||
renderErr = checked.failure
|
return configFailure("Configuration show was canceled.", commandkit.Explanation{}, invocation.Context.Err())
|
||||||
} else {
|
}
|
||||||
renderErr = errors.Join(renderErr, checked.failure)
|
return commandkit.Success()
|
||||||
|
}
|
||||||
|
|
||||||
|
err := reporter.RenderOutput(func(writer io.Writer) error {
|
||||||
|
for index, document := range documents {
|
||||||
|
if err := invocation.Context.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var native bytes.Buffer
|
||||||
|
if err := document.view.Effective.WriteEffective(&native); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := invocation.Context.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
framed, hasDirective := frameEffectiveYAML(document.name, native.Bytes())
|
||||||
|
if index > 0 && hasDirective {
|
||||||
|
if err := writeConfigReport(writer, []byte("...\n")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if index < len(documents)-1 && len(framed) > 0 && framed[len(framed)-1] != '\n' {
|
||||||
|
framed = append(framed, '\n')
|
||||||
|
}
|
||||||
|
if err := writeConfigReport(writer, framed); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if renderErr != nil {
|
return nil
|
||||||
outcome := configOperationFailure(renderErr, diagnostics, true)
|
})
|
||||||
outcome.DeliveryError = checked.failure
|
if err != nil && invocation.Context.Err() != nil {
|
||||||
return outcome
|
return configFailure("Configuration show was canceled.", commandkit.Explanation{}, invocation.Context.Err())
|
||||||
}
|
|
||||||
outcome := commandkit.Success()
|
|
||||||
outcome.Diagnostics = diagnostics
|
|
||||||
return outcome
|
|
||||||
}
|
}
|
||||||
|
return commandkit.Success()
|
||||||
written := false
|
|
||||||
for index, document := range documents {
|
|
||||||
if err := invocation.Context.Err(); err != nil {
|
|
||||||
return configOperationFailure(err, diagnostics, written)
|
|
||||||
}
|
|
||||||
var native bytes.Buffer
|
|
||||||
if err := document.view.Effective.WriteEffective(&native); err != nil {
|
|
||||||
return configOperationFailure(err, diagnostics, written)
|
|
||||||
}
|
|
||||||
if err := invocation.Context.Err(); err != nil {
|
|
||||||
return configOperationFailure(err, diagnostics, written)
|
|
||||||
}
|
|
||||||
framed, hasDirective := frameEffectiveYAML(document.name, native.Bytes())
|
|
||||||
if index > 0 && hasDirective {
|
|
||||||
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 := writeConfigReport(invocation.Streams.Out, framed); err != nil {
|
|
||||||
return configDeliveryFailure(err, diagnostics, true)
|
|
||||||
}
|
|
||||||
written = true
|
|
||||||
}
|
|
||||||
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 {
|
type yamlPreamble struct {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package ckconfig
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
||||||
@@ -10,44 +11,49 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func runConfigValidation[I, S any](invocation commandkit.Invocation, plan *configOperationPlan[I, S], state S) commandkit.Outcome {
|
func runConfigValidation[I, S any](invocation commandkit.Invocation, plan *configOperationPlan[I, S], state S) commandkit.Outcome {
|
||||||
diagnostics := make([]commandkit.Diagnostic, 0)
|
reporter := invocation.Reporter()
|
||||||
writtenRecord := false
|
writtenRecord := false
|
||||||
for _, selected := range plan.scopes {
|
for _, selected := range plan.scopes {
|
||||||
if err := invocation.Context.Err(); err != nil {
|
if err := invocation.Context.Err(); err != nil {
|
||||||
return configOperationFailure(err, diagnostics, writtenRecord)
|
return configFailure("Configuration validation was canceled.", commandkit.Explanation{}, err)
|
||||||
}
|
}
|
||||||
if selected.scope.scope.Acquire == nil {
|
if selected.scope.scope.Acquire == nil {
|
||||||
return configOperationFailure(fmt.Errorf("configuration scope %q has no acquisition callback", selected.scope.scope.Name), diagnostics, writtenRecord)
|
message := fmt.Sprintf("Configuration scope %q has no acquisition callback.", selected.scope.scope.Name)
|
||||||
|
return commandkit.Failure(message)
|
||||||
}
|
}
|
||||||
acquired, acquireErr := selected.scope.scope.Acquire(invocation.Context, Request{
|
acquired, acquireErr := selected.scope.scope.Acquire(invocation.Context, Request{
|
||||||
Operation: Validate,
|
Operation: Validate,
|
||||||
Selection: plan.selection,
|
Selection: plan.selection,
|
||||||
}, selected.input, state)
|
}, selected.input, state)
|
||||||
diagnostics = append(diagnostics, acquired.Diagnostics...)
|
|
||||||
if acquireErr != nil {
|
if acquireErr != nil {
|
||||||
return configOperationFailure(acquireErr, diagnostics, writtenRecord)
|
return configAcquisitionFailure(reporter, "validate", selected.scope.scope.Name, acquired, acquireErr)
|
||||||
}
|
}
|
||||||
|
queueConfigDiagnostics(reporter, acquired.Diagnostics)
|
||||||
if err := invocation.Context.Err(); err != nil {
|
if err := invocation.Context.Err(); err != nil {
|
||||||
return configOperationFailure(err, diagnostics, writtenRecord)
|
return configFailure("Configuration validation was canceled.", acquired.Explanation, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
record, err := validationSuccessRecord(selected.scope.scope.Name, plan.format)
|
err := reporter.RenderOutput(func(writer io.Writer) error {
|
||||||
if err != nil {
|
record, err := validationSuccessRecord(selected.scope.scope.Name, plan.format)
|
||||||
return configOperationFailure(err, diagnostics, writtenRecord)
|
if err != nil {
|
||||||
}
|
return err
|
||||||
if plan.format == "yaml" && !writtenRecord {
|
|
||||||
if err := writeConfigReport(invocation.Streams.Out, []byte("scopes:\n")); err != nil {
|
|
||||||
return configDeliveryFailure(err, diagnostics, true)
|
|
||||||
}
|
}
|
||||||
}
|
if plan.format == "yaml" && !writtenRecord {
|
||||||
if err := writeConfigReport(invocation.Streams.Out, record); err != nil {
|
if err := writeConfigReport(writer, []byte("scopes:\n")); err != nil {
|
||||||
return configDeliveryFailure(err, diagnostics, true)
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return writeConfigReport(writer, record)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if ctxErr := invocation.Context.Err(); ctxErr != nil {
|
||||||
|
return configFailure("Configuration validation was canceled.", acquired.Explanation, ctxErr)
|
||||||
|
}
|
||||||
|
return commandkit.Success()
|
||||||
}
|
}
|
||||||
writtenRecord = true
|
writtenRecord = true
|
||||||
}
|
}
|
||||||
outcome := commandkit.Success()
|
return commandkit.Success()
|
||||||
outcome.Diagnostics = diagnostics
|
|
||||||
return outcome
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func validationSuccessRecord(scope, format string) ([]byte, error) {
|
func validationSuccessRecord(scope, format string) ([]byte, error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user