Add focused configuration reports
This commit is contained in:
@@ -38,7 +38,7 @@ func TestComparisonHelpLimitsFlagsAndDefaultsFormatToYAML(t *testing.T) {
|
||||
if strings.Contains(operationHelp, "--against-config") || strings.Contains(operationHelp, "--against-overlay") {
|
||||
t.Errorf("%s exposes comparison-only flags:\n%s", operation, operationHelp)
|
||||
}
|
||||
if operation != "explain" && strings.Contains(operationHelp, "--path") {
|
||||
if operation != "explain" && operation != "show" && strings.Contains(operationHelp, "--path") {
|
||||
t.Errorf("%s exposes the explain-only path flag:\n%s", operation, operationHelp)
|
||||
}
|
||||
}
|
||||
|
||||
126
integrations/ckconfig/focused_reports_test.go
Normal file
126
integrations/ckconfig/focused_reports_test.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package ckconfig
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
commandkit "go.maximumdirect.net/commandkit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
func TestFocusedShowUsesSafeNativeValue(t *testing.T) {
|
||||
loads := 0
|
||||
scope := FromConfigKit("app", "", func(_ context.Context, selection Selection, _, _ struct{}) (*configkit.Result[map[string]any], error) {
|
||||
loads++
|
||||
return configkit.Load(configkit.Mapping("fixture", map[string]any{"object": map[string]any{"x": "y"}, "text": "hello", "false": false, "zero": 0, "null": nil, "gone": map[string]any{"x": 1}}), configkit.Options[map[string]any]{Overrides: selection.Overrides})
|
||||
})
|
||||
scope.Features.Overrides = true
|
||||
app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{scope}}))
|
||||
for _, test := range []struct {
|
||||
path string
|
||||
want any
|
||||
}{{"/object", map[string]any{"x": "y"}}, {"/text", "hello"}, {"/false", false}, {"/zero", float64(0)}, {"/null", nil}} {
|
||||
var out bytes.Buffer
|
||||
result := app.Execute(context.Background(), []string{"config", "show", "--path=" + test.path, "--format=json"}, commandkit.Streams{Out: &out, Err: io.Discard})
|
||||
var got any
|
||||
if result.Kind != commandkit.OutcomeSuccess || json.Unmarshal(out.Bytes(), &got) != nil || !reflect.DeepEqual(got, test.want) {
|
||||
t.Fatalf("%s: %#v %q", test.path, result, out.String())
|
||||
}
|
||||
}
|
||||
var out bytes.Buffer
|
||||
result := app.Execute(context.Background(), []string{"config", "show", "--path=/text"}, commandkit.Streams{Out: &out, Err: io.Discard})
|
||||
if result.Kind != commandkit.OutcomeSuccess || out.String() != "hello\n" {
|
||||
t.Fatalf("automatic text: %#v %q", result, out.String())
|
||||
}
|
||||
out.Reset()
|
||||
result = app.Execute(context.Background(), []string{"config", "show", "--path=/false", "--format=yaml"}, commandkit.Streams{Out: &out, Err: io.Discard})
|
||||
if result.Kind != commandkit.OutcomeSuccess || strings.TrimSpace(out.String()) != "false" {
|
||||
t.Fatalf("yaml false: %#v %q", result, out.String())
|
||||
}
|
||||
for _, args := range [][]string{{"--path=/missing"}, {"--path=/gone/x", "--set=/gone=null"}} {
|
||||
result = app.Execute(context.Background(), append([]string{"config", "show"}, args...), commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||
if result.Kind != commandkit.OutcomeOperational {
|
||||
t.Fatalf("missing %v: %#v", args, result)
|
||||
}
|
||||
}
|
||||
before := loads
|
||||
for _, args := range [][]string{{"--path=bad"}, {"--scope=all", "--path=/text"}} {
|
||||
result = app.Execute(context.Background(), append([]string{"config", "show"}, args...), commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||
if result.Kind != commandkit.OutcomeUsage || loads != before {
|
||||
t.Fatalf("preflight %v: %#v", args, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFocusedShowChecksCapabilityAndFacet(t *testing.T) {
|
||||
calls := 0
|
||||
scope := Scope[struct{}, struct{}]{Name: "app", Operations: []Operation{Show}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) {
|
||||
calls++
|
||||
return commandkit.Acquired[View]{}, nil
|
||||
}}
|
||||
app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{scope}}))
|
||||
result := app.Execute(context.Background(), []string{"config", "show", "--path=/x"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||
if result.Kind != commandkit.OutcomeUsage || calls != 0 {
|
||||
t.Fatalf("unsupported: %#v", result)
|
||||
}
|
||||
scope.Features.PathShow = true
|
||||
app = newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{scope}}))
|
||||
result = app.Execute(context.Background(), []string{"config", "show", "--path=/x"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||
if result.Kind != commandkit.OutcomeOperational || calls != 1 {
|
||||
t.Fatalf("missing facet: %#v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSourcesFilterAndFailureEvidence(t *testing.T) {
|
||||
cause := errors.New("load failed")
|
||||
reader := &sourceInventory{values: []configkit.SourceInfo{{ID: "one", Label: "first"}, {ID: "two", Label: "second"}}}
|
||||
var returnedErr error
|
||||
var source SourceReader = reader
|
||||
scope := 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: source}}, returnedErr
|
||||
}}
|
||||
app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{scope}}))
|
||||
run := func(args ...string) (commandkit.Outcome, string, string) {
|
||||
var out, diag bytes.Buffer
|
||||
result := app.Execute(context.Background(), append([]string{"config", "sources"}, args...), commandkit.Streams{Out: &out, Err: &diag})
|
||||
return result, out.String(), diag.String()
|
||||
}
|
||||
result, out, _ := run("--source-id=two", "--format=json")
|
||||
if result.Kind != commandkit.OutcomeSuccess || !strings.Contains(out, `"id": "two"`) || strings.Contains(out, `"id": "one"`) {
|
||||
t.Fatalf("selected source: %#v %q", result, out)
|
||||
}
|
||||
result, _, _ = run("--source-id=missing")
|
||||
if result.Kind != commandkit.OutcomeOperational {
|
||||
t.Fatalf("unknown source: %#v", result)
|
||||
}
|
||||
returnedErr = cause
|
||||
result, out, _ = run("--source-id=one", "--format=json")
|
||||
if !errors.Is(result.Cause, cause) || !strings.Contains(out, `"id": "one"`) || strings.Contains(out, `"id": "two"`) {
|
||||
t.Fatalf("failed selection: %#v %q", result, out)
|
||||
}
|
||||
result, _, diag := run("--source-id=missing")
|
||||
if !errors.Is(result.Cause, cause) || !strings.Contains(diag, "not found") {
|
||||
t.Fatalf("failed missing source: %#v %q", result, diag)
|
||||
}
|
||||
source = &sourceInventory{}
|
||||
result, out, _ = run("--format=json")
|
||||
if !errors.Is(result.Cause, cause) || !strings.Contains(out, `"sources": []`) {
|
||||
t.Fatalf("empty evidence: %#v %q", result, out)
|
||||
}
|
||||
source = nil
|
||||
result, out, diag = run()
|
||||
if !errors.Is(result.Cause, cause) || out != "" || !strings.Contains(diag, "unavailable") {
|
||||
t.Fatalf("unavailable evidence: %#v %q %q", result, out, diag)
|
||||
}
|
||||
source = reader
|
||||
result = app.Execute(context.Background(), []string{"config", "sources"}, commandkit.Streams{Out: shortWriter{}, Err: io.Discard})
|
||||
if !errors.Is(result.Cause, cause) || result.Presentation.Output.State != commandkit.DeliveryIncomplete {
|
||||
t.Fatalf("delivery failure: %#v", result)
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ type configCommandInput[I, S any] struct {
|
||||
historical commandkit.Supplied[bool]
|
||||
under commandkit.Supplied[string]
|
||||
explicit commandkit.Supplied[bool]
|
||||
sourceID commandkit.Supplied[string]
|
||||
values commandkit.FlagValues[I]
|
||||
againstValues commandkit.FlagValues[I]
|
||||
plan *configOperationPlan[I, S]
|
||||
@@ -38,6 +39,8 @@ type configOperationPlan[I, S any] struct {
|
||||
operation Operation
|
||||
scopeName string
|
||||
path configkit.Path
|
||||
pathSet bool
|
||||
sourceID commandkit.Supplied[string]
|
||||
list bool
|
||||
help bool
|
||||
historical bool
|
||||
@@ -106,6 +109,12 @@ func newConfigOperationCommand[I, S any](prepared *preparedSuite[I, S], operatio
|
||||
commandkit.BoolFlag(commandkit.FlagMeta{Name: "explicit", Usage: "Filter listed current paths by explicitness."}, false, func(input **configCommandInput[I, S], value commandkit.Supplied[bool]) { (*input).explicit = value }),
|
||||
)
|
||||
}
|
||||
if operation == Show {
|
||||
flags = append(flags, commandkit.ValueFlag(commandkit.FlagMeta{Name: "path", Usage: "JSON Pointer to one effective value."}, (*string)(nil), parseConfigString, func(input **configCommandInput[I, S], value commandkit.Supplied[string]) { (*input).path = value }))
|
||||
}
|
||||
if operation == Sources {
|
||||
flags = append(flags, commandkit.ValueFlag(commandkit.FlagMeta{Name: "source-id", Usage: "Select one source ID from this acquisition."}, (*string)(nil), parseConfigString, func(input **configCommandInput[I, S], value commandkit.Supplied[string]) { (*input).sourceID = value }))
|
||||
}
|
||||
flags = append(flags, commandkit.CaptureFlags(prepared.bindingsByOperation[operation], func(input **configCommandInput[I, S]) *commandkit.FlagValues[I] {
|
||||
if input == nil || *input == nil {
|
||||
return nil
|
||||
@@ -187,6 +196,9 @@ func prepareConfigOperation[I, S any](prepared *preparedSuite[I, S], operation O
|
||||
if scopeName == "all" && operation != Validate && operation != Show {
|
||||
return fmt.Errorf("scope all is supported only for validate and show")
|
||||
}
|
||||
if scopeName == "all" && operation == Show && input.path.Set {
|
||||
return fmt.Errorf("path-selected show requires one scope")
|
||||
}
|
||||
|
||||
var selected []*preparedScope[I, S]
|
||||
if scopeName == "all" {
|
||||
@@ -207,10 +219,13 @@ func prepareConfigOperation[I, S any](prepared *preparedSuite[I, S], operation O
|
||||
if operation == Explain && input.list.Value && !scope.scope.Features.PathDiscovery {
|
||||
return fmt.Errorf("configuration scope %q does not support path discovery", scope.scope.Name)
|
||||
}
|
||||
if operation == Show && input.path.Set && !scope.scope.Features.PathShow {
|
||||
return fmt.Errorf("configuration scope %q does not support path-selected show", scope.scope.Name)
|
||||
}
|
||||
}
|
||||
|
||||
var path configkit.Path
|
||||
if operation == Explain && input.path.Set {
|
||||
if (operation == Explain || operation == Show) && input.path.Set {
|
||||
parsed, err := configkit.ParsePath(input.path.Value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid configuration path: %w", err)
|
||||
@@ -231,6 +246,8 @@ func prepareConfigOperation[I, S any](prepared *preparedSuite[I, S], operation O
|
||||
operation: operation,
|
||||
scopeName: scopeName,
|
||||
path: path,
|
||||
pathSet: input.path.Set,
|
||||
sourceID: input.sourceID,
|
||||
list: input.list.Value,
|
||||
historical: input.historical.Value,
|
||||
under: under,
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
commandkit "go.maximumdirect.net/commandkit"
|
||||
)
|
||||
@@ -31,6 +32,31 @@ func runConfigShow[I, S any](invocation commandkit.Invocation, plan *configOpera
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
return configFailure("Configuration show was canceled.", acquired.Explanation, err)
|
||||
}
|
||||
if plan.pathSet {
|
||||
if acquired.Value.Explanation == nil {
|
||||
return configFailure("Configuration scope did not provide explanation data.", acquired.Explanation, fmt.Errorf("configuration scope %q declared path show without an explanation reader", selected.scope.scope.Name))
|
||||
}
|
||||
explanation, found := acquired.Value.Explanation.Explain(plan.path)
|
||||
if !found || !explanation.Effective.Present {
|
||||
return configFailure("The requested effective configuration path is not available.", acquired.Explanation, fmt.Errorf("configuration path %q is not effective in scope %q", plan.path.String(), selected.scope.scope.Name))
|
||||
}
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
return configFailure("Configuration show was canceled.", acquired.Explanation, err)
|
||||
}
|
||||
var value any
|
||||
err := reporter.RenderOutput(func(io.Writer) error {
|
||||
var err error
|
||||
value, err = commandkit.DecodeYAMLReport(strings.NewReader(explanation.Effective.YAML))
|
||||
return err
|
||||
})
|
||||
if err == nil {
|
||||
if canceled := invocation.Context.Err(); canceled != nil {
|
||||
return configFailure("Configuration show was canceled.", acquired.Explanation, canceled)
|
||||
}
|
||||
_ = reporter.Report(commandkit.Report{Value: value})
|
||||
}
|
||||
return commandkit.Success()
|
||||
}
|
||||
if acquired.Value.Effective == nil {
|
||||
return commandkit.Failure(fmt.Sprintf("Configuration scope %q did not provide an effective configuration writer.", selected.scope.scope.Name))
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package ckconfig
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"text/template"
|
||||
|
||||
commandkit "go.maximumdirect.net/commandkit"
|
||||
@@ -69,13 +70,15 @@ func runConfigSources[I, S any](invocation commandkit.Invocation, plan *configOp
|
||||
Selection: selected.selection,
|
||||
}, selected.input, state)
|
||||
queueConfigAcquisition(reporter, acquired)
|
||||
if acquireErr != nil {
|
||||
return configFailure(fmt.Sprintf("Unable to list sources for configuration scope %q.", name), acquired.Explanation, acquireErr)
|
||||
}
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
if acquireErr == nil && invocation.Context.Err() != nil {
|
||||
err := invocation.Context.Err()
|
||||
return configFailure("Configuration source listing was canceled.", acquired.Explanation, err)
|
||||
}
|
||||
if acquired.Value.Sources == nil {
|
||||
if acquireErr != nil {
|
||||
explanation := sourceSelectionExplanation(acquired.Explanation, "Source inventory is unavailable.")
|
||||
return configFailure(fmt.Sprintf("Unable to list sources for configuration scope %q.", name), explanation, acquireErr)
|
||||
}
|
||||
cause := fmt.Errorf("configuration scope %q did not provide a source reader", name)
|
||||
return configFailure("Configuration scope did not provide source information.", acquired.Explanation, cause)
|
||||
}
|
||||
@@ -83,6 +86,9 @@ func runConfigSources[I, S any](invocation commandkit.Invocation, plan *configOp
|
||||
sources := acquired.Value.Sources.Sources()
|
||||
document := sourceReport{Scope: name, Sources: make([]sourceDTO, 0, len(sources))}
|
||||
for _, source := range sources {
|
||||
if plan.sourceID.Set && source.ID != plan.sourceID.Value {
|
||||
continue
|
||||
}
|
||||
placement := "mapping"
|
||||
if source.PlacementMode == configkit.PlacementModeSequenceItem {
|
||||
placement = "sequence_item"
|
||||
@@ -121,14 +127,36 @@ func runConfigSources[I, S any](invocation commandkit.Invocation, plan *configOp
|
||||
item.EmptyImportSelections = append(item.EmptyImportSelections, emptyImportSelectionDTO{Path: selection.Path.String(), Location: locationDTO{SourceID: selection.Location.SourceID, Line: selection.Location.Line, Column: selection.Location.Column}, Selector: string(selection.Selector)})
|
||||
}
|
||||
document.Sources = append(document.Sources, item)
|
||||
if plan.sourceID.Set {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err := invocation.Context.Err(); err != nil {
|
||||
if plan.sourceID.Set && len(document.Sources) == 0 {
|
||||
message := fmt.Sprintf("Source ID %q was not found in configuration scope %q.", plan.sourceID.Value, name)
|
||||
if acquireErr != nil {
|
||||
return configFailure(fmt.Sprintf("Unable to list sources for configuration scope %q.", name), sourceSelectionExplanation(acquired.Explanation, message), acquireErr)
|
||||
}
|
||||
return configFailure(message, acquired.Explanation, fmt.Errorf("source ID %q was not found", plan.sourceID.Value))
|
||||
}
|
||||
if acquireErr == nil && invocation.Context.Err() != nil {
|
||||
err := invocation.Context.Err()
|
||||
return configFailure("Configuration source listing was canceled.", acquired.Explanation, err)
|
||||
}
|
||||
_ = reporter.Report(commandkit.Report{Value: document, TextTemplate: sourceTextTemplate})
|
||||
if acquireErr != nil {
|
||||
return configFailure(fmt.Sprintf("Unable to list sources for configuration scope %q.", name), acquired.Explanation, acquireErr)
|
||||
}
|
||||
return commandkit.Success()
|
||||
}
|
||||
|
||||
func sourceSelectionExplanation(explanation commandkit.Explanation, message string) commandkit.Explanation {
|
||||
explanation.Details = append(append([]commandkit.Detail(nil), explanation.Details...), func(writer io.Writer) error {
|
||||
_, err := fmt.Fprintln(writer, message)
|
||||
return err
|
||||
})
|
||||
return explanation
|
||||
}
|
||||
|
||||
var sourceTextTemplate = template.Must(commandkit.ParseTextTemplate("sources", `Scope: {{printf "%q" .Scope}}
|
||||
{{if .Sources}}Sources:
|
||||
{{range .Sources}} - id: {{printf "%q" .ID}}
|
||||
|
||||
Reference in New Issue
Block a user