Compose registered sequences by identity
This commit is contained in:
@@ -2,6 +2,7 @@ package engine
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||
@@ -190,6 +191,164 @@ func TestComposeDocumentsRejectsInvalidBoundaries(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestComposeDocumentsAppendsRegisteredSequencesAndReturnsRanges(t *testing.T) {
|
||||
first := parseCompositionDocument(t, "source-1", "records: [{id: '', left: 1}, {id: ' spaced ', right: 1}]\nother: [one]\n")
|
||||
second := parseCompositionDocument(t, "source-2", "records: [{id: ' ', text: second}]\n")
|
||||
firstBefore, secondBefore := document.Clone(first), document.Clone(second)
|
||||
policy := sequencePolicy{identityFields: map[string]string{"/records": "id"}}
|
||||
|
||||
composed, diagnostics := composeDocumentsWithPolicy(policy, first, second)
|
||||
if len(diagnostics) != 0 {
|
||||
t.Fatalf("composeDocumentsWithPolicy() diagnostics = %#v", diagnostics)
|
||||
}
|
||||
records, ok := documentAtPath(composed.root, Keys("records"))
|
||||
if !ok || records.Kind != document.Sequence || len(records.Sequence) != 3 {
|
||||
t.Fatalf("records = %#v, want three appended records", records)
|
||||
}
|
||||
if got := composed.sequenceRanges; !reflect.DeepEqual(got, []sequenceRange{
|
||||
{sourceIndex: 0, path: Keys("records"), start: 0, end: 2},
|
||||
{sourceIndex: 1, path: Keys("records"), start: 2, end: 3},
|
||||
}) {
|
||||
t.Fatalf("sequence ranges = %#v", got)
|
||||
}
|
||||
if !reflect.DeepEqual(first, firstBefore) || !reflect.DeepEqual(second, secondBefore) {
|
||||
t.Fatal("registered composition mutated an input")
|
||||
}
|
||||
composed.root.Mapping[0].Value.Sequence[0].Mapping[1].Value.Text = "changed"
|
||||
if !reflect.DeepEqual(first, firstBefore) || !reflect.DeepEqual(second, secondBefore) {
|
||||
t.Fatal("composed sequence aliases an input")
|
||||
}
|
||||
}
|
||||
|
||||
func TestComposeDocumentsValidatesRegisteredSequenceSingletons(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
path string
|
||||
wantCode Code
|
||||
}{
|
||||
{name: "nonmapping item", body: "records: [plain]\n", path: "/records/0", wantCode: CodeComposeInvalid},
|
||||
{name: "missing identity", body: "records: [{name: plain}]\n", path: "/records/0", wantCode: CodeComposeInvalid},
|
||||
{name: "null identity", body: "records: [{id: null}]\n", path: "/records/0/id", wantCode: CodeComposeInvalid},
|
||||
{name: "numeric identity", body: "records: [{id: 12}]\n", path: "/records/0/id", wantCode: CodeComposeInvalid},
|
||||
{name: "boolean identity", body: "records: [{id: false}]\n", path: "/records/0/id", wantCode: CodeComposeInvalid},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
_, diagnostics := composeDocumentsWithPolicy(
|
||||
sequencePolicy{identityFields: map[string]string{"/records": "id"}},
|
||||
parseCompositionDocument(t, "source-1", test.body),
|
||||
)
|
||||
if len(diagnostics) != 1 || diagnostics[0].Code != test.wantCode || diagnostics[0].Path.String() != test.path {
|
||||
t.Fatalf("diagnostics = %#v, want %s at %s", diagnostics, test.wantCode, test.path)
|
||||
}
|
||||
if diagnostics[0].Primary == nil {
|
||||
t.Fatal("malformed sequence diagnostic has no source location")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestComposeDocumentsRejectsRegisteredDuplicateIdentities(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
first string
|
||||
second string
|
||||
within bool
|
||||
}{
|
||||
{name: "within identical bodies", first: "records: [{id: '', value: same}, {id: '', value: same}]\n", within: true},
|
||||
{name: "within disjoint bodies", first: "records: [{id: same, left: 1}, {id: same, right: 2}]\n", within: true},
|
||||
{name: "across identical bodies", first: "records: [{id: same, value: same}]\n", second: "records: [{id: same, value: same}]\n"},
|
||||
{name: "across disjoint bodies", first: "records: [{id: same, left: 1}]\n", second: "records: [{id: same, right: 2}]\n"},
|
||||
{name: "empty identity across lists", first: "records: [{id: ''}]\n", second: "records: [{id: ''}]\n"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
inputs := []*document.Node{parseCompositionDocument(t, "source-1", test.first)}
|
||||
if !test.within {
|
||||
inputs = append(inputs, parseCompositionDocument(t, "source-2", test.second))
|
||||
}
|
||||
_, diagnostics := composeDocumentsWithPolicy(
|
||||
sequencePolicy{identityFields: map[string]string{"/records": "id"}}, inputs...,
|
||||
)
|
||||
if len(diagnostics) != 1 || diagnostics[0].Code != CodeComposeConflict || diagnostics[0].Path.String() != "/records" {
|
||||
t.Fatalf("diagnostics = %#v, want duplicate conflict at /records", diagnostics)
|
||||
}
|
||||
if diagnostics[0].Primary == nil || len(diagnostics[0].Related) != 1 {
|
||||
t.Fatalf("duplicate locations = %#v", diagnostics[0])
|
||||
}
|
||||
for _, secret := range []string{"same", "left", "right", "value"} {
|
||||
if strings.Contains(diagnostics[0].Message, secret) {
|
||||
t.Fatalf("diagnostic leaks %q: %q", secret, diagnostics[0].Message)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestComposeDocumentsSkipsMissingAndNonSequenceRegistrations(t *testing.T) {
|
||||
policy := sequencePolicy{identityFields: map[string]string{
|
||||
"/absent": "id", "/not-a-sequence": "id", "/catalog": "id", "/catalog/records": "id",
|
||||
}}
|
||||
composed, diagnostics := composeDocumentsWithPolicy(policy, parseCompositionDocument(t, "source-1", `
|
||||
not-a-sequence: null
|
||||
catalog:
|
||||
records: [{id: present}]
|
||||
`))
|
||||
if len(diagnostics) != 0 {
|
||||
t.Fatalf("composeDocumentsWithPolicy() diagnostics = %#v", diagnostics)
|
||||
}
|
||||
if len(composed.sequenceRanges) != 1 || composed.sequenceRanges[0].path.String() != "/catalog/records" {
|
||||
t.Fatalf("sequence ranges = %#v, want only the reached descendant sequence", composed.sequenceRanges)
|
||||
}
|
||||
|
||||
composed, diagnostics = composeDocumentsWithPolicy(
|
||||
sequencePolicy{identityFields: map[string]string{"/records": "id"}},
|
||||
parseCompositionDocument(t, "source-1", "records: []\n"),
|
||||
parseCompositionDocument(t, "source-2", "records: []\n"),
|
||||
)
|
||||
if len(diagnostics) != 0 || len(composed.root.Mapping[0].Value.Sequence) != 0 {
|
||||
t.Fatalf("empty registered sequences = (%#v, %#v)", composed, diagnostics)
|
||||
}
|
||||
if got := composed.sequenceRanges; !reflect.DeepEqual(got, []sequenceRange{
|
||||
{sourceIndex: 0, path: Keys("records"), start: 0, end: 0},
|
||||
{sourceIndex: 1, path: Keys("records"), start: 0, end: 0},
|
||||
}) {
|
||||
t.Fatalf("empty sequence ranges = %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestComposeDocumentsKeepsUnregisteredSequenceConflicts(t *testing.T) {
|
||||
_, diagnostics := composeDocumentsWithPolicy(
|
||||
sequencePolicy{identityFields: map[string]string{"/registered": "id"}},
|
||||
parseCompositionDocument(t, "source-1", "unregistered: [one]\n"),
|
||||
parseCompositionDocument(t, "source-2", "unregistered: [two]\n"),
|
||||
)
|
||||
assertCompositionConflict(t, diagnostics, "/unregistered", []string{"source-1", "source-2"})
|
||||
}
|
||||
|
||||
func TestComposeDocumentsKeepsNonSequenceRegisteredOverlapsConflicting(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
first string
|
||||
second string
|
||||
}{
|
||||
{name: "mapping and sequence", first: "records: {id: one}\n", second: "records: [{id: two}]\n"},
|
||||
{name: "null and sequence", first: "records: null\n", second: "records: [{id: two}]\n"},
|
||||
{name: "scalar and sequence", first: "records: value\n", second: "records: [{id: two}]\n"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
_, diagnostics := composeDocumentsWithPolicy(
|
||||
sequencePolicy{identityFields: map[string]string{"/records": "id"}},
|
||||
parseCompositionDocument(t, "source-1", test.first),
|
||||
parseCompositionDocument(t, "source-2", test.second),
|
||||
)
|
||||
assertCompositionConflict(t, diagnostics, "/records", []string{"source-1", "source-2"})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func parseCompositionDocument(t *testing.T, sourceID, input string) *document.Node {
|
||||
t.Helper()
|
||||
parsed, diagnostics := parseSource(Bytes(sourceID, []byte(input)), sourceID, defaultParseLimits())
|
||||
|
||||
Reference in New Issue
Block a user