Add ConfigKit scope contracts and adapters

This commit is contained in:
2026-10-11 01:30:18 +00:00
parent 7e5f84bf16
commit 1e1eca4a88
5 changed files with 401 additions and 465 deletions

View File

@@ -1,6 +1,6 @@
# Implement the optional integration modules and captured registration API
Status: implementation in progress. Stages 1 through 7 are complete; subsequent work follows the ordered roadmap.
Status: implementation in progress. Stages 1 through 8 are complete; subsequent work follows the ordered roadmap.
Execute the stages in order. Each stage is sized as a separate Luna coding-agent
assignment. This document authorizes no publication or changes to sibling
repositories.
@@ -392,9 +392,17 @@ snapshot-isolation guarantee is added.
core-owned severity. Preserve diagnostics returned with failures and appropriate
warning/error behavior; caller-owned effects remain caller-owned.
**Completion evidence:** adapter metadata without loader calls, native view facets,
nil original loaders, retained scope causes after repeated attachments/narrowing,
diagnostic provenance and warning/error mapping, and public signature ownership.
**Completion evidence — complete:** ckconfig now owns the selected typed
contracts, with ConfigKit paths and records confined to the integration module.
`WithScopeError` copies retained causes without mutating the original and keeps
wrapping identity through repeated attachment and operation narrowing. The
ConfigKit adapter declares all four capabilities without loading during
construction, retains original nil-loader issues, passes context/selection/input/
state to the loader, and exposes the result's native effective/source/explanation
facets directly. Failed ConfigKit loads retain their original error and structured
diagnostics; diagnostic severity and source legends use core severity and native
ConfigKit writing. Focused adapter tests pass. The complete ckconfig suite remains
for the later family and report implementation stages.
## Stage 9: Assemble configuration families and prepare selected inputs

View File

@@ -1,461 +0,0 @@
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,101 @@
package ckconfig
import (
"context"
"errors"
"fmt"
"io"
"strconv"
commandkit "gitea.maximumdirect.net/eric/CommandKit"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
)
// FromConfigKit adapts a typed ConfigKit loader to all four native
// configuration operations. It does not invoke load during construction. A
// nil original loader is retained as a construction issue, even if callers
// later narrow the scope's operation list.
func FromConfigKit[I, S, T any](
name, summary string,
load func(context.Context, Selection, I, S) (*configkit.Result[T], error),
) Scope[I, S] {
scope := Scope[I, S]{
Name: name,
Summary: summary,
Operations: []Operation{Validate, Show, Sources, Explain},
}
if load == nil {
return WithScopeError(scope, fmt.Errorf("ConfigKit scope %q loader must not be nil", name))
}
scope.Acquire = func(ctx context.Context, request Request, input I, state S) (commandkit.Acquired[View], error) {
result, err := load(ctx, request.Selection, input, state)
if err != nil {
var loadError *configkit.LoadError
if errors.As(err, &loadError) && loadError != nil {
return commandkit.Acquired[View]{Diagnostics: configKitDiagnostics(name, loadError.Sources(), loadError.Diagnostics())}, err
}
return commandkit.Acquired[View]{}, err
}
if result == nil {
return commandkit.Acquired[View]{}, fmt.Errorf("ConfigKit scope %q returned a nil result", name)
}
sources := result.Sources()
return commandkit.Acquired[View]{
Value: View{Effective: result, Sources: result, Explanation: result},
Diagnostics: configKitDiagnostics(name, sources, result.Diagnostics()),
}, nil
}
return scope
}
func configKitDiagnostics(scope string, sources []configkit.SourceInfo, records []configkit.Diagnostic) []commandkit.Diagnostic {
if len(records) == 0 {
return nil
}
severity := commandkit.SeverityWarning
for _, record := range records {
if record.Severity == configkit.SeverityError {
severity = commandkit.SeverityError
break
}
}
// Capture the upstream snapshots so later calls to Write do not depend on
// slices returned by the provider.
sources = append([]configkit.SourceInfo(nil), sources...)
records = append([]configkit.Diagnostic(nil), records...)
return []commandkit.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
}

View File

@@ -0,0 +1,101 @@
package ckconfig
import (
"context"
"io"
commandkit "gitea.maximumdirect.net/eric/CommandKit"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
)
// Selection carries the configuration and overlay choices supplied for a
// configuration scope.
type Selection struct {
Config commandkit.Supplied[string]
Overlay commandkit.Supplied[string]
}
// Operation identifies a supported configuration inspection operation.
type Operation string
const (
// Validate checks the selected configuration and reports diagnostics.
Validate Operation = "validate"
// Show writes the selected configuration's effective form.
Show Operation = "show"
// Sources reports the selected configuration's input sources.
Sources Operation = "sources"
// Explain reports the history and provenance of a selected path.
Explain Operation = "explain"
)
// Request describes one configuration operation and its selected inputs.
type Request struct {
Operation Operation
Selection Selection
}
// EffectiveWriter writes a configuration's native effective representation.
type EffectiveWriter interface {
WriteEffective(io.Writer) error
}
// SourceReader reports the native source inventory for a configuration.
type SourceReader interface {
Sources() []configkit.SourceInfo
}
// ExplanationReader reads and writes a configuration's native path explanation.
type ExplanationReader interface {
Explain(configkit.Path) (configkit.Explanation, bool)
WriteExplanation(io.Writer, configkit.Path) error
}
// View exposes native configuration facets without reconstructing their
// reports.
type View struct {
Effective EffectiveWriter
Sources SourceReader
Explanation ExplanationReader
}
// Scope declares the operations and acquisition callback for one named
// configuration. Construction issues are retained privately until family
// assembly attaches them to its command declaration.
type Scope[I, S any] struct {
Name string
Summary string
Operations []Operation
Acquire func(context.Context, Request, I, S) (commandkit.Acquired[View], error)
issues []error
}
// Selector binds a typed flag to the configuration scopes that use it.
type Selector[I any] struct {
Binding commandkit.Flag[I]
Scopes []string
}
// Suite declares the typed input, shared state, selectors, and configuration
// scopes used to assemble configuration commands.
type Suite[I, S any] struct {
NewInput func() I
NewState func() S
Selectors []Selector[I]
CheckInput func(scope string, selection Selection, input I) error
Scopes []Scope[I, S]
}
// WithScopeError returns a copy of scope that retains err as a construction
// issue. Existing issues remain available to family assembly, and scope is not
// mutated. A nil error returns scope unchanged.
func WithScopeError[I, S any](scope Scope[I, S], err error) Scope[I, S] {
if err == nil {
return scope
}
issues := make([]error, len(scope.issues), len(scope.issues)+1)
copy(issues, scope.issues)
scope.issues = append(issues, err)
return scope
}

View File

@@ -0,0 +1,187 @@
package ckconfig
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"strings"
"testing"
commandkit "gitea.maximumdirect.net/eric/CommandKit"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
)
func TestWithScopeErrorCopiesAndRetainsCauses(t *testing.T) {
first := &scopeCause{message: "first construction problem"}
second := errors.New("second construction problem")
original := Scope[struct{}, struct{}]{issues: []error{fmt.Errorf("wrapped: %w", first)}}
copyWithError := WithScopeError(original, second)
if len(original.issues) != 1 || !errors.Is(original.issues[0], first) {
t.Fatalf("original issues changed: %#v", original.issues)
}
if len(copyWithError.issues) != 2 || !errors.Is(copyWithError.issues[0], first) || !errors.Is(copyWithError.issues[1], second) {
t.Fatalf("attached issues = %#v", copyWithError.issues)
}
if copyWithError.issues[0] != original.issues[0] {
t.Fatal("existing cause identity was not preserved")
}
var retained *scopeCause
if !errors.As(copyWithError.issues[0], &retained) || retained != first {
t.Fatalf("retained concrete cause = %#v, want %#v", retained, first)
}
narrowed := copyWithError
narrowed.Operations = nil
third := errors.New("third construction problem")
withThird := WithScopeError(narrowed, third)
unchanged := WithScopeError(withThird, nil)
if len(unchanged.issues) != len(copyWithError.issues)+1 || !errors.Is(unchanged.issues[2], third) {
t.Fatalf("repeated attachment lost issues: %#v", unchanged.issues)
}
}
type scopeCause struct{ message string }
func (e *scopeCause) Error() string { return e.message }
func TestFromConfigKitRetainsNilLoaderIssue(t *testing.T) {
scope := FromConfigKit[struct{}, struct{}, struct{}]("app", "Application configuration", nil)
scope.Operations = nil
if scope.Acquire != nil {
t.Fatal("nil loader produced an acquisition callback")
}
if len(scope.issues) != 1 || !strings.Contains(scope.issues[0].Error(), "loader") {
t.Fatalf("retained nil-loader issues = %#v", scope.issues)
}
}
func TestFromConfigKitPassesInvocationValuesAndNativeFacets(t *testing.T) {
type input struct{ Label string }
type state struct{ Calls int }
type settings struct {
Name string `yaml:"name"`
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
selection := Selection{
Config: commandkit.Supplied[string]{Value: "chosen.yaml", Set: true},
Overlay: commandkit.Supplied[string]{Value: "", Set: true},
}
gotContext, gotSelection, gotInput, gotState := context.Context(nil), Selection{}, input{}, (*state)(nil)
result, err := configkit.Load(configkit.Mapping("source.yaml", map[string]any{"name": "native"}), configkit.Options[settings]{Definition: configkit.Definition[settings]{}})
if err != nil {
t.Fatal(err)
}
scope := FromConfigKit[input, *state, settings]("app", "Application configuration", func(got context.Context, selected Selection, in input, st *state) (*configkit.Result[settings], error) {
gotContext, gotSelection, gotInput, gotState = got, selected, in, st
st.Calls++
return result, nil
})
if len(scope.Operations) != 4 || scope.Operations[0] != Validate || scope.Operations[1] != Show || scope.Operations[2] != Sources || scope.Operations[3] != Explain {
t.Fatalf("operations = %#v", scope.Operations)
}
if gotContext != nil {
t.Fatal("loader ran during adapter construction")
}
stateValue := &state{}
acquired, err := scope.Acquire(ctx, Request{Operation: Show, Selection: selection}, input{Label: "typed"}, stateValue)
if err != nil {
t.Fatal(err)
}
if gotContext != ctx || gotSelection != selection || gotInput.Label != "typed" || gotState != stateValue || stateValue.Calls != 1 {
t.Fatalf("loader arguments/context/state = %v, %#v, %#v, %p, %d", gotContext, gotSelection, gotInput, gotState, stateValue.Calls)
}
if acquired.Value.Effective != result || acquired.Value.Sources != result || acquired.Value.Explanation != result {
t.Fatal("adapter did not expose the native result facets directly")
}
var effective bytes.Buffer
if err := acquired.Value.Effective.WriteEffective(&effective); err != nil {
t.Fatal(err)
}
if !strings.Contains(effective.String(), "name: native") {
t.Fatalf("native effective output = %q", effective.String())
}
sources := acquired.Value.Sources.Sources()
if len(sources) != 1 || sources[0].Label != "source.yaml" {
t.Fatalf("native sources = %#v", sources)
}
path, err := configkit.ParsePath("/name")
if err != nil {
t.Fatal(err)
}
if _, ok := acquired.Value.Explanation.Explain(path); !ok {
t.Fatal("native explanation facet did not resolve /name")
}
var explanation bytes.Buffer
if err := acquired.Value.Explanation.WriteExplanation(&explanation, path); err != nil {
t.Fatal(err)
}
if explanation.Len() == 0 {
t.Fatal("native explanation writer produced no output")
}
}
func TestFromConfigKitRetainsLoadErrorAndDiagnostics(t *testing.T) {
type settings struct {
Name string `yaml:"name"`
}
_, loadErr := configkit.Load(configkit.Mapping("bad source", map[string]any{"unknown": true}), configkit.Options[settings]{Definition: configkit.Definition[settings]{}})
if loadErr == nil {
t.Fatal("fixture did not produce a ConfigKit load error")
}
scope := FromConfigKit[struct{}, struct{}, settings]("app", "", func(context.Context, Selection, struct{}, struct{}) (*configkit.Result[settings], error) {
return nil, loadErr
})
acquired, gotErr := scope.Acquire(context.Background(), Request{}, struct{}{}, struct{}{})
if gotErr != loadErr {
t.Fatalf("load error identity = %v, want original %v", gotErr, loadErr)
}
var typed *configkit.LoadError
if !errors.As(gotErr, &typed) {
t.Fatalf("load error type was not retained: %T", gotErr)
}
if len(acquired.Diagnostics) != 1 || acquired.Diagnostics[0].Severity != commandkit.SeverityError {
t.Fatalf("failure 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("diagnostic output lacks scope/source legend: %q", rendered.String())
}
}
func TestConfigKitDiagnosticsMapSeverityAndPropagateWriterFailures(t *testing.T) {
warning := configKitDiagnostics("app", nil, []configkit.Diagnostic{{Severity: configkit.SeverityWarning, Message: "safe warning"}})
if len(warning) != 1 || warning[0].Severity != commandkit.SeverityWarning {
t.Fatalf("warning diagnostics = %#v", warning)
}
withError := configKitDiagnostics("app", nil, []configkit.Diagnostic{
{Severity: configkit.SeverityWarning, Message: "safe warning"},
{Severity: configkit.SeverityError, Message: "safe error"},
})
if withError[0].Severity != commandkit.SeverityError {
t.Fatalf("mixed diagnostics severity = %q", withError[0].Severity)
}
if err := withError[0].Write(shortWriter{}); !errors.Is(err, io.ErrShortWrite) {
t.Fatalf("short writer error = %v, want %v", err, io.ErrShortWrite)
}
wantErr := errors.New("writer failed")
if err := withError[0].Write(failingWriter{err: wantErr}); !errors.Is(err, wantErr) {
t.Fatalf("writer error = %v, want %v", err, wantErr)
}
}
type shortWriter struct{}
func (shortWriter) Write(value []byte) (int, error) { return len(value) - 1, nil }
type failingWriter struct{ err error }
func (w failingWriter) Write([]byte) (int, error) { return 0, w.err }