Add koral:rewrite to query and corpus transformations
Change-Id: I97e3050d39b936256616bdf46203a784de6a3414
diff --git a/cmd/koralmapper/main.go b/cmd/koralmapper/main.go
index d8e51a6..75a1d9c 100644
--- a/cmd/koralmapper/main.go
+++ b/cmd/koralmapper/main.go
@@ -77,6 +77,7 @@
FoundryB string
LayerA string
LayerB string
+ Rewrites *bool // nil = use mapping list default; non-nil = override
}
// MappingSectionData contains per-section UI metadata so request and response
@@ -187,6 +188,11 @@
LayerB: c.Query("layerB", ""),
}
+ if rewrites := c.Query("rewrites", ""); rewrites != "" {
+ v := rewrites == "true"
+ params.Rewrites = &v
+ }
+
// Validate input parameters
if err := validateInput(params.MapID, params.Dir, params.FoundryA, params.FoundryB, params.LayerA, params.LayerB, c.Body()); err != nil {
return nil, err
@@ -317,10 +323,10 @@
app.Post("/response/:cfg", handleCompositeResponseTransform(m, yamlConfig.Lists))
// Transformation endpoint
- app.Post("/:map/query", handleTransform(m))
+ app.Post("/:map/query", handleTransform(m, yamlConfig.Lists))
// Response transformation endpoint
- app.Post("/:map/response", handleResponseTransform(m))
+ app.Post("/:map/response", handleResponseTransform(m, yamlConfig.Lists))
// Kalamar plugin endpoint
app.Get("/", handleKalamarPlugin(yamlConfig, configTmpl, pluginTmpl))
@@ -405,6 +411,11 @@
}
func handleCompositeQueryTransform(m *mapper.Mapper, lists []config.MappingList) fiber.Handler {
+ listsByID := make(map[string]*config.MappingList, len(lists))
+ for i := range lists {
+ listsByID[lists[i].ID] = &lists[i]
+ }
+
return func(c *fiber.Ctx) error {
cfgRaw := c.Params("cfg")
if len(cfgRaw) > maxParamLength {
@@ -431,6 +442,13 @@
return c.JSON(jsonData)
}
+ rewrites := c.Query("rewrites", "")
+ var rewritesOverride *bool
+ if rewrites != "" {
+ v := rewrites == "true"
+ rewritesOverride = &v
+ }
+
orderedIDs := make([]string, 0, len(entries))
opts := make([]mapper.MappingOptions, 0, len(entries))
for _, entry := range entries {
@@ -439,15 +457,24 @@
dir = mapper.BtoA
}
+ addRewrites := false
+ if list, ok := listsByID[entry.ID]; ok {
+ addRewrites = list.Rewrites
+ }
+ if rewritesOverride != nil {
+ addRewrites = *rewritesOverride
+ }
+
orderedIDs = append(orderedIDs, entry.ID)
opts = append(opts, mapper.MappingOptions{
- Direction: dir,
- FoundryA: entry.FoundryA,
- LayerA: entry.LayerA,
- FoundryB: entry.FoundryB,
- LayerB: entry.LayerB,
- FieldA: entry.FieldA,
- FieldB: entry.FieldB,
+ Direction: dir,
+ FoundryA: entry.FoundryA,
+ LayerA: entry.LayerA,
+ FoundryB: entry.FoundryB,
+ LayerB: entry.LayerB,
+ FieldA: entry.FieldA,
+ FieldB: entry.FieldB,
+ AddRewrites: addRewrites,
})
}
@@ -464,6 +491,11 @@
}
func handleCompositeResponseTransform(m *mapper.Mapper, lists []config.MappingList) fiber.Handler {
+ listsByID := make(map[string]*config.MappingList, len(lists))
+ for i := range lists {
+ listsByID[lists[i].ID] = &lists[i]
+ }
+
return func(c *fiber.Ctx) error {
cfgRaw := c.Params("cfg")
if len(cfgRaw) > maxParamLength {
@@ -490,6 +522,13 @@
return c.JSON(jsonData)
}
+ rewrites := c.Query("rewrites", "")
+ var rewritesOverride *bool
+ if rewrites != "" {
+ v := rewrites == "true"
+ rewritesOverride = &v
+ }
+
orderedIDs := make([]string, 0, len(entries))
opts := make([]mapper.MappingOptions, 0, len(entries))
for _, entry := range entries {
@@ -498,15 +537,24 @@
dir = mapper.BtoA
}
+ addRewrites := false
+ if list, ok := listsByID[entry.ID]; ok {
+ addRewrites = list.Rewrites
+ }
+ if rewritesOverride != nil {
+ addRewrites = *rewritesOverride
+ }
+
orderedIDs = append(orderedIDs, entry.ID)
opts = append(opts, mapper.MappingOptions{
- Direction: dir,
- FoundryA: entry.FoundryA,
- LayerA: entry.LayerA,
- FoundryB: entry.FoundryB,
- LayerB: entry.LayerB,
- FieldA: entry.FieldA,
- FieldB: entry.FieldB,
+ Direction: dir,
+ FoundryA: entry.FoundryA,
+ LayerA: entry.LayerA,
+ FoundryB: entry.FoundryB,
+ LayerB: entry.LayerB,
+ FieldA: entry.FieldA,
+ FieldB: entry.FieldB,
+ AddRewrites: addRewrites,
})
}
@@ -522,7 +570,12 @@
}
}
-func handleTransform(m *mapper.Mapper) fiber.Handler {
+func handleTransform(m *mapper.Mapper, lists []config.MappingList) fiber.Handler {
+ listsByID := make(map[string]*config.MappingList, len(lists))
+ for i := range lists {
+ listsByID[lists[i].ID] = &lists[i]
+ }
+
return func(c *fiber.Ctx) error {
// Extract and validate parameters
params, err := extractRequestParams(c)
@@ -540,13 +593,23 @@
})
}
+ // Determine rewrites: query param overrides YAML default
+ addRewrites := false
+ if list, ok := listsByID[params.MapID]; ok {
+ addRewrites = list.Rewrites
+ }
+ if params.Rewrites != nil {
+ addRewrites = *params.Rewrites
+ }
+
// Apply mappings
result, err := m.ApplyQueryMappings(params.MapID, mapper.MappingOptions{
- Direction: direction,
- FoundryA: params.FoundryA,
- FoundryB: params.FoundryB,
- LayerA: params.LayerA,
- LayerB: params.LayerB,
+ Direction: direction,
+ FoundryA: params.FoundryA,
+ FoundryB: params.FoundryB,
+ LayerA: params.LayerA,
+ LayerB: params.LayerB,
+ AddRewrites: addRewrites,
}, jsonData)
if err != nil {
@@ -564,7 +627,12 @@
}
}
-func handleResponseTransform(m *mapper.Mapper) fiber.Handler {
+func handleResponseTransform(m *mapper.Mapper, lists []config.MappingList) fiber.Handler {
+ listsByID := make(map[string]*config.MappingList, len(lists))
+ for i := range lists {
+ listsByID[lists[i].ID] = &lists[i]
+ }
+
return func(c *fiber.Ctx) error {
// Extract and validate parameters
params, err := extractRequestParams(c)
@@ -582,13 +650,23 @@
})
}
+ // Determine rewrites: query param overrides YAML default
+ addRewrites := false
+ if list, ok := listsByID[params.MapID]; ok {
+ addRewrites = list.Rewrites
+ }
+ if params.Rewrites != nil {
+ addRewrites = *params.Rewrites
+ }
+
// Apply response mappings
result, err := m.ApplyResponseMappings(params.MapID, mapper.MappingOptions{
- Direction: direction,
- FoundryA: params.FoundryA,
- FoundryB: params.FoundryB,
- LayerA: params.LayerA,
- LayerB: params.LayerB,
+ Direction: direction,
+ FoundryA: params.FoundryA,
+ FoundryB: params.FoundryB,
+ LayerA: params.LayerA,
+ LayerB: params.LayerB,
+ AddRewrites: addRewrites,
}, jsonData)
if err != nil {
diff --git a/cmd/koralmapper/main_test.go b/cmd/koralmapper/main_test.go
index 64ea8be..a9bb530 100644
--- a/cmd/koralmapper/main_test.go
+++ b/cmd/koralmapper/main_test.go
@@ -59,6 +59,7 @@
layerA: p
foundryB: upos
layerB: p
+ rewrites: true
mappings:
- "[PIDAT] <> [opennlp/p=PIDAT & opennlp/p=AdjType:Pdt]"
- "[DET] <> [opennlp/p=DET]"
@@ -121,7 +122,20 @@
"value": "Pdt"
}
],
- "relation": "relation:and"
+ "relation": "relation:and",
+ "rewrites": [
+ {
+ "@type": "koral:rewrite",
+ "editor": "Koral-Mapper",
+ "original": {
+ "@type": "koral:term",
+ "foundry": "opennlp",
+ "key": "PIDAT",
+ "layer": "p",
+ "match": "match:eq"
+ }
+ }
+ ]
}
}`,
},
@@ -161,7 +175,34 @@
"foundry": "opennlp",
"key": "PIDAT",
"layer": "p",
- "match": "match:eq"
+ "match": "match:eq",
+ "rewrites": [
+ {
+ "@type": "koral:rewrite",
+ "editor": "Koral-Mapper",
+ "original": {
+ "@type": "koral:termGroup",
+ "operands": [
+ {
+ "@type": "koral:term",
+ "foundry": "opennlp",
+ "key": "PIDAT",
+ "layer": "p",
+ "match": "match:eq"
+ },
+ {
+ "@type": "koral:term",
+ "foundry": "opennlp",
+ "key": "AdjType",
+ "layer": "p",
+ "match": "match:eq",
+ "value": "Pdt"
+ }
+ ],
+ "relation": "relation:and"
+ }
+ }
+ ]
}
}`,
},
@@ -202,7 +243,20 @@
"value": "Pdt"
}
],
- "relation": "relation:and"
+ "relation": "relation:and",
+ "rewrites": [
+ {
+ "@type": "koral:rewrite",
+ "editor": "Koral-Mapper",
+ "original": {
+ "@type": "koral:term",
+ "foundry": "opennlp",
+ "key": "PIDAT",
+ "layer": "p",
+ "match": "match:eq"
+ }
+ }
+ ]
}
}`,
},
@@ -1570,6 +1624,7 @@
layerA: p
foundryB: opennlp
layerB: p
+ rewrites: true
mappings:
- "[PIDAT] <> [DET]"
- id: step2
@@ -1577,6 +1632,7 @@
layerA: p
foundryB: upos
layerB: p
+ rewrites: true
mappings:
- "[DET] <> [PRON]"
`)
@@ -1615,6 +1671,14 @@
"key": "PRON",
"layer": "p",
"match": "match:eq",
+ "rewrites": []any{
+ map[string]any{
+ "@type": "koral:rewrite",
+ "editor": "Koral-Mapper",
+ "scope": "foundry",
+ "original": "opennlp",
+ },
+ },
},
},
},
@@ -2212,6 +2276,189 @@
assert.Contains(t, htmlContent, `type="button"`)
}
+func TestAddRewritesDefaultOff(t *testing.T) {
+ cfg := loadConfigFromYAML(t, `
+lists:
+ - id: test-mapper
+ foundryA: opennlp
+ layerA: p
+ foundryB: upos
+ layerB: p
+ mappings:
+ - "[PIDAT] <> [DET]"
+`)
+ m, err := mapper.NewMapper(cfg.Lists)
+ require.NoError(t, err)
+
+ app := fiber.New()
+ setupRoutes(app, m, cfg)
+
+ input := `{
+ "@type": "koral:token",
+ "wrap": {
+ "@type": "koral:term",
+ "foundry": "opennlp",
+ "key": "PIDAT",
+ "layer": "p",
+ "match": "match:eq"
+ }
+ }`
+
+ req := httptest.NewRequest(http.MethodPost, "/test-mapper/query?dir=atob", bytes.NewBufferString(input))
+ req.Header.Set("Content-Type", "application/json")
+ resp, err := app.Test(req)
+ require.NoError(t, err)
+ defer resp.Body.Close()
+
+ assert.Equal(t, http.StatusOK, resp.StatusCode)
+
+ var result map[string]any
+ err = json.NewDecoder(resp.Body).Decode(&result)
+ require.NoError(t, err)
+
+ wrap := result["wrap"].(map[string]any)
+ assert.Equal(t, "DET", wrap["key"])
+ assert.Nil(t, wrap["rewrites"], "rewrites should not be present by default")
+}
+
+func TestAddRewritesEnabledViaYAML(t *testing.T) {
+ cfg := loadConfigFromYAML(t, `
+lists:
+ - id: test-mapper
+ foundryA: opennlp
+ layerA: p
+ foundryB: upos
+ layerB: p
+ rewrites: true
+ mappings:
+ - "[PIDAT] <> [DET]"
+`)
+ m, err := mapper.NewMapper(cfg.Lists)
+ require.NoError(t, err)
+
+ app := fiber.New()
+ setupRoutes(app, m, cfg)
+
+ input := `{
+ "@type": "koral:token",
+ "wrap": {
+ "@type": "koral:term",
+ "foundry": "opennlp",
+ "key": "PIDAT",
+ "layer": "p",
+ "match": "match:eq"
+ }
+ }`
+
+ req := httptest.NewRequest(http.MethodPost, "/test-mapper/query?dir=atob", bytes.NewBufferString(input))
+ req.Header.Set("Content-Type", "application/json")
+ resp, err := app.Test(req)
+ require.NoError(t, err)
+ defer resp.Body.Close()
+
+ assert.Equal(t, http.StatusOK, resp.StatusCode)
+
+ var result map[string]any
+ err = json.NewDecoder(resp.Body).Decode(&result)
+ require.NoError(t, err)
+
+ wrap := result["wrap"].(map[string]any)
+ assert.Equal(t, "DET", wrap["key"])
+ assert.NotNil(t, wrap["rewrites"], "rewrites should be present when rewrites are enabled in YAML")
+}
+
+func TestAddRewritesQueryParamOverridesYAML(t *testing.T) {
+ cfg := loadConfigFromYAML(t, `
+lists:
+ - id: test-mapper
+ foundryA: opennlp
+ layerA: p
+ foundryB: upos
+ layerB: p
+ rewrites: true
+ mappings:
+ - "[PIDAT] <> [DET]"
+`)
+ m, err := mapper.NewMapper(cfg.Lists)
+ require.NoError(t, err)
+
+ app := fiber.New()
+ setupRoutes(app, m, cfg)
+
+ input := `{
+ "@type": "koral:token",
+ "wrap": {
+ "@type": "koral:term",
+ "foundry": "opennlp",
+ "key": "PIDAT",
+ "layer": "p",
+ "match": "match:eq"
+ }
+ }`
+
+ // Override YAML rewrites=true with query param rewrites=false
+ req := httptest.NewRequest(http.MethodPost, "/test-mapper/query?dir=atob&rewrites=false", bytes.NewBufferString(input))
+ req.Header.Set("Content-Type", "application/json")
+ resp, err := app.Test(req)
+ require.NoError(t, err)
+ defer resp.Body.Close()
+
+ assert.Equal(t, http.StatusOK, resp.StatusCode)
+
+ var result map[string]any
+ err = json.NewDecoder(resp.Body).Decode(&result)
+ require.NoError(t, err)
+
+ wrap := result["wrap"].(map[string]any)
+ assert.Equal(t, "DET", wrap["key"])
+ assert.Nil(t, wrap["rewrites"], "rewrites should be suppressed by query param override")
+}
+
+func TestAddRewritesQueryParamEnablesWhenYAMLOff(t *testing.T) {
+ cfg := loadConfigFromYAML(t, `
+lists:
+ - id: test-mapper
+ foundryA: opennlp
+ layerA: p
+ foundryB: upos
+ layerB: p
+ mappings:
+ - "[PIDAT] <> [DET]"
+`)
+ m, err := mapper.NewMapper(cfg.Lists)
+ require.NoError(t, err)
+
+ app := fiber.New()
+ setupRoutes(app, m, cfg)
+
+ input := `{
+ "@type": "koral:token",
+ "wrap": {
+ "@type": "koral:term",
+ "foundry": "opennlp",
+ "key": "PIDAT",
+ "layer": "p",
+ "match": "match:eq"
+ }
+ }`
+
+ req := httptest.NewRequest(http.MethodPost, "/test-mapper/query?dir=atob&rewrites=true", bytes.NewBufferString(input))
+ req.Header.Set("Content-Type", "application/json")
+ resp, err := app.Test(req)
+ require.NoError(t, err)
+ defer resp.Body.Close()
+
+ assert.Equal(t, http.StatusOK, resp.StatusCode)
+
+ var result map[string]any
+ err = json.NewDecoder(resp.Body).Decode(&result)
+ require.NoError(t, err)
+
+ wrap := result["wrap"].(map[string]any)
+ assert.Equal(t, "DET", wrap["key"])
+ assert.NotNil(t, wrap["rewrites"], "rewrites should be present when enabled by query param")
+}
+
func TestConfigPagePreservesOrderOfMappings(t *testing.T) {
lists := []tmconfig.MappingList{
{