Move integration code into optional modules

This commit is contained in:
2026-10-11 01:06:21 +00:00
parent 0d23676144
commit 476e98a463
28 changed files with 348 additions and 285 deletions

View File

@@ -0,0 +1,78 @@
package ckconfig_test
import (
"bytes"
"context"
"errors"
"fmt"
commandkit "gitea.maximumdirect.net/eric/CommandKit"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
"strings"
"testing"
)
func TestStandardConfigurationCancellationBoundaries(t *testing.T) {
for _, operation := range []string{"validate", "show", "sources", "explain"} {
for _, before := range []bool{true, false} {
t.Run(fmt.Sprintf("%s/before=%t", operation, before), func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
if before {
cancel()
}
loads, contexts := 0, 0
scope, err := commandkit.NewConfigKitScope[struct{}, struct{}]("app", "", func(context.Context, commandkit.Selection, struct{}) (*configkit.Result[struct{}], error) {
loads++
cancel()
return configkit.Load(configkit.NoSource(), configkit.Options[struct{}]{})
})
if err != nil {
t.Fatal(err)
}
app, _ := commandkit.New(commandkit.Identity{Name: "example"})
if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{scope}, NewContext: func() struct{} { contexts++; return struct{}{} }}); err != nil {
t.Fatal(err)
}
args := []string{"config", operation}
if operation == "explain" {
args = append(args, "--path", "")
}
var out bytes.Buffer
result := app.Execute(ctx, args, commandkit.Streams{Out: &out})
want := 1
if before {
want = 0
}
if result.Kind != commandkit.OutcomeOperational || result.Status != 1 || !errors.Is(result.Cause, context.Canceled) || loads != want || contexts != want || out.Len() != 0 {
t.Fatalf("result=%#v loads=%d contexts=%d out=%q", result, loads, contexts, out.String())
}
})
}
}
}
func TestAggregateCancellationRetainsCompletedValidationAndDiagnostics(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
app, _ := commandkit.New(commandkit.Identity{Name: "example"})
calls := 0
acquire := func(context.Context, commandkit.ConfigRequest, struct{}) (commandkit.Acquired[commandkit.ConfigView], error) {
calls++
if calls == 2 {
cancel()
}
return commandkit.Acquired[commandkit.ConfigView]{Diagnostics: []commandkit.Diagnostic{commandkit.MessageDiagnostic(configkit.SeverityWarning, "acquired warning")}}, nil
}
if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{
{Name: "app", Operations: []commandkit.ConfigOperation{commandkit.ConfigValidate}, Acquire: acquire},
{Name: "promptkit", Operations: []commandkit.ConfigOperation{commandkit.ConfigValidate}, Acquire: acquire},
{Name: "zeta", Operations: []commandkit.ConfigOperation{commandkit.ConfigValidate}, Acquire: acquire},
}}); err != nil {
t.Fatal(err)
}
var out, diagnostics bytes.Buffer
result := app.Execute(ctx, []string{"config", "validate", "--scope", "all"}, commandkit.Streams{Out: &out, Err: &diagnostics})
if !errors.Is(result.Cause, context.Canceled) || !result.ReportIncomplete || calls != 2 || out.String() != "app: valid\n" || strings.Count(diagnostics.String(), "acquired warning") != 2 {
t.Fatalf("result=%#v calls=%d out=%q diagnostics=%q", result, calls, out.String(), diagnostics.String())
}
}

View File

@@ -0,0 +1,461 @@
package ckconfig
import (
"context"
"errors"
"fmt"
"io"
"sort"
"strconv"
"strings"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
"go.yaml.in/yaml/v4"
)
// Selection identifies the application configuration inputs supplied to a
// configuration scope.
type Selection struct {
Config Supplied[string]
Overlay Supplied[string]
}
// ConfigOperation identifies a supported configuration inspection operation.
type ConfigOperation string
const (
ConfigValidate ConfigOperation = "validate"
ConfigShow ConfigOperation = "show"
ConfigSources ConfigOperation = "sources"
ConfigExplain ConfigOperation = "explain"
)
// ConfigRequest describes one configuration operation and its selected inputs.
type ConfigRequest struct {
Operation ConfigOperation
Selection Selection
}
// EffectiveWriter writes an owning library's native effective configuration.
type EffectiveWriter interface {
WriteEffective(io.Writer) error
}
// SourceReader reports the sources used to construct a configuration.
type SourceReader interface {
Sources() []configkit.SourceInfo
}
// ExplanationReader writes an owning library's path explanation.
type ExplanationReader interface {
Explain(configkit.Path) (configkit.Explanation, bool)
WriteExplanation(io.Writer, configkit.Path) error
}
// ConfigView contains optional views relevant to supported operations.
type ConfigView struct {
Effective EffectiveWriter
Sources SourceReader
Explanation ExplanationReader
}
// Acquired contains a configuration view and diagnostics from its acquisition.
type Acquired[T any] struct {
Value T
Diagnostics []Diagnostic
}
// ConfigScope declares one named configuration and its supported operations.
type ConfigScope[C any] struct {
Name string
Summary string
Operations []ConfigOperation
Acquire func(context.Context, ConfigRequest, C) (Acquired[ConfigView], error)
}
// Selector binds one typed command flag to selected configuration scopes.
// A required binding is required only when a selected scope uses it.
type Selector[C any] struct {
Binding Flag[C]
Scopes []string
}
// ConfigSuite declares selectors and configuration scopes for an App.
type ConfigSuite[C any] struct {
NewContext func() C
Selectors []Selector[C]
CheckInput func(Selection, C) error
Scopes []ConfigScope[C]
}
type configScopeInfo struct {
Name string `yaml:"name"`
Operations []ConfigOperation `yaml:"operations"`
}
type scopesInventory struct {
Scopes []configScopeInfo `yaml:"scopes"`
}
type preparedConfigSuite[C any] struct {
suite ConfigSuite[C]
ordered []ConfigScope[C]
byName map[string]ConfigScope[C]
installed []ConfigOperation
inventory scopesInventory
}
type configScopesInput struct {
Format string
}
type selectorState[C any] struct {
name string
required bool
scopes []string
supplied bool
apply func(*C)
clear func(*C)
}
type configInput[C any] struct {
Selection Selection
Scope string
Format string
Path Supplied[string]
SelectorContext C
selectors []selectorState[C]
}
// RegisterConfig installs the available config handlers and the scope discovery
// command. Its registration and discovery paths do not invoke scope loaders.
func RegisterConfig[C any](app *App, suite ConfigSuite[C]) error {
if app == nil {
return fmt.Errorf("cannot register configuration on a nil app")
}
if app.configRegistered {
return fmt.Errorf("configuration suite is already registered")
}
normalized, err := normalizeConfigSuite(suite)
if err != nil {
return err
}
if _, exists := app.commands["config"]; exists {
return fmt.Errorf("command path %q is already registered", "config")
}
prepared := prepareConfigSuite(normalized)
commands := make([]Command, 0, 5)
for _, operation := range prepared.installed {
var command Command
switch operation {
case ConfigValidate:
command, err = newConfigValidateCommand(prepared)
case ConfigShow:
command, err = newConfigShowCommand(prepared)
case ConfigSources:
command, err = newConfigSourcesCommand(prepared)
case ConfigExplain:
command, err = newConfigExplainCommand(prepared)
}
if err != nil {
return err
}
commands = append(commands, command)
}
command, err := newConfigScopesCommandWithPrepared(prepared)
if err != nil {
return err
}
commands = append(commands, command)
root, err := Group("config", "Inspect configuration.", commands...)
if err != nil {
return err
}
if err := app.Register(root); err != nil {
return err
}
app.configRegistered = true
return nil
}
// NewConfigKitScope adapts a ConfigKit loader into a scope with validation,
// effective display, source listing, and path explanation capabilities. The
// loader runs once per selected operation. Successful loads expose the result's
// public facets directly; ConfigKit LoadError records become CommandKit
// diagnostics while the original error is returned.
func NewConfigKitScope[C, T any](name, summary string, load func(context.Context, Selection, C) (*configkit.Result[T], error)) (ConfigScope[C], error) {
if load == nil {
return ConfigScope[C]{}, fmt.Errorf("ConfigKit scope loader must not be nil")
}
return ConfigScope[C]{
Name: name,
Summary: summary,
Operations: []ConfigOperation{ConfigValidate, ConfigShow, ConfigSources, ConfigExplain},
Acquire: func(ctx context.Context, request ConfigRequest, contextValue C) (Acquired[ConfigView], error) {
result, err := load(ctx, request.Selection, contextValue)
if err != nil {
var loadError *configkit.LoadError
if errors.As(err, &loadError) {
return Acquired[ConfigView]{Diagnostics: ConfigKitDiagnostics(name, loadError.Sources(), loadError.Diagnostics())}, err
}
return Acquired[ConfigView]{}, err
}
if result == nil {
return Acquired[ConfigView]{}, fmt.Errorf("ConfigKit scope %q returned a nil result", name)
}
return Acquired[ConfigView]{
Value: ConfigView{Effective: result, Sources: result, Explanation: result},
Diagnostics: ConfigKitDiagnostics(name, result.Sources(), result.Diagnostics()),
}, nil
},
}, nil
}
// ConfigKitDiagnostics groups ConfigKit records with their scope and source legend.
func ConfigKitDiagnostics(scope string, sources []configkit.SourceInfo, records []configkit.Diagnostic) []Diagnostic {
if len(records) == 0 {
return nil
}
severity := configkit.SeverityWarning
for _, record := range records {
if record.Severity == configkit.SeverityError {
severity = configkit.SeverityError
break
}
}
return []Diagnostic{{
Severity: severity,
Write: func(writer io.Writer) error {
if err := writeDiagnosticText(writer, "Configuration scope "+strconv.Quote(scope)+":\n"); err != nil {
return err
}
if len(sources) > 0 {
if err := writeDiagnosticText(writer, "Source IDs:\n"); err != nil {
return err
}
for _, source := range sources {
line := " " + strconv.Quote(source.ID) + ": label=" + strconv.Quote(source.Label) + ", path=" + strconv.Quote(source.Path) + "\n"
if err := writeDiagnosticText(writer, line); err != nil {
return err
}
}
}
return configkit.WriteDiagnostics(writer, records)
},
}}
}
func writeDiagnosticText(writer io.Writer, value string) error {
written, err := io.WriteString(writer, value)
if err != nil {
return err
}
if written != len(value) {
return io.ErrShortWrite
}
return nil
}
func normalizeConfigSuite[C any](suite ConfigSuite[C]) (ConfigSuite[C], error) {
normalized := suite
normalized.Selectors = append([]Selector[C](nil), suite.Selectors...)
normalized.Scopes = make([]ConfigScope[C], 0, len(suite.Scopes))
scopeNames := make(map[string]struct{}, len(suite.Scopes))
for _, scope := range suite.Scopes {
if scope.Name == "" {
return ConfigSuite[C]{}, fmt.Errorf("configuration scope name must not be empty")
}
if scope.Name == "all" {
return ConfigSuite[C]{}, fmt.Errorf("configuration scope name %q is reserved", scope.Name)
}
if _, exists := scopeNames[scope.Name]; exists {
return ConfigSuite[C]{}, fmt.Errorf("duplicate configuration scope %q", scope.Name)
}
scopeNames[scope.Name] = struct{}{}
operations, err := normalizeConfigOperations(scope.Operations)
if err != nil {
return ConfigSuite[C]{}, fmt.Errorf("configuration scope %q: %w", scope.Name, err)
}
if len(operations) > 0 && scope.Acquire == nil {
return ConfigSuite[C]{}, fmt.Errorf("configuration scope %q declares operations without an acquisition callback", scope.Name)
}
scope.Operations = operations
normalized.Scopes = append(normalized.Scopes, scope)
}
seenNames := make(map[string]struct{}, len(normalized.Selectors))
seenShorts := make(map[string]struct{}, len(normalized.Selectors))
for index := range normalized.Selectors {
selector := &normalized.Selectors[index]
binding := selector.Binding.binding
if !binding.valid || binding.clear == nil || binding.meta.Name == "" {
return ConfigSuite[C]{}, fmt.Errorf("configuration selector %d has an invalid flag binding", index)
}
if !validFlagName(binding.meta.Name) {
return ConfigSuite[C]{}, fmt.Errorf("configuration selector flag name %q is invalid", binding.meta.Name)
}
if binding.meta.Name == "config" || binding.meta.Name == "overlay" || binding.meta.Name == "scope" || binding.meta.Name == "format" || binding.meta.Name == "help" {
return ConfigSuite[C]{}, fmt.Errorf("configuration selector flag --%s conflicts with a standard flag", binding.meta.Name)
}
if binding.meta.Short == "h" {
return ConfigSuite[C]{}, fmt.Errorf("configuration selector short flag -%s conflicts with help", binding.meta.Short)
}
if binding.meta.Short != "" && (len(binding.meta.Short) != 1 || binding.meta.Short == "-") {
return ConfigSuite[C]{}, fmt.Errorf("configuration selector short spelling %q must be one byte", binding.meta.Short)
}
seen := make(map[string]struct{}, len(selector.Scopes))
unique := make([]string, 0, len(selector.Scopes))
for _, scopeName := range selector.Scopes {
if _, exists := scopeNames[scopeName]; !exists {
return ConfigSuite[C]{}, fmt.Errorf("selector --%s references unknown configuration scope %q", binding.meta.Name, scopeName)
}
if _, exists := seen[scopeName]; exists {
continue
}
seen[scopeName] = struct{}{}
unique = append(unique, scopeName)
}
selector.Scopes = append([]string(nil), unique...)
if _, exists := seenNames[binding.meta.Name]; exists {
return ConfigSuite[C]{}, fmt.Errorf("duplicate configuration selector flag --%s", binding.meta.Name)
}
if binding.meta.Short != "" {
if _, exists := seenShorts[binding.meta.Short]; exists {
return ConfigSuite[C]{}, fmt.Errorf("duplicate configuration selector flag --%s", binding.meta.Name)
}
seenShorts[binding.meta.Short] = struct{}{}
}
seenNames[binding.meta.Name] = struct{}{}
}
return normalized, nil
}
func prepareConfigSuite[C any](suite ConfigSuite[C]) *preparedConfigSuite[C] {
prepared := &preparedConfigSuite[C]{
suite: suite,
ordered: orderedConfigScopes(suite.Scopes),
byName: make(map[string]ConfigScope[C], len(suite.Scopes)),
installed: make([]ConfigOperation, 0, 4),
inventory: scopesInventory{Scopes: make([]configScopeInfo, 0, len(suite.Scopes))},
}
for _, scope := range suite.Scopes {
prepared.byName[scope.Name] = scope
}
for _, operation := range []ConfigOperation{ConfigValidate, ConfigShow, ConfigSources, ConfigExplain} {
if configOperationDeclared(suite.Scopes, operation) {
prepared.installed = append(prepared.installed, operation)
}
}
for _, scope := range prepared.ordered {
operations := make([]ConfigOperation, 0, len(scope.Operations))
for _, operation := range scope.Operations {
if containsOperation(prepared.installed, operation) {
operations = append(operations, operation)
}
}
prepared.inventory.Scopes = append(prepared.inventory.Scopes, configScopeInfo{Name: scope.Name, Operations: operations})
}
return prepared
}
func normalizeConfigOperations(operations []ConfigOperation) ([]ConfigOperation, error) {
seen := make(map[ConfigOperation]struct{}, len(operations))
for _, operation := range operations {
switch operation {
case ConfigValidate, ConfigShow, ConfigSources, ConfigExplain:
default:
return nil, fmt.Errorf("unknown operation %q", operation)
}
seen[operation] = struct{}{}
}
ordered := make([]ConfigOperation, 0, len(seen))
for _, operation := range []ConfigOperation{ConfigValidate, ConfigShow, ConfigSources, ConfigExplain} {
if _, exists := seen[operation]; exists {
ordered = append(ordered, operation)
}
}
return ordered, nil
}
func newConfigScopesCommandWithPrepared[C any](prepared *preparedConfigSuite[C]) (Command, error) {
formatDefault := "text"
spec := CommandSpec[configScopesInput]{
Name: "scopes",
Summary: "List configuration scopes and supported operations.",
Flags: []Flag[configScopesInput]{ValueFlag[configScopesInput](FlagMeta{
Name: "format", Usage: "Output format: text or yaml.",
}, &formatDefault, parseConfigFormat, func(input *configScopesInput, value Supplied[string]) {
input.Format = value.Value
})},
CheckInput: func(input configScopesInput) error { return checkConfigFormat(input.Format) },
Run: func(invocation Invocation, input configScopesInput) Outcome {
var output []byte
var err error
if input.Format == "yaml" {
output, err = yaml.Marshal(prepared.inventory)
} else {
output = []byte(formatScopeInventory(prepared.inventory))
}
if err != nil {
return Failure(err)
}
if err := writeReport(invocation.Streams.Out, output); err != nil {
return incompleteReport(err)
}
return Success()
},
}
return NewCommand(spec)
}
func orderedConfigScopes[C any](scopes []ConfigScope[C]) []ConfigScope[C] {
ordered := append([]ConfigScope[C](nil), scopes...)
sort.SliceStable(ordered, func(i, j int) bool {
return configScopeOrder(ordered[i].Name) < configScopeOrder(ordered[j].Name)
})
return ordered
}
func configScopeOrder(name string) string {
switch name {
case "app":
return "\x00"
case "promptkit":
return "\x01"
default:
return "\x02" + name
}
}
func formatScopeInventory(inventory scopesInventory) string {
var output strings.Builder
for _, scope := range inventory.Scopes {
output.WriteString(scope.Name)
output.WriteString(":")
if len(scope.Operations) > 0 {
output.WriteString(" ")
for index, operation := range scope.Operations {
if index > 0 {
output.WriteString(", ")
}
output.WriteString(string(operation))
}
}
output.WriteByte('\n')
}
return output.String()
}
func parseConfigFormat(value string) (string, error) {
if err := checkConfigFormat(value); err != nil {
return "", err
}
return value, nil
}
func checkConfigFormat(value string) error {
if value != "text" && value != "yaml" {
return fmt.Errorf("format must be text or yaml")
}
return nil
}

View File

@@ -0,0 +1,359 @@
package ckconfig
import (
"bytes"
"context"
"reflect"
"strconv"
"strings"
"testing"
)
func TestConfigScopeDiscoveryOrdersAndDeduplicatesCapabilities(t *testing.T) {
app, err := New(Identity{Name: "example"})
if err != nil {
t.Fatal(err)
}
newContextCalls := 0
acquireCalls := 0
suite := ConfigSuite[struct{}]{
NewContext: func() struct{} { newContextCalls++; return struct{}{} },
Scopes: []ConfigScope[struct{}]{
{Name: "zeta", Operations: []ConfigOperation{ConfigExplain, ConfigShow, ConfigExplain}, Acquire: func(context.Context, ConfigRequest, struct{}) (Acquired[ConfigView], error) {
acquireCalls++
return Acquired[ConfigView]{}, nil
}},
{Name: "promptkit", Operations: []ConfigOperation{ConfigSources}, Acquire: noopConfigAcquire[struct{}]},
{Name: "app", Operations: []ConfigOperation{ConfigShow, ConfigValidate, ConfigShow}, Acquire: func(context.Context, ConfigRequest, struct{}) (Acquired[ConfigView], error) {
acquireCalls++
return Acquired[ConfigView]{}, nil
}},
{Name: "metadata", Summary: "No provider is required."},
},
}
normalized, err := normalizeConfigSuite(suite)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(normalized.Scopes[0].Operations, []ConfigOperation{ConfigShow, ConfigExplain}) || !reflect.DeepEqual(normalized.Scopes[2].Operations, []ConfigOperation{ConfigValidate, ConfigShow}) {
t.Fatalf("normalized capabilities = %#v", normalized.Scopes)
}
if err := RegisterConfig(app, suite); err != nil {
t.Fatal(err)
}
var textOutput bytes.Buffer
outcome := app.Execute(context.Background(), []string{"config", "scopes"}, Streams{Out: &textOutput})
if outcome.Kind != OutcomeSuccess {
t.Fatalf("text inventory outcome = %#v", outcome)
}
wantText := "app: validate, show\npromptkit: sources\nmetadata:\nzeta: show, explain\n"
if textOutput.String() != wantText {
t.Fatalf("text inventory = %q, want %q", textOutput.String(), wantText)
}
var yamlOutput bytes.Buffer
outcome = app.Execute(context.Background(), []string{"config", "scopes", "--format", "yaml"}, Streams{Out: &yamlOutput})
if outcome.Kind != OutcomeSuccess {
t.Fatalf("YAML inventory outcome = %#v", outcome)
}
if got := yamlOutput.String(); !strings.Contains(got, "scopes:") || strings.Index(got, "name: app") > strings.Index(got, "name: promptkit") || !strings.Contains(got, "operations: []") {
t.Fatalf("YAML inventory = %q", got)
}
outcome = app.Execute(context.Background(), []string{"config", "scopes", "--help"}, Streams{})
if outcome.Kind != OutcomeSuccess {
t.Fatalf("scope help outcome = %#v", outcome)
}
outcome = app.Execute(context.Background(), []string{"config", "scopes", "--format", "json"}, Streams{})
if outcome.Kind != OutcomeUsage {
t.Fatalf("invalid format outcome = %#v", outcome)
}
if newContextCalls != 0 || acquireCalls != 0 {
t.Fatalf("discovery called runtime callbacks: NewContext=%d Acquire=%d", newContextCalls, acquireCalls)
}
}
func TestConfigInputDefaultsToAppAndRetainsAbsentSelections(t *testing.T) {
suite := ConfigSuite[struct{}]{Scopes: []ConfigScope[struct{}]{{Name: "app"}}}
prepared, err := prepareInput(CommandSpec[configInput[struct{}]]{Flags: configInputFlags(suite, "yaml")}, make([][]string, 4), nil, true)
if err != nil {
t.Fatal(err)
}
input := prepared.build()
if input.Scope != "app" || input.Format != "yaml" || input.Selection.Config.Set || input.Selection.Overlay.Set {
t.Fatalf("configuration defaults = %#v", input)
}
}
func TestRegisterConfigOwnsPreparedScopeAndSelectorMetadata(t *testing.T) {
type selectorInput struct{ Region Supplied[string] }
var acquired selectorInput
suite := ConfigSuite[selectorInput]{
Selectors: []Selector[selectorInput]{{
Binding: ValueFlag[selectorInput](FlagMeta{Name: "region"}, nil, parseString, func(input *selectorInput, value Supplied[string]) { input.Region = value }),
Scopes: []string{"app"},
}},
Scopes: []ConfigScope[selectorInput]{{
Name: "app",
Operations: []ConfigOperation{ConfigValidate},
Acquire: func(_ context.Context, _ ConfigRequest, input selectorInput) (Acquired[ConfigView], error) {
acquired = input
return Acquired[ConfigView]{}, nil
},
}},
}
app, err := New(Identity{Name: "example"})
if err != nil {
t.Fatal(err)
}
if err := RegisterConfig(app, suite); err != nil {
t.Fatal(err)
}
suite.Scopes[0].Name = "renamed"
suite.Scopes[0].Operations[0] = ConfigShow
suite.Selectors[0].Scopes[0] = "renamed"
var inventory bytes.Buffer
if result := app.Execute(context.Background(), []string{"config", "scopes"}, Streams{Out: &inventory}); result.Kind != OutcomeSuccess {
t.Fatalf("scope inventory outcome = %#v", result)
}
if got := inventory.String(); got != "app: validate\n" {
t.Fatalf("scope inventory after caller mutation = %q", got)
}
if result := app.Execute(context.Background(), []string{"config", "validate", "--scope=app", "--region=west"}, Streams{}); result.Kind != OutcomeSuccess {
t.Fatalf("registered scope execution after caller mutation = %#v", result)
}
if acquired.Region != (Supplied[string]{Value: "west", Set: true}) {
t.Fatalf("acquired selector after caller mutation = %#v", acquired)
}
}
func TestRegisterConfigRejectsDuplicateSelectorNamesAndShorts(t *testing.T) {
type selectorInput struct{ First, Second string }
tests := []struct {
name string
firstMeta FlagMeta
secondMeta FlagMeta
}{
{name: "name", firstMeta: FlagMeta{Name: "region", Short: "r"}, secondMeta: FlagMeta{Name: "region", Short: "g"}},
{name: "short", firstMeta: FlagMeta{Name: "region", Short: "r"}, secondMeta: FlagMeta{Name: "group", Short: "r"}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
app, err := New(Identity{Name: "example"})
if err != nil {
t.Fatal(err)
}
suite := ConfigSuite[selectorInput]{Selectors: []Selector[selectorInput]{
{Binding: ValueFlag[selectorInput](test.firstMeta, nil, parseString, func(input *selectorInput, value Supplied[string]) { input.First = value.Value })},
{Binding: ValueFlag[selectorInput](test.secondMeta, nil, parseString, func(input *selectorInput, value Supplied[string]) { input.Second = value.Value })},
}}
if err := RegisterConfig(app, suite); err == nil || !strings.Contains(err.Error(), "duplicate configuration selector flag") {
t.Fatalf("RegisterConfig error = %v", err)
}
})
}
}
func TestRegisterConfigRejectsInvalidSuitesAndCollisions(t *testing.T) {
tests := []struct {
name string
scope ConfigScope[struct{}]
want string
}{
{name: "empty name", scope: ConfigScope[struct{}]{}, want: "name must not be empty"},
{name: "all name", scope: ConfigScope[struct{}]{Name: "all"}, want: "reserved"},
{name: "unknown operation", scope: ConfigScope[struct{}]{Name: "app", Operations: []ConfigOperation{"diff"}}, want: "unknown operation"},
{name: "missing acquisition", scope: ConfigScope[struct{}]{Name: "app", Operations: []ConfigOperation{ConfigValidate}}, want: "without an acquisition"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
app, _ := New(Identity{Name: "example"})
err := RegisterConfig(app, ConfigSuite[struct{}]{Scopes: []ConfigScope[struct{}]{test.scope}})
if err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("RegisterConfig error = %v, want %q", err, test.want)
}
})
}
app, _ := New(Identity{Name: "example"})
custom, err := NewCommand(CommandSpec[struct{}]{Name: "config", Run: func(Invocation, struct{}) Outcome { return Success() }})
if err != nil {
t.Fatal(err)
}
if err := app.Register(custom); err != nil {
t.Fatal(err)
}
if err := RegisterConfig(app, ConfigSuite[struct{}]{}); err == nil {
t.Fatal("replaced a pre-existing config command")
}
}
func TestConfigSelectorPreflightAndScopedContextProjection(t *testing.T) {
type selectorInput struct {
Count Supplied[int]
Enabled Supplied[bool]
Other Supplied[int]
Runtime string
}
baseContextCalls := 0
checkCalls := 0
var checked selectorInput
countDefault := 9
suite := ConfigSuite[selectorInput]{
NewContext: func() selectorInput { baseContextCalls++; return selectorInput{Runtime: "invocation cache"} },
Selectors: []Selector[selectorInput]{
{Binding: ValueFlag[selectorInput](FlagMeta{Name: "count"}, &countDefault, strconv.Atoi, func(input *selectorInput, value Supplied[int]) { input.Count = value }), Scopes: []string{"app"}},
{Binding: BoolFlag[selectorInput](FlagMeta{Name: "enabled"}, true, func(input *selectorInput, value Supplied[bool]) { input.Enabled = value }), Scopes: []string{"promptkit"}},
{Binding: ValueFlag[selectorInput](FlagMeta{Name: "other"}, nil, strconv.Atoi, func(input *selectorInput, value Supplied[int]) { input.Other = value })},
},
CheckInput: func(_ Selection, input selectorInput) error { checkCalls++; checked = input; return nil },
Scopes: []ConfigScope[selectorInput]{
{Name: "app", Operations: []ConfigOperation{ConfigValidate}, Acquire: noopConfigAcquire[selectorInput]},
{Name: "promptkit", Operations: []ConfigOperation{ConfigValidate}, Acquire: noopConfigAcquire[selectorInput]},
},
}
normalized, err := normalizeConfigSuite(suite)
if err != nil {
t.Fatal(err)
}
preparedSuite := prepareConfigSuite(normalized)
spec := CommandSpec[configInput[selectorInput]]{Flags: configInputFlags(normalized, "text")}
prepared, err := prepareInput(spec, [][]string{{"all"}, {"text"}, {""}, nil, {"0"}, {"false"}, nil}, nil, true)
if err != nil {
t.Fatal(err)
}
input := prepared.build()
scopeNames, err := preflightConfig(preparedSuite, input, ConfigValidate)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(scopeNames, []string{"app", "promptkit"}) || checkCalls != 1 || checked.Count != (Supplied[int]{Value: 0, Set: true}) || checked.Enabled != (Supplied[bool]{Value: false, Set: true}) {
t.Fatalf("preflight result=%v checked=%#v calls=%d", scopeNames, checked, checkCalls)
}
if !input.Selection.Config.Set || input.Selection.Config.Value != "" || input.Selection.Overlay.Set {
t.Fatalf("configuration selection presence = %#v", input.Selection)
}
if baseContextCalls != 0 {
t.Fatalf("NewContext called during preflight %d times", baseContextCalls)
}
contexts := contextsForScopes(preparedSuite, input, scopeNames)
if baseContextCalls != 1 {
t.Fatalf("NewContext calls = %d, want one", baseContextCalls)
}
if contexts["app"].Count != (Supplied[int]{Value: 0, Set: true}) || contexts["app"].Enabled != (Supplied[bool]{}) || contexts["app"].Other != (Supplied[int]{}) {
t.Fatalf("app selectors = %#v", contexts["app"])
}
if contexts["promptkit"].Count != (Supplied[int]{}) || contexts["promptkit"].Enabled != (Supplied[bool]{Value: false, Set: true}) || contexts["promptkit"].Runtime != "invocation cache" {
t.Fatalf("promptkit selectors = %#v", contexts["promptkit"])
}
}
func TestConfigPreflightRejectsUnsupportedAggregateAndInapplicableSelector(t *testing.T) {
type selectorInput struct{ Region Supplied[string] }
newContextCalls := 0
suite := ConfigSuite[selectorInput]{
NewContext: func() selectorInput { newContextCalls++; return selectorInput{} },
Selectors: []Selector[selectorInput]{{Binding: ValueFlag[selectorInput](FlagMeta{Name: "region"}, nil, parseString, func(input *selectorInput, value Supplied[string]) { input.Region = value }), Scopes: []string{"promptkit"}}},
Scopes: []ConfigScope[selectorInput]{
{Name: "app", Operations: []ConfigOperation{ConfigValidate}, Acquire: noopConfigAcquire[selectorInput]},
{Name: "promptkit", Operations: []ConfigOperation{ConfigShow}, Acquire: noopConfigAcquire[selectorInput]},
},
}
normalized, err := normalizeConfigSuite(suite)
if err != nil {
t.Fatal(err)
}
preparedSuite := prepareConfigSuite(normalized)
prepared, err := prepareInput(CommandSpec[configInput[selectorInput]]{Flags: configInputFlags(normalized, "text")}, [][]string{{"all"}, {"text"}, nil, nil, nil}, nil, true)
if err != nil {
t.Fatal(err)
}
if _, err := preflightConfig(preparedSuite, prepared.build(), ConfigValidate); err == nil || !strings.Contains(err.Error(), "does not support validate") {
t.Fatalf("unsupported aggregate error = %v", err)
}
if newContextCalls != 0 {
t.Fatalf("unsupported aggregate created context %d times", newContextCalls)
}
prepared, err = prepareInput(CommandSpec[configInput[selectorInput]]{Flags: configInputFlags(normalized, "text")}, [][]string{{"app"}, {"text"}, nil, nil, {"west"}}, nil, true)
if err != nil {
t.Fatal(err)
}
if _, err := preflightConfig(preparedSuite, prepared.build(), ConfigValidate); err == nil || !strings.Contains(err.Error(), "not applicable") {
t.Fatalf("inapplicable selector error = %v", err)
}
}
func noopConfigAcquire[C any](context.Context, ConfigRequest, C) (Acquired[ConfigView], error) {
return Acquired[ConfigView]{}, nil
}
func TestRequiredConfigSelectorOnlyAppliesToSelectedScopes(t *testing.T) {
app, _ := New(Identity{Name: "example"})
calls := 0
acquire := func(context.Context, ConfigRequest, string) (Acquired[ConfigView], error) {
calls++
return Acquired[ConfigView]{}, nil
}
err := RegisterConfig(app, ConfigSuite[string]{
Selectors: []Selector[string]{{Scopes: []string{"app"}, Binding: ValueFlag[string](FlagMeta{Name: "campaign", Required: true}, nil, parseString, func(c *string, v Supplied[string]) { *c = v.Value })}},
Scopes: []ConfigScope[string]{{Name: "app", Operations: []ConfigOperation{ConfigValidate}, Acquire: acquire}, {Name: "promptkit", Operations: []ConfigOperation{ConfigValidate}, Acquire: acquire}},
})
if err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
args []string
kind OutcomeKind
calls int
}{
{[]string{"--scope", "promptkit"}, OutcomeSuccess, 1},
{nil, OutcomeUsage, 0},
{[]string{"--scope", "all"}, OutcomeUsage, 0},
{[]string{"--scope", "all", "--campaign", "spring"}, OutcomeSuccess, 2},
{[]string{"--scope", "promptkit", "--campaign", "spring"}, OutcomeUsage, 0},
{[]string{"--help"}, OutcomeSuccess, 0},
} {
calls = 0
var out bytes.Buffer
result := app.Execute(context.Background(), append([]string{"config", "validate"}, tc.args...), Streams{Out: &out})
if result.Kind != tc.kind || calls != tc.calls {
t.Fatalf("%v: result=%#v calls=%d", tc.args, result, calls)
}
if len(tc.args) > 0 && tc.args[0] == "--help" && !strings.Contains(out.String(), "required where applicable") {
t.Fatalf("help=%q", out.String())
}
}
}
func TestUnknownConfigScopeReportsChoicesWithoutRuntimeWork(t *testing.T) {
for _, tc := range []struct {
names []string
scope []string
choices string
}{
{[]string{"promptkit"}, nil, "available: promptkit"},
{[]string{"zeta", "alpha", "promptkit", "app"}, []string{"--scope", "unknown"}, "available: app, promptkit, alpha, zeta"},
} {
runtimeCalls := 0
suite := ConfigSuite[struct{}]{NewContext: func() struct{} { runtimeCalls++; return struct{}{} }}
for _, name := range tc.names {
suite.Scopes = append(suite.Scopes, ConfigScope[struct{}]{Name: name, Operations: []ConfigOperation{ConfigValidate}, Acquire: func(context.Context, ConfigRequest, struct{}) (Acquired[ConfigView], error) {
runtimeCalls++
return Acquired[ConfigView]{}, nil
}})
}
app, _ := New(Identity{Name: "example"})
if err := RegisterConfig(app, suite); err != nil {
t.Fatal(err)
}
var out, diagnostics bytes.Buffer
result := app.Execute(context.Background(), append([]string{"config", "validate"}, tc.scope...), Streams{Out: &out, Err: &diagnostics})
if result.Kind != OutcomeUsage || runtimeCalls != 0 || out.Len() != 0 || !strings.Contains(diagnostics.String(), tc.choices) {
t.Fatalf("result=%#v calls=%d out=%q diagnostics=%q", result, runtimeCalls, out.String(), diagnostics.String())
}
}
}

View File

@@ -0,0 +1,720 @@
package ckconfig_test
import (
"bytes"
"context"
"errors"
"io"
"reflect"
"strings"
"testing"
"testing/fstest"
commandkit "gitea.maximumdirect.net/eric/CommandKit"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
"go.yaml.in/yaml/v4"
)
type effectiveYAML string
func (document effectiveYAML) WriteEffective(writer io.Writer) error {
_, err := io.WriteString(writer, string(document))
return err
}
type capturedSourceReader struct {
entries []configkit.SourceInfo
calls int
}
func TestConfigExplainPreservesConfigKitProvenanceAndRedaction(t *testing.T) {
type entry struct {
Enabled bool `yaml:"enabled"`
Credential configkit.Secret `yaml:"credential"`
}
type configuration struct {
Entries map[string]entry `yaml:"entries"`
Odd map[string]string `yaml:"odd"`
Value string `yaml:"value"`
Dependency string `yaml:"dependency"`
Secret configkit.Secret `yaml:"secret"`
}
const secretValue = "EXPLAIN_TEST_SECRET_VALUE"
t.Setenv("EXPLAIN_TEST_SECRET", secretValue)
input := []byte("entries:\n parent:\n enabled: false\n credential: EXPLAIN_TEST_SECRET\n child:\n extends: parent\nodd:\n \"key/name~\": escaped\nvalue: original\ndependency: source\nsecret: EXPLAIN_TEST_SECRET\n")
result, err := configkit.Load[configuration](configkit.Bytes("explanation.yml", input), configkit.Options[configuration]{Definition: configkit.Definition[configuration]{
Secret: func(path configkit.Path) bool { return path.String() == "/entries/parent/credential" },
Resolve: func(value *configuration, resolution *configkit.Resolution) error {
resolution.Record(configkit.Keys("value"), configkit.ChangeNormalization, "upper case", configkit.Keys("dependency"))
value.Value = strings.ToUpper(value.Value)
return nil
},
}})
if err != nil {
t.Fatal(err)
}
loads, contexts := 0, 0
scope, err := commandkit.NewConfigKitScope[struct{}, configuration]("app", "", func(context.Context, commandkit.Selection, struct{}) (*configkit.Result[configuration], error) {
loads++
return result, nil
})
if err != nil {
t.Fatal(err)
}
app, err := commandkit.New(commandkit.Identity{Name: "example"})
if err != nil {
t.Fatal(err)
}
if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{NewContext: func() struct{} { contexts++; return struct{}{} }, Scopes: []commandkit.ConfigScope[struct{}]{scope}}); err != nil {
t.Fatal(err)
}
runYAML := func(pointer string) (map[string]any, commandkit.Outcome, string) {
var output bytes.Buffer
argv := []string{"config", "explain", "--path", pointer, "--format", "yaml"}
if pointer == "" {
argv = []string{"config", "explain", "--path=", "--format", "yaml"}
}
outcome := app.Execute(context.Background(), argv, commandkit.Streams{Out: &output, Err: io.Discard})
var report map[string]any
if outcome.Kind == commandkit.OutcomeSuccess {
if err := yaml.Unmarshal(output.Bytes(), &report); err != nil {
t.Fatalf("decode explanation YAML: %v; output=%q", err, output.String())
}
}
return report, outcome, output.String()
}
path := "/entries/child/enabled"
var native bytes.Buffer
if err := result.WriteExplanation(&native, configkit.Keys("entries", "child", "enabled")); err != nil {
t.Fatal(err)
}
var textOutput bytes.Buffer
outcome := app.Execute(context.Background(), []string{"config", "explain", "--path", path}, commandkit.Streams{Out: &textOutput, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess || textOutput.String() != native.String() {
t.Fatalf("native text outcome=%#v output=%q want=%q", outcome, textOutput.String(), native.String())
}
var inherited map[string]any
inherited, outcome, _ = runYAML(path)
if outcome.Kind != commandkit.OutcomeSuccess || inherited["path"] != path {
t.Fatalf("inherited explanation outcome=%#v report=%#v", outcome, inherited)
}
effective := inherited["effective"].(map[string]any)
if effective["present"] != true || effective["yaml"] != "false\n" {
t.Fatalf("inherited effective snapshot = %#v", effective)
}
origin := inherited["origin"].(map[string]any)
steps := origin["inheritance"].([]any)
step := steps[0].(map[string]any)
if step["from"] != "/entries/parent/enabled" || step["to"] != path || step["reference"] != "/entries/child/extends" {
t.Fatalf("inheritance paths = %#v", step)
}
escaped, outcome, _ := runYAML("/odd/key~1name~0")
if outcome.Kind != commandkit.OutcomeSuccess || escaped["path"] != "/odd/key~1name~0" || escaped["effective"].(map[string]any)["yaml"] != "escaped\n" {
t.Fatalf("escaped path outcome=%#v report=%#v", outcome, escaped)
}
historical, outcome, _ := runYAML("/entries/child/extends")
if outcome.Kind != commandkit.OutcomeSuccess || historical["effective"].(map[string]any)["present"] != false || len(historical["history"].([]any)) == 0 {
t.Fatalf("historical path outcome=%#v report=%#v", outcome, historical)
}
resolved, outcome, _ := runYAML("/value")
if outcome.Kind != commandkit.OutcomeSuccess || resolved["effective"].(map[string]any)["yaml"] != "ORIGINAL\n" {
t.Fatalf("resolved snapshot outcome=%#v report=%#v", outcome, resolved)
}
changes := resolved["history"].([]any)
last := changes[len(changes)-1].(map[string]any)
if last["reasons"].([]any)[0] != "upper case" || last["dependencies"].([]any)[0] != "/dependency" {
t.Fatalf("resolution change = %#v", last)
}
secretReport, outcome, secretText := runYAML("/entries/child/credential")
secretEffective := secretReport["effective"].(map[string]any)
if outcome.Kind != commandkit.OutcomeSuccess || secretEffective["redacted"] != true || strings.Contains(secretText, secretValue) {
t.Fatalf("secret report outcome=%#v report=%#v", outcome, secretReport)
}
for _, change := range secretReport["history"].([]any) {
entry := change.(map[string]any)
for _, key := range []string{"before", "after"} {
snapshot := entry[key].(map[string]any)
if snapshot["present"] == true && snapshot["redacted"] != true {
t.Fatalf("secret history was not redacted: %#v", entry)
}
}
}
directSecret, outcome, directSecretText := runYAML("/secret")
secretRef, refPresent := directSecret["secret_reference"].(map[string]any)
if !refPresent {
t.Fatalf("secret reference was absent: %#v", directSecret)
}
if outcome.Kind != commandkit.OutcomeSuccess || secretRef["name"] != "EXPLAIN_TEST_SECRET" || strings.Contains(directSecretText, secretValue) {
t.Fatalf("direct secret reference outcome=%#v report=%#v", outcome, directSecret)
}
root, outcome, _ := runYAML("")
if outcome.Kind != commandkit.OutcomeSuccess || root["path"] != "" {
t.Fatalf("root path outcome=%#v report=%#v", outcome, root)
}
loadsBeforeInvalid, contextsBeforeInvalid := loads, contexts
outcome = app.Execute(context.Background(), []string{"config", "explain"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeUsage || loads != loadsBeforeInvalid || contexts != contextsBeforeInvalid {
t.Fatalf("missing path outcome=%#v loads=%d contexts=%d", outcome, loads, contexts)
}
outcome = app.Execute(context.Background(), []string{"config", "explain", "--path", "/bad~2path"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeUsage || outcome.Status != 2 || loads != loadsBeforeInvalid || contexts != contextsBeforeInvalid {
t.Fatalf("malformed path outcome=%#v loads=%d contexts=%d", outcome, loads, contexts)
}
outcome = app.Execute(context.Background(), []string{"config", "explain", "--path", "/does-not-exist"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeOperational || outcome.Status != 1 {
t.Fatalf("unknown path outcome = %#v", outcome)
}
partial := &configKitShowPartialWriter{failure: io.ErrClosedPipe}
outcome = app.Execute(context.Background(), []string{"config", "explain", "--path", path}, commandkit.Streams{Out: partial, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeOperational || !outcome.ReportIncomplete || !errors.Is(outcome.DeliveryError, io.ErrClosedPipe) {
t.Fatalf("explanation writer failure = %#v", outcome)
}
}
func (reader *capturedSourceReader) Sources() []configkit.SourceInfo {
reader.calls++
return reader.entries
}
func TestConfigSourcesReportsCapturedConfigKitSourceInventory(t *testing.T) {
type configuration struct {
Value string `yaml:"value"`
Shared string `yaml:"shared"`
Synthetic string `yaml:"synthetic"`
}
filesystem := configkit.NewFilesystem("fixture", fstest.MapFS{
"root/config/main.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: ../shared.yml}]}\nvalue: base\n")},
"root/shared.yml": &fstest.MapFile{Data: []byte("shared: imported\n")},
"root/overlays/dev.yml": &fstest.MapFile{Data: []byte("value: overlay\n")},
})
filesystemContext := &configkit.FilesystemContext{Root: "root", FS: &filesystem}
overlayFilesystemContext := &configkit.FilesystemContext{Root: "root", BaseDir: "root/config", FS: &filesystem}
loaderCalls := 0
reader := &capturedSourceReader{}
scope, err := commandkit.NewConfigKitScope[struct{}, configuration]("app", "", func(context.Context, commandkit.Selection, struct{}) (*configkit.Result[configuration], error) {
loaderCalls++
result, err := configkit.LoadLayered(configkit.Input{
Source: filesystem.File("root/config/main.yml"),
Imports: true,
Filesystem: filesystemContext,
}, configkit.LayeredOptions[configuration]{
Definition: configkit.Definition[configuration]{},
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.Mapping("synthetic source", map[string]any{"synthetic": "underlay"})}}}},
Overlay: "dev",
OverlayFilesystem: overlayFilesystemContext,
})
if err != nil {
return nil, err
}
reader.entries = result.Sources()
return result, nil
})
if err != nil {
t.Fatal(err)
}
// Replace the result's reader after the owning loader has captured its source inventory.
originalAcquire := scope.Acquire
scope.Acquire = func(ctx context.Context, request commandkit.ConfigRequest, input struct{}) (commandkit.Acquired[commandkit.ConfigView], error) {
acquired, err := originalAcquire(ctx, request, input)
if err == nil {
acquired.Value.Sources = reader
}
return acquired, err
}
// Register the wrapped scope so the command reads the captured public facet once.
app, _ := commandkit.New(commandkit.Identity{Name: "example"})
if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{scope}}); err != nil {
t.Fatal(err)
}
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 || loaderCalls != 1 || reader.calls != 1 {
var details bytes.Buffer
for _, diagnostic := range outcome.Diagnostics {
_ = diagnostic.Write(&details)
}
t.Fatalf("sources outcome=%#v cause=%v details=%q loader calls=%d reader calls=%d", outcome, outcome.Cause, details.String(), loaderCalls, reader.calls)
}
var report struct {
Scope string `yaml:"scope"`
Sources []struct {
ID string `yaml:"id"`
Label string `yaml:"label"`
Path string `yaml:"path"`
Layer string `yaml:"layer"`
Kind string `yaml:"kind"`
UnderlayIndex int `yaml:"underlay_index"`
ImportedFrom *struct {
SourceID string `yaml:"source_id"`
Line int `yaml:"line"`
Column int `yaml:"column"`
} `yaml:"imported_from"`
} `yaml:"sources"`
}
if err := yaml.Unmarshal(output.Bytes(), &report); err != nil {
t.Fatal(err)
}
if report.Scope != "app" || len(report.Sources) != 4 {
t.Fatalf("source report = %#v", report)
}
if report.Sources[0].Path != "root/config/main.yml" || report.Sources[0].ID != "source-1" {
t.Fatalf("main source = %#v", report.Sources[0])
}
if report.Sources[0].ImportedFrom != nil {
t.Fatalf("root source imported_from = %#v, want null", report.Sources[0].ImportedFrom)
}
imported := report.Sources[1]
if imported.Path != "root/shared.yml" || imported.ImportedFrom == nil || imported.ImportedFrom.SourceID != report.Sources[0].ID || imported.ImportedFrom.Line == 0 {
t.Fatalf("import source correlation = %#v", imported)
}
synthetic := report.Sources[2]
if synthetic.Kind != string(configkit.SourceKindMapping) || synthetic.Path != "" || synthetic.Layer != string(configkit.LayerUnderlay) || synthetic.UnderlayIndex != 1 {
t.Fatalf("synthetic underlay source = %#v", synthetic)
}
if report.Sources[3].Layer != string(configkit.LayerOverlay) || report.Sources[3].Path != "root/overlays/dev.yml" {
t.Fatalf("overlay source = %#v", report.Sources[3])
}
}
func TestConfigSourcesHandlesEmptyInventoryAllPreflightAndWriterFailure(t *testing.T) {
reader := &capturedSourceReader{}
newContextCalls, acquireCalls := 0, 0
app, err := commandkit.New(commandkit.Identity{Name: "example"})
if err != nil {
t.Fatal(err)
}
suite := commandkit.ConfigSuite[string]{
NewContext: func() string { newContextCalls++; return "context" },
Scopes: []commandkit.ConfigScope[string]{{Name: "app", Operations: []commandkit.ConfigOperation{commandkit.ConfigSources}, Acquire: func(context.Context, commandkit.ConfigRequest, string) (commandkit.Acquired[commandkit.ConfigView], error) {
acquireCalls++
return commandkit.Acquired[commandkit.ConfigView]{Value: commandkit.ConfigView{Sources: reader}}, nil
}}},
}
if err := commandkit.RegisterConfig(app, suite); err != nil {
t.Fatal(err)
}
var output bytes.Buffer
outcome := app.Execute(context.Background(), []string{"config", "sources", "--scope", "all"}, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeUsage || newContextCalls != 0 || acquireCalls != 0 || output.Len() != 0 {
t.Fatalf("all preflight outcome=%#v contexts=%d acquires=%d output=%q", outcome, newContextCalls, acquireCalls, output.String())
}
outcome = app.Execute(context.Background(), []string{"config", "sources", "--format", "yaml"}, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess || output.String() != "scope: app\nsources: []\n" || reader.calls != 1 {
t.Fatalf("empty source report outcome=%#v output=%q reader calls=%d", outcome, output.String(), reader.calls)
}
reader.entries = []configkit.SourceInfo{{ID: "source-1", Label: "label with \"quotes\"", Path: "path with spaces.yml", Layer: configkit.LayerBase, Kind: configkit.SourceKindFile}}
var textOutput bytes.Buffer
outcome = app.Execute(context.Background(), []string{"config", "sources"}, commandkit.Streams{Out: &textOutput, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(textOutput.String(), `label: "label with \"quotes\""`) || !strings.Contains(textOutput.String(), `path: "path with spaces.yml"`) {
t.Fatalf("quoted text source output outcome=%#v output=%q", outcome, textOutput.String())
}
partial := &configKitShowPartialWriter{failure: io.ErrClosedPipe}
outcome = app.Execute(context.Background(), []string{"config", "sources"}, commandkit.Streams{Out: partial, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeOperational || !outcome.ReportIncomplete || !errors.Is(outcome.DeliveryError, io.ErrClosedPipe) {
t.Fatalf("source writer failure = %#v", outcome)
}
}
func TestConfigShowWritesNativeSingleScopeAndFramesAggregateYAML(t *testing.T) {
const native = "%YAML 1.1\n# authored comment\n---\nvalue: null\nitems: []\nmissing: {}\n"
app, err := commandkit.New(commandkit.Identity{Name: "example"})
if err != nil {
t.Fatal(err)
}
var calls []string
suite := commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{
{Name: "zeta", Operations: []commandkit.ConfigOperation{commandkit.ConfigShow}, Acquire: func(context.Context, commandkit.ConfigRequest, struct{}) (commandkit.Acquired[commandkit.ConfigView], error) {
calls = append(calls, "zeta")
return commandkit.Acquired[commandkit.ConfigView]{Value: commandkit.ConfigView{Effective: effectiveYAML("%YAML 1.1\n# zeta comment\nvalue: zeta\n")}}, nil
}},
{Name: "app", Operations: []commandkit.ConfigOperation{commandkit.ConfigShow}, Acquire: func(context.Context, commandkit.ConfigRequest, struct{}) (commandkit.Acquired[commandkit.ConfigView], error) {
calls = append(calls, "app")
return commandkit.Acquired[commandkit.ConfigView]{Value: commandkit.ConfigView{Effective: effectiveYAML(native)}}, nil
}},
}}
if err := commandkit.RegisterConfig(app, suite); err != nil {
t.Fatal(err)
}
var single bytes.Buffer
outcome := app.Execute(context.Background(), []string{"config", "show"}, commandkit.Streams{Out: &single, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess || single.String() != native {
t.Fatalf("single show outcome=%#v output=%q", outcome, single.String())
}
calls = nil
outcome = app.Execute(context.Background(), []string{"config", "show", "--format", "text"}, commandkit.Streams{Out: &single, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeUsage || len(calls) != 0 {
t.Fatalf("text format outcome=%#v acquisitions=%v", outcome, calls)
}
calls = nil
var aggregate bytes.Buffer
outcome = app.Execute(context.Background(), []string{"config", "show", "--scope", "all"}, commandkit.Streams{Out: &aggregate, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess || !reflect.DeepEqual(calls, []string{"app", "zeta"}) {
t.Fatalf("aggregate outcome=%#v acquisitions=%v", outcome, calls)
}
if !strings.Contains(aggregate.String(), "%YAML 1.1\n# authored comment\n---\n# scope: app\n") || !strings.Contains(aggregate.String(), "...\n%YAML 1.1\n# zeta comment\n---\n# scope: zeta\nvalue: zeta\n") {
t.Fatalf("aggregate framing = %q", aggregate.String())
}
decoder := yaml.NewDecoder(bytes.NewReader(aggregate.Bytes()))
var first, second map[string]any
if err := decoder.Decode(&first); err != nil {
t.Fatal(err)
}
if err := decoder.Decode(&second); err != nil {
t.Fatal(err)
}
var extra any
if err := decoder.Decode(&extra); err != io.EOF {
t.Fatalf("third YAML document decode error = %v, value=%#v", err, extra)
}
if first["value"] != nil || first["items"] == nil || first["missing"] == nil || second["value"] != "zeta" {
t.Fatalf("decoded documents = %#v and %#v", first, second)
}
if _, exists := first["absent"]; exists {
t.Fatalf("absent field was added to native document: %#v", first)
}
}
func TestConfigShowAcquiresAllBeforeOutputAndTracksWriterFailures(t *testing.T) {
failure := errors.New("second scope failed")
app, err := commandkit.New(commandkit.Identity{Name: "example"})
if err != nil {
t.Fatal(err)
}
calls := 0
suite := commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{
{Name: "app", Operations: []commandkit.ConfigOperation{commandkit.ConfigShow}, Acquire: func(context.Context, commandkit.ConfigRequest, struct{}) (commandkit.Acquired[commandkit.ConfigView], error) {
calls++
return commandkit.Acquired[commandkit.ConfigView]{Value: commandkit.ConfigView{Effective: effectiveYAML("value: app\n")}}, nil
}},
{Name: "promptkit", Operations: []commandkit.ConfigOperation{commandkit.ConfigShow}, Acquire: func(context.Context, commandkit.ConfigRequest, struct{}) (commandkit.Acquired[commandkit.ConfigView], error) {
calls++
return commandkit.Acquired[commandkit.ConfigView]{Diagnostics: []commandkit.Diagnostic{commandkit.MessageDiagnostic(configkit.SeverityWarning, "provider warning")}}, failure
}},
}}
if err := commandkit.RegisterConfig(app, suite); err != nil {
t.Fatal(err)
}
var output bytes.Buffer
outcome := app.Execute(context.Background(), []string{"config", "show", "--scope", "all"}, commandkit.Streams{Out: &output, Err: io.Discard})
if !errors.Is(outcome.Cause, failure) || outcome.ReportIncomplete || output.Len() != 0 || calls != 2 || len(outcome.Diagnostics) < 1 || outcome.Diagnostics[0].Severity != configkit.SeverityWarning {
t.Fatalf("acquisition failure outcome=%#v output=%q calls=%d", outcome, output.String(), calls)
}
app, _ = commandkit.New(commandkit.Identity{Name: "example"})
if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{{Name: "app", Operations: []commandkit.ConfigOperation{commandkit.ConfigShow}, Acquire: func(context.Context, commandkit.ConfigRequest, struct{}) (commandkit.Acquired[commandkit.ConfigView], error) {
return commandkit.Acquired[commandkit.ConfigView]{Value: commandkit.ConfigView{Effective: effectiveYAML("value: app\n")}}, nil
}}}}); err != nil {
t.Fatal(err)
}
partial := &configKitShowPartialWriter{failure: io.ErrClosedPipe}
outcome = app.Execute(context.Background(), []string{"config", "show", "--scope", "all"}, commandkit.Streams{Out: partial, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeOperational || !outcome.ReportIncomplete || !errors.Is(outcome.Cause, io.ErrClosedPipe) || !errors.Is(outcome.DeliveryError, io.ErrClosedPipe) || len(partial.written) == 0 {
t.Fatalf("writer failure outcome = %#v", outcome)
}
}
type configKitShowPartialWriter struct {
written []byte
failure error
}
func (writer *configKitShowPartialWriter) Write(data []byte) (int, error) {
n := len(data)
if n > 4 {
n = 4
}
writer.written = append(writer.written, data[:n]...)
return n, writer.failure
}
func TestConfigKitScopeUsesDefaultsOverlayAndRecordedResolution(t *testing.T) {
type configuration struct {
Mode string `yaml:"mode"`
Defaulted string `yaml:"defaulted"`
}
filesystem := configkit.NewFilesystem("fixture", fstest.MapFS{
"config/main.yml": &fstest.MapFile{Data: []byte("mode: base\n")},
"config/overlays/dev.yml": &fstest.MapFile{Data: []byte("mode: overlay\n")},
})
definition := configkit.Definition[configuration]{
Defaults: func() configuration { return configuration{Defaulted: "from default"} },
Resolve: func(input *configuration, resolution *configkit.Resolution) error {
resolution.Record(configkit.Keys("mode"), configkit.ChangeNormalization, "normalized for use")
input.Mode = strings.ToUpper(input.Mode)
return nil
},
}
var received commandkit.Selection
scope, err := commandkit.NewConfigKitScope[struct{}, configuration]("app", "Application settings.", func(_ context.Context, selection commandkit.Selection, _ struct{}) (*configkit.Result[configuration], error) {
received = selection
return configkit.LoadLayered(configkit.Input{Source: filesystem.File(selection.Config.Value)}, configkit.LayeredOptions[configuration]{
Definition: definition,
Overlay: selection.Overlay.Value,
OverlayFilesystem: &configkit.FilesystemContext{Root: "config", BaseDir: "config", FS: &filesystem},
})
})
if err != nil {
t.Fatal(err)
}
selection := commandkit.Selection{
Config: commandkit.Supplied[string]{Value: "config/main.yml", Set: true},
Overlay: commandkit.Supplied[string]{Value: "dev", Set: true},
}
acquired, err := scope.Acquire(context.Background(), commandkit.ConfigRequest{Operation: commandkit.ConfigValidate, Selection: selection}, struct{}{})
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(received, selection) {
t.Fatalf("loader selection = %#v, want %#v", received, selection)
}
if acquired.Value.Effective == nil || acquired.Value.Sources == nil || acquired.Value.Explanation == nil {
t.Fatalf("ConfigKit result facets = %#v", acquired.Value)
}
if got := acquired.Value.Sources.Sources(); len(got) < 2 {
t.Fatalf("sources = %#v, want base and overlay", got)
}
var effective bytes.Buffer
if err := acquired.Value.Effective.WriteEffective(&effective); err != nil {
t.Fatal(err)
}
if !strings.Contains(effective.String(), "mode: OVERLAY") || !strings.Contains(effective.String(), "defaulted: from default") {
t.Fatalf("effective configuration = %q", effective.String())
}
if explanation, ok := acquired.Value.Explanation.Explain(configkit.Keys("mode")); !ok || len(explanation.History) == 0 {
t.Fatalf("resolution explanation = %#v, present=%t", explanation, ok)
}
}
func TestConfigKitScopePreservesAbsentAndEmptySelections(t *testing.T) {
type configuration struct {
Value string `yaml:"value"`
}
var seen []commandkit.Selection
scope, err := commandkit.NewConfigKitScope[struct{}, configuration]("app", "", func(_ context.Context, selection commandkit.Selection, _ struct{}) (*configkit.Result[configuration], error) {
seen = append(seen, selection)
return configkit.Load(configkit.NoSource(), configkit.Options[configuration]{Definition: configkit.Definition[configuration]{Defaults: func() configuration { return configuration{Value: "default"} }}})
})
if err != nil {
t.Fatal(err)
}
_, err = scope.Acquire(context.Background(), commandkit.ConfigRequest{}, struct{}{})
if err != nil {
t.Fatal(err)
}
empty := commandkit.Selection{Config: commandkit.Supplied[string]{Set: true}, Overlay: commandkit.Supplied[string]{Set: true}}
_, err = scope.Acquire(context.Background(), commandkit.ConfigRequest{Selection: empty}, struct{}{})
if err != nil {
t.Fatal(err)
}
want := []commandkit.Selection{{}, empty}
if !reflect.DeepEqual(seen, want) {
t.Fatalf("loader selections = %#v, want %#v", seen, want)
}
}
func TestConfigKitScopeRequiresLoaderAndNonNilResult(t *testing.T) {
if _, err := commandkit.NewConfigKitScope[struct{}, struct{}]("app", "", nil); err == nil {
t.Fatal("accepted a nil ConfigKit loader")
}
scope, err := commandkit.NewConfigKitScope[struct{}, struct{}]("app", "", func(context.Context, commandkit.Selection, struct{}) (*configkit.Result[struct{}], error) {
return nil, nil
})
if err != nil {
t.Fatal(err)
}
if _, err := scope.Acquire(context.Background(), commandkit.ConfigRequest{}, struct{}{}); err == nil || !strings.Contains(err.Error(), "nil result") {
t.Fatalf("nil result error = %v", err)
}
}
func TestConfigKitLoadErrorRetainsIdentityAndStructuredDiagnostics(t *testing.T) {
type configuration struct {
Name string `yaml:"name"`
}
var loadError error
scope, err := commandkit.NewConfigKitScope[struct{}, configuration]("app", "", func(context.Context, commandkit.Selection, struct{}) (*configkit.Result[configuration], error) {
_, loadError = configkit.Load(configkit.Mapping("bad source", map[string]any{"unknown": true}), configkit.Options[configuration]{Definition: configkit.Definition[configuration]{}})
return nil, loadError
})
if err != nil {
t.Fatal(err)
}
acquired, acquireError := scope.Acquire(context.Background(), commandkit.ConfigRequest{}, struct{}{})
var publicLoadError *configkit.LoadError
if acquireError != loadError || !errors.As(acquireError, &publicLoadError) {
t.Fatalf("load error identity = %v (original %v), public type=%T", acquireError, loadError, acquireError)
}
if len(acquired.Diagnostics) != 1 || acquired.Diagnostics[0].Severity != configkit.SeverityError {
t.Fatalf("structured diagnostics = %#v", acquired.Diagnostics)
}
var rendered bytes.Buffer
if err := acquired.Diagnostics[0].Write(&rendered); err != nil {
t.Fatal(err)
}
if !strings.Contains(rendered.String(), `Configuration scope "app":`) || !strings.Contains(rendered.String(), `"bad source"`) {
t.Fatalf("grouped diagnostics = %q", rendered.String())
}
app, _ := commandkit.New(commandkit.Identity{Name: "example"})
if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{scope}}); err != nil {
t.Fatal(err)
}
var output, diagnostic bytes.Buffer
outcome := app.Execute(context.Background(), []string{"config", "validate"}, commandkit.Streams{Out: &output, Err: &diagnostic})
if outcome.Cause != loadError || len(outcome.Diagnostics) != 1 || strings.Count(diagnostic.String(), `Configuration scope "app":`) != 1 || output.Len() != 0 {
t.Fatalf("presented load failure outcome=%#v output=%q diagnostics=%q", outcome, output.String(), diagnostic.String())
}
}
func TestConfigValidationEmitsSuccessAsScopesCompleteAndStopsAtFailure(t *testing.T) {
app, err := commandkit.New(commandkit.Identity{Name: "example"})
if err != nil {
t.Fatal(err)
}
var calls []string
newContextCalls := 0
failure := errors.New("promptkit could not load")
suite := commandkit.ConfigSuite[string]{
NewContext: func() string { newContextCalls++; return "runtime" },
Scopes: []commandkit.ConfigScope[string]{
{Name: "zeta", Operations: []commandkit.ConfigOperation{commandkit.ConfigValidate}, Acquire: func(context.Context, commandkit.ConfigRequest, string) (commandkit.Acquired[commandkit.ConfigView], error) {
calls = append(calls, "zeta")
return commandkit.Acquired[commandkit.ConfigView]{}, nil
}},
{Name: "promptkit", Operations: []commandkit.ConfigOperation{commandkit.ConfigValidate}, Acquire: func(context.Context, commandkit.ConfigRequest, string) (commandkit.Acquired[commandkit.ConfigView], error) {
calls = append(calls, "promptkit")
return commandkit.Acquired[commandkit.ConfigView]{}, failure
}},
{Name: "app", Operations: []commandkit.ConfigOperation{commandkit.ConfigValidate}, Acquire: func(context.Context, commandkit.ConfigRequest, string) (commandkit.Acquired[commandkit.ConfigView], error) {
calls = append(calls, "app")
return commandkit.Acquired[commandkit.ConfigView]{}, nil
}},
},
}
if err := commandkit.RegisterConfig(app, suite); err != nil {
t.Fatal(err)
}
var output, diagnostic bytes.Buffer
outcome := app.Execute(context.Background(), []string{"config", "validate", "--scope", "all"}, commandkit.Streams{Out: &output, Err: &diagnostic})
if outcome.Kind != commandkit.OutcomeOperational || !errors.Is(outcome.Cause, failure) || !outcome.ReportIncomplete {
t.Fatalf("validation outcome = %#v", outcome)
}
if !reflect.DeepEqual(calls, []string{"app", "promptkit"}) || newContextCalls != 1 {
t.Fatalf("acquisitions = %v, NewContext calls=%d", calls, newContextCalls)
}
if output.String() != "app: valid\n" || !strings.Contains(diagnostic.String(), failure.Error()) {
t.Fatalf("output=%q diagnostics=%q", output.String(), diagnostic.String())
}
}
func TestConfigValidationYAMLWarningsAndShortWrites(t *testing.T) {
type configuration struct {
Name string `yaml:"name"`
}
warningScope, err := commandkit.NewConfigKitScope[struct{}, configuration]("app", "", func(context.Context, commandkit.Selection, struct{}) (*configkit.Result[configuration], error) {
return configkit.Load(configkit.NoSource(), configkit.Options[configuration]{Definition: configkit.Definition[configuration]{
Validate: func(_ *configuration, validation *configkit.Validation) {
validation.Warning(configkit.Keys("name"), "uses a demonstration name")
},
}})
})
if err != nil {
t.Fatal(err)
}
app, _ := commandkit.New(commandkit.Identity{Name: "example"})
if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{warningScope}}); err != nil {
t.Fatal(err)
}
var output, diagnostic bytes.Buffer
outcome := app.Execute(context.Background(), []string{"config", "validate", "--format", "yaml"}, commandkit.Streams{Out: &output, Err: &diagnostic})
if outcome.Kind != commandkit.OutcomeSuccess || outcome.Status != 0 || outcome.DiagnosticDelivery != commandkit.DeliveryComplete {
t.Fatalf("warning validation outcome = %#v", outcome)
}
if output.String() != "scopes:\n - name: app\n valid: true\n" || !strings.Contains(diagnostic.String(), "uses a demonstration name") {
t.Fatalf("output=%q diagnostics=%q", output.String(), diagnostic.String())
}
outcome = app.Execute(context.Background(), []string{"config", "validate"}, commandkit.Streams{Out: configKitPartialWriter{}, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeOperational || !outcome.ReportIncomplete || !errors.Is(outcome.Cause, io.ErrShortWrite) || !errors.Is(outcome.DeliveryError, io.ErrShortWrite) {
t.Fatalf("report short write outcome = %#v", outcome)
}
var delivered bytes.Buffer
outcome = app.Execute(context.Background(), []string{"config", "validate"}, commandkit.Streams{Out: &delivered, Err: configKitPartialWriter{}})
if outcome.DiagnosticDelivery != commandkit.DeliveryIncomplete || !errors.Is(outcome.DeliveryError, io.ErrShortWrite) {
t.Fatalf("diagnostic short write outcome = %#v", outcome)
}
}
type configKitPartialWriter struct{}
func (configKitPartialWriter) Write(data []byte) (int, error) {
if len(data) == 0 {
return 0, nil
}
return len(data) - 1, nil
}
func TestValidationReportDeliveryErrorsAtEachWriteBoundary(t *testing.T) {
reportErr := errors.New("report failed")
diagnosticErr := errors.New("diagnostic failed")
app, _ := commandkit.New(commandkit.Identity{Name: "example"})
acquire := func(context.Context, commandkit.ConfigRequest, struct{}) (commandkit.Acquired[commandkit.ConfigView], error) {
return commandkit.Acquired[commandkit.ConfigView]{}, nil
}
if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{
{Name: "app", Operations: []commandkit.ConfigOperation{commandkit.ConfigValidate}, Acquire: acquire},
{Name: "promptkit", Operations: []commandkit.ConfigOperation{commandkit.ConfigValidate}, Acquire: acquire},
}}); err != nil {
t.Fatal(err)
}
for _, format := range []string{"text", "yaml"} {
writes := 2
if format == "yaml" {
writes = 3
}
for at := 1; at <= writes; at++ {
for _, brokenErr := range []bool{false, true} {
out := &validationBoundaryWriter{failAt: at, err: reportErr}
errStream := io.Writer(io.Discard)
if brokenErr {
errStream = failingWriter{diagnosticErr}
}
result := app.Execute(context.Background(), []string{"config", "validate", "--scope", "all", "--format", format}, commandkit.Streams{Out: out, Err: errStream})
if result.Kind != commandkit.OutcomeOperational || !result.ReportIncomplete || !errors.Is(result.Cause, reportErr) || !errors.Is(result.DeliveryError, reportErr) || (brokenErr && !errors.Is(result.DeliveryError, diagnosticErr)) {
t.Fatalf("format=%s at=%d brokenErr=%t result=%#v", format, at, brokenErr, result)
}
if at > 1 && out.written.Len() == 0 {
t.Fatal("lost completed prefix")
}
}
}
}
failedApp, _ := commandkit.New(commandkit.Identity{Name: "example"})
if err := commandkit.RegisterConfig(failedApp, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{{Name: "app", Operations: []commandkit.ConfigOperation{commandkit.ConfigValidate}, Acquire: func(context.Context, commandkit.ConfigRequest, struct{}) (commandkit.Acquired[commandkit.ConfigView], error) {
return commandkit.Acquired[commandkit.ConfigView]{}, reportErr
}}}}); err != nil {
t.Fatal(err)
}
result := failedApp.Execute(context.Background(), []string{"config", "validate"}, commandkit.Streams{})
if !errors.Is(result.Cause, reportErr) || result.DeliveryError != nil {
t.Fatalf("acquisition failure=%#v", result)
}
}
type validationBoundaryWriter struct {
failAt, calls int
err error
written bytes.Buffer
}
func (w *validationBoundaryWriter) Write(p []byte) (int, error) {
w.calls++
if w.calls == w.failAt {
return 0, w.err
}
return w.written.Write(p)
}

View File

@@ -0,0 +1,2 @@
// Package ckconfig provides CommandKit's optional ConfigKit integration.
package ckconfig

View File

@@ -0,0 +1,14 @@
# Simple consumer
Run the maintained example with:
```sh
go run . config show
go run . greet
go run . greet --count=0 --tag=first --tag second
```
The `config` family comes from one typed ConfigKit scope. The custom `greet`
command uses CommandKit's typed bindings and shows the difference between an
omitted `--count` and an explicitly supplied zero, plus ordered repeated tags.
The configuration is synthetic and stays in memory.

View File

@@ -0,0 +1,90 @@
package main
import (
"context"
"fmt"
"os"
"strconv"
commandkit "gitea.maximumdirect.net/eric/CommandKit"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
)
type appConfiguration struct {
Greeting string `yaml:"greeting"`
}
type greetInput struct {
Count commandkit.Supplied[int]
Tags commandkit.Supplied[[]string]
}
func loadAppConfiguration(context.Context, commandkit.Selection, struct{}) (*configkit.Result[appConfiguration], error) {
return configkit.Load[appConfiguration](configkit.Bytes("simple example", []byte("greeting: hello\n")))
}
func newApp() (*commandkit.App, error) {
app, err := commandkit.New(commandkit.Identity{Name: "simple-example", Version: "1.0"})
if err != nil {
return nil, err
}
scope, err := commandkit.NewConfigKitScope[struct{}, appConfiguration]("app", "Application settings.", loadAppConfiguration)
if err != nil {
return nil, err
}
if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{Scopes: []commandkit.ConfigScope[struct{}]{scope}}); err != nil {
return nil, err
}
countDefault := 1
tagDefaults := []string{"example"}
greet, err := commandkit.NewCommand(commandkit.CommandSpec[greetInput]{
Name: "greet",
Summary: "Show flag presence and repeated values.",
Flags: []commandkit.Flag[greetInput]{
commandkit.ValueFlag[greetInput](commandkit.FlagMeta{Name: "count", Usage: "Greeting count."}, &countDefault, strconv.Atoi, func(input *greetInput, value commandkit.Supplied[int]) {
input.Count = value
}),
commandkit.RepeatFlag[greetInput](commandkit.FlagMeta{Name: "tag", Usage: "A repeatable label."}, tagDefaults, func(value string) (string, error) { return value, nil }, func(input *greetInput, value commandkit.Supplied[[]string]) {
input.Tags = value
}),
},
Run: func(invocation commandkit.Invocation, input greetInput) commandkit.Outcome {
configuration, err := loadAppConfiguration(invocation.Context, commandkit.Selection{}, struct{}{})
if err != nil {
return commandkit.Failure(err)
}
count := fmt.Sprintf("default %d (not supplied)", input.Count.Value)
if input.Count.Set {
count = fmt.Sprintf("explicit %d", input.Count.Value)
}
tags := fmt.Sprintf("default %v (not supplied)", input.Tags.Value)
if input.Tags.Set {
tags = fmt.Sprintf("explicit %v", input.Tags.Value)
}
if _, err := fmt.Fprintf(invocation.Streams.Out, "%s; count=%s; tags=%s\n", configuration.Config.Greeting, count, tags); err != nil {
return commandkit.Failure(err)
}
return commandkit.Success()
},
})
if err != nil {
return nil, err
}
if err := app.Register(greet); err != nil {
return nil, err
}
return app, nil
}
func main() {
app, err := newApp()
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
outcome := app.Execute(context.Background(), os.Args[1:], commandkit.Streams{Out: os.Stdout, Err: os.Stderr})
if outcome.Status != 0 {
os.Exit(outcome.Status)
}
}

View File

@@ -0,0 +1,230 @@
package ckconfig
import (
"bytes"
"errors"
"fmt"
"io"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
)
func newConfigExplainCommand[C any](prepared *preparedConfigSuite[C]) (Command, error) {
flags := configInputFlags(prepared.suite, "text")
flags = append(flags, ValueFlag[configInput[C]](FlagMeta{
Name: "path",
Usage: "Configuration path as a JSON Pointer; an empty value selects the root.",
Required: true,
}, nil, parseString, func(input *configInput[C], value Supplied[string]) {
input.Path = value
}))
spec := CommandSpec[configInput[C]]{
Name: string(ConfigExplain),
Summary: "Explain the effective and historical value at a configuration path.",
Flags: flags,
CheckInput: func(input configInput[C]) error {
if _, err := preflightConfig(prepared, input, ConfigExplain); err != nil {
return err
}
if !input.Path.Set {
return fmt.Errorf("--path is required")
}
if _, err := configkit.ParsePath(input.Path.Value); err != nil {
return fmt.Errorf("invalid configuration path %q: %w", input.Path.Value, err)
}
return nil
},
Run: func(invocation Invocation, input configInput[C]) Outcome {
return runConfigExplanation(invocation, prepared, input)
},
}
return NewCommand(spec)
}
type explanationReport struct {
Scope string `yaml:"scope"`
Path string `yaml:"path"`
Effective snapshotDTO `yaml:"effective"`
Origin explanationOriginDTO `yaml:"origin"`
History []explanationChangeDTO `yaml:"history"`
SecretReference *secretReferenceDTO `yaml:"secret_reference"`
}
type snapshotDTO struct {
Present bool `yaml:"present"`
Redacted bool `yaml:"redacted"`
YAML string `yaml:"yaml"`
}
type explanationOriginDTO struct {
Layer string `yaml:"layer"`
Locations []locationDTO `yaml:"locations"`
Operation string `yaml:"operation"`
Inheritance []inheritanceStepDTO `yaml:"inheritance"`
}
type inheritanceStepDTO struct {
From string `yaml:"from"`
To string `yaml:"to"`
Reference string `yaml:"reference"`
Locations []locationDTO `yaml:"locations"`
}
type explanationChangeDTO struct {
Before snapshotDTO `yaml:"before"`
After snapshotDTO `yaml:"after"`
Origin explanationOriginDTO `yaml:"origin"`
Reasons []string `yaml:"reasons"`
Dependencies []string `yaml:"dependencies"`
}
type secretReferenceDTO struct {
Name string `yaml:"name"`
}
func runConfigExplanation[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, ConfigExplain)
if err != nil {
return Failure(err)
}
if !input.Path.Set {
return Failure(fmt.Errorf("--path is required"))
}
path, err := configkit.ParsePath(input.Path.Value)
if err != nil {
return Usage(fmt.Errorf("invalid configuration path %q: %w", input.Path.Value, err))
}
name := scopeNames[0]
contexts := contextsForScopes(prepared, input, scopeNames)
scope, ok := findConfigScope(prepared, name)
if !ok || scope.Acquire == nil {
return Failure(fmt.Errorf("configuration scope %q has no acquisition callback", name))
}
if err := invocation.Context.Err(); err != nil {
return explanationFailure(err, nil, false)
}
acquired, acquireErr := scope.Acquire(invocation.Context, ConfigRequest{Operation: ConfigExplain, Selection: input.Selection}, contexts[name])
diagnostics := append([]Diagnostic(nil), acquired.Diagnostics...)
if acquireErr != nil {
return explanationFailure(acquireErr, diagnostics, false)
}
if err := invocation.Context.Err(); err != nil {
return explanationFailure(err, diagnostics, false)
}
if acquired.Value.Explanation == nil {
return explanationFailure(fmt.Errorf("configuration scope %q did not provide an explanation reader", name), diagnostics, false)
}
explanation, found := acquired.Value.Explanation.Explain(path)
if !found {
return explanationFailure(fmt.Errorf("configuration path %q is not known in scope %q", path.String(), name), diagnostics, false)
}
if err := invocation.Context.Err(); err != nil {
return explanationFailure(err, diagnostics, false)
}
if input.Format != "yaml" {
return writeNativeExplanation(invocation.Streams.Out, acquired.Value.Explanation, path, diagnostics)
}
var report bytes.Buffer
if err := encodeYAML(&report, makeExplanationReport(name, explanation)); err != nil {
return explanationFailure(err, diagnostics, false)
}
if err := invocation.Context.Err(); err != nil {
return explanationFailure(err, diagnostics, false)
}
if err := writeReport(invocation.Streams.Out, report.Bytes()); err != nil {
outcome := explanationFailure(err, diagnostics, true)
outcome.DeliveryError = err
return outcome
}
outcome := Success()
outcome.Diagnostics = diagnostics
return outcome
}
func writeNativeExplanation(writer io.Writer, reader ExplanationReader, path configkit.Path, diagnostics []Diagnostic) Outcome {
checked := &checkedWriter{writer: writer}
renderErr := reader.WriteExplanation(checked, path)
if checked.failure != nil {
if renderErr == nil {
renderErr = checked.failure
} else {
renderErr = errors.Join(renderErr, checked.failure)
}
}
if renderErr != nil {
outcome := explanationFailure(renderErr, diagnostics, true)
outcome.DeliveryError = checked.failure
return outcome
}
outcome := Success()
outcome.Diagnostics = diagnostics
return outcome
}
func makeExplanationReport(scope string, explanation configkit.Explanation) explanationReport {
report := explanationReport{
Scope: scope,
Path: explanation.Path.String(),
Effective: snapshotReport(explanation.Effective),
Origin: originReport(explanation.Origin),
History: make([]explanationChangeDTO, 0, len(explanation.History)),
}
for _, change := range explanation.History {
entry := explanationChangeDTO{
Before: snapshotReport(change.Before),
After: snapshotReport(change.After),
Origin: originReport(change.Origin),
Reasons: append([]string{}, change.Reasons...),
Dependencies: make([]string, 0, len(change.Dependencies)),
}
for _, dependency := range change.Dependencies {
entry.Dependencies = append(entry.Dependencies, dependency.String())
}
report.History = append(report.History, entry)
}
if explanation.SecretReference != nil {
report.SecretReference = &secretReferenceDTO{Name: explanation.SecretReference.Name}
}
return report
}
func snapshotReport(snapshot configkit.ValueSnapshot) snapshotDTO {
return snapshotDTO{Present: snapshot.Present, Redacted: snapshot.Redacted, YAML: snapshot.YAML}
}
func originReport(origin configkit.Origin) explanationOriginDTO {
report := explanationOriginDTO{
Layer: string(origin.Layer),
Locations: locationReports(origin.Locations),
Operation: string(origin.Operation),
Inheritance: make([]inheritanceStepDTO, 0, len(origin.Inheritance)),
}
for _, step := range origin.Inheritance {
report.Inheritance = append(report.Inheritance, inheritanceStepDTO{
From: step.From.String(),
To: step.To.String(),
Reference: step.Reference.String(),
Locations: locationReports(step.Locations),
})
}
return report
}
func locationReports(locations []configkit.Location) []locationDTO {
report := make([]locationDTO, 0, len(locations))
for _, location := range locations {
report = append(report, locationDTO{SourceID: location.SourceID, Line: location.Line, Column: location.Column})
}
return report
}
func explanationFailure(cause error, diagnostics []Diagnostic, reportStarted bool) Outcome {
outcome := Failure(cause)
outcome.Diagnostics = diagnostics
outcome.ReportIncomplete = reportStarted
return outcome
}

View File

@@ -0,0 +1,9 @@
module gitea.maximumdirect.net/eric/CommandKit/integrations/ckconfig
go 1.26.0
require (
gitea.maximumdirect.net/eric/CommandKit v0.2.0
gitea.maximumdirect.net/eric/ConfigKit v0.5.0
go.yaml.in/yaml/v4 v4.0.0-rc.6
)

View File

@@ -0,0 +1,200 @@
package ckconfig
import (
"context"
"fmt"
"io"
"strings"
"testing"
)
var performanceFramedBytes []byte
func BenchmarkPerformanceYAMLFraming(b *testing.B) {
fixtures := []struct {
name string
scope string
native []byte
}{
{name: "4k_ordinary", scope: "app", native: performanceNativeDocument(4<<10, false)},
{name: "1m_directive", scope: "promptkit", native: performanceNativeDocument(1<<20, true)},
}
for _, fixture := range fixtures {
b.Run("framing/"+fixture.name, func(b *testing.B) {
framed, hasDirective := frameEffectiveYAML(fixture.scope, fixture.native)
if len(framed) <= len(fixture.native) || hasDirective != strings.Contains(fixture.name, "directive") {
b.Fatalf("framing fixture result has unexpected size or directive classification: native=%d framed=%d", len(fixture.native), len(framed))
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
performanceFramedBytes, _ = frameEffectiveYAML(fixture.scope, fixture.native)
}
})
b.Run("aggregate/"+fixture.name, func(b *testing.B) {
app := performanceShowApp(b, fixture.native, fixture.native)
args := []string{"config", "show", "--scope", "all"}
verifyConfigReport(b, app, args, len(fixture.native)*2)
streams := Streams{Out: io.Discard, Err: io.Discard}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if outcome := app.Execute(context.Background(), args, streams); outcome.Kind != OutcomeSuccess {
b.Fatalf("aggregate config show outcome = %#v", outcome)
}
}
})
}
single := performanceNativeDocument(4<<10, false)
app := performanceShowApp(b, single)
args := []string{"config", "show"}
b.Run("single_scope/4k_ordinary", func(b *testing.B) {
verifyConfigReport(b, app, args, len(single))
streams := Streams{Out: io.Discard, Err: io.Discard}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if outcome := app.Execute(context.Background(), args, streams); outcome.Kind != OutcomeSuccess {
b.Fatalf("single-scope config show outcome = %#v", outcome)
}
}
})
}
func BenchmarkPerformanceConfigScopes(b *testing.B) {
for _, scopeCount := range []int{2, 32} {
suite := performanceConfigSuite(scopeCount, []ConfigOperation{ConfigValidate, ConfigShow})
app := performanceConfigApp(b, suite)
selected := fmt.Sprintf("scope%02d", scopeCount-1)
for _, operation := range []struct {
name string
args []string
}{
{name: "discovery", args: []string{"config", "scopes"}},
{name: "named_selection", args: []string{"config", "validate", "--scope", selected}},
{name: "aggregate_validation", args: []string{"config", "validate", "--scope", "all"}},
} {
b.Run(fmt.Sprintf("%s/scopes_%d", operation.name, scopeCount), func(b *testing.B) {
verifyConfigReport(b, app, operation.args, 1)
streams := Streams{Out: io.Discard, Err: io.Discard}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if outcome := app.Execute(context.Background(), operation.args, streams); outcome.Kind != OutcomeSuccess {
b.Fatalf("configuration operation outcome = %#v", outcome)
}
}
})
}
}
}
func BenchmarkPerformanceConfigRegistration(b *testing.B) {
for _, selectorCount := range []int{4, 64} {
suite := performanceSelectorSuite(selectorCount)
performanceConfigApp(b, suite)
b.Run(fmt.Sprintf("selectors_%d", selectorCount), func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
app, err := New(Identity{Name: "bench"})
if err != nil {
b.Fatal(err)
}
if err := RegisterConfig(app, suite); err != nil {
b.Fatal(err)
}
}
})
}
}
func performanceNativeDocument(targetBytes int, directive bool) []byte {
preamble := "# generated native configuration\n"
if directive {
preamble = "%YAML 1.2\n---\n"
}
var body strings.Builder
body.Grow(targetBytes)
for index := 0; len(preamble)+body.Len() < targetBytes; index++ {
fmt.Fprintf(&body, "key%06d: value\n", index)
}
return []byte(preamble + body.String())
}
type performanceEffectiveWriter []byte
func (document performanceEffectiveWriter) WriteEffective(writer io.Writer) error {
_, err := writer.Write(document)
return err
}
func performanceShowApp(tb testing.TB, documents ...[]byte) *App {
tb.Helper()
scopes := make([]ConfigScope[struct{}], len(documents))
for index, document := range documents {
name := "app"
if index > 0 {
name = fmt.Sprintf("scope%02d", index)
}
writer := performanceEffectiveWriter(document)
scopes[index] = ConfigScope[struct{}]{
Name: name,
Operations: []ConfigOperation{ConfigShow},
Acquire: func(context.Context, ConfigRequest, struct{}) (Acquired[ConfigView], error) {
return Acquired[ConfigView]{Value: ConfigView{Effective: writer}}, nil
},
}
}
suite := ConfigSuite[struct{}]{Scopes: scopes}
return performanceConfigApp(tb, suite)
}
func performanceConfigSuite(scopeCount int, operations []ConfigOperation) ConfigSuite[struct{}] {
scopes := make([]ConfigScope[struct{}], scopeCount)
for index := range scopes {
name := "app"
if index > 0 {
name = fmt.Sprintf("scope%02d", index)
}
scopes[index] = ConfigScope[struct{}]{
Name: name,
Operations: append([]ConfigOperation(nil), operations...),
Acquire: func(context.Context, ConfigRequest, struct{}) (Acquired[ConfigView], error) {
return Acquired[ConfigView]{}, nil
},
}
}
return ConfigSuite[struct{}]{Scopes: scopes}
}
func performanceSelectorSuite(selectorCount int) ConfigSuite[struct{}] {
suite := performanceConfigSuite(1, []ConfigOperation{ConfigValidate})
suite.Selectors = make([]Selector[struct{}], selectorCount)
for index := range suite.Selectors {
flag := ValueFlag(FlagMeta{Name: fmt.Sprintf("selector%02d", index)}, (*string)(nil), parseString, func(*struct{}, Supplied[string]) {})
suite.Selectors[index] = Selector[struct{}]{Binding: flag, Scopes: []string{"app"}}
}
return suite
}
func performanceConfigApp(tb testing.TB, suite ConfigSuite[struct{}]) *App {
tb.Helper()
app, err := New(Identity{Name: "bench"})
if err != nil {
tb.Fatal(err)
}
if err := RegisterConfig(app, suite); err != nil {
tb.Fatal(err)
}
return app
}
func verifyConfigReport(tb testing.TB, app *App, args []string, minimumBytes int) {
tb.Helper()
var output bytes.Buffer
outcome := app.Execute(context.Background(), args, Streams{Out: &output, Err: io.Discard})
if outcome.Kind != OutcomeSuccess || output.Len() < minimumBytes {
tb.Fatalf("configuration report outcome = %#v, output bytes %d, want at least %d", outcome, output.Len(), minimumBytes)
}
}

View File

@@ -0,0 +1,228 @@
package ckconfig
import (
"fmt"
"sort"
"strings"
)
func configInputFlags[C any](suite ConfigSuite[C], formatDefault string) []Flag[configInput[C]] {
scopeDefault := "app"
flags := []Flag[configInput[C]]{
ValueFlag[configInput[C]](FlagMeta{Name: "scope", Usage: "Configuration scope name or all where supported."}, &scopeDefault, parseString, func(input *configInput[C], value Supplied[string]) {
input.Scope = value.Value
}),
ValueFlag[configInput[C]](FlagMeta{Name: "format", Usage: "Output format."}, &formatDefault, parseString, func(input *configInput[C], value Supplied[string]) {
input.Format = value.Value
}),
}
flags = append(flags, configurationSelectionFlags(
func(input *configInput[C], value Supplied[string]) { input.Selection.Config = value },
func(input *configInput[C], value Supplied[string]) { input.Selection.Overlay = value },
)...)
for _, selector := range suite.Selectors {
flags = append(flags, adaptSelector(selector))
}
return flags
}
func adaptSelector[C any](selector Selector[C]) Flag[configInput[C]] {
flag := adaptContextSelector(selector,
func(input *configInput[C]) *C { return &input.SelectorContext },
func(input *configInput[C], state selectorState[C]) { input.selectors = append(input.selectors, state) },
)
if flag.binding.meta.Required {
flag.binding.meta.Required = false // Scope-aware preflight enforces this requirement.
flag.binding.meta.Usage += " (required where applicable)"
}
return flag
}
func adaptContextSelector[C any, I any](selector Selector[C], selectContext func(*I) *C, record func(*I, selectorState[C])) Flag[I] {
binding := selector.Binding.binding
meta := binding.meta
if len(selector.Scopes) == 0 {
meta.Usage += " (applies to all scopes)"
} else {
meta.Usage += " (applies to: " + joinNames(selector.Scopes) + ")"
}
return Flag[I]{binding: flagBinding[I]{
meta: meta,
defaultText: binding.defaultText,
repeated: binding.repeated,
boolean: binding.boolean,
valid: binding.valid,
prepare: func(values []string) (func(*I), error) {
apply, err := binding.prepare(values)
if err != nil {
return nil, err
}
state := selectorState[C]{
name: binding.meta.Name,
required: binding.meta.Required,
scopes: append([]string(nil), selector.Scopes...),
supplied: len(values) > 0,
apply: apply,
clear: binding.clear,
}
return func(input *I) {
apply(selectContext(input))
record(input, state)
}, nil
},
}}
}
func configurationSelectionFlags[I any](setConfig, setOverlay func(*I, Supplied[string])) []Flag[I] {
return []Flag[I]{
ValueFlag[I](FlagMeta{Name: "config", Usage: "Configuration source."}, nil, parseString, setConfig),
ValueFlag[I](FlagMeta{Name: "overlay", Usage: "Overlay source."}, nil, parseString, setOverlay),
}
}
func parseString(value string) (string, error) { return value, nil }
func preflightConfig[C any](prepared *preparedConfigSuite[C], input configInput[C], operation ConfigOperation) ([]string, error) {
scopes, err := selectConfigScopes(prepared, input, operation)
if err != nil {
return nil, err
}
if prepared.suite.CheckInput != nil {
if err := prepared.suite.CheckInput(input.Selection, input.SelectorContext); err != nil {
return nil, err
}
}
return scopes, nil
}
func selectConfigScopes[C any](prepared *preparedConfigSuite[C], input configInput[C], operation ConfigOperation) ([]string, error) {
if err := checkConfigFormat(input.Format); err != nil {
return nil, err
}
if operation == ConfigShow && input.Format != "yaml" {
return nil, fmt.Errorf("unsupported configuration show format %q", input.Format)
}
if input.Scope == "all" {
if operation != ConfigValidate && operation != ConfigShow {
return nil, fmt.Errorf("scope all is supported only for validate and show")
}
} else if input.Scope == "" {
return nil, fmt.Errorf("configuration scope must not be empty")
}
scopes := make([]string, 0, len(prepared.ordered))
if input.Scope == "all" {
for _, scope := range prepared.ordered {
scopes = append(scopes, scope.Name)
}
} else {
scope, ok := prepared.byName[input.Scope]
if !ok {
choices := make([]string, 0, len(prepared.ordered))
for _, available := range prepared.ordered {
choices = append(choices, available.Name)
}
return nil, fmt.Errorf("unknown configuration scope %q (available: %s)", input.Scope, joinStrings(choices))
}
if !containsOperation(scope.Operations, operation) {
return nil, unsupportedConfigOperation(scope, operation)
}
scopes = append(scopes, scope.Name)
}
if input.Scope == "all" {
for _, scopeName := range scopes {
scope := prepared.byName[scopeName]
if !containsOperation(scope.Operations, operation) {
return nil, unsupportedConfigOperation(scope, operation)
}
}
}
for _, selector := range input.selectors {
applies := false
for _, scopeName := range scopes {
if selectorApplies(selector.scopes, scopeName) {
applies = true
break
}
}
if applies && selector.required && !selector.supplied {
return nil, fmt.Errorf("required flag --%s was not supplied", selector.name)
}
if selector.supplied && !applies {
return nil, fmt.Errorf("selector is not applicable to configuration scope %q", input.Scope)
}
}
return scopes, nil
}
func contextsForScopes[C any](prepared *preparedConfigSuite[C], input configInput[C], scopeNames []string) map[string]C {
base := *new(C)
if prepared.suite.NewContext != nil {
base = prepared.suite.NewContext()
}
contexts := make(map[string]C, len(scopeNames))
for _, name := range scopeNames {
context := base
for _, selector := range input.selectors {
if selectorApplies(selector.scopes, name) {
selector.apply(&context)
} else {
selector.clear(&context)
}
}
contexts[name] = context
}
return contexts
}
func findConfigScope[C any](prepared *preparedConfigSuite[C], name string) (ConfigScope[C], bool) {
scope, ok := prepared.byName[name]
return scope, ok
}
func containsOperation(operations []ConfigOperation, requested ConfigOperation) bool {
for _, operation := range operations {
if operation == requested {
return true
}
}
return false
}
func unsupportedConfigOperation[C any](scope ConfigScope[C], operation ConfigOperation) error {
return fmt.Errorf("configuration scope %q does not support %s (available: %s)", scope.Name, operation, joinOperations(scope.Operations))
}
func selectorApplies(scopes []string, name string) bool {
if len(scopes) == 0 {
return true
}
for _, scope := range scopes {
if scope == name {
return true
}
}
return false
}
func joinNames(names []string) string {
ordered := append([]string(nil), names...)
sort.Strings(ordered)
return joinStrings(ordered)
}
func joinOperations(operations []ConfigOperation) string {
values := make([]string, len(operations))
for index, operation := range operations {
values[index] = string(operation)
}
return joinStrings(values)
}
func joinStrings(values []string) string {
if len(values) == 0 {
return "none"
}
return strings.Join(values, ", ")
}

View File

@@ -0,0 +1,217 @@
package ckconfig
import (
"bytes"
"errors"
"fmt"
"strconv"
"unicode"
)
func newConfigShowCommand[C any](prepared *preparedConfigSuite[C]) (Command, error) {
spec := CommandSpec[configInput[C]]{
Name: string(ConfigShow),
Summary: "Show effective configuration as YAML.",
Flags: configInputFlags(prepared.suite, "yaml"),
CheckInput: func(input configInput[C]) error {
_, err := preflightConfig(prepared, input, ConfigShow)
return err
},
Run: func(invocation Invocation, input configInput[C]) Outcome {
return runConfigShow(invocation, prepared, input)
},
}
return NewCommand(spec)
}
type effectiveDocument struct {
name string
view ConfigView
}
func runConfigShow[C any](invocation Invocation, prepared *preparedConfigSuite[C], input configInput[C]) Outcome {
if err := invocation.Context.Err(); err != nil {
return Failure(err)
}
scopeNames, err := selectConfigScopes(prepared, input, ConfigShow)
if err != nil {
return Failure(err)
}
contexts := contextsForScopes(prepared, input, scopeNames)
documents := make([]effectiveDocument, 0, len(scopeNames))
diagnostics := make([]Diagnostic, 0)
for _, name := range scopeNames {
scope, ok := findConfigScope(prepared, name)
if !ok || scope.Acquire == nil {
return showFailure(fmt.Errorf("configuration scope %q has no acquisition callback", name), diagnostics, false)
}
if err := invocation.Context.Err(); err != nil {
return showFailure(err, diagnostics, false)
}
acquired, acquireErr := scope.Acquire(invocation.Context, ConfigRequest{Operation: ConfigShow, Selection: input.Selection}, contexts[name])
diagnostics = append(diagnostics, acquired.Diagnostics...)
if acquireErr != nil {
return showFailure(acquireErr, diagnostics, false)
}
if err := invocation.Context.Err(); err != nil {
return showFailure(err, diagnostics, false)
}
if acquired.Value.Effective == nil {
return showFailure(fmt.Errorf("configuration scope %q did not provide an effective configuration writer", name), diagnostics, false)
}
documents = append(documents, effectiveDocument{name: name, view: acquired.Value})
}
if err := invocation.Context.Err(); err != nil {
return showFailure(err, diagnostics, false)
}
if len(documents) == 1 && input.Scope != "all" {
checked := &checkedWriter{writer: invocation.Streams.Out}
renderErr := documents[0].view.Effective.WriteEffective(checked)
if checked.failure != nil {
if renderErr == nil {
renderErr = checked.failure
} else {
renderErr = errors.Join(renderErr, checked.failure)
}
}
if renderErr != nil {
outcome := showFailure(renderErr, diagnostics, true)
outcome.DeliveryError = checked.failure
return outcome
}
outcome := Success()
outcome.Diagnostics = diagnostics
return outcome
}
written := false
for index, document := range documents {
if err := invocation.Context.Err(); err != nil {
return showFailure(err, diagnostics, written)
}
var native bytes.Buffer
if err := document.view.Effective.WriteEffective(&native); err != nil {
return showFailure(err, diagnostics, written)
}
if err := invocation.Context.Err(); err != nil {
return showFailure(err, diagnostics, written)
}
framed, hasDirective := frameEffectiveYAML(document.name, native.Bytes())
if index > 0 && hasDirective {
if err := writeReport(invocation.Streams.Out, []byte("...\n")); err != nil {
outcome := showFailure(err, diagnostics, true)
outcome.DeliveryError = err
return outcome
}
}
if index < len(documents)-1 && len(framed) > 0 && framed[len(framed)-1] != '\n' {
framed = append(framed, '\n')
}
if err := writeReport(invocation.Streams.Out, framed); err != nil {
outcome := showFailure(err, diagnostics, true)
outcome.DeliveryError = err
return outcome
}
written = true
}
outcome := Success()
outcome.Diagnostics = diagnostics
return outcome
}
type yamlPreamble struct {
markerStart int
markerEnd int
markerFound bool
inlineMarker bool
preambleEnd int
hasDirective bool
}
func analyzeYAMLPreamble(native []byte) yamlPreamble {
var analysis yamlPreamble
for start := 0; start < len(native); {
end := len(native)
if newline := bytes.IndexByte(native[start:], '\n'); newline >= 0 {
end = start + newline + 1
}
lineEnd := end
if lineEnd > start && native[lineEnd-1] == '\n' {
lineEnd--
}
trimmed := bytes.TrimSpace(native[start:lineEnd])
if isYAMLDocumentStart(trimmed) {
content := bytes.TrimSpace(trimmed[3:])
analysis.markerStart = start
analysis.markerEnd = end
analysis.markerFound = true
analysis.inlineMarker = len(content) > 0 && content[0] != '#'
return analysis
}
if bytes.HasPrefix(trimmed, []byte("%YAML ")) || bytes.HasPrefix(trimmed, []byte("%TAG ")) {
analysis.hasDirective = true
}
if len(trimmed) == 0 || trimmed[0] == '#' || bytes.HasPrefix(trimmed, []byte("%YAML ")) || bytes.HasPrefix(trimmed, []byte("%TAG ")) {
analysis.preambleEnd = end
} else {
return analysis
}
start = end
}
return analysis
}
func frameEffectiveYAML(scope string, native []byte) ([]byte, bool) {
analysis := analyzeYAMLPreamble(native)
label := scope
for _, r := range scope {
if unicode.IsControl(r) {
label = strconv.Quote(scope)
break
}
}
comment := []byte("# scope: " + label + "\n")
if analysis.markerFound {
out := make([]byte, 0, len(native)+len(comment))
// Inline content may be a block scalar; place the label before
// its marker rather than interrupting the native value.
if analysis.inlineMarker {
out = append(out, native[:analysis.markerStart]...)
out = append(out, comment...)
out = append(out, native[analysis.markerStart:]...)
return out, analysis.hasDirective
}
out = append(out, native[:analysis.markerEnd]...)
if analysis.markerEnd > analysis.markerStart && native[analysis.markerEnd-1] != '\n' {
out = append(out, '\n')
}
out = append(out, comment...)
out = append(out, native[analysis.markerEnd:]...)
return out, analysis.hasDirective
}
result := make([]byte, 0, len(native)+len(comment)+5)
if analysis.hasDirective {
result = append(result, native[:analysis.preambleEnd]...)
}
result = append(result, []byte("---\n")...)
result = append(result, comment...)
if analysis.hasDirective {
result = append(result, native[analysis.preambleEnd:]...)
} else {
result = append(result, native...)
}
return result, analysis.hasDirective
}
func showFailure(cause error, diagnostics []Diagnostic, reportStarted bool) Outcome {
outcome := Failure(cause)
outcome.Diagnostics = diagnostics
outcome.ReportIncomplete = reportStarted
return outcome
}
func isYAMLDocumentStart(line []byte) bool {
return bytes.HasPrefix(line, []byte("---")) && (len(line) == 3 || line[3] == ' ' || line[3] == '\t')
}

View File

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

View File

@@ -0,0 +1,141 @@
package ckconfig
import (
"bytes"
"fmt"
"strconv"
)
func newConfigSourcesCommand[C any](prepared *preparedConfigSuite[C]) (Command, error) {
spec := CommandSpec[configInput[C]]{
Name: string(ConfigSources),
Summary: "List configuration sources used by a scope.",
Flags: configInputFlags(prepared.suite, "text"),
CheckInput: func(input configInput[C]) error {
_, err := preflightConfig(prepared, input, ConfigSources)
return err
},
Run: func(invocation Invocation, input configInput[C]) Outcome {
return runConfigSources(invocation, prepared, input)
},
}
return NewCommand(spec)
}
type sourceReport struct {
Scope string `yaml:"scope"`
Sources []sourceDTO `yaml:"sources"`
}
type sourceDTO struct {
ID string `yaml:"id"`
Label string `yaml:"label"`
Path string `yaml:"path"`
Layer string `yaml:"layer"`
Kind string `yaml:"kind"`
UnderlayIndex int `yaml:"underlay_index"`
ImportedFrom *locationDTO `yaml:"imported_from"`
}
type locationDTO struct {
SourceID string `yaml:"source_id"`
Line int `yaml:"line"`
Column int `yaml:"column"`
}
func runConfigSources[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, ConfigSources)
if err != nil {
return Failure(err)
}
contexts := contextsForScopes(prepared, input, scopeNames)
name := scopeNames[0]
scope, ok := findConfigScope(prepared, name)
if !ok || scope.Acquire == nil {
return sourceReportFailure(fmt.Errorf("configuration scope %q has no acquisition callback", name), nil, false)
}
if err := invocation.Context.Err(); err != nil {
return sourceReportFailure(err, nil, false)
}
acquired, acquireErr := scope.Acquire(invocation.Context, ConfigRequest{Operation: ConfigSources, Selection: input.Selection}, contexts[name])
diagnostics := append([]Diagnostic(nil), acquired.Diagnostics...)
if acquireErr != nil {
return sourceReportFailure(acquireErr, diagnostics, false)
}
if err := invocation.Context.Err(); err != nil {
return sourceReportFailure(err, diagnostics, false)
}
if acquired.Value.Sources == nil {
return sourceReportFailure(fmt.Errorf("configuration scope %q did not provide a source reader", name), diagnostics, false)
}
sources := acquired.Value.Sources.Sources()
document := sourceReport{Scope: name, Sources: make([]sourceDTO, 0, len(sources))}
for _, source := range sources {
item := sourceDTO{
ID: source.ID,
Label: source.Label,
Path: source.Path,
Layer: string(source.Layer),
Kind: string(source.Kind),
UnderlayIndex: source.UnderlayIndex,
}
if source.ImportedFrom != nil {
item.ImportedFrom = &locationDTO{SourceID: source.ImportedFrom.SourceID, Line: source.ImportedFrom.Line, Column: source.ImportedFrom.Column}
}
document.Sources = append(document.Sources, item)
}
var report bytes.Buffer
if input.Format == "yaml" {
if err := encodeYAML(&report, document); err != nil {
return sourceReportFailure(err, diagnostics, false)
}
} else {
writeSourceReportText(&report, document)
}
if err := invocation.Context.Err(); err != nil {
return sourceReportFailure(err, diagnostics, false)
}
if err := writeReport(invocation.Streams.Out, report.Bytes()); err != nil {
outcome := sourceReportFailure(err, diagnostics, true)
outcome.DeliveryError = err
return outcome
}
outcome := Success()
outcome.Diagnostics = diagnostics
return outcome
}
func writeSourceReportText(writer *bytes.Buffer, document sourceReport) {
_, _ = fmt.Fprintf(writer, "Scope: %s\n", strconv.Quote(document.Scope))
if len(document.Sources) == 0 {
_, _ = writer.WriteString("Sources: []\n")
return
}
_, _ = writer.WriteString("Sources:\n")
for _, source := range document.Sources {
_, _ = fmt.Fprintf(writer, " - id: %s\n", strconv.Quote(source.ID))
_, _ = fmt.Fprintf(writer, " label: %s\n", strconv.Quote(source.Label))
_, _ = fmt.Fprintf(writer, " path: %s\n", strconv.Quote(source.Path))
_, _ = fmt.Fprintf(writer, " layer: %s\n", strconv.Quote(source.Layer))
_, _ = fmt.Fprintf(writer, " kind: %s\n", strconv.Quote(source.Kind))
_, _ = fmt.Fprintf(writer, " underlay_index: %d\n", source.UnderlayIndex)
if source.ImportedFrom == nil {
_, _ = writer.WriteString(" imported_from: null\n")
} else {
_, _ = fmt.Fprintf(writer, " imported_from: {source_id: %s, line: %d, column: %d}\n",
strconv.Quote(source.ImportedFrom.SourceID), source.ImportedFrom.Line, source.ImportedFrom.Column)
}
}
}
func sourceReportFailure(cause error, diagnostics []Diagnostic, reportStarted bool) Outcome {
outcome := Failure(cause)
outcome.Diagnostics = diagnostics
outcome.ReportIncomplete = reportStarted
return outcome
}

View File

@@ -0,0 +1,118 @@
package ckconfig
import (
"bytes"
"fmt"
"strings"
"go.yaml.in/yaml/v4"
)
func configOperationDeclared[C any](scopes []ConfigScope[C], operation ConfigOperation) bool {
for _, scope := range scopes {
if containsOperation(scope.Operations, operation) {
return true
}
}
return false
}
func newConfigValidateCommand[C any](prepared *preparedConfigSuite[C]) (Command, error) {
spec := CommandSpec[configInput[C]]{
Name: string(ConfigValidate),
Summary: "Validate selected configuration scopes.",
Flags: configInputFlags(prepared.suite, "text"),
CheckInput: func(input configInput[C]) error {
_, err := preflightConfig(prepared, input, ConfigValidate)
return err
},
Run: func(invocation Invocation, input configInput[C]) Outcome {
return runConfigValidation(invocation, prepared, input)
},
}
return NewCommand(spec)
}
func runConfigValidation[C any](invocation Invocation, prepared *preparedConfigSuite[C], input configInput[C]) Outcome {
if err := invocation.Context.Err(); err != nil {
return Failure(err)
}
scopeNames, err := selectConfigScopes(prepared, input, ConfigValidate)
if err != nil {
return Failure(err)
}
contexts := contextsForScopes(prepared, input, scopeNames)
diagnostics := make([]Diagnostic, 0)
writtenRecord := false
for _, name := range scopeNames {
scope, ok := findConfigScope(prepared, name)
if !ok || scope.Acquire == nil {
return validationFailure(fmt.Errorf("configuration scope %q has no acquisition callback", name), diagnostics, writtenRecord)
}
if err := invocation.Context.Err(); err != nil {
return validationFailure(err, diagnostics, writtenRecord)
}
acquired, acquireErr := scope.Acquire(invocation.Context, ConfigRequest{Operation: ConfigValidate, Selection: input.Selection}, contexts[name])
diagnostics = append(diagnostics, acquired.Diagnostics...)
if acquireErr != nil {
return validationFailure(acquireErr, diagnostics, writtenRecord)
}
if err := invocation.Context.Err(); err != nil {
return validationFailure(err, diagnostics, writtenRecord)
}
record, err := validationSuccessRecord(name, input.Format)
if err != nil {
return validationFailure(err, diagnostics, writtenRecord)
}
if input.Format == "yaml" && !writtenRecord {
if err := writeReport(invocation.Streams.Out, []byte("scopes:\n")); err != nil {
outcome := validationFailure(err, diagnostics, true)
outcome.DeliveryError = err
return outcome
}
}
if err := writeReport(invocation.Streams.Out, record); err != nil {
outcome := validationFailure(err, diagnostics, true)
outcome.DeliveryError = err
return outcome
}
writtenRecord = true
}
outcome := Success()
outcome.Diagnostics = diagnostics
return outcome
}
func validationFailure(cause error, diagnostics []Diagnostic, reportStarted bool) Outcome {
outcome := Failure(cause)
outcome.Diagnostics = diagnostics
outcome.ReportIncomplete = reportStarted
return outcome
}
func validationSuccessRecord(scope, format string) ([]byte, error) {
if format == "text" {
return []byte(scope + ": valid\n"), nil
}
encoded, err := yaml.Marshal(struct {
Name string `yaml:"name"`
Valid bool `yaml:"valid"`
}{Name: scope, Valid: true})
if err != nil {
return nil, err
}
lines := strings.Split(strings.TrimSuffix(string(encoded), "\n"), "\n")
var record bytes.Buffer
for index, line := range lines {
if index == 0 {
record.WriteString(" - ")
} else {
record.WriteString(" ")
}
record.WriteString(line)
record.WriteByte('\n')
}
return record.Bytes(), nil
}