Expose sequence identity registrations through the facade

This commit is contained in:
2026-10-23 20:59:23 +00:00
parent 33a51606da
commit 0023617a07
20 changed files with 530 additions and 212 deletions

View File

@@ -7,54 +7,44 @@ import (
"testing/fstest"
)
func TestCompileSequencePolicyNormalizesRegistrations(t *testing.T) {
parsed, err := ParsePath("/items/0/a~1b")
if err != nil {
t.Fatal(err)
}
func TestCompileSequenceIdentityPolicyNormalizesRegistrations(t *testing.T) {
tests := []struct {
name string
registrations []sequenceRegistration
registrations []sequenceIdentityRegistration
want map[string]string
wantError bool
}{
{
name: "identical registrations coalesce",
registrations: []sequenceRegistration{
{path: Keys("items"), identityField: "id"},
{path: Keys("items"), identityField: "id"},
registrations: []sequenceIdentityRegistration{
{selector: sequenceIdentitySelector{literalSequenceSelector("items")}, identityFields: []string{"id"}},
{selector: sequenceIdentitySelector{literalSequenceSelector("items")}, identityFields: []string{"id"}},
},
want: map[string]string{"/items": "id"},
},
{
name: "different identity fields conflict",
registrations: []sequenceRegistration{
{path: Keys("items"), identityField: "id"},
{path: Keys("items"), identityField: "name"},
registrations: []sequenceIdentityRegistration{
{selector: sequenceIdentitySelector{literalSequenceSelector("items")}, identityFields: []string{"id"}},
{selector: sequenceIdentitySelector{literalSequenceSelector("items")}, identityFields: []string{"name"}},
},
wantError: true,
},
{
name: "empty identity field is literal",
registrations: []sequenceRegistration{{path: Keys("items"), identityField: ""}},
registrations: []sequenceIdentityRegistration{{selector: sequenceIdentitySelector{literalSequenceSelector("items")}, identityFields: []string{""}}},
want: map[string]string{"/items": ""},
},
{
name: "numeric and escaped tokens remain literal keys",
registrations: []sequenceRegistration{{path: parsed, identityField: "id"}},
registrations: []sequenceIdentityRegistration{{selector: sequenceIdentitySelector{literalSequenceSelector("items"), literalSequenceSelector("0"), literalSequenceSelector("a/b")}, identityFields: []string{"id"}}},
want: map[string]string{"/items/0/a~1b": "id"},
},
{
name: "explicit index is invalid",
registrations: []sequenceRegistration{{path: Keys("items").Index(0), identityField: "id"}},
wantError: true,
},
{
name: "unused and root registrations are accepted",
registrations: []sequenceRegistration{
{path: Path{}, identityField: "id"},
{path: Keys("unused"), identityField: "name"},
registrations: []sequenceIdentityRegistration{
{selector: nil, identityFields: []string{"id"}},
{selector: sequenceIdentitySelector{literalSequenceSelector("unused")}, identityFields: []string{"name"}},
},
want: map[string]string{"": "id", "/unused": "name"},
},
@@ -63,7 +53,7 @@ func TestCompileSequencePolicyNormalizesRegistrations(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
policy, diagnostics := compileSequencePolicy(test.registrations)
policy, diagnostics := compileSequenceIdentityPolicy(test.registrations)
if (len(diagnostics) != 0) != test.wantError {
t.Fatalf("diagnostics = %#v, wantError = %t", diagnostics, test.wantError)
}
@@ -73,12 +63,14 @@ func TestCompileSequencePolicyNormalizesRegistrations(t *testing.T) {
}
return
}
if len(policy.identityFields) != len(test.want) {
t.Fatalf("compiled fields = %#v, want %#v", policy.identityFields, test.want)
}
for path, field := range test.want {
if got := policy.identityFields[path]; got != field {
t.Errorf("identity field at %q = %q, want %q", path, got, field)
parsed, err := ParsePath(path)
if err != nil {
t.Fatal(err)
}
gotFields, ok := policy.identityFieldsAt(parsed)
if !ok || len(gotFields) != 1 || gotFields[0] != field {
t.Errorf("identity fields at %q = %#v, want [%q]", path, gotFields, field)
}
}
})
@@ -247,7 +239,7 @@ func TestSequenceRegistrationPreflightDoesNotReadSources(t *testing.T) {
ordinaryReader := strings.NewReader("value: ordinary\n")
_, err := Load[config](Reader("ordinary.yml", ordinaryReader), SecretBinding{}, Options[config]{
Definition: Definition[config]{sequenceRegistrations: []sequenceRegistration{{path: Keys("items").Index(0), identityField: "id"}}},
Definition: DefinitionWithSequenceIdentityRegistrations(Definition[config]{}, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items").Index(0)), IdentityFields: []string{"id"}}}),
})
requireSequenceRegistrationOptionError(t, err)
if ordinaryReader.Len() == 0 {
@@ -257,7 +249,7 @@ func TestSequenceRegistrationPreflightDoesNotReadSources(t *testing.T) {
mainReader := strings.NewReader("value: main\n")
underlayReader := strings.NewReader("value: underlay\n")
_, err = LoadLayered[config](Input{Source: Reader("main.yml", mainReader)}, SecretBinding{}, LayeredOptions[config]{
Definition: Definition[config]{sequenceRegistrations: []sequenceRegistration{{path: Keys("items").Index(0), identityField: "id"}}},
Definition: DefinitionWithSequenceIdentityRegistrations(Definition[config]{}, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items").Index(0)), IdentityFields: []string{"id"}}}),
Underlays: []Underlay{{Inputs: []Input{{Source: Reader("underlay.yml", underlayReader)}}}},
})
requireSequenceRegistrationOptionError(t, err)
@@ -266,6 +258,8 @@ func TestSequenceRegistrationPreflightDoesNotReadSources(t *testing.T) {
}
}
func pathPointer(path Path) *Path { return &path }
func requireSequenceRegistrationOptionError(t *testing.T, err error) {
t.Helper()
if err == nil {
@@ -289,7 +283,7 @@ func TestRegisteredSequencePolicyAcrossLoadingLifecycle(t *testing.T) {
type config struct {
Items []item `yaml:"items"`
}
definition := Definition[config]{sequenceRegistrations: []sequenceRegistration{{path: Keys("items"), identityField: "id"}}}
definition := Definition[config]{sequenceIdentityRegistrations: []sequenceIdentityRegistration{{selector: sequenceIdentitySelector{literalSequenceSelector("items")}, identityFields: []string{"id"}}}}
registered := Options[config]{Definition: definition}
loadCode := func(t *testing.T, err error) Code {
t.Helper()
@@ -327,7 +321,7 @@ func TestRegisteredSequencePolicyAcrossLoadingLifecycle(t *testing.T) {
{source: Bytes("first", []byte("items: [{id: lower}]\n"))},
{source: Bytes("second", []byte("items: [{id: other}]\n"))},
}}},
definition: definition, sequencePolicy: mustCompileSequencePolicy(t, definition.sequenceRegistrations),
definition: definition, sequencePolicy: mustCompileSequenceIdentityPolicy(t, definition.sequenceIdentityRegistrations),
}
result, err := testGroupedLoad(request)
if err != nil || len(result.Config.Items) != 3 || result.Config.Items[0].ID != "lower" || result.Config.Items[1].ID != "other" || result.Config.Items[2].ID != "shared" {
@@ -352,7 +346,7 @@ func TestRegisteredSequencePolicyAcrossLoadingLifecycle(t *testing.T) {
request := loadRequest[config]{
main: loadInput{source: Bytes("main", []byte("items: []\n"))},
underlays: []loadGroup{{inputs: []loadInput{{source: Bytes("lower", []byte("items: [{id: lower}]\n"))}}}},
definition: definition, sequencePolicy: mustCompileSequencePolicy(t, definition.sequenceRegistrations),
definition: definition, sequencePolicy: mustCompileSequenceIdentityPolicy(t, definition.sequenceIdentityRegistrations),
}
result, err = testGroupedLoad(request)
if err != nil || len(result.Config.Items) != 1 || result.Config.Items[0].ID != "lower" {
@@ -378,7 +372,7 @@ func TestRegisteredSequencePolicyAcrossLoadingLifecycle(t *testing.T) {
request := loadRequest[config]{
main: loadInput{source: Bytes("main", []byte("items: [{id: main}]\n"))},
underlays: []loadGroup{{inputs: []loadInput{{source: Bytes("lower", []byte("items: [{id: same}, {id: same}]\n"))}}}},
definition: definition, sequencePolicy: mustCompileSequencePolicy(t, definition.sequenceRegistrations),
definition: definition, sequencePolicy: mustCompileSequenceIdentityPolicy(t, definition.sequenceIdentityRegistrations),
}
if got := loadCode(t, func() error { _, err := testGroupedLoad(request); return err }()); got != CodeComposeConflict {
t.Fatalf("hidden lower-tier error = %s", got)
@@ -447,9 +441,9 @@ func TestRegisteredSequencePolicyAcrossLoadingLifecycle(t *testing.T) {
})
}
func mustCompileSequencePolicy(t *testing.T, registrations []sequenceRegistration) sequencePolicy {
func mustCompileSequenceIdentityPolicy(t *testing.T, registrations []sequenceIdentityRegistration) sequencePolicy {
t.Helper()
policy, diagnostics := compileSequencePolicy(registrations)
policy, diagnostics := compileSequenceIdentityPolicy(registrations)
if len(diagnostics) != 0 {
t.Fatal(diagnostics)
}