Separate comparison side selectors
This commit is contained in:
@@ -465,10 +465,11 @@ configuration inspection API. PromptKit owns the meaning of that native
|
|||||||
comparison; CommandKit uses the existing `config diff` preparation and report
|
comparison; CommandKit uses the existing `config diff` preparation and report
|
||||||
path.
|
path.
|
||||||
`--config` and `--overlay` select the left side. `--against-config` and
|
`--config` and `--overlay` select the left side. `--against-config` and
|
||||||
`--against-overlay` independently override the right side. When a right-side
|
`--against-overlay` select the right side. An omitted field keeps that side's
|
||||||
field is omitted, it inherits the corresponding left value and supplied state;
|
ordinary application loading behavior and unsupplied state; an explicit empty
|
||||||
an explicit empty string overrides it and remains supplied. Equal selections are
|
string remains supplied. Application selectors affect the left side, while
|
||||||
allowed; their equality alone does not cause CommandKit to skip the provider.
|
declared `--against-*` counterparts affect the right. Equal selections are
|
||||||
|
allowed and do not cause CommandKit to skip the provider.
|
||||||
|
|
||||||
Automatic output is YAML. The shared `--format` also accepts YAML, JSON, and
|
Automatic output is YAML. The shared `--format` also accepts YAML, JSON, and
|
||||||
text; text uses the same structured representation. For example:
|
text; text uses the same structured representation. For example:
|
||||||
|
|||||||
@@ -237,8 +237,10 @@ Use `ckconfig.Command(ckconfig.Suite[I, S]{...})` when the application needs
|
|||||||
multiple named scopes, selector flags, scope-aware input checks, or shared
|
multiple named scopes, selector flags, scope-aware input checks, or shared
|
||||||
acquisition state or a custom comparison provider. A declared `Diff` capability
|
acquisition state or a custom comparison provider. A declared `Diff` capability
|
||||||
uses `Scope.Compare`; `ckconfig.FromConfigKit` and `AppCommand` provide the
|
uses `Scope.Compare`; `ckconfig.FromConfigKit` and `AppCommand` provide the
|
||||||
ConfigKit-backed implementation. Each comparison side gets its own initialized
|
ConfigKit-backed implementation. `Selector.AgainstName` opts a selector into an
|
||||||
and checked input, while state is shared by the invocation. Factory results
|
independent right-side flag. Each comparison side gets its own initialized and
|
||||||
|
checked input, with only its supplied selectors applied; state is shared by the
|
||||||
|
invocation. Factory results
|
||||||
remain caller-owned, so separate initialization calls do not guarantee distinct
|
remain caller-owned, so separate initialization calls do not guarantee distinct
|
||||||
objects. See
|
objects. See
|
||||||
[ckconfig.Suite's GoDoc](../../integrations/ckconfig/contracts.go) for factory
|
[ckconfig.Suite's GoDoc](../../integrations/ckconfig/contracts.go) for factory
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Shared reporting and PromptKit v0.20.0 implementation plan
|
# Shared reporting and PromptKit v0.20.0 implementation plan
|
||||||
|
|
||||||
Status: implementation in progress; Stages 1–14 complete.
|
Status: implementation in progress; Stages 1–15 complete.
|
||||||
|
|
||||||
## Scope, authority, and execution
|
## Scope, authority, and execution
|
||||||
|
|
||||||
@@ -539,6 +539,12 @@ failure, independently supplied diagnostics, and metadata/help without loading.
|
|||||||
|
|
||||||
## Stage 15: Independent comparison selector inputs
|
## Stage 15: Independent comparison selector inputs
|
||||||
|
|
||||||
|
**Complete.** Comparison sides now capture and apply independently declared
|
||||||
|
selectors and configuration selections. `FlagValues.ApplySuppliedTo` was added
|
||||||
|
to core because ordinary captured replay also applies flag defaults; this keeps
|
||||||
|
each side's factory defaults intact while retaining required checks and ordered
|
||||||
|
occurrences. Root, ckconfig, and ckprompt workspace suites passed with Go 1.27.0.
|
||||||
|
|
||||||
Replace inheritance in [input.go](../../integrations/ckconfig/input.go).
|
Replace inheritance in [input.go](../../integrations/ckconfig/input.go).
|
||||||
Add `Selector.AgainstName string`; an empty name declares no right-side flag.
|
Add `Selector.AgainstName string`; an empty name declares no right-side flag.
|
||||||
For a declared counterpart use Stage 6's `RenameFlag` to retain domain parsing
|
For a declared counterpart use Stage 6's `RenameFlag` to retain domain parsing
|
||||||
|
|||||||
@@ -76,6 +76,17 @@ func CaptureFlags[Outer, I any](flags []Flag[I], target func(*Outer) *FlagValues
|
|||||||
// occurrence values in command-line order. Repeated core-owned values are copied
|
// occurrence values in command-line order. Repeated core-owned values are copied
|
||||||
// for each application.
|
// for each application.
|
||||||
func (values FlagValues[I]) ApplyTo(target *I, selected ...Flag[I]) error {
|
func (values FlagValues[I]) ApplyTo(target *I, selected ...Flag[I]) error {
|
||||||
|
return values.applyTo(target, false, selected...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplySuppliedTo applies only explicitly supplied selected flags, preserving
|
||||||
|
// the destination's existing values for omitted flags. It checks selected
|
||||||
|
// bindings and required values like ApplyTo and preserves occurrence order.
|
||||||
|
func (values FlagValues[I]) ApplySuppliedTo(target *I, selected ...Flag[I]) error {
|
||||||
|
return values.applyTo(target, true, selected...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (values FlagValues[I]) applyTo(target *I, suppliedOnly bool, selected ...Flag[I]) error {
|
||||||
if target == nil {
|
if target == nil {
|
||||||
return fmt.Errorf("%w: destination is nil", ErrFlagWiring)
|
return fmt.Errorf("%w: destination is nil", ErrFlagWiring)
|
||||||
}
|
}
|
||||||
@@ -92,6 +103,9 @@ func (values FlagValues[I]) ApplyTo(target *I, selected ...Flag[I]) error {
|
|||||||
if binding.meta.Required && !prepared.supplied {
|
if binding.meta.Required && !prepared.supplied {
|
||||||
return fmt.Errorf("flag --%s: %w", binding.meta.Name, ErrFlagInput)
|
return fmt.Errorf("flag --%s: %w", binding.meta.Name, ErrFlagInput)
|
||||||
}
|
}
|
||||||
|
if suppliedOnly && !prepared.supplied {
|
||||||
|
continue
|
||||||
|
}
|
||||||
preparedValues = append(preparedValues, prepared)
|
preparedValues = append(preparedValues, prepared)
|
||||||
}
|
}
|
||||||
var ordered []preparedOccurrence[I]
|
var ordered []preparedOccurrence[I]
|
||||||
|
|||||||
@@ -10,57 +10,6 @@ import (
|
|||||||
configkit "go.maximumdirect.net/configkit"
|
configkit "go.maximumdirect.net/configkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestComparisonSelectionInheritsFieldsIndependently(t *testing.T) {
|
|
||||||
leftConfig := commandkit.Supplied[string]{Value: "before.yml", Set: true}
|
|
||||||
leftOverlay := commandkit.Supplied[string]{Value: "base", Set: true}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
left Selection
|
|
||||||
right Selection
|
|
||||||
want Selection
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "omitted fields stay omitted",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "both fields inherit supplied state",
|
|
||||||
left: Selection{Config: leftConfig, Overlay: leftOverlay},
|
|
||||||
want: Selection{Config: leftConfig, Overlay: leftOverlay},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "explicit empty config overrides while overlay inherits",
|
|
||||||
left: Selection{Config: leftConfig, Overlay: leftOverlay},
|
|
||||||
right: Selection{Config: commandkit.Supplied[string]{Set: true}},
|
|
||||||
want: Selection{Config: commandkit.Supplied[string]{Set: true}, Overlay: leftOverlay},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "explicit overlay overrides while config inherits",
|
|
||||||
left: Selection{Config: leftConfig, Overlay: leftOverlay},
|
|
||||||
right: Selection{Overlay: commandkit.Supplied[string]{Value: "candidate", Set: true}},
|
|
||||||
want: Selection{Config: leftConfig, Overlay: commandkit.Supplied[string]{Value: "candidate", Set: true}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "explicit empty overlay remains supplied",
|
|
||||||
left: Selection{Config: leftConfig, Overlay: leftOverlay},
|
|
||||||
right: Selection{Overlay: commandkit.Supplied[string]{Set: true}},
|
|
||||||
want: Selection{Config: leftConfig, Overlay: commandkit.Supplied[string]{Set: true}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "both explicit right fields replace left",
|
|
||||||
left: Selection{Config: leftConfig, Overlay: leftOverlay},
|
|
||||||
right: Selection{Config: commandkit.Supplied[string]{Value: "after.yml", Set: true}, Overlay: commandkit.Supplied[string]{Value: "new", Set: true}},
|
|
||||||
want: Selection{Config: commandkit.Supplied[string]{Value: "after.yml", Set: true}, Overlay: commandkit.Supplied[string]{Value: "new", Set: true}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
if got := inheritComparisonSelection(test.left, test.right); got != test.want {
|
|
||||||
t.Fatalf("inherited selection = %#v, want %#v", got, test.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestComparisonHelpLimitsFlagsAndDefaultsFormatToYAML(t *testing.T) {
|
func TestComparisonHelpLimitsFlagsAndDefaultsFormatToYAML(t *testing.T) {
|
||||||
scope := Scope[struct{}, struct{}]{
|
scope := Scope[struct{}, struct{}]{
|
||||||
Name: "app", Operations: []Operation{Diff, Show, Explain, Validate, Sources},
|
Name: "app", Operations: []Operation{Diff, Show, Explain, Validate, Sources},
|
||||||
@@ -125,7 +74,6 @@ func TestComparisonPreparation(t *testing.T) {
|
|||||||
t.Fatalf("comparison default scope = %q, want app", input.plan.scopeName)
|
t.Fatalf("comparison default scope = %q, want app", input.plan.scopeName)
|
||||||
}
|
}
|
||||||
if want := (Selection{
|
if want := (Selection{
|
||||||
Config: commandkit.Supplied[string]{Value: "before.yml", Set: true},
|
|
||||||
Overlay: commandkit.Supplied[string]{Value: "candidate", Set: true},
|
Overlay: commandkit.Supplied[string]{Value: "candidate", Set: true},
|
||||||
}); input.against != want {
|
}); input.against != want {
|
||||||
t.Fatalf("resolved right selection = %#v, want %#v", input.against, want)
|
t.Fatalf("resolved right selection = %#v, want %#v", input.against, want)
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ func TestComparisonPreparesEqualSelectionsWithSeparateTypedInputs(t *testing.T)
|
|||||||
}
|
}
|
||||||
return struct{}{}
|
return struct{}{}
|
||||||
},
|
},
|
||||||
Selectors: []Selector[*input]{{Binding: enabled}, {Binding: limit}, {Binding: tags}},
|
Selectors: []Selector[*input]{{Binding: enabled, AgainstName: "against-enabled"}, {Binding: limit, AgainstName: "against-limit"}, {Binding: tags, AgainstName: "against-tag"}},
|
||||||
CheckInput: func(scope string, selection Selection, value *input) error {
|
CheckInput: func(scope string, selection Selection, value *input) error {
|
||||||
checkCalls++
|
checkCalls++
|
||||||
preparedInputs = append(preparedInputs, value)
|
preparedInputs = append(preparedInputs, value)
|
||||||
@@ -91,7 +91,7 @@ func TestComparisonPreparesEqualSelectionsWithSeparateTypedInputs(t *testing.T)
|
|||||||
app := newConfigTestApp(t, Command(suite))
|
app := newConfigTestApp(t, Command(suite))
|
||||||
outcome := app.Execute(context.Background(), []string{
|
outcome := app.Execute(context.Background(), []string{
|
||||||
"config", "diff", "--config", "same.yml", "--against-config", "same.yml", "--overlay=",
|
"config", "diff", "--config", "same.yml", "--against-config", "same.yml", "--overlay=",
|
||||||
"--enabled=false", "--limit=0", "--tag", "alpha", "--tag", "beta",
|
"--enabled=false", "--limit=0", "--tag", "alpha", "--tag", "beta", "--against-enabled=false", "--against-limit=0", "--against-tag", "alpha", "--against-tag", "beta", "--against-overlay=",
|
||||||
}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||||
if outcome.Kind != commandkit.OutcomeSuccess {
|
if outcome.Kind != commandkit.OutcomeSuccess {
|
||||||
t.Fatalf("valid comparison outcome = %#v", outcome)
|
t.Fatalf("valid comparison outcome = %#v", outcome)
|
||||||
@@ -99,7 +99,7 @@ func TestComparisonPreparesEqualSelectionsWithSeparateTypedInputs(t *testing.T)
|
|||||||
if inputCalls != 2 || checkCalls != 2 || stateCalls != 1 {
|
if inputCalls != 2 || checkCalls != 2 || stateCalls != 1 {
|
||||||
t.Fatalf("preparation calls input=%d check=%d state=%d", inputCalls, checkCalls, stateCalls)
|
t.Fatalf("preparation calls input=%d check=%d state=%d", inputCalls, checkCalls, stateCalls)
|
||||||
}
|
}
|
||||||
if parseCalls != 3 {
|
if parseCalls != 6 {
|
||||||
t.Fatalf("typed values were converted %d times, want once per supplied occurrence", parseCalls)
|
t.Fatalf("typed values were converted %d times, want once per supplied occurrence", parseCalls)
|
||||||
}
|
}
|
||||||
wantSelection := Selection{
|
wantSelection := Selection{
|
||||||
@@ -161,7 +161,7 @@ func TestComparisonProviderReceivesCheckedPairAndSharedState(t *testing.T) {
|
|||||||
t.Fatalf("comparison outcome = %#v", outcome)
|
t.Fatalf("comparison outcome = %#v", outcome)
|
||||||
}
|
}
|
||||||
left := Selection{Config: commandkit.Supplied[string]{Value: "before.yml", Set: true}}
|
left := Selection{Config: commandkit.Supplied[string]{Value: "before.yml", Set: true}}
|
||||||
right := Selection{Config: left.Config, Overlay: commandkit.Supplied[string]{Set: true}}
|
right := Selection{Overlay: commandkit.Supplied[string]{Set: true}}
|
||||||
if request.Left.Selection != left || request.Right.Selection != right {
|
if request.Left.Selection != left || request.Right.Selection != right {
|
||||||
t.Fatalf("provider selections = %#v", request)
|
t.Fatalf("provider selections = %#v", request)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,6 +141,9 @@ type Selector[I any] struct {
|
|||||||
Binding commandkit.Flag[I]
|
Binding commandkit.Flag[I]
|
||||||
// Scopes lists the scope names that receive this flag's value.
|
// Scopes lists the scope names that receive this flag's value.
|
||||||
Scopes []string
|
Scopes []string
|
||||||
|
// AgainstName optionally declares the long name of an independent
|
||||||
|
// right-side comparison flag using the same parser and setter.
|
||||||
|
AgainstName string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Suite declares the typed input, shared state, selectors, and configuration
|
// Suite declares the typed input, shared state, selectors, and configuration
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
"go.yaml.in/yaml/v4"
|
"go.yaml.in/yaml/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConfigDiffExecutesWithDefaultAndExplicitYAMLAndInheritedSelections(t *testing.T) {
|
func TestConfigDiffExecutesWithDefaultAndExplicitYAMLAndIndependentSelections(t *testing.T) {
|
||||||
type settings struct {
|
type settings struct {
|
||||||
Greeting string `yaml:"greeting"`
|
Greeting string `yaml:"greeting"`
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,7 @@ func TestConfigDiffExecutesWithDefaultAndExplicitYAMLAndInheritedSelections(t *t
|
|||||||
if outcome.Kind != commandkit.OutcomeSuccess {
|
if outcome.Kind != commandkit.OutcomeSuccess {
|
||||||
t.Fatalf("default YAML comparison outcome = %#v", outcome)
|
t.Fatalf("default YAML comparison outcome = %#v", outcome)
|
||||||
}
|
}
|
||||||
if len(selections) != 2 || !selections[0].Config.Set || selections[0].Config.Value != "before.yml" || !selections[1].Config.Set || selections[1].Config.Value != "after.yml" || selections[1].Overlay != selections[0].Overlay {
|
if len(selections) != 2 || !selections[0].Config.Set || selections[0].Config.Value != "before.yml" || !selections[1].Config.Set || selections[1].Config.Value != "after.yml" || selections[1].Overlay.Set {
|
||||||
t.Fatalf("comparison selections = %#v", selections)
|
t.Fatalf("comparison selections = %#v", selections)
|
||||||
}
|
}
|
||||||
var document yaml.Node
|
var document yaml.Node
|
||||||
@@ -256,7 +256,7 @@ func TestConfigDiffRepeatedInvocationsPrepareFreshInputsAndState(t *testing.T) {
|
|||||||
if len(observed) != 2 || observed[0].left.ID == observed[0].right.ID || observed[1].left.ID == observed[1].right.ID || observed[0].left.ID == observed[1].left.ID || observed[0].right.ID == observed[1].right.ID {
|
if len(observed) != 2 || observed[0].left.ID == observed[0].right.ID || observed[1].left.ID == observed[1].right.ID || observed[0].left.ID == observed[1].left.ID || observed[0].right.ID == observed[1].right.ID {
|
||||||
t.Fatalf("input instances were reused: %#v", observed)
|
t.Fatalf("input instances were reused: %#v", observed)
|
||||||
}
|
}
|
||||||
if observed[0].left.Label != "current" || observed[0].right.Label != "current" || observed[1].left.Label != "current" || observed[1].right.Label != "current" {
|
if observed[0].left.Label != "current" || observed[0].right.Label != "" || observed[1].left.Label != "current" || observed[1].right.Label != "" {
|
||||||
t.Fatalf("comparison inputs missed captured flag values: %#v", observed)
|
t.Fatalf("comparison inputs missed captured flag values: %#v", observed)
|
||||||
}
|
}
|
||||||
if observed[0].state == nil || observed[1].state == nil || observed[0].state == observed[1].state || observed[0].state.ID != 1 || observed[1].state.ID != 2 {
|
if observed[0].state == nil || observed[1].state == nil || observed[0].state == observed[1].state || observed[0].state.ID != 1 || observed[1].state.ID != 2 {
|
||||||
|
|||||||
@@ -23,8 +23,9 @@ type scopeInventory struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type preparedScope[I, S any] struct {
|
type preparedScope[I, S any] struct {
|
||||||
scope Scope[I, S]
|
scope Scope[I, S]
|
||||||
bindings []commandkit.Flag[I]
|
bindings []commandkit.Flag[I]
|
||||||
|
againstBindings []commandkit.Flag[I]
|
||||||
}
|
}
|
||||||
|
|
||||||
type preparedSuite[I, S any] struct {
|
type preparedSuite[I, S any] struct {
|
||||||
@@ -34,6 +35,7 @@ type preparedSuite[I, S any] struct {
|
|||||||
scopes []*preparedScope[I, S]
|
scopes []*preparedScope[I, S]
|
||||||
byName map[string]*preparedScope[I, S]
|
byName map[string]*preparedScope[I, S]
|
||||||
bindingsByOperation map[Operation][]commandkit.Flag[I]
|
bindingsByOperation map[Operation][]commandkit.Flag[I]
|
||||||
|
againstBindings []commandkit.Flag[I]
|
||||||
scopeChoices string
|
scopeChoices string
|
||||||
operations []Operation
|
operations []Operation
|
||||||
inventory scopeInventory
|
inventory scopeInventory
|
||||||
@@ -90,9 +92,13 @@ func captureSuite[I, S any](suite Suite[I, S]) *preparedSuite[I, S] {
|
|||||||
bindingsByOperation: make(map[Operation][]commandkit.Flag[I]),
|
bindingsByOperation: make(map[Operation][]commandkit.Flag[I]),
|
||||||
}
|
}
|
||||||
selectors := make([]Selector[I], len(suite.Selectors))
|
selectors := make([]Selector[I], len(suite.Selectors))
|
||||||
|
against := make([]commandkit.Flag[I], len(suite.Selectors))
|
||||||
for index, selector := range suite.Selectors {
|
for index, selector := range suite.Selectors {
|
||||||
selector.Scopes = append([]string(nil), selector.Scopes...)
|
selector.Scopes = append([]string(nil), selector.Scopes...)
|
||||||
selectors[index] = selector
|
selectors[index] = selector
|
||||||
|
if selector.AgainstName != "" {
|
||||||
|
against[index] = commandkit.RenameFlag(selector.Binding, selector.AgainstName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
seenNames := make(map[string]struct{}, len(suite.Scopes))
|
seenNames := make(map[string]struct{}, len(suite.Scopes))
|
||||||
@@ -158,12 +164,18 @@ func captureSuite[I, S any](suite Suite[I, S]) *preparedSuite[I, S] {
|
|||||||
for scopeIndex := range prepared.scopes {
|
for scopeIndex := range prepared.scopes {
|
||||||
if len(prepared.scopes[scopeIndex].scope.Operations) > 0 {
|
if len(prepared.scopes[scopeIndex].scope.Operations) > 0 {
|
||||||
prepared.scopes[scopeIndex].bindings = append(prepared.scopes[scopeIndex].bindings, selector.Binding)
|
prepared.scopes[scopeIndex].bindings = append(prepared.scopes[scopeIndex].bindings, selector.Binding)
|
||||||
|
if selector.AgainstName != "" && containsOperation(prepared.scopes[scopeIndex].scope.Operations, Diff) {
|
||||||
|
prepared.scopes[scopeIndex].againstBindings = append(prepared.scopes[scopeIndex].againstBindings, against[index])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for _, name := range uniqueScopes {
|
for _, name := range uniqueScopes {
|
||||||
if scope, exists := prepared.byName[name]; exists && len(scope.scope.Operations) > 0 {
|
if scope, exists := prepared.byName[name]; exists && len(scope.scope.Operations) > 0 {
|
||||||
scope.bindings = append(scope.bindings, selector.Binding)
|
scope.bindings = append(scope.bindings, selector.Binding)
|
||||||
|
if selector.AgainstName != "" && containsOperation(scope.scope.Operations, Diff) {
|
||||||
|
scope.againstBindings = append(scope.againstBindings, against[index])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,10 +188,13 @@ func captureSuite[I, S any](suite Suite[I, S]) *preparedSuite[I, S] {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, selector := range selectors {
|
for index, selector := range selectors {
|
||||||
for _, scope := range prepared.scopes {
|
for _, scope := range prepared.scopes {
|
||||||
if containsOperation(scope.scope.Operations, operation) && selectorApplies(selector.Scopes, scope.scope.Name) {
|
if containsOperation(scope.scope.Operations, operation) && selectorApplies(selector.Scopes, scope.scope.Name) {
|
||||||
prepared.bindingsByOperation[operation] = append(prepared.bindingsByOperation[operation], selector.Binding)
|
prepared.bindingsByOperation[operation] = append(prepared.bindingsByOperation[operation], selector.Binding)
|
||||||
|
if operation == Diff && selector.AgainstName != "" {
|
||||||
|
prepared.againstBindings = append(prepared.againstBindings, against[index])
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
79
integrations/ckconfig/independent_comparison_test.go
Normal file
79
integrations/ckconfig/independent_comparison_test.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
package ckconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
commandkit "go.maximumdirect.net/commandkit"
|
||||||
|
configkit "go.maximumdirect.net/configkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestComparisonSidesKeepIndependentSelectionsAndDefaults(t *testing.T) {
|
||||||
|
type input struct {
|
||||||
|
Label commandkit.Supplied[string]
|
||||||
|
Tags []string
|
||||||
|
}
|
||||||
|
label := commandkit.ValueFlag(commandkit.FlagMeta{Name: "label"}, (*string)(nil), parseConfigString, func(i *input, value commandkit.Supplied[string]) { i.Label = value })
|
||||||
|
tags := commandkit.RepeatFlag(commandkit.FlagMeta{Name: "tag"}, []string(nil), parseConfigString, func(i *input, value commandkit.Supplied[[]string]) { i.Tags = value.Value })
|
||||||
|
var pair ComparisonRequest[input]
|
||||||
|
calls := 0
|
||||||
|
suite := Suite[input, struct{}]{
|
||||||
|
NewInput: func() input {
|
||||||
|
return input{Label: commandkit.Supplied[string]{Value: "factory"}, Tags: []string{"factory"}}
|
||||||
|
},
|
||||||
|
Selectors: []Selector[input]{{Binding: label, AgainstName: "against-label"}, {Binding: tags, AgainstName: "against-tag"}},
|
||||||
|
Scopes: []Scope[input, struct{}]{{Name: "app", Operations: []Operation{Diff}, Compare: func(_ context.Context, request ComparisonRequest[input], _ struct{}) (commandkit.Acquired[[]configkit.Difference], error) {
|
||||||
|
calls++
|
||||||
|
pair = request
|
||||||
|
return commandkit.Acquired[[]configkit.Difference]{}, nil
|
||||||
|
}}},
|
||||||
|
}
|
||||||
|
app := newConfigTestApp(t, Command(suite))
|
||||||
|
tests := []struct {
|
||||||
|
args []string
|
||||||
|
left, right Selection
|
||||||
|
leftLabel, rightLabel commandkit.Supplied[string]
|
||||||
|
leftTags, rightTags []string
|
||||||
|
}{
|
||||||
|
{leftLabel: commandkit.Supplied[string]{Value: "factory"}, rightLabel: commandkit.Supplied[string]{Value: "factory"}, leftTags: []string{"factory"}, rightTags: []string{"factory"}},
|
||||||
|
{args: []string{"--config=left", "--overlay=base", "--label=", "--tag=a", "--tag=b"}, left: Selection{Config: commandkit.Supplied[string]{Value: "left", Set: true}, Overlay: commandkit.Supplied[string]{Value: "base", Set: true}}, leftLabel: commandkit.Supplied[string]{Set: true}, rightLabel: commandkit.Supplied[string]{Value: "factory"}, leftTags: []string{"a", "b"}, rightTags: []string{"factory"}},
|
||||||
|
{args: []string{"--against-config=right", "--against-label=r", "--against-tag=x"}, right: Selection{Config: commandkit.Supplied[string]{Value: "right", Set: true}}, leftLabel: commandkit.Supplied[string]{Value: "factory"}, rightLabel: commandkit.Supplied[string]{Value: "r", Set: true}, leftTags: []string{"factory"}, rightTags: []string{"x"}},
|
||||||
|
{args: []string{"--config=left", "--against-overlay=", "--label=l", "--against-label=r", "--tag=a", "--against-tag=b"}, left: Selection{Config: commandkit.Supplied[string]{Value: "left", Set: true}}, right: Selection{Overlay: commandkit.Supplied[string]{Set: true}}, leftLabel: commandkit.Supplied[string]{Value: "l", Set: true}, rightLabel: commandkit.Supplied[string]{Value: "r", Set: true}, leftTags: []string{"a"}, rightTags: []string{"b"}},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
result := app.Execute(context.Background(), append([]string{"config", "diff"}, test.args...), commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||||
|
if result.Kind != commandkit.OutcomeSuccess || pair.Left.Selection != test.left || pair.Right.Selection != test.right || pair.Left.Input.Label != test.leftLabel || pair.Right.Input.Label != test.rightLabel || !reflect.DeepEqual(pair.Left.Input.Tags, test.leftTags) || !reflect.DeepEqual(pair.Right.Input.Tags, test.rightTags) {
|
||||||
|
t.Fatalf("args=%v outcome=%#v pair=%#v", test.args, result, pair)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if calls != len(tests) {
|
||||||
|
t.Fatalf("provider calls = %d", calls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestComparisonRequiredCounterpartsAndCollisions(t *testing.T) {
|
||||||
|
flag := commandkit.ValueFlag(commandkit.FlagMeta{Name: "choice", Required: true}, (*string)(nil), parseConfigString, func(i *string, value commandkit.Supplied[string]) { *i = value.Value })
|
||||||
|
calls := 0
|
||||||
|
scope := Scope[string, struct{}]{Name: "app", Operations: []Operation{Diff}, Compare: func(context.Context, ComparisonRequest[string], struct{}) (commandkit.Acquired[[]configkit.Difference], error) {
|
||||||
|
calls++
|
||||||
|
return commandkit.Acquired[[]configkit.Difference]{}, nil
|
||||||
|
}}
|
||||||
|
suite := Suite[string, struct{}]{Selectors: []Selector[string]{{Binding: flag, AgainstName: "against-choice"}}, Scopes: []Scope[string, struct{}]{scope}}
|
||||||
|
app := newConfigTestApp(t, Command(suite))
|
||||||
|
for _, args := range [][]string{{"--choice=x"}, {"--against-choice=x"}} {
|
||||||
|
result := app.Execute(context.Background(), append([]string{"config", "diff"}, args...), commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||||
|
if result.Kind != commandkit.OutcomeUsage || calls != 0 {
|
||||||
|
t.Fatalf("missing side %v: %#v", args, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result := app.Execute(context.Background(), []string{"config", "diff", "--choice=x", "--against-choice=y"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||||
|
if result.Kind != commandkit.OutcomeSuccess || calls != 1 {
|
||||||
|
t.Fatalf("complete sides: %#v", result)
|
||||||
|
}
|
||||||
|
suite.Selectors[0].AgainstName = "against-config"
|
||||||
|
if _, err := commandkit.New(commandkit.Identity{Name: "app"}, Command(suite)); err == nil {
|
||||||
|
t.Fatal("conflicting counterpart accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,12 +9,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type configCommandInput[I, S any] struct {
|
type configCommandInput[I, S any] struct {
|
||||||
selection Selection
|
selection Selection
|
||||||
against Selection
|
against Selection
|
||||||
scope commandkit.Supplied[string]
|
scope commandkit.Supplied[string]
|
||||||
path commandkit.Supplied[string]
|
path commandkit.Supplied[string]
|
||||||
values commandkit.FlagValues[I]
|
values commandkit.FlagValues[I]
|
||||||
plan *configOperationPlan[I, S]
|
againstValues commandkit.FlagValues[I]
|
||||||
|
plan *configOperationPlan[I, S]
|
||||||
}
|
}
|
||||||
|
|
||||||
type selectedScopeInput[I, S any] struct {
|
type selectedScopeInput[I, S any] struct {
|
||||||
@@ -85,6 +86,14 @@ func newConfigOperationCommand[I, S any](prepared *preparedSuite[I, S], operatio
|
|||||||
}
|
}
|
||||||
return &(*input).values
|
return &(*input).values
|
||||||
})...)
|
})...)
|
||||||
|
if operation == Diff {
|
||||||
|
flags = append(flags, commandkit.CaptureFlags(prepared.againstBindings, func(input **configCommandInput[I, S]) *commandkit.FlagValues[I] {
|
||||||
|
if input == nil || *input == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &(*input).againstValues
|
||||||
|
})...)
|
||||||
|
}
|
||||||
|
|
||||||
name, summary := operationDeclaration(operation)
|
name, summary := operationDeclaration(operation)
|
||||||
return commandkit.NewCommand(commandkit.CommandSpec[*configCommandInput[I, S]]{
|
return commandkit.NewCommand(commandkit.CommandSpec[*configCommandInput[I, S]]{
|
||||||
@@ -169,13 +178,12 @@ func prepareConfigOperation[I, S any](prepared *preparedSuite[I, S], operation O
|
|||||||
scopes: make([]selectedScopeInput[I, S], 0, len(selected)),
|
scopes: make([]selectedScopeInput[I, S], 0, len(selected)),
|
||||||
}
|
}
|
||||||
if operation == Diff {
|
if operation == Diff {
|
||||||
input.against = inheritComparisonSelection(input.selection, input.against)
|
|
||||||
scope := selected[0]
|
scope := selected[0]
|
||||||
left, err := prepareScopeInput(prepared, input.values, scope, input.selection)
|
left, err := prepareScopeInput(prepared, input.values, scope, scope.bindings, input.selection, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("configuration scope %q left input: %w", scope.scope.Name, err)
|
return fmt.Errorf("configuration scope %q left input: %w", scope.scope.Name, err)
|
||||||
}
|
}
|
||||||
right, err := prepareScopeInput(prepared, input.values, scope, input.against)
|
right, err := prepareScopeInput(prepared, input.againstValues, scope, scope.againstBindings, input.against, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("configuration scope %q right input: %w", scope.scope.Name, err)
|
return fmt.Errorf("configuration scope %q right input: %w", scope.scope.Name, err)
|
||||||
}
|
}
|
||||||
@@ -190,7 +198,7 @@ func prepareConfigOperation[I, S any](prepared *preparedSuite[I, S], operation O
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
for _, scope := range selected {
|
for _, scope := range selected {
|
||||||
value, err := prepareScopeInput(prepared, input.values, scope, input.selection)
|
value, err := prepareScopeInput(prepared, input.values, scope, scope.bindings, input.selection, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("configuration scope %q: %w", scope.scope.Name, err)
|
return fmt.Errorf("configuration scope %q: %w", scope.scope.Name, err)
|
||||||
}
|
}
|
||||||
@@ -200,14 +208,18 @@ func prepareConfigOperation[I, S any](prepared *preparedSuite[I, S], operation O
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepareScopeInput[I, S any](prepared *preparedSuite[I, S], values commandkit.FlagValues[I], scope *preparedScope[I, S], selection Selection) (I, error) {
|
func prepareScopeInput[I, S any](prepared *preparedSuite[I, S], values commandkit.FlagValues[I], scope *preparedScope[I, S], bindings []commandkit.Flag[I], selection Selection, suppliedOnly bool) (I, error) {
|
||||||
var value I
|
var value I
|
||||||
if prepared.newInput != nil {
|
if prepared.newInput != nil {
|
||||||
value = prepared.newInput()
|
value = prepared.newInput()
|
||||||
}
|
}
|
||||||
// Availability was checked before this helper, so every binding for the
|
// Availability was checked before this helper, so every binding for the
|
||||||
// selected scope belongs to the operation's captured union.
|
// selected scope belongs to the operation's captured union.
|
||||||
if err := values.ApplyTo(&value, scope.bindings...); err != nil {
|
apply := values.ApplyTo
|
||||||
|
if suppliedOnly {
|
||||||
|
apply = values.ApplySuppliedTo
|
||||||
|
}
|
||||||
|
if err := apply(&value, bindings...); err != nil {
|
||||||
return value, err
|
return value, err
|
||||||
}
|
}
|
||||||
if prepared.checkInput != nil {
|
if prepared.checkInput != nil {
|
||||||
@@ -218,16 +230,6 @@ func prepareScopeInput[I, S any](prepared *preparedSuite[I, S], values commandki
|
|||||||
return value, nil
|
return value, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func inheritComparisonSelection(left, right Selection) Selection {
|
|
||||||
if !right.Config.Set {
|
|
||||||
right.Config = left.Config
|
|
||||||
}
|
|
||||||
if !right.Overlay.Set {
|
|
||||||
right.Overlay = left.Overlay
|
|
||||||
}
|
|
||||||
return right
|
|
||||||
}
|
|
||||||
|
|
||||||
func joinOperations(operations []Operation) string {
|
func joinOperations(operations []Operation) string {
|
||||||
values := make([]string, len(operations))
|
values := make([]string, len(operations))
|
||||||
for index, operation := range operations {
|
for index, operation := range operations {
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ func TestPublicConfigDiffUsesPromptKitPairAndPreparedInputs(t *testing.T) {
|
|||||||
events = append(events, "state")
|
events = append(events, "state")
|
||||||
return &compositionState{token: len(events)}
|
return &compositionState{token: len(events)}
|
||||||
},
|
},
|
||||||
Selectors: []ckconfig.Selector[*compositionInput]{{Binding: flag, Scopes: []string{"app", "promptkit"}}},
|
Selectors: []ckconfig.Selector[*compositionInput]{{Binding: flag, AgainstName: "against-campaign", Scopes: []string{"app", "promptkit"}}},
|
||||||
CheckInput: func(scope string, selection ckconfig.Selection, input *compositionInput) error {
|
CheckInput: func(scope string, selection ckconfig.Selection, input *compositionInput) error {
|
||||||
if scope == "promptkit" {
|
if scope == "promptkit" {
|
||||||
events = append(events, "check")
|
events = append(events, "check")
|
||||||
@@ -90,7 +90,7 @@ func TestPublicConfigDiffUsesPromptKitPairAndPreparedInputs(t *testing.T) {
|
|||||||
rightSet bool
|
rightSet bool
|
||||||
}{
|
}{
|
||||||
{name: "right selection is explicit", rightArgs: []string{"--against-config", "after.yml"}, rightValue: "after.yml", rightSet: true},
|
{name: "right selection is explicit", rightArgs: []string{"--against-config", "after.yml"}, rightValue: "after.yml", rightSet: true},
|
||||||
{name: "right selection inherits", rightValue: "before.yml", rightSet: true},
|
{name: "right selection remains omitted"},
|
||||||
{name: "empty right selection is supplied", rightArgs: []string{"--against-config", ""}, rightSet: true},
|
{name: "empty right selection is supplied", rightArgs: []string{"--against-config", ""}, rightSet: true},
|
||||||
} {
|
} {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
@@ -100,7 +100,7 @@ func TestPublicConfigDiffUsesPromptKitPairAndPreparedInputs(t *testing.T) {
|
|||||||
loadedInputs = nil
|
loadedInputs = nil
|
||||||
loadedStates = nil
|
loadedStates = nil
|
||||||
loadedSelections = nil
|
loadedSelections = nil
|
||||||
args := []string{"config", "diff", "--scope", "promptkit", "--config", "before.yml", "--campaign", "spring"}
|
args := []string{"config", "diff", "--scope", "promptkit", "--config", "before.yml", "--campaign", "spring", "--against-campaign", "spring"}
|
||||||
args = append(args, test.rightArgs...)
|
args = append(args, test.rightArgs...)
|
||||||
var output bytes.Buffer
|
var output bytes.Buffer
|
||||||
outcome := app.Execute(context.Background(), args, commandkit.Streams{Out: &output, Err: io.Discard})
|
outcome := app.Execute(context.Background(), args, commandkit.Streams{Out: &output, Err: io.Discard})
|
||||||
@@ -153,7 +153,7 @@ func TestPublicConfigDiffUsesPromptKitPairAndPreparedInputs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var diagnostics bytes.Buffer
|
var diagnostics bytes.Buffer
|
||||||
failure := app.Execute(context.Background(), []string{"config", "diff", "--scope", "promptkit", "--config", "bad-left.yml", "--against-config", "right.yml", "--campaign", "spring"}, commandkit.Streams{Out: io.Discard, Err: &diagnostics})
|
failure := app.Execute(context.Background(), []string{"config", "diff", "--scope", "promptkit", "--config", "bad-left.yml", "--against-config", "right.yml", "--campaign", "spring", "--against-campaign", "spring"}, commandkit.Streams{Out: io.Discard, Err: &diagnostics})
|
||||||
if failure.Kind != commandkit.OutcomeOperational || !strings.Contains(diagnostics.String(), `Configuration scope "promptkit" (left):`) {
|
if failure.Kind != commandkit.OutcomeOperational || !strings.Contains(diagnostics.String(), `Configuration scope "promptkit" (left):`) {
|
||||||
t.Fatalf("installed load failure = %#v, diagnostics %q", failure, diagnostics.String())
|
t.Fatalf("installed load failure = %#v, diagnostics %q", failure, diagnostics.String())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ go run . config scopes
|
|||||||
go run . config validate --scope all --campaign spring --limit=0
|
go run . config validate --scope all --campaign spring --limit=0
|
||||||
go run . config diff --scope app --config fixtures/before.yml --against-config fixtures/after.yml
|
go run . config diff --scope app --config fixtures/before.yml --against-config fixtures/after.yml
|
||||||
go run . config diff --scope app --config fixtures/before.yml --against-config fixtures/after.yml --campaign spring --limit=0
|
go run . config diff --scope app --config fixtures/before.yml --against-config fixtures/after.yml --campaign spring --limit=0
|
||||||
go run . config diff --scope promptkit --config fixtures/before.yml --against-config fixtures/after.yml --campaign spring
|
go run . config diff --scope promptkit --config fixtures/before.yml --against-config fixtures/after.yml --campaign spring --against-campaign spring
|
||||||
go run . promptkit list prompts --config fixtures/before.yml --campaign spring
|
go run . promptkit list prompts --config fixtures/before.yml --campaign spring
|
||||||
go run . promptkit list profiles --config fixtures/after.yml --campaign autumn --format yaml
|
go run . promptkit list profiles --config fixtures/after.yml --campaign autumn --format yaml
|
||||||
go run . promptkit list backends --campaign spring
|
go run . promptkit list backends --campaign spring
|
||||||
|
|||||||
@@ -234,8 +234,8 @@ func newAppWithEngineLoader(
|
|||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Selectors: []ckconfig.Selector[*inspectionInput]{
|
Selectors: []ckconfig.Selector[*inspectionInput]{
|
||||||
{Binding: campaignSelector(), Scopes: []string{"app", "promptkit"}},
|
{Binding: campaignSelector(), AgainstName: "against-campaign", Scopes: []string{"app", "promptkit"}},
|
||||||
{Binding: limitFlag, Scopes: []string{"app"}},
|
{Binding: limitFlag, AgainstName: "against-limit", Scopes: []string{"app"}},
|
||||||
},
|
},
|
||||||
Scopes: []ckconfig.Scope[*inspectionInput, *runCache]{appScope, engineScope},
|
Scopes: []ckconfig.Scope[*inspectionInput, *runCache]{appScope, engineScope},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -297,7 +297,7 @@ func TestPromptKitScopeDiffAndCatalogsUseSelectedCampaign(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
var output bytes.Buffer
|
var output bytes.Buffer
|
||||||
outcome := app.Execute(context.Background(), []string{"config", "diff", "--scope", "promptkit", "--config", "fixtures/before.yml", "--against-config", "fixtures/after.yml", "--campaign", "spring"}, commandkit.Streams{Out: &output, Err: io.Discard})
|
outcome := app.Execute(context.Background(), []string{"config", "diff", "--scope", "promptkit", "--config", "fixtures/before.yml", "--against-config", "fixtures/after.yml", "--campaign", "spring", "--against-campaign", "spring"}, commandkit.Streams{Out: &output, Err: io.Discard})
|
||||||
if outcome.Kind != commandkit.OutcomeSuccess {
|
if outcome.Kind != commandkit.OutcomeSuccess {
|
||||||
t.Fatalf("PromptKit diff outcome = %#v", outcome)
|
t.Fatalf("PromptKit diff outcome = %#v", outcome)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user