Expose sequence identity registrations through the facade
This commit is contained in:
240
facade_sequence_identity_test.go
Normal file
240
facade_sequence_identity_test.go
Normal file
@@ -0,0 +1,240 @@
|
||||
package configkit_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
)
|
||||
|
||||
func TestFacadeSequenceIdentitySnapshotsSelectorsAndFields(t *testing.T) {
|
||||
fields := []string{"namespace", "id", "id"}
|
||||
selector := configkit.SequenceSelector{
|
||||
configkit.SequenceLiteralKey("collections"),
|
||||
configkit.SequenceAnyMappingKey(),
|
||||
configkit.SequenceLiteralKey("prompts"),
|
||||
}
|
||||
identity := configkit.SequenceIdentityMatching(selector, fields...)
|
||||
selector[1] = configkit.SequenceLiteralKey("other")
|
||||
fields[0] = "changed"
|
||||
definition := configkit.Definition[map[string]any]{SequenceIdentities: []configkit.SequenceIdentity{identity}}
|
||||
|
||||
underlay := configkit.Mapping("underlay", map[string]any{"collections": map[string]any{
|
||||
"team": map[string]any{"prompts": []any{map[string]any{"namespace": "team", "id": "kept"}}},
|
||||
}})
|
||||
main := configkit.Mapping("main", map[string]any{"collections": map[string]any{
|
||||
"team": map[string]any{"prompts": []any{map[string]any{"namespace": "team", "id": "added"}}},
|
||||
}})
|
||||
loaded, err := configkit.LoadLayered(configkit.Input{Source: main}, configkit.LayeredOptions[map[string]any]{
|
||||
Definition: definition,
|
||||
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: underlay}}}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("load after mutating constructor inputs: %v", err)
|
||||
}
|
||||
prompts := loaded.Config["collections"].(map[string]any)["team"].(map[string]any)["prompts"].([]any)
|
||||
if len(prompts) != 2 {
|
||||
t.Fatalf("registered compound identity did not append records: %#v", prompts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFacadeSequenceIdentityUsesExactAndDynamicSelectors(t *testing.T) {
|
||||
dynamic := configkit.SequenceIdentityMatching(configkit.SequenceSelector{
|
||||
configkit.SequenceLiteralKey("collections"),
|
||||
configkit.SequenceAnyMappingKey(),
|
||||
configkit.SequenceLiteralKey("prompts"),
|
||||
}, "id")
|
||||
definition := configkit.Definition[map[string]any]{SequenceIdentities: []configkit.SequenceIdentity{
|
||||
configkit.SequenceIdentityAt(configkit.Keys("prompts"), "namespace", "id"),
|
||||
dynamic,
|
||||
}}
|
||||
underlay := configkit.Mapping("underlay", map[string]any{
|
||||
"prompts": []any{map[string]any{"namespace": "alpha", "id": "same"}},
|
||||
"collections": map[string]any{
|
||||
"team": map[string]any{"prompts": []any{map[string]any{"id": "team-one"}}},
|
||||
"cloud": map[string]any{"prompts": []any{map[string]any{"id": "cloud-one"}}},
|
||||
},
|
||||
})
|
||||
main := configkit.Mapping("main", map[string]any{
|
||||
"prompts": []any{map[string]any{"namespace": "beta", "id": "same"}},
|
||||
"collections": map[string]any{
|
||||
"team": map[string]any{"prompts": []any{map[string]any{"id": "team-two"}}},
|
||||
"cloud": map[string]any{"prompts": []any{map[string]any{"id": "cloud-two"}}},
|
||||
},
|
||||
})
|
||||
loaded, err := configkit.LoadLayered(configkit.Input{Source: main}, configkit.LayeredOptions[map[string]any]{
|
||||
Definition: definition,
|
||||
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: underlay}}}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("load mixed exact and dynamic registrations: %v", err)
|
||||
}
|
||||
rootRecords := loaded.Config["prompts"].([]any)
|
||||
if len(rootRecords) != 2 {
|
||||
t.Fatalf("compound root identities = %#v", rootRecords)
|
||||
}
|
||||
collections := loaded.Config["collections"].(map[string]any)
|
||||
for _, name := range []string{"team", "cloud"} {
|
||||
prompts := collections[name].(map[string]any)["prompts"].([]any)
|
||||
if len(prompts) != 2 {
|
||||
t.Errorf("dynamic prompts for %q = %#v", name, prompts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFacadeSequenceIdentityLiteralStarIsNotWildcard(t *testing.T) {
|
||||
definition := configkit.Definition[map[string]any]{SequenceIdentities: []configkit.SequenceIdentity{
|
||||
configkit.SequenceIdentityMatching(configkit.SequenceSelector{
|
||||
configkit.SequenceLiteralKey("collections"),
|
||||
configkit.SequenceLiteralKey("*"),
|
||||
configkit.SequenceLiteralKey("prompts"),
|
||||
}, "id"),
|
||||
}}
|
||||
underlay := configkit.Mapping("underlay", map[string]any{"collections": map[string]any{
|
||||
"*": map[string]any{"prompts": []any{map[string]any{"id": "one"}}},
|
||||
"team": map[string]any{"prompts": []any{map[string]any{"id": "team-one"}}},
|
||||
}})
|
||||
main := configkit.Mapping("main", map[string]any{"collections": map[string]any{
|
||||
"*": map[string]any{"prompts": []any{map[string]any{"id": "two"}}},
|
||||
"team": map[string]any{"prompts": []any{map[string]any{"id": "team-two"}}},
|
||||
}})
|
||||
_, err := configkit.LoadLayered(configkit.Input{Source: configkit.Mapping("main", map[string]any{})}, configkit.LayeredOptions[map[string]any]{
|
||||
Definition: definition,
|
||||
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: underlay}, {Source: main}}}},
|
||||
})
|
||||
loadErr := requireFacadeLoadError(t, err)
|
||||
if len(loadErr.Diagnostics()) == 0 || loadErr.Diagnostics()[0].Path.String() != "/collections/team/prompts" {
|
||||
t.Fatalf("literal star registration also matched another key: %#v", loadErr.Diagnostics())
|
||||
}
|
||||
|
||||
wildcard := configkit.Definition[map[string]any]{SequenceIdentities: []configkit.SequenceIdentity{
|
||||
configkit.SequenceIdentityMatching(configkit.SequenceSelector{
|
||||
configkit.SequenceLiteralKey("collections"),
|
||||
configkit.SequenceAnyMappingKey(),
|
||||
configkit.SequenceLiteralKey("prompts"),
|
||||
}, "id"),
|
||||
}}
|
||||
loaded, err := configkit.LoadLayered(configkit.Input{Source: configkit.Mapping("main", map[string]any{})}, configkit.LayeredOptions[map[string]any]{
|
||||
Definition: wildcard,
|
||||
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: underlay}, {Source: main}}}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("wildcard registration did not match mapping keys: %v", err)
|
||||
}
|
||||
for name, raw := range loaded.Config["collections"].(map[string]any) {
|
||||
if got := len(raw.(map[string]any)["prompts"].([]any)); got != 2 {
|
||||
t.Errorf("wildcard result for %q has %d records", name, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFacadeSequenceIdentityRejectsInvalidDefinitionsBeforeReadingSource(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
identity configkit.SequenceIdentity
|
||||
wantPath string
|
||||
}{
|
||||
{
|
||||
name: "index path",
|
||||
identity: configkit.SequenceIdentityAt(configkit.Keys("records").Index(0), "id"),
|
||||
wantPath: "/records/0",
|
||||
},
|
||||
{
|
||||
name: "empty fields",
|
||||
identity: configkit.SequenceIdentityAt(configkit.Keys("records")),
|
||||
wantPath: "/records",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
reader := strings.NewReader("records: []\n")
|
||||
_, err := configkit.Load(configkit.Reader("input.yml", reader), configkit.Options[map[string]any]{
|
||||
Definition: configkit.Definition[map[string]any]{SequenceIdentities: []configkit.SequenceIdentity{test.identity}},
|
||||
})
|
||||
loadErr := requireFacadeLoadError(t, err)
|
||||
if len(loadErr.Diagnostics()) != 1 || loadErr.Diagnostics()[0].Code != configkit.CodeOptionsInvalid || loadErr.Diagnostics()[0].Path.String() != test.wantPath {
|
||||
t.Fatalf("definition diagnostics = %#v", loadErr.Diagnostics())
|
||||
}
|
||||
if reader.Len() == 0 {
|
||||
t.Fatal("source was read before registration validation")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFacadeSequenceIdentityOverlapFailsBeforeSourceAccess(t *testing.T) {
|
||||
selector := configkit.SequenceSelector{configkit.SequenceLiteralKey("collections"), configkit.SequenceAnyMappingKey()}
|
||||
definition := configkit.Definition[map[string]any]{SequenceIdentities: []configkit.SequenceIdentity{
|
||||
configkit.SequenceIdentityMatching(selector, "id"),
|
||||
configkit.SequenceIdentityMatching(configkit.SequenceSelector{
|
||||
configkit.SequenceLiteralKey("collections"), configkit.SequenceLiteralKey("team"),
|
||||
}, "slug"),
|
||||
}}
|
||||
reader := strings.NewReader("collections: {}\n")
|
||||
_, err := configkit.Load(configkit.Reader("input.yml", reader), configkit.Options[map[string]any]{Definition: definition})
|
||||
loadErr := requireFacadeLoadError(t, err)
|
||||
if len(loadErr.Diagnostics()) == 0 || loadErr.Diagnostics()[0].Code != configkit.CodeOptionsInvalid {
|
||||
t.Fatalf("overlap diagnostics = %#v", loadErr.Diagnostics())
|
||||
}
|
||||
if reader.Len() == 0 {
|
||||
t.Fatal("source was read before overlapping registrations were rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFacadeSequenceIdentityConstraintsApplyAtInheritedDestinations(t *testing.T) {
|
||||
definition := configkit.Definition[map[string]any]{SequenceIdentities: []configkit.SequenceIdentity{
|
||||
configkit.SequenceIdentityMatching(configkit.SequenceSelector{
|
||||
configkit.SequenceLiteralKey("entries"),
|
||||
configkit.SequenceLiteralKey("child"),
|
||||
configkit.SequenceLiteralKey("collections"),
|
||||
configkit.SequenceAnyMappingKey(),
|
||||
configkit.SequenceLiteralKey("prompts"),
|
||||
}, "id"),
|
||||
}}
|
||||
loaded, err := configkit.Load(configkit.Bytes("main", []byte(`jobs:
|
||||
entries:
|
||||
parent:
|
||||
collections:
|
||||
team:
|
||||
prompts: [{id: inherited, text: base}]
|
||||
child:
|
||||
extends: parent
|
||||
collections:
|
||||
team:
|
||||
prompts: [{id: added, text: child}]
|
||||
`)), configkit.Options[map[string]any]{Definition: definition})
|
||||
if err != nil {
|
||||
t.Fatalf("load with dynamic inherited destination: %v", err)
|
||||
}
|
||||
child := loaded.Config["entries"].(map[string]any)["child"].(map[string]any)
|
||||
prompts := child["collections"].(map[string]any)["team"].(map[string]any)["prompts"].([]any)
|
||||
if len(prompts) != 2 {
|
||||
t.Fatalf("inherited dynamic collection = %#v, want parent and child records", prompts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFacadeSequenceIdentityLoadsUseIndependentScopes(t *testing.T) {
|
||||
definition := configkit.Definition[map[string]any]{SequenceIdentities: []configkit.SequenceIdentity{
|
||||
configkit.SequenceIdentityMatching(configkit.SequenceSelector{
|
||||
configkit.SequenceLiteralKey("collections"), configkit.SequenceAnyMappingKey(),
|
||||
}, "id"),
|
||||
}}
|
||||
for _, source := range []string{"first", "second"} {
|
||||
loaded, err := configkit.Load(configkit.Mapping(source, map[string]any{"collections": map[string]any{
|
||||
source: []any{map[string]any{"id": "reused"}},
|
||||
}}), configkit.Options[map[string]any]{Definition: definition})
|
||||
if err != nil || len(loaded.Config["collections"].(map[string]any)[source].([]any)) != 1 {
|
||||
t.Fatalf("independent load %q = %#v, %v", source, loaded, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func requireFacadeLoadError(t *testing.T, err error) *configkit.LoadError {
|
||||
t.Helper()
|
||||
var loadErr *configkit.LoadError
|
||||
if !errors.As(err, &loadErr) {
|
||||
t.Fatalf("error = %v, want *LoadError", err)
|
||||
}
|
||||
return loadErr
|
||||
}
|
||||
Reference in New Issue
Block a user