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 }