Add configuration path discovery
This commit is contained in:
19
docs/cli.md
19
docs/cli.md
@@ -421,21 +421,32 @@ for native evidence definitions.
|
|||||||
### Explain a configuration path
|
### Explain a configuration path
|
||||||
|
|
||||||
`config explain --path POINTER` explains the effective and historical value at a
|
`config explain --path POINTER` explains the effective and historical value at a
|
||||||
JSON Pointer path. `--path` is required; an explicitly empty value selects the
|
JSON Pointer path. An explicitly empty path selects the root. `config explain
|
||||||
root. The default scope is `app`; `--format yaml` or `--format json` selects
|
--list` discovers paths when the selected scope supports path discovery. An
|
||||||
|
invocation with neither mode displays help; the two modes cannot be combined.
|
||||||
|
The default scope is `app`; `--format yaml` or `--format json` selects
|
||||||
structured output, and automatic output uses text. `--scope all` is unsupported.
|
structured output, and automatic output uses text. `--scope all` is unsupported.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
example config explain --path /server/addr
|
example config explain --path /server/addr
|
||||||
example config explain --path /profiles/team~1summary --format yaml
|
example config explain --path /profiles/team~1summary --format yaml
|
||||||
example config explain --path= --format yaml
|
example config explain --path= --format yaml
|
||||||
|
example config explain --list --under /profiles --historical
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Listing normally includes current paths. `--historical` adds paths that only
|
||||||
|
occurred in history; `--under POINTER` selects a subtree, and `--explicit=true`
|
||||||
|
or `--explicit=false` filters current paths by native explicitness. Historical-only
|
||||||
|
paths do not match an explicitness filter. These options require `--list`.
|
||||||
|
The report contains the scope and each selected path's current presence and,
|
||||||
|
when present, its explicitness. Path discovery does not include values or history.
|
||||||
|
|
||||||
Pointer tokens use JSON Pointer escaping: `~1` represents `/` and `~0`
|
Pointer tokens use JSON Pointer escaping: `~1` represents `/` and `~0`
|
||||||
represents `~`. A malformed pointer is a usage error with status `2` and is
|
represents `~`. A malformed pointer is a usage error with status `2` and is
|
||||||
rejected before acquisition. A well-formed path that is unknown in the selected
|
rejected before acquisition. A well-formed path that is unknown in the selected
|
||||||
configuration is an operational error with status `1`. Text output comes from
|
configuration is an operational error with status `1`. Text output includes
|
||||||
the scope's native explanation writer.
|
the scope's native explanation and available field metadata. Structured output
|
||||||
|
includes metadata when supplied by the scope.
|
||||||
|
|
||||||
Structured output uses these root fields:
|
Structured output uses these root fields:
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Shared reporting and PromptKit v0.20.0 implementation plan
|
# Shared reporting and PromptKit v0.20.0 implementation plan
|
||||||
|
|
||||||
Status: implementation in progress; Stages 1–16 complete.
|
Status: implementation in progress; Stages 1–17 complete.
|
||||||
|
|
||||||
## Scope, authority, and execution
|
## Scope, authority, and execution
|
||||||
|
|
||||||
@@ -610,6 +610,12 @@ algorithms remain upstream tests.
|
|||||||
|
|
||||||
## Stage 17: Explanation discovery and metadata
|
## Stage 17: Explanation discovery and metadata
|
||||||
|
|
||||||
|
**Complete.** Explain supports discovery filters and field metadata, with
|
||||||
|
mode and capability checks before acquisition. `Invocation.Help` renders the
|
||||||
|
selected command's existing help when no mode is selected, avoiding application
|
||||||
|
factories. Root, ckconfig, and ckprompt workspace suites passed with Go 1.27.0,
|
||||||
|
including historical paths, escaped subtrees, and help delivery.
|
||||||
|
|
||||||
Extend `config explain` under [OD-4](promptkit-v0.20.0.md#od-4-configuration-path-and-metadata-presentation).
|
Extend `config explain` under [OD-4](promptkit-v0.20.0.md#od-4-configuration-path-and-metadata-presentation).
|
||||||
Track supplied `--path` separately from its value; root `--path ''` is an
|
Track supplied `--path` separately from its value; root `--path ''` is an
|
||||||
operation. Bare invocation shows successful command help before input/state
|
operation. Bare invocation shows successful command help before input/state
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ func (current *execution) invocation() Invocation {
|
|||||||
reporter: current.reporter,
|
reporter: current.reporter,
|
||||||
enteredPath: current.route.enteredPath.String(),
|
enteredPath: current.route.enteredPath.String(),
|
||||||
canonicalPath: current.route.canonicalPath.String(),
|
canonicalPath: current.route.canonicalPath.String(),
|
||||||
|
renderHelp: func() Outcome { return current.renderHelp(nil) },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
10
identity.go
10
identity.go
@@ -52,12 +52,22 @@ type Invocation struct {
|
|||||||
reporter *Reporter
|
reporter *Reporter
|
||||||
enteredPath string
|
enteredPath string
|
||||||
canonicalPath string
|
canonicalPath string
|
||||||
|
renderHelp func() Outcome
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reporter returns the reporting facility supplied for this invocation. A
|
// Reporter returns the reporting facility supplied for this invocation. A
|
||||||
// manually constructed Invocation has no reporter.
|
// manually constructed Invocation has no reporter.
|
||||||
func (inv Invocation) Reporter() *Reporter { return inv.reporter }
|
func (inv Invocation) Reporter() *Reporter { return inv.reporter }
|
||||||
|
|
||||||
|
// Help renders the selected command's help through this invocation's output.
|
||||||
|
// It returns an operational failure when the invocation has no help renderer.
|
||||||
|
func (inv Invocation) Help() Outcome {
|
||||||
|
if inv.renderHelp == nil {
|
||||||
|
return Failure("Command help is unavailable.")
|
||||||
|
}
|
||||||
|
return inv.renderHelp()
|
||||||
|
}
|
||||||
|
|
||||||
// EnteredPath returns the command path used to invoke the selected command,
|
// EnteredPath returns the command path used to invoke the selected command,
|
||||||
// excluding the executable name, flags, and operands. It is empty for an
|
// excluding the executable name, flags, and operands. It is empty for an
|
||||||
// Invocation that was not created by command execution.
|
// Invocation that was not created by command execution.
|
||||||
|
|||||||
116
integrations/ckconfig/discovery_test.go
Normal file
116
integrations/ckconfig/discovery_test.go
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
package ckconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
commandkit "go.maximumdirect.net/commandkit"
|
||||||
|
configkit "go.maximumdirect.net/configkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExplainDiscoveryModesAndMetadata(t *testing.T) {
|
||||||
|
loads, inputs, states := 0, 0, 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{"a/b": map[string]any{"x": 1}, "ab": 2, "abacus": 3, "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{}]{NewInput: func() struct{} { inputs++; return struct{}{} }, NewState: func() struct{} { states++; return struct{}{} }, Scopes: []Scope[struct{}, struct{}]{scope}}))
|
||||||
|
run := func(args ...string) (commandkit.Outcome, string) {
|
||||||
|
var out bytes.Buffer
|
||||||
|
result := app.Execute(context.Background(), append([]string{"config", "explain"}, args...), commandkit.Streams{Out: &out, Err: io.Discard})
|
||||||
|
return result, out.String()
|
||||||
|
}
|
||||||
|
result, out := run()
|
||||||
|
if result.Kind != commandkit.OutcomeSuccess || !strings.Contains(out, "--list") || !strings.Contains(out, "--path") || loads != 0 || inputs != 0 || states != 0 {
|
||||||
|
t.Fatalf("bare help: %#v %q, %d/%d/%d", result, out, loads, inputs, states)
|
||||||
|
}
|
||||||
|
for _, args := range [][]string{{"--list", "--path=/ab"}, {"--historical"}, {"--under=/ab"}, {"--explicit=false"}, {"--path=/ab", "--under=/ab"}, {"--list", "--under=bad"}} {
|
||||||
|
result, _ := run(args...)
|
||||||
|
if result.Kind != commandkit.OutcomeUsage || loads != 0 || inputs != 0 || states != 0 {
|
||||||
|
t.Fatalf("invalid %v: %#v, %d/%d/%d", args, result, loads, inputs, states)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result, out = run("--list", "--under=/a~1b", "--format=json")
|
||||||
|
var report pathDiscoveryReport
|
||||||
|
if result.Kind != commandkit.OutcomeSuccess || json.Unmarshal([]byte(out), &report) != nil || len(report.Paths) != 2 {
|
||||||
|
t.Fatalf("escaped subtree: %#v %q", result, out)
|
||||||
|
}
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for _, entry := range report.Paths {
|
||||||
|
seen[entry.Path] = true
|
||||||
|
}
|
||||||
|
if !seen["/a~1b"] || !seen["/a~1b/x"] {
|
||||||
|
t.Fatalf("escaped paths: %#v", report.Paths)
|
||||||
|
}
|
||||||
|
result, out = run("--list", "--under=/ab", "--format=json")
|
||||||
|
if result.Kind != commandkit.OutcomeSuccess || json.Unmarshal([]byte(out), &report) != nil || len(report.Paths) != 1 || report.Paths[0].Path != "/ab" {
|
||||||
|
t.Fatalf("token boundary: %#v %q", result, out)
|
||||||
|
}
|
||||||
|
result, out = run("--list", "--historical", "--set=/gone=null", "--format=json")
|
||||||
|
if result.Kind != commandkit.OutcomeSuccess || json.Unmarshal([]byte(out), &report) != nil {
|
||||||
|
t.Fatalf("historical: %#v %q", result, out)
|
||||||
|
}
|
||||||
|
removed := false
|
||||||
|
for _, entry := range report.Paths {
|
||||||
|
if entry.Path == "/gone/x" && !entry.Present && entry.Metadata == nil {
|
||||||
|
removed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !removed {
|
||||||
|
t.Fatalf("missing historical entry: %q", out)
|
||||||
|
}
|
||||||
|
result, out = run("--list", "--historical", "--explicit=false", "--set=/gone=null", "--format=json")
|
||||||
|
if result.Kind != commandkit.OutcomeSuccess || json.Unmarshal([]byte(out), &report) != nil {
|
||||||
|
t.Fatalf("explicit filter: %#v %q", result, out)
|
||||||
|
}
|
||||||
|
for _, entry := range report.Paths {
|
||||||
|
if !entry.Present || entry.Metadata == nil || entry.Metadata.Explicit {
|
||||||
|
t.Fatalf("invalid filtered entry: %#v", entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result, out = run("--path=", "--format=json")
|
||||||
|
if result.Kind != commandkit.OutcomeSuccess || !strings.Contains(out, `"path": ""`) || !strings.Contains(out, `"metadata"`) {
|
||||||
|
t.Fatalf("root explanation: %#v %q", result, out)
|
||||||
|
}
|
||||||
|
result, out = run("--path=/gone/x", "--set=/gone=null", "--format=json")
|
||||||
|
if result.Kind != commandkit.OutcomeSuccess || !strings.Contains(out, `"metadata"`) || !strings.Contains(out, `"history"`) {
|
||||||
|
t.Fatalf("historical explanation: %#v %q", result, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExplainDiscoveryCapabilityBeforeAcquisition(t *testing.T) {
|
||||||
|
calls := 0
|
||||||
|
scope := Scope[struct{}, struct{}]{Name: "app", Operations: []Operation{Explain}, 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", "explain", "--list"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||||
|
if result.Kind != commandkit.OutcomeUsage || calls != 0 {
|
||||||
|
t.Fatalf("unsupported discovery: %#v calls=%d", result, calls)
|
||||||
|
}
|
||||||
|
scope.Features.PathDiscovery = true
|
||||||
|
app = newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{scope}}))
|
||||||
|
result = app.Execute(context.Background(), []string{"config", "explain", "--list"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||||
|
if result.Kind != commandkit.OutcomeOperational || calls != 1 {
|
||||||
|
t.Fatalf("missing metadata: %#v calls=%d", result, calls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExplainBareHelpRecordsDeliveryFailure(t *testing.T) {
|
||||||
|
inputs, states, calls := 0, 0, 0
|
||||||
|
scope := Scope[struct{}, struct{}]{Name: "app", Operations: []Operation{Explain}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) {
|
||||||
|
calls++
|
||||||
|
return commandkit.Acquired[View]{}, nil
|
||||||
|
}}
|
||||||
|
app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{NewInput: func() struct{} { inputs++; return struct{}{} }, NewState: func() struct{} { states++; return struct{}{} }, Scopes: []Scope[struct{}, struct{}]{scope}}))
|
||||||
|
result := app.Execute(context.Background(), []string{"config", "explain"}, commandkit.Streams{Out: shortWriter{}, Err: io.Discard})
|
||||||
|
if result.Presentation.Output.State != commandkit.DeliveryIncomplete || inputs != 0 || states != 0 || calls != 0 {
|
||||||
|
t.Fatalf("help delivery = %#v, input/state/acquire=%d/%d/%d", result, inputs, states, calls)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package ckconfig
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
commandkit "go.maximumdirect.net/commandkit"
|
commandkit "go.maximumdirect.net/commandkit"
|
||||||
@@ -16,8 +17,37 @@ type explanationReport struct {
|
|||||||
Origin explanationOriginDTO `json:"origin" yaml:"origin"`
|
Origin explanationOriginDTO `json:"origin" yaml:"origin"`
|
||||||
History []explanationChangeDTO `json:"history" yaml:"history"`
|
History []explanationChangeDTO `json:"history" yaml:"history"`
|
||||||
SecretReference *secretReferenceDTO `json:"secret_reference" yaml:"secret_reference"`
|
SecretReference *secretReferenceDTO `json:"secret_reference" yaml:"secret_reference"`
|
||||||
|
Metadata *explanationMetadata `json:"metadata,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type explanationMetadata struct {
|
||||||
|
Explicit *bool `json:"explicit,omitempty"`
|
||||||
|
Null *bool `json:"null,omitempty"`
|
||||||
|
History []metadataHistoryRecord `json:"history,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type metadataHistoryRecord struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
Previous []explanationOriginDTO `json:"previous,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type pathDiscoveryReport struct {
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
Paths []pathDiscoveryEntry `json:"paths"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type pathDiscoveryEntry struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
Present bool `json:"present"`
|
||||||
|
Metadata *pathMetadata `json:"metadata,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type pathMetadata struct {
|
||||||
|
Explicit bool `json:"explicit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var pathDiscoveryTemplate = template.Must(commandkit.ParseTextTemplate("paths", "{{range .Paths}}{{printf \"%q\" .Path}} {{if .Present}}{{if .Metadata.Explicit}}explicit{{else}}non-explicit{{end}}{{else}}historical{{end}}\n{{end}}", nil))
|
||||||
|
|
||||||
type snapshotDTO struct {
|
type snapshotDTO struct {
|
||||||
Present bool `json:"present" yaml:"present"`
|
Present bool `json:"present" yaml:"present"`
|
||||||
Redacted bool `json:"redacted" yaml:"redacted"`
|
Redacted bool `json:"redacted" yaml:"redacted"`
|
||||||
@@ -72,6 +102,31 @@ func runConfigExplanation[I, S any](invocation commandkit.Invocation, plan *conf
|
|||||||
if err := invocation.Context.Err(); err != nil {
|
if err := invocation.Context.Err(); err != nil {
|
||||||
return configFailure("Configuration explanation was canceled.", acquired.Explanation, err)
|
return configFailure("Configuration explanation was canceled.", acquired.Explanation, err)
|
||||||
}
|
}
|
||||||
|
if plan.list {
|
||||||
|
if acquired.Value.Metadata == nil {
|
||||||
|
return configFailure("Configuration scope did not provide path metadata.", acquired.Explanation, fmt.Errorf("configuration scope %q declared path discovery without metadata", name))
|
||||||
|
}
|
||||||
|
report := pathDiscoveryReport{Scope: name, Paths: []pathDiscoveryEntry{}}
|
||||||
|
for _, path := range acquired.Value.Metadata.HistoricalPaths() {
|
||||||
|
if plan.under != nil && !pathWithin(path, *plan.under) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
info, present := acquired.Value.Metadata.Lookup(path)
|
||||||
|
if !present && (!plan.historical || plan.explicit.Set) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if present && plan.explicit.Set && info.Explicit != plan.explicit.Value {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
entry := pathDiscoveryEntry{Path: path.String(), Present: present}
|
||||||
|
if present {
|
||||||
|
entry.Metadata = &pathMetadata{Explicit: info.Explicit}
|
||||||
|
}
|
||||||
|
report.Paths = append(report.Paths, entry)
|
||||||
|
}
|
||||||
|
_ = reporter.Report(commandkit.Report{Value: report, TextTemplate: pathDiscoveryTemplate})
|
||||||
|
return commandkit.Success()
|
||||||
|
}
|
||||||
if acquired.Value.Explanation == nil {
|
if acquired.Value.Explanation == nil {
|
||||||
cause := fmt.Errorf("configuration scope %q did not provide an explanation reader", name)
|
cause := fmt.Errorf("configuration scope %q did not provide an explanation reader", name)
|
||||||
return configFailure("Configuration scope did not provide explanation data.", acquired.Explanation, cause)
|
return configFailure("Configuration scope did not provide explanation data.", acquired.Explanation, cause)
|
||||||
@@ -85,20 +140,59 @@ func runConfigExplanation[I, S any](invocation commandkit.Invocation, plan *conf
|
|||||||
if err := invocation.Context.Err(); err != nil {
|
if err := invocation.Context.Err(); err != nil {
|
||||||
return configFailure("Configuration explanation was canceled.", acquired.Explanation, err)
|
return configFailure("Configuration explanation was canceled.", acquired.Explanation, err)
|
||||||
}
|
}
|
||||||
|
report := makeExplanationReport(name, explanation)
|
||||||
|
if acquired.Value.Metadata != nil {
|
||||||
|
metadata := &explanationMetadata{}
|
||||||
|
if info, ok := acquired.Value.Metadata.Lookup(plan.path); ok {
|
||||||
|
metadata.Explicit, metadata.Null = &info.Explicit, &info.Null
|
||||||
|
}
|
||||||
|
for _, change := range acquired.Value.Metadata.History(plan.path) {
|
||||||
|
entry := metadataHistoryRecord{Path: change.Path.String()}
|
||||||
|
for _, previous := range change.Previous {
|
||||||
|
entry.Previous = append(entry.Previous, originReport(previous))
|
||||||
|
}
|
||||||
|
metadata.History = append(metadata.History, entry)
|
||||||
|
}
|
||||||
|
if metadata.Explicit != nil || len(metadata.History) > 0 {
|
||||||
|
report.Metadata = metadata
|
||||||
|
}
|
||||||
|
}
|
||||||
text := template.Must(commandkit.ParseTextTemplate("explanation", "{{render .}}", template.FuncMap{
|
text := template.Must(commandkit.ParseTextTemplate("explanation", "{{render .}}", template.FuncMap{
|
||||||
"render": func(explanationReport) (string, error) {
|
"render": func(report explanationReport) (string, error) {
|
||||||
var output bytes.Buffer
|
var output bytes.Buffer
|
||||||
err := acquired.Value.Explanation.WriteExplanation(&output, plan.path)
|
err := acquired.Value.Explanation.WriteExplanation(&output, plan.path)
|
||||||
|
if err == nil && report.Metadata != nil && report.Metadata.Explicit != nil {
|
||||||
|
_, err = fmt.Fprintf(&output, "Explicit: %t\nNull: %t\n", *report.Metadata.Explicit, *report.Metadata.Null)
|
||||||
|
}
|
||||||
return output.String(), err
|
return output.String(), err
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
renderErr := reporter.Report(commandkit.Report{Value: makeExplanationReport(name, explanation), TextTemplate: text})
|
renderErr := reporter.Report(commandkit.Report{Value: report, TextTemplate: text})
|
||||||
if renderErr != nil && invocation.Context.Err() != nil {
|
if renderErr != nil && invocation.Context.Err() != nil {
|
||||||
return configFailure("Configuration explanation was canceled.", acquired.Explanation, invocation.Context.Err())
|
return configFailure("Configuration explanation was canceled.", acquired.Explanation, invocation.Context.Err())
|
||||||
}
|
}
|
||||||
return commandkit.Success()
|
return commandkit.Success()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func pathWithin(path, parent configkit.Path) bool {
|
||||||
|
tokens := func(value string) []string {
|
||||||
|
if value == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return strings.Split(strings.TrimPrefix(value, "/"), "/")
|
||||||
|
}
|
||||||
|
child, prefix := tokens(path.String()), tokens(parent.String())
|
||||||
|
if len(prefix) > len(child) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for index := range prefix {
|
||||||
|
if prefix[index] != child[index] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func makeExplanationReport(scope string, explanation configkit.Explanation) explanationReport {
|
func makeExplanationReport(scope string, explanation configkit.Explanation) explanationReport {
|
||||||
report := explanationReport{
|
report := explanationReport{
|
||||||
Scope: scope,
|
Scope: scope,
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ type configCommandInput[I, S any] struct {
|
|||||||
against Selection
|
against Selection
|
||||||
scope commandkit.Supplied[string]
|
scope commandkit.Supplied[string]
|
||||||
path commandkit.Supplied[string]
|
path commandkit.Supplied[string]
|
||||||
|
list commandkit.Supplied[bool]
|
||||||
|
historical commandkit.Supplied[bool]
|
||||||
|
under commandkit.Supplied[string]
|
||||||
|
explicit commandkit.Supplied[bool]
|
||||||
values commandkit.FlagValues[I]
|
values commandkit.FlagValues[I]
|
||||||
againstValues commandkit.FlagValues[I]
|
againstValues commandkit.FlagValues[I]
|
||||||
plan *configOperationPlan[I, S]
|
plan *configOperationPlan[I, S]
|
||||||
@@ -34,6 +38,11 @@ type configOperationPlan[I, S any] struct {
|
|||||||
operation Operation
|
operation Operation
|
||||||
scopeName string
|
scopeName string
|
||||||
path configkit.Path
|
path configkit.Path
|
||||||
|
list bool
|
||||||
|
help bool
|
||||||
|
historical bool
|
||||||
|
under *configkit.Path
|
||||||
|
explicit commandkit.Supplied[bool]
|
||||||
scopes []selectedScopeInput[I, S]
|
scopes []selectedScopeInput[I, S]
|
||||||
comparison *preparedComparison[I, S]
|
comparison *preparedComparison[I, S]
|
||||||
}
|
}
|
||||||
@@ -89,8 +98,13 @@ func newConfigOperationCommand[I, S any](prepared *preparedSuite[I, S], operatio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if operation == Explain {
|
if operation == Explain {
|
||||||
flags = append(flags, commandkit.ValueFlag(commandkit.FlagMeta{Name: "path", Usage: "JSON Pointer path to explain.", Required: true}, (*string)(nil), parseConfigString,
|
flags = append(flags,
|
||||||
func(input **configCommandInput[I, S], value commandkit.Supplied[string]) { (*input).path = value }))
|
commandkit.ValueFlag(commandkit.FlagMeta{Name: "path", Usage: "JSON Pointer path to explain."}, (*string)(nil), parseConfigString, func(input **configCommandInput[I, S], value commandkit.Supplied[string]) { (*input).path = value }),
|
||||||
|
commandkit.BoolFlag(commandkit.FlagMeta{Name: "list", Usage: "List configuration paths."}, false, func(input **configCommandInput[I, S], value commandkit.Supplied[bool]) { (*input).list = value }),
|
||||||
|
commandkit.BoolFlag(commandkit.FlagMeta{Name: "historical", Usage: "Include historical-only paths when listing."}, false, func(input **configCommandInput[I, S], value commandkit.Supplied[bool]) { (*input).historical = value }),
|
||||||
|
commandkit.ValueFlag(commandkit.FlagMeta{Name: "under", Usage: "List paths under a JSON Pointer."}, (*string)(nil), parseConfigString, func(input **configCommandInput[I, S], value commandkit.Supplied[string]) { (*input).under = value }),
|
||||||
|
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 }),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
flags = append(flags, commandkit.CaptureFlags(prepared.bindingsByOperation[operation], func(input **configCommandInput[I, S]) *commandkit.FlagValues[I] {
|
flags = append(flags, commandkit.CaptureFlags(prepared.bindingsByOperation[operation], func(input **configCommandInput[I, S]) *commandkit.FlagValues[I] {
|
||||||
if input == nil || *input == nil {
|
if input == nil || *input == nil {
|
||||||
@@ -110,8 +124,9 @@ func newConfigOperationCommand[I, S any](prepared *preparedSuite[I, S], operatio
|
|||||||
name, summary := operationDeclaration(operation)
|
name, summary := operationDeclaration(operation)
|
||||||
return commandkit.NewCommand(commandkit.CommandSpec[*configCommandInput[I, S]]{
|
return commandkit.NewCommand(commandkit.CommandSpec[*configCommandInput[I, S]]{
|
||||||
Name: name, Summary: summary,
|
Name: name, Summary: summary,
|
||||||
NewInput: func() *configCommandInput[I, S] { return &configCommandInput[I, S]{} },
|
Description: operationDescription(operation),
|
||||||
Flags: flags,
|
NewInput: func() *configCommandInput[I, S] { return &configCommandInput[I, S]{} },
|
||||||
|
Flags: flags,
|
||||||
CheckInput: func(input *configCommandInput[I, S]) error {
|
CheckInput: func(input *configCommandInput[I, S]) error {
|
||||||
if input == nil {
|
if input == nil {
|
||||||
return fmt.Errorf("configuration input must not be nil")
|
return fmt.Errorf("configuration input must not be nil")
|
||||||
@@ -124,6 +139,13 @@ func newConfigOperationCommand[I, S any](prepared *preparedSuite[I, S], operatio
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func operationDescription(operation Operation) string {
|
||||||
|
if operation == Explain {
|
||||||
|
return "Use --list to discover paths or --path POINTER to explain one path."
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func operationDeclaration(operation Operation) (string, string) {
|
func operationDeclaration(operation Operation) (string, string) {
|
||||||
switch operation {
|
switch operation {
|
||||||
case Validate:
|
case Validate:
|
||||||
@@ -142,6 +164,18 @@ func operationDeclaration(operation Operation) (string, string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func prepareConfigOperation[I, S any](prepared *preparedSuite[I, S], operation Operation, input *configCommandInput[I, S]) error {
|
func prepareConfigOperation[I, S any](prepared *preparedSuite[I, S], operation Operation, input *configCommandInput[I, S]) error {
|
||||||
|
if operation == Explain {
|
||||||
|
if input.list.Value && input.path.Set {
|
||||||
|
return fmt.Errorf("--list and --path cannot be combined")
|
||||||
|
}
|
||||||
|
if !input.list.Value && (input.historical.Set || input.under.Set || input.explicit.Set) {
|
||||||
|
return fmt.Errorf("--historical, --under, and --explicit require --list")
|
||||||
|
}
|
||||||
|
if !input.list.Value && !input.path.Set {
|
||||||
|
input.plan = &configOperationPlan[I, S]{prepared: prepared, operation: operation, help: true}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
scopeName := "app"
|
scopeName := "app"
|
||||||
if input.scope.Set {
|
if input.scope.Set {
|
||||||
@@ -170,26 +204,38 @@ func prepareConfigOperation[I, S any](prepared *preparedSuite[I, S], operation O
|
|||||||
if (len(input.selection.Overrides) > 0 || len(input.against.Overrides) > 0) && !scope.scope.Features.Overrides {
|
if (len(input.selection.Overrides) > 0 || len(input.against.Overrides) > 0) && !scope.scope.Features.Overrides {
|
||||||
return fmt.Errorf("configuration scope %q does not support overrides", scope.scope.Name)
|
return fmt.Errorf("configuration scope %q does not support overrides", scope.scope.Name)
|
||||||
}
|
}
|
||||||
|
if operation == Explain && input.list.Value && !scope.scope.Features.PathDiscovery {
|
||||||
|
return fmt.Errorf("configuration scope %q does not support path discovery", scope.scope.Name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var path configkit.Path
|
var path configkit.Path
|
||||||
if operation == Explain {
|
if operation == Explain && input.path.Set {
|
||||||
if !input.path.Set {
|
|
||||||
return fmt.Errorf("configuration explanation requires --path")
|
|
||||||
}
|
|
||||||
parsed, err := configkit.ParsePath(input.path.Value)
|
parsed, err := configkit.ParsePath(input.path.Value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid configuration path: %w", err)
|
return fmt.Errorf("invalid configuration path: %w", err)
|
||||||
}
|
}
|
||||||
path = parsed
|
path = parsed
|
||||||
}
|
}
|
||||||
|
var under *configkit.Path
|
||||||
|
if input.under.Set {
|
||||||
|
parsed, err := configkit.ParsePath(input.under.Value)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid subtree path: %w", err)
|
||||||
|
}
|
||||||
|
under = &parsed
|
||||||
|
}
|
||||||
|
|
||||||
plan := &configOperationPlan[I, S]{
|
plan := &configOperationPlan[I, S]{
|
||||||
prepared: prepared,
|
prepared: prepared,
|
||||||
operation: operation,
|
operation: operation,
|
||||||
scopeName: scopeName,
|
scopeName: scopeName,
|
||||||
path: path,
|
path: path,
|
||||||
scopes: make([]selectedScopeInput[I, S], 0, len(selected)),
|
list: input.list.Value,
|
||||||
|
historical: input.historical.Value,
|
||||||
|
under: under,
|
||||||
|
explicit: input.explicit,
|
||||||
|
scopes: make([]selectedScopeInput[I, S], 0, len(selected)),
|
||||||
}
|
}
|
||||||
if operation == Diff {
|
if operation == Diff {
|
||||||
scope := selected[0]
|
scope := selected[0]
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ func runConfigOperation[I, S any](invocation commandkit.Invocation, operation Op
|
|||||||
if input == nil || input.plan == nil {
|
if input == nil || input.plan == nil {
|
||||||
return commandkit.Failure("Configuration operation was not prepared.")
|
return commandkit.Failure("Configuration operation was not prepared.")
|
||||||
}
|
}
|
||||||
|
if input.plan.help {
|
||||||
|
return invocation.Help()
|
||||||
|
}
|
||||||
if err := invocation.Context.Err(); err != nil {
|
if err := invocation.Context.Err(); err != nil {
|
||||||
return configFailure("Configuration operation was canceled.", commandkit.Explanation{}, err)
|
return configFailure("Configuration operation was canceled.", commandkit.Explanation{}, err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user