Reject identical source/target in annotation and corpus mappings
Change-Id: I09a410e5d42392680c1ac1c5c9928e3a37aca0cc
diff --git a/mapper/mapper_test.go b/mapper/mapper_test.go
index 4d3a411..4e08d14 100644
--- a/mapper/mapper_test.go
+++ b/mapper/mapper_test.go
@@ -440,8 +440,8 @@
ID: "test-token-to-termgroup",
FoundryA: "opennlp",
LayerA: "p",
- FoundryB: "opennlp", // Keep the same foundry for both sides
- LayerB: "p",
+ FoundryB: "tt",
+ LayerB: "pos",
Mappings: []config.MappingRule{
"[PIDAT] <> [opennlp/p=PIDAT & opennlp/p=AdjType:Pdt]",
},
@@ -809,7 +809,7 @@
FoundryA: "opennlp",
LayerA: "p",
FoundryB: "opennlp",
- LayerB: "p",
+ LayerB: "pos",
Mappings: []config.MappingRule{
"[DET] <> [PRON]",
},
@@ -844,12 +844,18 @@
"@type": "koral:term",
"foundry": "opennlp",
"key": "PRON",
- "layer": "p",
+ "layer": "pos",
"match": "match:eq",
"rewrites": [
{
"@type": "koral:rewrite",
"editor": "Koral-Mapper",
+ "scope": "layer",
+ "original": "p"
+ },
+ {
+ "@type": "koral:rewrite",
+ "editor": "Koral-Mapper",
"scope": "key",
"original": "DET"
}
@@ -1120,3 +1126,211 @@
})
}
}
+
+func TestIdenticalEffectiveFoundryLayerRejected(t *testing.T) {
+ tests := []struct {
+ name string
+ list config.MappingList
+ opts MappingOptions
+ wantErr string
+ }{
+ {
+ name: "YAML defaults identical",
+ list: config.MappingList{
+ ID: "test", FoundryA: "opennlp", LayerA: "p",
+ FoundryB: "opennlp", LayerB: "p",
+ Mappings: []config.MappingRule{"[A] <> [B]"},
+ },
+ opts: MappingOptions{Direction: AtoB},
+ wantErr: "identical source and target",
+ },
+ {
+ name: "Query param override makes them identical",
+ list: config.MappingList{
+ ID: "test", FoundryA: "opennlp", LayerA: "p",
+ FoundryB: "upos", LayerB: "p",
+ Mappings: []config.MappingRule{"[A] <> [B]"},
+ },
+ opts: MappingOptions{Direction: AtoB, FoundryB: "opennlp"},
+ wantErr: "identical source and target",
+ },
+ {
+ name: "Query param override resolves the conflict",
+ list: config.MappingList{
+ ID: "test", FoundryA: "opennlp", LayerA: "p",
+ FoundryB: "opennlp", LayerB: "p",
+ Mappings: []config.MappingRule{"[A] <> [B]"},
+ },
+ opts: MappingOptions{Direction: AtoB, FoundryB: "upos"},
+ wantErr: "",
+ },
+ {
+ name: "Different foundry same layer is allowed",
+ list: config.MappingList{
+ ID: "test", FoundryA: "opennlp", LayerA: "p",
+ FoundryB: "upos", LayerB: "p",
+ Mappings: []config.MappingRule{"[A] <> [B]"},
+ },
+ opts: MappingOptions{Direction: AtoB},
+ wantErr: "",
+ },
+ {
+ name: "Both foundries empty is allowed",
+ list: config.MappingList{
+ ID: "test",
+ Mappings: []config.MappingRule{"[A] <> [B]"},
+ },
+ opts: MappingOptions{Direction: AtoB},
+ wantErr: "",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ m, err := NewMapper([]config.MappingList{tt.list})
+ require.NoError(t, err)
+
+ input := map[string]any{
+ "@type": "koral:token",
+ "wrap": map[string]any{
+ "@type": "koral:term",
+ "key": "A",
+ },
+ }
+
+ _, err = m.ApplyQueryMappings("test", tt.opts, input)
+ if tt.wantErr != "" {
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), tt.wantErr)
+ } else {
+ assert.NoError(t, err)
+ }
+ })
+ }
+}
+
+func TestIdenticalEffectiveFieldRejected(t *testing.T) {
+ tests := []struct {
+ name string
+ list config.MappingList
+ opts MappingOptions
+ wantErr string
+ }{
+ {
+ name: "YAML defaults identical",
+ list: config.MappingList{
+ ID: "test", Type: "corpus",
+ FieldA: "textClass", FieldB: "textClass",
+ Mappings: []config.MappingRule{"novel <> fiction"},
+ },
+ opts: MappingOptions{Direction: AtoB},
+ wantErr: "identical source and target field",
+ },
+ {
+ name: "Query param override makes them identical",
+ list: config.MappingList{
+ ID: "test", Type: "corpus",
+ FieldA: "textClass", FieldB: "genre",
+ Mappings: []config.MappingRule{"novel <> fiction"},
+ },
+ opts: MappingOptions{Direction: AtoB, FieldB: "textClass"},
+ wantErr: "identical source and target field",
+ },
+ {
+ name: "Query param override resolves the conflict",
+ list: config.MappingList{
+ ID: "test", Type: "corpus",
+ FieldA: "textClass", FieldB: "textClass",
+ Mappings: []config.MappingRule{"novel <> fiction"},
+ },
+ opts: MappingOptions{Direction: AtoB, FieldB: "genre"},
+ wantErr: "",
+ },
+ {
+ name: "Different fields is allowed",
+ list: config.MappingList{
+ ID: "test", Type: "corpus",
+ FieldA: "textClass", FieldB: "genre",
+ Mappings: []config.MappingRule{"novel <> fiction"},
+ },
+ opts: MappingOptions{Direction: AtoB},
+ wantErr: "",
+ },
+ {
+ name: "Both fields empty is allowed",
+ list: config.MappingList{
+ ID: "test", Type: "corpus",
+ Mappings: []config.MappingRule{"textClass=novel <> genre=fiction"},
+ },
+ opts: MappingOptions{Direction: AtoB},
+ wantErr: "",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ m, err := NewMapper([]config.MappingList{tt.list})
+ require.NoError(t, err)
+
+ input := map[string]any{
+ "collection": map[string]any{
+ "@type": "koral:doc",
+ "key": "textClass",
+ "value": "novel",
+ "match": "match:eq",
+ },
+ }
+
+ _, err = m.ApplyQueryMappings("test", tt.opts, input)
+ if tt.wantErr != "" {
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), tt.wantErr)
+ } else {
+ assert.NoError(t, err)
+ }
+ })
+ }
+}
+
+func TestIdenticalEffectiveValuesResponseEndpoint(t *testing.T) {
+ t.Run("annotation response rejects identical effective foundry/layer", func(t *testing.T) {
+ m, err := NewMapper([]config.MappingList{{
+ ID: "test", FoundryA: "marmot", LayerA: "p",
+ FoundryB: "marmot", LayerB: "p",
+ Mappings: []config.MappingRule{"[DET] <> [PRON]"},
+ }})
+ require.NoError(t, err)
+
+ input := map[string]any{
+ "snippet": `<span title="marmot/p:DET">Der</span>`,
+ }
+
+ _, err = m.ApplyResponseMappings("test", MappingOptions{Direction: AtoB}, input)
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "identical source and target")
+ })
+
+ t.Run("corpus response rejects identical effective field", func(t *testing.T) {
+ m, err := NewMapper([]config.MappingList{{
+ ID: "test", Type: "corpus",
+ FieldA: "textClass", FieldB: "textClass",
+ Mappings: []config.MappingRule{"novel <> fiction"},
+ }})
+ require.NoError(t, err)
+
+ input := map[string]any{
+ "fields": []any{
+ map[string]any{
+ "@type": "koral:field",
+ "key": "textClass",
+ "value": "novel",
+ "type": "type:string",
+ },
+ },
+ }
+
+ _, err = m.ApplyResponseMappings("test", MappingOptions{Direction: AtoB}, input)
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "identical source and target field")
+ })
+}