Allow configuration names in application paths
This commit is contained in:
@@ -102,6 +102,8 @@ must not remove validation of fields inside the actual control block.
|
|||||||
|
|
||||||
## Stage 3 — Permit application paths and CLI data
|
## Stage 3 — Permit application paths and CLI data
|
||||||
|
|
||||||
|
**Status:** Complete. Focused destination, registration, and override tests pass.
|
||||||
|
|
||||||
**Owners:** [destination.go](../../internal/engine/destination.go),
|
**Owners:** [destination.go](../../internal/engine/destination.go),
|
||||||
[sequence_registration.go](../../internal/engine/sequence_registration.go),
|
[sequence_registration.go](../../internal/engine/sequence_registration.go),
|
||||||
[override.go](../../internal/engine/override.go), and their existing tests.
|
[override.go](../../internal/engine/override.go), and their existing tests.
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func normalizeDestination(path Path) (Path, error) {
|
|||||||
if segment.kind != pathSegmentKey && segment.kind != pathSegmentParsed {
|
if segment.kind != pathSegmentKey && segment.kind != pathSegmentParsed {
|
||||||
return Path{}, errInvalidDestination
|
return Path{}, errInvalidDestination
|
||||||
}
|
}
|
||||||
if segment.key == "configkit" || segment.key == "extends" {
|
if segment.key == "extends" {
|
||||||
return Path{}, errInvalidDestination
|
return Path{}, errInvalidDestination
|
||||||
}
|
}
|
||||||
segments = append(segments, pathSegment{kind: pathSegmentKey, key: segment.key})
|
segments = append(segments, pathSegment{kind: pathSegmentKey, key: segment.key})
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ func TestLayeredDestinationDiagnosticsUseEffectivePathsAndAuthoredLocations(t *t
|
|||||||
t.Run("invalid destination has no fictional source location", func(t *testing.T) {
|
t.Run("invalid destination has no fictional source location", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
loaded, err := LoadLayered(
|
loaded, err := LoadLayered(
|
||||||
Input{Source: Bytes("main", []byte("value: present\n")), Destination: Keys("configkit")},
|
Input{Source: Bytes("main", []byte("value: present\n")), Destination: Keys("extends")},
|
||||||
testSecretBinding(),
|
testSecretBinding(),
|
||||||
LayeredOptions[map[string]any]{},
|
LayeredOptions[map[string]any]{},
|
||||||
)
|
)
|
||||||
@@ -100,7 +100,7 @@ func TestLayeredDestinationDiagnosticsUseEffectivePathsAndAuthoredLocations(t *t
|
|||||||
t.Fatalf("invalid destination returned a result: %#v", loaded)
|
t.Fatalf("invalid destination returned a result: %#v", loaded)
|
||||||
}
|
}
|
||||||
diagnostic := firstDestinationDiagnostic(t, err)
|
diagnostic := firstDestinationDiagnostic(t, err)
|
||||||
if diagnostic.Code != CodeOptionsInvalid || diagnostic.Path.String() != "/configkit" || diagnostic.Primary != nil {
|
if diagnostic.Code != CodeOptionsInvalid || diagnostic.Path.String() != "/extends" || diagnostic.Primary != nil {
|
||||||
t.Fatalf("invalid destination diagnostic = %#v", diagnostic)
|
t.Fatalf("invalid destination diagnostic = %#v", diagnostic)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ func TestLayeredLoadPreflightsDestinationsBeforeReadingSources(t *testing.T) {
|
|||||||
Input{Source: NoSource()},
|
Input{Source: NoSource()},
|
||||||
testSecretBinding(),
|
testSecretBinding(),
|
||||||
LayeredOptions[destinationLoadConfig]{Underlays: []Underlay{{Inputs: []Input{{
|
LayeredOptions[destinationLoadConfig]{Underlays: []Underlay{{Inputs: []Input{{
|
||||||
Source: Reader("reader", reader), Destination: Keys("configkit"),
|
Source: Reader("reader", reader), Destination: Keys("extends"),
|
||||||
}}}}},
|
}}}}},
|
||||||
)
|
)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -204,7 +204,7 @@ func TestLayeredLoadPreflightsDestinationsBeforeReadingSources(t *testing.T) {
|
|||||||
t.Fatalf("error = %#v", err)
|
t.Fatalf("error = %#v", err)
|
||||||
}
|
}
|
||||||
diagnostic := loadError.Diagnostics()[0]
|
diagnostic := loadError.Diagnostics()[0]
|
||||||
if diagnostic.Code != CodeOptionsInvalid || diagnostic.Path.String() != "/configkit" || diagnostic.Primary != nil {
|
if diagnostic.Code != CodeOptionsInvalid || diagnostic.Path.String() != "/extends" || diagnostic.Primary != nil {
|
||||||
t.Fatalf("destination diagnostic = %#v", diagnostic)
|
t.Fatalf("destination diagnostic = %#v", diagnostic)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,20 @@ func TestNormalizeDestinationUsesLiteralMappingKeys(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNormalizeDestinationAllowsConfigApplicationPaths(t *testing.T) {
|
||||||
|
for _, path := range []Path{Keys("config"), Keys("configkit"), Keys("outer", "config"), Keys("outer", "configkit"), Keys("config", "relative")} {
|
||||||
|
normalized, err := normalizeDestination(path)
|
||||||
|
if err != nil || normalized.String() != path.String() {
|
||||||
|
t.Errorf("normalizeDestination(%q) = %q, %v", path.String(), normalized.String(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parent := Keys("outer")
|
||||||
|
relative, err := normalizeDestination(Keys("config", "child"))
|
||||||
|
if err != nil || appendDestination(parent, relative).String() != "/outer/config/child" {
|
||||||
|
t.Fatalf("relative destination = %q, %v", relative.String(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMappingPlacementModeMatchesZeroValue(t *testing.T) {
|
func TestMappingPlacementModeMatchesZeroValue(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -95,13 +109,11 @@ func TestNormalizeDestinationRejectsIndicesAndReservedKeys(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
for name, path := range map[string]Path{
|
for name, path := range map[string]Path{
|
||||||
"explicit index": Keys("items").Index(0),
|
"explicit index": Keys("items").Index(0),
|
||||||
"negative index": Keys("items").Index(-1),
|
"negative index": Keys("items").Index(-1),
|
||||||
"invalid path": Path{invalid: true},
|
"invalid path": Path{invalid: true},
|
||||||
"configkit": Keys("configkit"),
|
"extends": Keys("extends"),
|
||||||
"extends": Keys("extends"),
|
"nested extends": Keys("outer", "extends"),
|
||||||
"nested configkit": Keys("outer", "configkit"),
|
|
||||||
"nested extends": Keys("outer", "extends"),
|
|
||||||
} {
|
} {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
@@ -111,7 +123,7 @@ func TestNormalizeDestinationRejectsIndicesAndReservedKeys(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, key := range []string{"myextends", "configkit2", "", ".", "..", "../x", "a.b", "a/b", "~"} {
|
for _, key := range []string{"config", "configkit", "myextends", "configkit2", "", ".", "..", "../x", "a.b", "a/b", "~"} {
|
||||||
t.Run("ordinary key "+key, func(t *testing.T) {
|
t.Run("ordinary key "+key, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
if _, err := normalizeDestination(Keys(key)); err != nil {
|
if _, err := normalizeDestination(Keys(key)); err != nil {
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ func prepareOverrideBatch(base overridePreparationInput, sources []SourceInfo, o
|
|||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if pathContainsDirective(override.Path) || pathContainsInheritanceDirective(override.Path) {
|
if pathContainsInheritanceDirective(override.Path) {
|
||||||
diagnostics = append(diagnostics, Diagnostic{
|
diagnostics = append(diagnostics, Diagnostic{
|
||||||
Code: CodeOverrideDirective, Severity: SeverityError, Path: override.Path.clone(),
|
Code: CodeOverrideDirective, Severity: SeverityError, Path: override.Path.clone(),
|
||||||
Message: "configuration overrides cannot create configuration directives", Primary: location,
|
Message: "configuration overrides cannot create configuration directives", Primary: location,
|
||||||
@@ -125,7 +125,7 @@ func prepareOverrideBatch(base overridePreparationInput, sources []SourceInfo, o
|
|||||||
diagnostics = append(diagnostics, diagnostic)
|
diagnostics = append(diagnostics, diagnostic)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if relative, ok := overrideStructuralDirectivePath(value, Path{}); ok {
|
if relative, ok := overrideInheritancePath(value, Path{}); ok {
|
||||||
diagnostics = append(diagnostics, Diagnostic{
|
diagnostics = append(diagnostics, Diagnostic{
|
||||||
Code: CodeOverrideDirective, Severity: SeverityError, Path: appendOverridePath(override.Path, relative),
|
Code: CodeOverrideDirective, Severity: SeverityError, Path: appendOverridePath(override.Path, relative),
|
||||||
Message: "configuration overrides cannot create configuration directives", Primary: location,
|
Message: "configuration overrides cannot create configuration directives", Primary: location,
|
||||||
@@ -201,15 +201,6 @@ func validateOverridePath(path Path, root *document.Node, lookup *documentLookup
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func pathContainsDirective(path Path) bool {
|
|
||||||
for _, segment := range path.segments {
|
|
||||||
if segment.key == directiveKey {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func pathContainsInheritanceDirective(path Path) bool {
|
func pathContainsInheritanceDirective(path Path) bool {
|
||||||
for _, segment := range path.segments {
|
for _, segment := range path.segments {
|
||||||
if segment.key == inheritanceKey {
|
if segment.key == inheritanceKey {
|
||||||
@@ -225,27 +216,23 @@ func appendOverridePath(base, relative Path) Path {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func overrideStructuralDirectivePath(node *document.Node, path Path) (Path, bool) {
|
func overrideInheritancePath(node *document.Node, path Path) (Path, bool) {
|
||||||
return overrideStructuralDirectivePathAt(node, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func overrideStructuralDirectivePathAt(node *document.Node, path Path) (Path, bool) {
|
|
||||||
if node == nil {
|
if node == nil {
|
||||||
return Path{}, false
|
return Path{}, false
|
||||||
}
|
}
|
||||||
if node.Kind == document.Mapping {
|
if node.Kind == document.Mapping {
|
||||||
for _, entry := range node.Mapping {
|
for _, entry := range node.Mapping {
|
||||||
child := path.Key(entry.Key)
|
child := path.Key(entry.Key)
|
||||||
if entry.Key == directiveKey || entry.Key == inheritanceKey {
|
if entry.Key == inheritanceKey {
|
||||||
return child, true
|
return child, true
|
||||||
}
|
}
|
||||||
if found, ok := overrideStructuralDirectivePathAt(entry.Value, child); ok {
|
if found, ok := overrideInheritancePath(entry.Value, child); ok {
|
||||||
return found, true
|
return found, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if node.Kind == document.Sequence {
|
} else if node.Kind == document.Sequence {
|
||||||
for index, child := range node.Sequence {
|
for index, child := range node.Sequence {
|
||||||
if found, ok := overrideStructuralDirectivePathAt(child, path.Index(index)); ok {
|
if found, ok := overrideInheritancePath(child, path.Index(index)); ok {
|
||||||
return found, true
|
return found, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -187,8 +187,8 @@ func TestOverrideValidationRejectsPathsOverlapValuesAndProtectedTargets(t *testi
|
|||||||
{name: "nonfinite", overrides: []Override{{Path: Keys("scalar"), Value: math.Inf(1), Flag: "--scalar"}}, code: CodeOverrideInvalidValue},
|
{name: "nonfinite", overrides: []Override{{Path: Keys("scalar"), Value: math.Inf(1), Flag: "--scalar"}}, code: CodeOverrideInvalidValue},
|
||||||
{name: "depth", overrides: []Override{{Path: Keys("nested"), Value: deep, Flag: "--nested"}}, code: CodeOverrideLimitExceeded},
|
{name: "depth", overrides: []Override{{Path: Keys("nested"), Value: deep, Flag: "--nested"}}, code: CodeOverrideLimitExceeded},
|
||||||
{name: "record bytes", overrides: []Override{{Path: Keys("scalar"), Value: strings.Repeat("x", maxSourceBytes+1), Flag: "--scalar"}}, code: CodeOverrideLimitExceeded},
|
{name: "record bytes", overrides: []Override{{Path: Keys("scalar"), Value: strings.Repeat("x", maxSourceBytes+1), Flag: "--scalar"}}, code: CodeOverrideLimitExceeded},
|
||||||
{name: "directive path", overrides: []Override{{Path: Keys("configkit", "x"), Value: true, Flag: "--directive"}}, code: CodeOverrideDirective},
|
{name: "inheritance path", overrides: []Override{{Path: Keys("extends", "x"), Value: true, Flag: "--directive"}}, code: CodeOverrideDirective},
|
||||||
{name: "directive value", overrides: []Override{{Path: Keys("nested"), Value: map[string]any{"configkit": map[string]any{}}, Flag: "--nested"}}, code: CodeOverrideDirective},
|
{name: "inheritance value", overrides: []Override{{Path: Keys("nested"), Value: map[string]any{"extends": map[string]any{}}, Flag: "--nested"}}, code: CodeOverrideDirective},
|
||||||
{name: "version", overrides: []Override{{Path: Keys("version"), Value: "v2", Flag: "--version"}}, code: CodeOverrideVersion},
|
{name: "version", overrides: []Override{{Path: Keys("version"), Value: "v2", Flag: "--version"}}, code: CodeOverrideVersion},
|
||||||
{name: "version ancestor", overrides: []Override{{Path: Keys("version"), Value: map[string]any{"child": "v2"}, Flag: "--version"}}, code: CodeOverrideVersion},
|
{name: "version ancestor", overrides: []Override{{Path: Keys("version"), Value: map[string]any{"child": "v2"}, Flag: "--version"}}, code: CodeOverrideVersion},
|
||||||
{name: "label", overrides: []Override{{Path: Keys("scalar"), Value: "x", Flag: "\nCANARY"}}, code: CodeOverrideInvalidLabel},
|
{name: "label", overrides: []Override{{Path: Keys("scalar"), Value: "x", Flag: "\nCANARY"}}, code: CodeOverrideInvalidLabel},
|
||||||
@@ -206,6 +206,28 @@ func TestOverrideValidationRejectsPathsOverlapValuesAndProtectedTargets(t *testi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOverrideAcceptsConfigApplicationData(t *testing.T) {
|
||||||
|
type config struct {
|
||||||
|
Config map[string]any `yaml:"config"`
|
||||||
|
ConfigKit map[string]any `yaml:"configkit"`
|
||||||
|
Nested map[string]any `yaml:"nested"`
|
||||||
|
}
|
||||||
|
result, err := testLoad(NoSource(), Options[config]{Overrides: []Override{
|
||||||
|
{Path: Keys("config", "imports"), Value: "ordinary", Flag: "--imports"},
|
||||||
|
{Path: Keys("configkit", "sequences"), Value: "ordinary", Flag: "--sequences"},
|
||||||
|
{Path: Keys("nested"), Value: map[string]any{"config": map[string]any{"sequences": "ordinary"}, "configkit": "old"}, Flag: "--nested"},
|
||||||
|
}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if result.Config.Config["imports"] != "ordinary" || result.Config.ConfigKit["sequences"] != "ordinary" || result.Config.Nested["configkit"] != "old" {
|
||||||
|
t.Fatalf("overrides = %#v", result.Config)
|
||||||
|
}
|
||||||
|
if nested, ok := result.Config.Nested["config"].(map[string]any); !ok || nested["sequences"] != "ordinary" {
|
||||||
|
t.Fatalf("nested data = %#v", result.Config.Nested)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestOverrideOverlapIndexPreservesFirstAcceptedSource(t *testing.T) {
|
func TestOverrideOverlapIndexPreservesFirstAcceptedSource(t *testing.T) {
|
||||||
type config struct {
|
type config struct {
|
||||||
Nested map[string]any `yaml:"nested"`
|
Nested map[string]any `yaml:"nested"`
|
||||||
|
|||||||
@@ -226,6 +226,21 @@ func TestCompileSequenceIdentityPolicyAllowsDisjointSelectorsAndRoot(t *testing.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSequenceIdentitySelectorsAllowConfigKeys(t *testing.T) {
|
||||||
|
policy, diagnostics := compileSequenceIdentityPolicy([]sequenceIdentityRegistration{
|
||||||
|
{selector: sequenceIdentitySelector{literalSequenceSelector("config"), literalSequenceSelector("items")}, identityFields: []string{"id"}},
|
||||||
|
{selector: sequenceIdentitySelector{literalSequenceSelector("configkit"), anyMappingSequenceSelector(), literalSequenceSelector("items")}, identityFields: []string{"name"}},
|
||||||
|
})
|
||||||
|
if len(diagnostics) != 0 {
|
||||||
|
t.Fatalf("diagnostics = %#v", diagnostics)
|
||||||
|
}
|
||||||
|
for _, path := range []Path{Keys("config", "items"), Keys("configkit", "config", "items")} {
|
||||||
|
if _, matched := policy.identityFieldsAt(path); !matched {
|
||||||
|
t.Errorf("selector failed to match %q", path.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCompileSequenceIdentityPolicySnapshotsSelectorAndFields(t *testing.T) {
|
func TestCompileSequenceIdentityPolicySnapshotsSelectorAndFields(t *testing.T) {
|
||||||
selector := sequenceIdentitySelector{literalSequenceSelector("items")}
|
selector := sequenceIdentitySelector{literalSequenceSelector("items")}
|
||||||
fields := []string{"id", "namespace"}
|
fields := []string{"id", "namespace"}
|
||||||
@@ -242,7 +257,7 @@ func TestCompileSequenceIdentityPolicySnapshotsSelectorAndFields(t *testing.T) {
|
|||||||
|
|
||||||
func TestCompileSequenceIdentityPolicyRejectsInvalidSelectorsAndEmptyFields(t *testing.T) {
|
func TestCompileSequenceIdentityPolicyRejectsInvalidSelectorsAndEmptyFields(t *testing.T) {
|
||||||
_, diagnostics := compileSequenceIdentityPolicy([]sequenceIdentityRegistration{
|
_, diagnostics := compileSequenceIdentityPolicy([]sequenceIdentityRegistration{
|
||||||
{selector: sequenceIdentitySelector{literalSequenceSelector("configkit")}, identityFields: []string{"id"}},
|
{selector: sequenceIdentitySelector{literalSequenceSelector("extends")}, identityFields: []string{"id"}},
|
||||||
{selector: sequenceIdentitySelector{{kind: sequenceSelectorSegmentKind(99)}}, identityFields: []string{"id"}},
|
{selector: sequenceIdentitySelector{{kind: sequenceSelectorSegmentKind(99)}}, identityFields: []string{"id"}},
|
||||||
{selector: sequenceIdentitySelector{literalSequenceSelector("unused")}, identityFields: nil},
|
{selector: sequenceIdentitySelector{literalSequenceSelector("unused")}, identityFields: nil},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user