Support rewrite default config from config file or ENV var

Change-Id: Ibd8529b545f3c49fe1e57043e8d5293ff72dfdd1
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) {