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
 		}