Allow field names as rule options
Change-Id: Ife4f15a09818cf6daf86b96e7c916854d0299be8
diff --git a/cmd/koralmapper/cfgparam.go b/cmd/koralmapper/cfgparam.go
index dc00da5..e5225b0 100644
--- a/cmd/koralmapper/cfgparam.go
+++ b/cmd/koralmapper/cfgparam.go
@@ -17,6 +17,8 @@
LayerA string
FoundryB string
LayerB string
+ FieldA string
+ FieldB string
}
// ParseCfgParam parses the compact cfg URL parameter into a slice of
@@ -26,9 +28,12 @@
// Format: entry (";" entry)*
//
// entry = id ":" dir [ ":" foundryA ":" layerA ":" foundryB ":" layerB ]
+// | id ":" dir [ ":" fieldA ":" fieldB ]
//
-// An entry has either 2 fields (all foundry/layer use defaults) or
-// 6 fields (explicit values, empty means use default).
+// Annotation entries have either 2 fields (all foundry/layer use defaults)
+// or 6 fields (explicit values, empty means use default).
+// Corpus entries have either 2 fields (all field overrides use defaults)
+// or 4 fields (explicit values, empty means use default).
func ParseCfgParam(raw string, lists []config.MappingList) ([]CascadeEntry, error) {
if raw == "" {
return nil, nil
@@ -45,9 +50,8 @@
for _, part := range parts {
fields := strings.Split(part, ":")
n := len(fields)
-
- if n != 2 && n != 6 {
- return nil, fmt.Errorf("invalid entry %q: expected 2 or 6 colon-separated fields, got %d", part, n)
+ if n < 2 {
+ return nil, fmt.Errorf("invalid entry %q: expected at least 2 colon-separated fields, got %d", part, n)
}
id := fields[0]
@@ -61,30 +65,52 @@
if !ok {
return nil, fmt.Errorf("unknown mapping ID %q", id)
}
+ isCorpus := list.IsCorpus()
+
+ if isCorpus {
+ if n != 2 && n != 4 {
+ return nil, fmt.Errorf("invalid corpus entry %q: expected 2 or 4 colon-separated fields, got %d", part, n)
+ }
+ } else if n != 2 && n != 6 {
+ return nil, fmt.Errorf("invalid annotation entry %q: expected 2 or 6 colon-separated fields, got %d", part, n)
+ }
ce := CascadeEntry{
ID: id,
Direction: dir,
}
- if n == 6 {
- ce.FoundryA = fields[2]
- ce.LayerA = fields[3]
- ce.FoundryB = fields[4]
- ce.LayerB = fields[5]
- }
+ if isCorpus {
+ if n == 4 {
+ ce.FieldA = fields[2]
+ ce.FieldB = fields[3]
+ }
+ if ce.FieldA == "" {
+ ce.FieldA = list.FieldA
+ }
+ if ce.FieldB == "" {
+ ce.FieldB = list.FieldB
+ }
+ } else {
+ if n == 6 {
+ ce.FoundryA = fields[2]
+ ce.LayerA = fields[3]
+ ce.FoundryB = fields[4]
+ ce.LayerB = fields[5]
+ }
- if ce.FoundryA == "" {
- ce.FoundryA = list.FoundryA
- }
- if ce.LayerA == "" {
- ce.LayerA = list.LayerA
- }
- if ce.FoundryB == "" {
- ce.FoundryB = list.FoundryB
- }
- if ce.LayerB == "" {
- ce.LayerB = list.LayerB
+ if ce.FoundryA == "" {
+ ce.FoundryA = list.FoundryA
+ }
+ if ce.LayerA == "" {
+ ce.LayerA = list.LayerA
+ }
+ if ce.FoundryB == "" {
+ ce.FoundryB = list.FoundryB
+ }
+ if ce.LayerB == "" {
+ ce.LayerB = list.LayerB
+ }
}
result = append(result, ce)
@@ -94,9 +120,11 @@
}
// BuildCfgParam serialises a slice of CascadeEntry back to the compact
-// cfg string format. Entries with all foundry/layer fields empty use
-// the short 2-field format (id:dir). Entries with any non-empty
-// foundry/layer field use the full 6-field format.
+// cfg string format. Entries with all override fields empty use the
+// short 2-field format (id:dir). Entries with any non-empty
+// foundry/layer field use the full 6-field annotation format.
+// Entries with any non-empty fieldA/fieldB use the full 4-field
+// corpus format.
func BuildCfgParam(entries []CascadeEntry) string {
if len(entries) == 0 {
return ""
@@ -104,8 +132,10 @@
parts := make([]string, len(entries))
for i, e := range entries {
- if e.FoundryA == "" && e.LayerA == "" && e.FoundryB == "" && e.LayerB == "" {
+ if e.FoundryA == "" && e.LayerA == "" && e.FoundryB == "" && e.LayerB == "" && e.FieldA == "" && e.FieldB == "" {
parts[i] = e.ID + ":" + e.Direction
+ } else if e.FoundryA == "" && e.LayerA == "" && e.FoundryB == "" && e.LayerB == "" {
+ parts[i] = e.ID + ":" + e.Direction + ":" + e.FieldA + ":" + e.FieldB
} else {
parts[i] = e.ID + ":" + e.Direction + ":" + e.FoundryA + ":" + e.LayerA + ":" + e.FoundryB + ":" + e.LayerB
}
diff --git a/cmd/koralmapper/cfgparam_test.go b/cmd/koralmapper/cfgparam_test.go
index 398869c..2fb2e7f 100644
--- a/cmd/koralmapper/cfgparam_test.go
+++ b/cmd/koralmapper/cfgparam_test.go
@@ -28,6 +28,8 @@
{
ID: "corpus-map",
Type: "corpus",
+ FieldA: "wikiCat",
+ FieldB: "textClass",
Mappings: []tmconfig.MappingRule{"textClass=science <> textClass=akademisch"},
},
}
@@ -91,17 +93,17 @@
{
name: "Malformed entry with 3 fields",
raw: "stts-upos:atob:extra",
- wantErr: "invalid entry",
+ wantErr: "invalid annotation entry",
},
{
name: "Malformed entry with 4 fields",
raw: "stts-upos:atob:a:b",
- wantErr: "invalid entry",
+ wantErr: "invalid annotation entry",
},
{
name: "Malformed entry with 5 fields",
raw: "stts-upos:atob:a:b:c",
- wantErr: "invalid entry",
+ wantErr: "invalid annotation entry",
},
{
name: "Empty override fields fall back to YAML defaults",
@@ -118,13 +120,25 @@
},
},
{
- name: "Corpus mapping 2-field entry has no foundry/layer defaults",
+ name: "Corpus mapping 2-field entry uses field defaults",
raw: "corpus-map:atob",
expected: []CascadeEntry{
- {ID: "corpus-map", Direction: "atob"},
+ {ID: "corpus-map", Direction: "atob", FieldA: "wikiCat", FieldB: "textClass"},
},
},
{
+ name: "Corpus mapping 4-field entry overrides defaults",
+ raw: "corpus-map:btoa:genre:topic",
+ expected: []CascadeEntry{
+ {ID: "corpus-map", Direction: "btoa", FieldA: "genre", FieldB: "topic"},
+ },
+ },
+ {
+ name: "Annotation mapping 4-field entry is invalid",
+ raw: "stts-upos:atob:foo:bar",
+ wantErr: "invalid annotation entry",
+ },
+ {
name: "Invalid direction",
raw: "stts-upos:invalid",
wantErr: "invalid direction",
@@ -134,7 +148,7 @@
raw: "stts-upos:atob;corpus-map:atob;other-mapper:btoa",
expected: []CascadeEntry{
{ID: "stts-upos", Direction: "atob", FoundryA: "opennlp", LayerA: "p", FoundryB: "upos", LayerB: "p"},
- {ID: "corpus-map", Direction: "atob"},
+ {ID: "corpus-map", Direction: "atob", FieldA: "wikiCat", FieldB: "textClass"},
{ID: "other-mapper", Direction: "btoa", FoundryA: "stts", LayerA: "p", FoundryB: "ud", LayerB: "pos"},
},
},
@@ -175,6 +189,13 @@
expected: "corpus-map:atob",
},
{
+ name: "Corpus entry with field overrides uses 4-field format",
+ entries: []CascadeEntry{
+ {ID: "corpus-map", Direction: "atob", FieldA: "genre", FieldB: "topic"},
+ },
+ expected: "corpus-map:atob:genre:topic",
+ },
+ {
name: "Multiple entries",
entries: []CascadeEntry{
{ID: "stts-upos", Direction: "atob", FoundryA: "opennlp", LayerA: "p", FoundryB: "upos", LayerB: "p"},
@@ -218,7 +239,7 @@
}
func TestBuildAndParseCfgParamRoundTrip(t *testing.T) {
- original := "stts-upos:atob:opennlp:p:upos:p;corpus-map:btoa"
+ original := "stts-upos:atob:opennlp:p:upos:p;corpus-map:btoa:wikiCat:textClass"
entries, err := ParseCfgParam(original, cfgTestLists)
require.NoError(t, err)
diff --git a/cmd/koralmapper/main.go b/cmd/koralmapper/main.go
index 9b9b95d..3a2b318 100644
--- a/cmd/koralmapper/main.go
+++ b/cmd/koralmapper/main.go
@@ -448,6 +448,8 @@
LayerA: entry.LayerA,
FoundryB: entry.FoundryB,
LayerB: entry.LayerB,
+ FieldA: entry.FieldA,
+ FieldB: entry.FieldB,
})
}
@@ -505,6 +507,8 @@
LayerA: entry.LayerA,
FoundryB: entry.FoundryB,
LayerB: entry.LayerB,
+ FieldA: entry.FieldA,
+ FieldB: entry.FieldB,
})
}
diff --git a/cmd/koralmapper/main_test.go b/cmd/koralmapper/main_test.go
index ca287f0..c9b1be4 100644
--- a/cmd/koralmapper/main_test.go
+++ b/cmd/koralmapper/main_test.go
@@ -1748,7 +1748,7 @@
expectedCode: http.StatusBadRequest,
input: `{"fields": []}`,
assertBody: func(t *testing.T, actual map[string]any) {
- assert.Contains(t, actual["error"], "expected 2 or 6 colon-separated fields")
+ assert.Contains(t, actual["error"], "expected at least 2 colon-separated fields")
},
},
}
@@ -1892,6 +1892,8 @@
ID: "corpus-mapper",
Type: "corpus",
Description: "Corpus mapping",
+ FieldA: "wikiCat",
+ FieldB: "textClass",
Mappings: []tmconfig.MappingRule{"textClass=science <> textClass=akademisch"},
},
}
@@ -1951,9 +1953,16 @@
assert.Contains(t, htmlContent, "Annotation mapping")
// Corpus mapping entries
- assert.Contains(t, htmlContent, "(corpus) corpus-mapper")
+ assert.Contains(t, htmlContent, "(corpus)")
+ assert.Contains(t, htmlContent, "<strong>corpus-mapper</strong>")
assert.Contains(t, htmlContent, `data-id="corpus-mapper"`)
assert.Contains(t, htmlContent, `data-type="corpus"`)
+ assert.Contains(t, htmlContent, `data-default-field-a="wikiCat"`)
+ assert.Contains(t, htmlContent, `data-default-field-b="textClass"`)
+ assert.Contains(t, htmlContent, `class="request-fieldA"`)
+ assert.Contains(t, htmlContent, `class="request-fieldB"`)
+ assert.Contains(t, htmlContent, `class="response-fieldA"`)
+ assert.Contains(t, htmlContent, `class="response-fieldB"`)
assert.Contains(t, htmlContent, "Corpus mapping")
}
@@ -2013,11 +2022,13 @@
assert.Contains(t, htmlContent, `class="checkbox response-cb"`)
}
-func TestConfigPageCorpusMappingHasNoFoundryInputs(t *testing.T) {
+func TestConfigPageCorpusMappingHasFieldAndDirectionInputs(t *testing.T) {
lists := []tmconfig.MappingList{
{
ID: "corpus-mapper",
Type: "corpus",
+ FieldA: "genre",
+ FieldB: "topic",
Mappings: []tmconfig.MappingRule{"textClass=science <> textClass=akademisch"},
},
}
@@ -2045,12 +2056,20 @@
assert.Contains(t, htmlContent, `data-type="corpus"`)
// Checkboxes present
- assert.Contains(t, htmlContent, `class="request-cb"`)
- assert.Contains(t, htmlContent, `class="response-cb"`)
+ assert.Contains(t, htmlContent, `class="checkbox request-cb"`)
+ assert.Contains(t, htmlContent, `class="checkbox response-cb"`)
- // No foundry/layer inputs (only corpus mappings, no annotation section)
+ // No annotation foundry/layer inputs (only corpus mappings)
assert.NotContains(t, htmlContent, `class="request-foundryA"`)
- assert.NotContains(t, htmlContent, `class="request-dir-arrow"`)
+ assert.NotContains(t, htmlContent, `class="request-layerA"`)
+ assert.Contains(t, htmlContent, `class="request-dir-arrow"`)
+ assert.Contains(t, htmlContent, `class="response-dir-arrow"`)
+ assert.Contains(t, htmlContent, `class="request-fieldA"`)
+ assert.Contains(t, htmlContent, `class="request-fieldB"`)
+ assert.Contains(t, htmlContent, `class="response-fieldA"`)
+ assert.Contains(t, htmlContent, `class="response-fieldB"`)
+ assert.Contains(t, htmlContent, `value="genre"`)
+ assert.Contains(t, htmlContent, `value="topic"`)
}
func TestConfigPageBackwardCompatibility(t *testing.T) {
diff --git a/cmd/koralmapper/static/config.html b/cmd/koralmapper/static/config.html
index b98254d..c258267 100644
--- a/cmd/koralmapper/static/config.html
+++ b/cmd/koralmapper/static/config.html
@@ -33,9 +33,16 @@
</div>
{{end}}
{{range $.CorpusMappings}}
- <div class="mapping" data-id="{{.ID}}" data-type="corpus" data-mode="{{$section.Mode}}">
+ <div class="mapping" data-id="{{.ID}}" data-type="corpus" data-mode="{{$section.Mode}}"
+ data-default-field-a="{{.FieldA}}" data-default-field-b="{{.FieldB}}">
<div class="mapping-row">
- <label><input type="checkbox" class="{{$section.CheckboxClass}}" name="{{$section.CheckboxName}}"> (corpus) {{.ID}}</label>
+ <input type="checkbox" id="check-{{.ID}}-{{$section.Mode}}" class="checkbox {{$section.CheckboxClass}}" name="{{$section.CheckboxName}}">
+ <label for="check-{{.ID}}-{{$section.Mode}}"><span></span>(corpus) <strong>{{.ID}}</strong></label>
+ <div class="mapping-fields {{$section.FieldsClass}}">
+ <input type="text" class="{{$section.Mode}}-fieldA" value="{{.FieldA}}" placeholder="{{.FieldA}}" size="10">
+ <button type="button" class="{{$section.ArrowClass}}" data-dir="{{$section.ArrowDirection}}">{{$section.ArrowLabel}}</button>
+ <input type="text" class="{{$section.Mode}}-fieldB" value="{{.FieldB}}" placeholder="{{.FieldB}}" size="10">
+ </div>
</div>
</div>
{{end}}
diff --git a/cmd/koralmapper/static/config.js b/cmd/koralmapper/static/config.js
index 3bf9224..9dff479 100644
--- a/cmd/koralmapper/static/config.js
+++ b/cmd/koralmapper/static/config.js
@@ -50,6 +50,8 @@
layerA: "." + mode + "-layerA",
foundryB: "." + mode + "-foundryB",
layerB: "." + mode + "-layerB",
+ fieldA: "." + mode + "-fieldA",
+ fieldB: "." + mode + "-fieldB",
dirArrow: "." + mode + "-dir-arrow"
};
}
@@ -68,7 +70,9 @@
foundryA: inputValue(div, classes.foundryA),
layerA: inputValue(div, classes.layerA),
foundryB: inputValue(div, classes.foundryB),
- layerB: inputValue(div, classes.layerB)
+ layerB: inputValue(div, classes.layerB),
+ fieldA: inputValue(div, classes.fieldA),
+ fieldB: inputValue(div, classes.fieldB)
};
}
@@ -82,13 +86,8 @@
id: requestDiv.dataset.id
};
- if (requestDiv.dataset.type !== "corpus") {
- entry.request = getModeState(requestDiv, "request");
- entry.response = responseDiv ? getModeState(responseDiv, "response") : { enabled: false };
- } else {
- entry.request = { enabled: requestDiv.querySelector(".request-cb").checked };
- entry.response = { enabled: responseDiv && responseDiv.querySelector(".response-cb").checked };
- }
+ entry.request = getModeState(requestDiv, "request");
+ entry.response = responseDiv ? getModeState(responseDiv, "response") : { enabled: false };
state.mappings.push(entry);
}
@@ -121,6 +120,8 @@
setInputValue(div, classes.layerA, modeState.layerA);
setInputValue(div, classes.foundryB, modeState.foundryB);
setInputValue(div, classes.layerB, modeState.layerB);
+ setInputValue(div, classes.fieldA, modeState.fieldA);
+ setInputValue(div, classes.fieldB, modeState.fieldB);
}
function restoreFormState(saved) {
@@ -167,13 +168,21 @@
}
}
} else {
- var requestCb = requestDiv.querySelector(".request-cb");
- var responseCb = responseDiv ? responseDiv.querySelector(".response-cb") : null;
- if (requestCb) {
- requestCb.checked = !!(entry.request && entry.request.enabled);
- }
- if (responseCb) {
- responseCb.checked = !!(entry.response && entry.response.enabled);
+ // Backward compatibility with old cookie schema.
+ if (entry.request && typeof entry.request === "object") {
+ restoreModeState(requestDiv, "request", entry.request);
+ if (responseDiv) {
+ restoreModeState(responseDiv, "response", entry.response);
+ }
+ } else {
+ var requestCb = requestDiv.querySelector(".request-cb");
+ var responseCb = responseDiv ? responseDiv.querySelector(".response-cb") : null;
+ if (requestCb) {
+ requestCb.checked = !!entry.request;
+ }
+ if (responseCb) {
+ responseCb.checked = !!entry.response;
+ }
}
}
}
@@ -221,7 +230,13 @@
parts.push(id + ":" + dir);
}
} else {
- parts.push(id + ":" + dir);
+ var fieldA = cfgFieldValue(div, classes.fieldA, "defaultFieldA");
+ var fieldB = cfgFieldValue(div, classes.fieldB, "defaultFieldB");
+ if (fieldA || fieldB) {
+ parts.push(id + ":" + dir + ":" + fieldA + ":" + fieldB);
+ } else {
+ parts.push(id + ":" + dir);
+ }
}
}
@@ -250,18 +265,21 @@
if (newQueryPipe === lastQueryPipe && newResponsePipe === lastResponsePipe) return;
if (typeof KorAPlugin !== "undefined") {
- if (lastQueryPipe) {
- KorAPlugin.sendMsg({ action: "pipe", job: "del", service: lastQueryPipe });
+ if (newQueryPipe !== lastQueryPipe) {
+ if (lastQueryPipe) {
+ KorAPlugin.sendMsg({ action: "pipe", job: "del", service: lastQueryPipe });
+ }
+ if (newQueryPipe) {
+ KorAPlugin.sendMsg({ action: "pipe", job: "add", service: newQueryPipe });
+ }
}
- if (lastResponsePipe) {
- KorAPlugin.sendMsg({ action: "pipe", job: "del-after", service: lastResponsePipe });
- }
-
- if (newQueryPipe) {
- KorAPlugin.sendMsg({ action: "pipe", job: "add", service: newQueryPipe });
- }
- if (newResponsePipe) {
- KorAPlugin.sendMsg({ action: "pipe", job: "add-after", service: newResponsePipe });
+ if (newResponsePipe !== lastResponsePipe) {
+ if (lastResponsePipe) {
+ KorAPlugin.sendMsg({ action: "pipe", job: "del-after", service: lastResponsePipe });
+ }
+ if (newResponsePipe) {
+ KorAPlugin.sendMsg({ action: "pipe", job: "add-after", service: newResponsePipe });
+ }
}
}
diff --git a/cmd/koralmapper/static/style.css b/cmd/koralmapper/static/style.css
index 8aa173d..6680a1c 100644
--- a/cmd/koralmapper/static/style.css
+++ b/cmd/koralmapper/static/style.css
@@ -1,4 +1,10 @@
-.mapping-row { display: flex; align-items: center; gap: 0.75rem; margin: 0.35rem 0; }
+.mapping-row {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ margin: 0.35rem 0;
+ background-color: var(--color-bg-secondary);
+}
.mapping-fields { display: inline-flex; align-items: center; gap: 0.35rem; }
.mapping-fields input[type="text"] { font-family: monospace; }
.request-fields, .response-fields { flex-wrap: wrap; }
diff --git a/mapper/corpus.go b/mapper/corpus.go
index f5800ba..9aad21e 100644
--- a/mapper/corpus.go
+++ b/mapper/corpus.go
@@ -10,7 +10,7 @@
// Rules are applied iteratively: each rule is applied to the entire tree,
// and subsequent rules see the transformed result.
func (m *Mapper) applyCorpusQueryMappings(mappingID string, opts MappingOptions, jsonData any) (any, error) {
- rules := m.parsedCorpusRules[mappingID]
+ rules := m.rulesWithFieldOverrides(m.parsedCorpusRules[mappingID], opts)
jsonMap, ok := jsonData.(map[string]any)
if !ok {
@@ -383,7 +383,7 @@
// applyCorpusResponseMappings processes fields arrays with corpus rules.
func (m *Mapper) applyCorpusResponseMappings(mappingID string, opts MappingOptions, jsonData any) (any, error) {
- rules := m.parsedCorpusRules[mappingID]
+ rules := m.rulesWithFieldOverrides(m.parsedCorpusRules[mappingID], opts)
jsonMap, ok := jsonData.(map[string]any)
if !ok {
@@ -535,3 +535,40 @@
return result
}
+func (m *Mapper) rulesWithFieldOverrides(rules []*parser.CorpusMappingResult, opts MappingOptions) []*parser.CorpusMappingResult {
+ if opts.FieldA == "" && opts.FieldB == "" {
+ return rules
+ }
+
+ result := make([]*parser.CorpusMappingResult, len(rules))
+ for i, rule := range rules {
+ upper := rule.Upper.Clone()
+ lower := rule.Lower.Clone()
+
+ if opts.FieldA != "" {
+ applyCorpusKeyOverride(upper, opts.FieldA)
+ }
+ if opts.FieldB != "" {
+ applyCorpusKeyOverride(lower, opts.FieldB)
+ }
+
+ result[i] = &parser.CorpusMappingResult{
+ Upper: upper,
+ Lower: lower,
+ }
+ }
+
+ return result
+}
+
+func applyCorpusKeyOverride(node parser.CorpusNode, key string) {
+ switch n := node.(type) {
+ case *parser.CorpusField:
+ n.Key = key
+ case *parser.CorpusGroup:
+ for _, op := range n.Operands {
+ applyCorpusKeyOverride(op, key)
+ }
+ }
+}
+
diff --git a/mapper/corpus_test.go b/mapper/corpus_test.go
index df2ae85..c5e129c 100644
--- a/mapper/corpus_test.go
+++ b/mapper/corpus_test.go
@@ -162,6 +162,66 @@
assert.Equal(t, "fiction", corpus["value"])
}
+func TestCorpusQueryFieldOverridesAtoB(t *testing.T) {
+ m, err := NewMapper([]config.MappingList{{
+ ID: "corpus-test",
+ Type: "corpus",
+ FieldA: "textClass",
+ FieldB: "genre",
+ Mappings: []config.MappingRule{"novel <> fiction"},
+ }})
+ require.NoError(t, err)
+
+ input := map[string]any{
+ "corpus": map[string]any{
+ "@type": "koral:doc",
+ "key": "domain",
+ "value": "novel",
+ "match": "match:eq",
+ },
+ }
+ result, err := m.ApplyQueryMappings("corpus-test", MappingOptions{
+ Direction: AtoB,
+ FieldA: "domain",
+ FieldB: "subject",
+ }, input)
+ require.NoError(t, err)
+
+ corpus := result.(map[string]any)["corpus"].(map[string]any)
+ assert.Equal(t, "subject", corpus["key"])
+ assert.Equal(t, "fiction", corpus["value"])
+}
+
+func TestCorpusQueryFieldOverridesBtoA(t *testing.T) {
+ m, err := NewMapper([]config.MappingList{{
+ ID: "corpus-test",
+ Type: "corpus",
+ FieldA: "textClass",
+ FieldB: "genre",
+ Mappings: []config.MappingRule{"novel <> fiction"},
+ }})
+ require.NoError(t, err)
+
+ input := map[string]any{
+ "corpus": map[string]any{
+ "@type": "koral:doc",
+ "key": "subject",
+ "value": "fiction",
+ "match": "match:eq",
+ },
+ }
+ result, err := m.ApplyQueryMappings("corpus-test", MappingOptions{
+ Direction: BtoA,
+ FieldA: "domain",
+ FieldB: "subject",
+ }, input)
+ require.NoError(t, err)
+
+ corpus := result.(map[string]any)["corpus"].(map[string]any)
+ assert.Equal(t, "domain", corpus["key"])
+ assert.Equal(t, "novel", corpus["value"])
+}
+
func TestCorpusQueryFieldGroupAlias(t *testing.T) {
m := newCorpusMapper(t, "textClass=novel <> genre=fiction")
diff --git a/mapper/mapper.go b/mapper/mapper.go
index 4869c7b..ce172de 100644
--- a/mapper/mapper.go
+++ b/mapper/mapper.go
@@ -100,6 +100,8 @@
LayerA string
FoundryB string
LayerB string
+ FieldA string
+ FieldB string
Direction Direction
AddRewrites bool
}