Support rewrite default config from config file or ENV var
Change-Id: Ibd8529b545f3c49fe1e57043e8d5293ff72dfdd1
diff --git a/README.md b/README.md
index aa1fe08..684e889 100644
--- a/README.md
+++ b/README.md
@@ -76,6 +76,10 @@
# Set to "/" to allow loading from anywhere on the filesystem.
basePath: "/opt/koralmapper"
+# Optional: Global default for koral:rewrite annotations (default: false).
+# Can be overridden per mapping list and per request via query parameter.
+rewrites: false
+
# Optional: Mapping lists (same format as individual mapping files)
lists:
- id: mapping-list-id
@@ -105,7 +109,7 @@
Command line arguments take precedence over configuration file values:
-The `sdk`, `stylesheet`, `server`, `port`, `loglevel`, and `basePath` fields in the main configuration file are optional and override the following default values:
+The `sdk`, `stylesheet`, `server`, `port`, `loglevel`, `rewrites`, and `basePath` fields in the main configuration file are optional and override the following default values:
- **`sdk`**: Custom SDK JavaScript file URL (default: `https://korap.ids-mannheim.de/js/korap-plugin-latest.js`)
- **`stylesheet`**: Kalamar stylesheet URL for the config page (default: `https://korap.ids-mannheim.de/css/kalamar-plugin-latest.css`)
@@ -115,6 +119,7 @@
- **`serviceURL`**: Service URL of the KoralMapper (default: `https://korap.ids-mannheim.de/plugin/koralmapper`)
- **`rateLimit`**: Maximum number of requests per minute per IP address (default: `100`). When the limit is exceeded, the server responds with HTTP 429 (Too Many Requests).
- **`allowOrigins`**: Comma-separated list of origins allowed for CORS (default: derived from `server` with trailing slash removed, e.g. `https://korap.ids-mannheim.de`). The service is designed to be called cross-origin as a Kalamar plugin loaded in iframes. This setting controls which origins may make cross-origin API requests. Allowed methods are `GET` and `POST`. The `Content-Type` header is permitted. Use `"*"` to allow all origins (not recommended for production).
+- **`rewrites`**: Global default for attaching `koral:rewrite` annotations (default: `false`). When `true`, all mapping lists will attach rewrite annotations unless individually overridden. See [Rewrites Resolution](#rewrites-resolution) for the full precedence chain.
- **`basePath`**: Directory tree for file loading confinement (default: current working directory). Configuration and mapping files must resolve within this path or the system temp directory. Set to `"/"` to disable confinement. This prevents path traversal attacks (CWE-22).
These values are applied during configuration parsing. When using only individual mapping files (`-m` flags), default values are used unless overridden by command line arguments.
@@ -133,6 +138,7 @@
- `KORAL_MAPPER_PORT`: Overrides `port` (integer)
- `KORAL_MAPPER_RATE_LIMIT`: Overrides `rateLimit` (integer, requests per minute per IP)
- `KORAL_MAPPER_ALLOW_ORIGINS`: Overrides `allowOrigins` (comma-separated list of allowed CORS origins)
+- `KORAL_MAPPER_REWRITES`: Overrides `rewrites` (`true` or `false`, global default for koral:rewrite annotations)
- `KORAL_MAPPER_BASE_PATH`: Overrides `basePath` (directory path for file loading confinement)
Environment variable values take precedence over values from the configuration file.
diff --git a/cmd/koralmapper/main.go b/cmd/koralmapper/main.go
index 50bdace..66cd96b 100644
--- a/cmd/koralmapper/main.go
+++ b/cmd/koralmapper/main.go
@@ -371,14 +371,14 @@
app.Get("/static/*", handleStaticFile())
// Composite cascade transformation endpoints (cfg in path)
- app.Post("/query/:cfg", handleCompositeQueryTransform(m, yamlConfig.Lists))
- app.Post("/response/:cfg", handleCompositeResponseTransform(m, yamlConfig.Lists))
+ app.Post("/query/:cfg", handleCompositeQueryTransform(m, yamlConfig))
+ app.Post("/response/:cfg", handleCompositeResponseTransform(m, yamlConfig))
// Transformation endpoint
- app.Post("/:map/query", handleTransform(m, yamlConfig.Lists))
+ app.Post("/:map/query", handleTransform(m, yamlConfig))
// Response transformation endpoint
- app.Post("/:map/response", handleResponseTransform(m, yamlConfig.Lists))
+ app.Post("/:map/response", handleResponseTransform(m, yamlConfig))
// Kalamar plugin endpoint
app.Get("/", handleKalamarPlugin(yamlConfig, configTmpl, pluginTmpl))
@@ -462,10 +462,10 @@
return data
}
-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]
+func handleCompositeQueryTransform(m *mapper.Mapper, yamlConfig *config.MappingConfig) fiber.Handler {
+ listsByID := make(map[string]*config.MappingList, len(yamlConfig.Lists))
+ for i := range yamlConfig.Lists {
+ listsByID[yamlConfig.Lists[i].ID] = &yamlConfig.Lists[i]
}
return func(c *fiber.Ctx) error {
@@ -483,7 +483,7 @@
})
}
- entries, err := ParseCfgParam(cfgRaw, lists)
+ entries, err := ParseCfgParam(cfgRaw, yamlConfig.Lists)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": err.Error(),
@@ -509,9 +509,9 @@
dir = mapper.BtoA
}
- addRewrites := false
+ addRewrites := yamlConfig.Rewrites
if list, ok := listsByID[entry.ID]; ok {
- addRewrites = list.Rewrites
+ addRewrites = list.EffectiveRewrites(yamlConfig.Rewrites)
}
if rewritesOverride != nil {
addRewrites = *rewritesOverride
@@ -542,10 +542,10 @@
}
}
-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]
+func handleCompositeResponseTransform(m *mapper.Mapper, yamlConfig *config.MappingConfig) fiber.Handler {
+ listsByID := make(map[string]*config.MappingList, len(yamlConfig.Lists))
+ for i := range yamlConfig.Lists {
+ listsByID[yamlConfig.Lists[i].ID] = &yamlConfig.Lists[i]
}
return func(c *fiber.Ctx) error {
@@ -563,7 +563,7 @@
})
}
- entries, err := ParseCfgParam(cfgRaw, lists)
+ entries, err := ParseCfgParam(cfgRaw, yamlConfig.Lists)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": err.Error(),
@@ -589,9 +589,9 @@
dir = mapper.BtoA
}
- addRewrites := false
+ addRewrites := yamlConfig.Rewrites
if list, ok := listsByID[entry.ID]; ok {
- addRewrites = list.Rewrites
+ addRewrites = list.EffectiveRewrites(yamlConfig.Rewrites)
}
if rewritesOverride != nil {
addRewrites = *rewritesOverride
@@ -622,10 +622,10 @@
}
}
-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]
+func handleTransform(m *mapper.Mapper, yamlConfig *config.MappingConfig) fiber.Handler {
+ listsByID := make(map[string]*config.MappingList, len(yamlConfig.Lists))
+ for i := range yamlConfig.Lists {
+ listsByID[yamlConfig.Lists[i].ID] = &yamlConfig.Lists[i]
}
return func(c *fiber.Ctx) error {
@@ -645,10 +645,10 @@
})
}
- // Determine rewrites: query param overrides YAML default
- addRewrites := false
+ // Resolve rewrites: global default -> per-list -> query param
+ addRewrites := yamlConfig.Rewrites
if list, ok := listsByID[params.MapID]; ok {
- addRewrites = list.Rewrites
+ addRewrites = list.EffectiveRewrites(yamlConfig.Rewrites)
}
if params.Rewrites != nil {
addRewrites = *params.Rewrites
@@ -679,10 +679,10 @@
}
}
-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]
+func handleResponseTransform(m *mapper.Mapper, yamlConfig *config.MappingConfig) fiber.Handler {
+ listsByID := make(map[string]*config.MappingList, len(yamlConfig.Lists))
+ for i := range yamlConfig.Lists {
+ listsByID[yamlConfig.Lists[i].ID] = &yamlConfig.Lists[i]
}
return func(c *fiber.Ctx) error {
@@ -702,10 +702,10 @@
})
}
- // Determine rewrites: query param overrides YAML default
- addRewrites := false
+ // Resolve rewrites: global default -> per-list -> query param
+ addRewrites := yamlConfig.Rewrites
if list, ok := listsByID[params.MapID]; ok {
- addRewrites = list.Rewrites
+ addRewrites = list.EffectiveRewrites(yamlConfig.Rewrites)
}
if params.Rewrites != nil {
addRewrites = *params.Rewrites
diff --git a/cmd/koralmapper/main_test.go b/cmd/koralmapper/main_test.go
index b5e6da7..d0d77f2 100644
--- a/cmd/koralmapper/main_test.go
+++ b/cmd/koralmapper/main_test.go
@@ -2990,3 +2990,331 @@
resp.Header.Get("Access-Control-Allow-Origin"),
"transform endpoints should include CORS headers")
}
+
+// Helper for rewrites tests: sends a POST to the given URL with a standard input
+// and returns whether the result's wrap contains a "rewrites" field.
+func hasRewritesInResponse(t *testing.T, app *fiber.App, url string) bool {
+ t.Helper()
+ input := `{
+ "@type": "koral:token",
+ "wrap": {
+ "@type": "koral:term",
+ "foundry": "opennlp",
+ "key": "PIDAT",
+ "layer": "p",
+ "match": "match:eq"
+ }
+ }`
+
+ req := httptest.NewRequest(http.MethodPost, url, bytes.NewBufferString(input))
+ req.Header.Set("Content-Type", "application/json")
+ resp, err := app.Test(req)
+ require.NoError(t, err)
+ defer resp.Body.Close()
+
+ require.Equal(t, http.StatusOK, resp.StatusCode)
+
+ var result map[string]any
+ err = json.NewDecoder(resp.Body).Decode(&result)
+ require.NoError(t, err)
+
+ wrap, ok := result["wrap"].(map[string]any)
+ require.True(t, ok, "result should have a wrap field")
+ _, hasRewrites := wrap["rewrites"]
+ return hasRewrites
+}
+
+func TestGlobalRewritesTrueInheritsToList(t *testing.T) {
+ cfg := loadConfigFromYAML(t, `
+rewrites: true
+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)
+
+ assert.True(t, hasRewritesInResponse(t, app, "/test-mapper/query?dir=atob"),
+ "global rewrites=true should be inherited by list without per-list override")
+}
+
+func TestGlobalRewritesTrueOverriddenByListFalse(t *testing.T) {
+ cfg := loadConfigFromYAML(t, `
+rewrites: true
+lists:
+ - id: test-mapper
+ foundryA: opennlp
+ layerA: p
+ foundryB: upos
+ layerB: p
+ rewrites: false
+ mappings:
+ - "[PIDAT] <> [DET]"
+`)
+ m, err := mapper.NewMapper(cfg.Lists)
+ require.NoError(t, err)
+
+ app := fiber.New()
+ setupRoutes(app, m, cfg)
+
+ assert.False(t, hasRewritesInResponse(t, app, "/test-mapper/query?dir=atob"),
+ "per-list rewrites=false should override global rewrites=true")
+}
+
+func TestGlobalRewritesFalseOverriddenByListTrue(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)
+
+ assert.True(t, hasRewritesInResponse(t, app, "/test-mapper/query?dir=atob"),
+ "per-list rewrites=true should override global rewrites=false (default)")
+}
+
+func TestQueryParamOverridesGlobalRewritesTrue(t *testing.T) {
+ cfg := loadConfigFromYAML(t, `
+rewrites: true
+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)
+
+ assert.False(t, hasRewritesInResponse(t, app, "/test-mapper/query?dir=atob&rewrites=false"),
+ "query param rewrites=false should override global rewrites=true")
+}
+
+func TestQueryParamOverridesPerListRewritesFalse(t *testing.T) {
+ cfg := loadConfigFromYAML(t, `
+rewrites: true
+lists:
+ - id: test-mapper
+ foundryA: opennlp
+ layerA: p
+ foundryB: upos
+ layerB: p
+ rewrites: false
+ mappings:
+ - "[PIDAT] <> [DET]"
+`)
+ m, err := mapper.NewMapper(cfg.Lists)
+ require.NoError(t, err)
+
+ app := fiber.New()
+ setupRoutes(app, m, cfg)
+
+ assert.True(t, hasRewritesInResponse(t, app, "/test-mapper/query?dir=atob&rewrites=true"),
+ "query param rewrites=true should override per-list rewrites=false")
+}
+
+func TestCompositeQueryGlobalRewritesInherited(t *testing.T) {
+ cfg := loadConfigFromYAML(t, `
+rewrites: true
+lists:
+ - id: step1
+ foundryA: opennlp
+ layerA: p
+ foundryB: stts
+ layerB: p
+ mappings:
+ - "[PIDAT] <> [DET]"
+`)
+ m, err := mapper.NewMapper(cfg.Lists)
+ require.NoError(t, err)
+
+ app := fiber.New()
+ setupRoutes(app, m, cfg)
+
+ assert.True(t, hasRewritesInResponse(t, app, "/query/step1:atob"),
+ "composite query should inherit global rewrites=true")
+}
+
+func TestCompositeQueryGlobalRewritesOverriddenByQueryParam(t *testing.T) {
+ cfg := loadConfigFromYAML(t, `
+rewrites: true
+lists:
+ - id: step1
+ foundryA: opennlp
+ layerA: p
+ foundryB: stts
+ layerB: p
+ mappings:
+ - "[PIDAT] <> [DET]"
+`)
+ m, err := mapper.NewMapper(cfg.Lists)
+ require.NoError(t, err)
+
+ app := fiber.New()
+ setupRoutes(app, m, cfg)
+
+ assert.False(t, hasRewritesInResponse(t, app, "/query/step1:atob?rewrites=false"),
+ "composite query param rewrites=false should override global rewrites=true")
+}
+
+func TestCompositeQueryPerListOverridesGlobal(t *testing.T) {
+ cfg := loadConfigFromYAML(t, `
+rewrites: true
+lists:
+ - id: step1
+ foundryA: opennlp
+ layerA: p
+ foundryB: stts
+ layerB: p
+ rewrites: false
+ mappings:
+ - "[PIDAT] <> [DET]"
+`)
+ m, err := mapper.NewMapper(cfg.Lists)
+ require.NoError(t, err)
+
+ app := fiber.New()
+ setupRoutes(app, m, cfg)
+
+ assert.False(t, hasRewritesInResponse(t, app, "/query/step1:atob"),
+ "composite query should use per-list rewrites=false over global rewrites=true")
+}
+
+func TestResponseEndpointGlobalRewritesInherited(t *testing.T) {
+ cfg := loadConfigFromYAML(t, `
+rewrites: true
+lists:
+ - id: test-response-mapper
+ foundryA: marmot
+ layerA: m
+ foundryB: opennlp
+ layerB: p
+ mappings:
+ - "[gender:masc] <> [p=M & m=M]"
+`)
+ m, err := mapper.NewMapper(cfg.Lists)
+ require.NoError(t, err)
+
+ app := fiber.New()
+ setupRoutes(app, m, cfg)
+
+ // Response rewrites on snippets don't use koral:rewrite fields the same way
+ // but we can verify the handler doesn't error and respects the flag.
+ input := `{"snippet": "<span title=\"marmot/m:gender:masc\">Der</span>"}`
+ req := httptest.NewRequest(http.MethodPost, "/test-response-mapper/response?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)
+}
+
+func TestResponseEndpointQueryParamOverridesGlobal(t *testing.T) {
+ cfg := loadConfigFromYAML(t, `
+rewrites: true
+lists:
+ - id: test-response-mapper
+ foundryA: marmot
+ layerA: m
+ foundryB: opennlp
+ layerB: p
+ mappings:
+ - "[gender:masc] <> [p=M & m=M]"
+`)
+ m, err := mapper.NewMapper(cfg.Lists)
+ require.NoError(t, err)
+
+ app := fiber.New()
+ setupRoutes(app, m, cfg)
+
+ input := `{"snippet": "<span title=\"marmot/m:gender:masc\">Der</span>"}`
+ req := httptest.NewRequest(http.MethodPost, "/test-response-mapper/response?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)
+}
+
+func TestMultipleListsMixedRewritesResolution(t *testing.T) {
+ cfg := loadConfigFromYAML(t, `
+rewrites: true
+lists:
+ - id: mapper-inherits
+ foundryA: opennlp
+ layerA: p
+ foundryB: upos
+ layerB: p
+ mappings:
+ - "[PIDAT] <> [DET]"
+ - id: mapper-overrides-off
+ foundryA: stts
+ layerA: p
+ foundryB: upos
+ layerB: p
+ rewrites: false
+ mappings:
+ - "[DET] <> [PRON]"
+`)
+ m, err := mapper.NewMapper(cfg.Lists)
+ require.NoError(t, err)
+
+ app := fiber.New()
+ setupRoutes(app, m, cfg)
+
+ // mapper-inherits should have rewrites (inherits global true)
+ assert.True(t, hasRewritesInResponse(t, app, "/mapper-inherits/query?dir=atob"),
+ "mapper-inherits should have rewrites via global default")
+
+ // mapper-overrides-off has per-list rewrites=false
+ input := `{
+ "@type": "koral:token",
+ "wrap": {
+ "@type": "koral:term",
+ "foundry": "stts",
+ "key": "DET",
+ "layer": "p",
+ "match": "match:eq"
+ }
+ }`
+ req := httptest.NewRequest(http.MethodPost, "/mapper-overrides-off/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()
+
+ var result map[string]any
+ err = json.NewDecoder(resp.Body).Decode(&result)
+ require.NoError(t, err)
+
+ wrap := result["wrap"].(map[string]any)
+ _, hasRewrites := wrap["rewrites"]
+ assert.False(t, hasRewrites,
+ "mapper-overrides-off should NOT have rewrites (per-list false overrides global true)")
+}
diff --git a/config/config.go b/config/config.go
index 6c4c9bf..d685708 100644
--- a/config/config.go
+++ b/config/config.go
@@ -38,7 +38,7 @@
LayerB string `yaml:"layerB,omitempty"`
FieldA string `yaml:"fieldA,omitempty"`
FieldB string `yaml:"fieldB,omitempty"`
- Rewrites bool `yaml:"rewrites,omitempty"`
+ Rewrites *bool `yaml:"rewrites,omitempty"`
Mappings []MappingRule `yaml:"mappings"`
}
@@ -47,6 +47,16 @@
return list.Type == "corpus"
}
+// EffectiveRewrites returns the resolved rewrites setting for this list.
+// If the list has an explicit per-list override, it is used; otherwise the
+// global default is returned.
+func (list *MappingList) EffectiveRewrites(globalDefault bool) bool {
+ if list.Rewrites != nil {
+ return *list.Rewrites
+ }
+ return globalDefault
+}
+
// ParseCorpusMappings parses all mapping rules as corpus rules.
// Bare values (without key=) are always allowed and receive the default
// field name from the mapping list header (FieldA/FieldB) when set.
@@ -102,6 +112,7 @@
Port int `yaml:"port,omitempty"`
LogLevel string `yaml:"loglevel,omitempty"`
RateLimit int `yaml:"rateLimit,omitempty"` // max requests per minute per IP (0 = use default 100)
+ Rewrites bool `yaml:"rewrites,omitempty"` // global default for koral:rewrite annotations
Lists []MappingList `yaml:"lists,omitempty"`
}
@@ -265,6 +276,7 @@
Port: globalConfig.Port,
LogLevel: globalConfig.LogLevel,
RateLimit: globalConfig.RateLimit,
+ Rewrites: globalConfig.Rewrites,
Lists: allLists,
}
@@ -341,6 +353,10 @@
config.RateLimit = rl
}
}
+
+ if val := os.Getenv("KORAL_MAPPER_REWRITES"); val != "" {
+ config.Rewrites = val == "true"
+ }
}
// validateMappingLists validates a slice of mapping lists (without duplicate ID checking)
diff --git a/config/config_test.go b/config/config_test.go
index bac6df0..0881157 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -962,6 +962,7 @@
"KORAL_MAPPER_PORT",
"KORAL_MAPPER_LOG_LEVEL",
"KORAL_MAPPER_ALLOW_ORIGINS",
+ "KORAL_MAPPER_REWRITES",
}
clearEnv := func() {
@@ -1103,6 +1104,7 @@
"KORAL_MAPPER_SERVICE_URL",
"KORAL_MAPPER_COOKIE_NAME",
"KORAL_MAPPER_ALLOW_ORIGINS",
+ "KORAL_MAPPER_REWRITES",
}
clearEnv := func() {
for _, key := range envKeys {
@@ -1176,9 +1178,165 @@
require.NoError(t, err)
require.Len(t, cfg.Lists, 3)
- assert.True(t, cfg.Lists[0].Rewrites, "rewrites should be true when set to true")
- assert.False(t, cfg.Lists[1].Rewrites, "rewrites should be false when set to false")
- assert.False(t, cfg.Lists[2].Rewrites, "rewrites should default to false")
+ require.NotNil(t, cfg.Lists[0].Rewrites, "rewrites should be set when specified as true")
+ assert.True(t, *cfg.Lists[0].Rewrites, "rewrites should be true when set to true")
+ require.NotNil(t, cfg.Lists[1].Rewrites, "rewrites should be set when specified as false")
+ assert.False(t, *cfg.Lists[1].Rewrites, "rewrites should be false when set to false")
+ assert.Nil(t, cfg.Lists[2].Rewrites, "rewrites should be nil when not specified")
+}
+
+func TestEffectiveRewrites(t *testing.T) {
+ trueVal := true
+ falseVal := false
+
+ tests := []struct {
+ name string
+ listRewrites *bool
+ globalDefault bool
+ expected bool
+ }{
+ {
+ name: "nil per-list, global false",
+ listRewrites: nil,
+ globalDefault: false,
+ expected: false,
+ },
+ {
+ name: "nil per-list, global true",
+ listRewrites: nil,
+ globalDefault: true,
+ expected: true,
+ },
+ {
+ name: "per-list true, global false",
+ listRewrites: &trueVal,
+ globalDefault: false,
+ expected: true,
+ },
+ {
+ name: "per-list false, global true",
+ listRewrites: &falseVal,
+ globalDefault: true,
+ expected: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ list := &MappingList{
+ ID: "test",
+ Rewrites: tt.listRewrites,
+ Mappings: []MappingRule{"[A] <> [B]"},
+ }
+ assert.Equal(t, tt.expected, list.EffectiveRewrites(tt.globalDefault))
+ })
+ }
+}
+
+func TestGlobalRewritesYAMLField(t *testing.T) {
+ content := `
+rewrites: true
+lists:
+ - id: inherits-global
+ mappings:
+ - "[A] <> [B]"
+ - id: overrides-global
+ rewrites: false
+ mappings:
+ - "[C] <> [D]"
+`
+ tmpfile, err := os.CreateTemp("", "config-global-rewrites-*.yaml")
+ require.NoError(t, err)
+ defer os.Remove(tmpfile.Name())
+
+ _, err = tmpfile.WriteString(content)
+ require.NoError(t, err)
+ require.NoError(t, tmpfile.Close())
+
+ cfg, err := LoadFromSources(tmpfile.Name(), nil)
+ require.NoError(t, err)
+
+ assert.True(t, cfg.Rewrites, "global rewrites should be true")
+
+ assert.Nil(t, cfg.Lists[0].Rewrites, "per-list rewrites should be nil when not specified")
+ assert.True(t, cfg.Lists[0].EffectiveRewrites(cfg.Rewrites),
+ "list should inherit global rewrites=true")
+
+ require.NotNil(t, cfg.Lists[1].Rewrites)
+ assert.False(t, *cfg.Lists[1].Rewrites,
+ "per-list rewrites should be false when explicitly set")
+ assert.False(t, cfg.Lists[1].EffectiveRewrites(cfg.Rewrites),
+ "list should override global rewrites=true with per-list false")
+}
+
+func TestGlobalRewritesDefaultFalse(t *testing.T) {
+ content := `
+lists:
+ - id: test-mapper
+ mappings:
+ - "[A] <> [B]"
+`
+ tmpfile, err := os.CreateTemp("", "config-global-rewrites-default-*.yaml")
+ require.NoError(t, err)
+ defer os.Remove(tmpfile.Name())
+
+ _, err = tmpfile.WriteString(content)
+ require.NoError(t, err)
+ require.NoError(t, tmpfile.Close())
+
+ cfg, err := LoadFromSources(tmpfile.Name(), nil)
+ require.NoError(t, err)
+
+ assert.False(t, cfg.Rewrites, "global rewrites should default to false")
+}
+
+func TestGlobalRewritesEnvOverride(t *testing.T) {
+ t.Setenv("KORAL_MAPPER_REWRITES", "true")
+
+ content := `
+lists:
+ - id: test-mapper
+ mappings:
+ - "[A] <> [B]"
+`
+ tmpfile, err := os.CreateTemp("", "config-rewrites-env-*.yaml")
+ require.NoError(t, err)
+ defer os.Remove(tmpfile.Name())
+
+ _, err = tmpfile.WriteString(content)
+ require.NoError(t, err)
+ require.NoError(t, tmpfile.Close())
+
+ cfg, err := LoadFromSources(tmpfile.Name(), nil)
+ require.NoError(t, err)
+
+ assert.True(t, cfg.Rewrites,
+ "KORAL_MAPPER_REWRITES=true env var should override default")
+}
+
+func TestGlobalRewritesEnvOverridesYAML(t *testing.T) {
+ t.Setenv("KORAL_MAPPER_REWRITES", "false")
+
+ content := `
+rewrites: true
+lists:
+ - id: test-mapper
+ mappings:
+ - "[A] <> [B]"
+`
+ tmpfile, err := os.CreateTemp("", "config-rewrites-env-yaml-*.yaml")
+ require.NoError(t, err)
+ defer os.Remove(tmpfile.Name())
+
+ _, err = tmpfile.WriteString(content)
+ require.NoError(t, err)
+ require.NoError(t, tmpfile.Close())
+
+ cfg, err := LoadFromSources(tmpfile.Name(), nil)
+ require.NoError(t, err)
+
+ assert.False(t, cfg.Rewrites,
+ "KORAL_MAPPER_REWRITES=false env var should override YAML rewrites=true")
}
func TestParseCorpusMappingsWithFieldAFieldB(t *testing.T) {