Add mapping-configuration string builder and parser
Change-Id: I78833fe8666d501846e7ce05d13097e02701a5f4
diff --git a/cmd/koralmapper/cfgparam.go b/cmd/koralmapper/cfgparam.go
new file mode 100644
index 0000000..dc00da5
--- /dev/null
+++ b/cmd/koralmapper/cfgparam.go
@@ -0,0 +1,115 @@
+package main
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/KorAP/Koral-Mapper/config"
+)
+
+// CascadeEntry represents a single mapping configuration parsed from
+// the cfg URL parameter. After parsing, empty override fields are
+// merged with the YAML defaults from the corresponding MappingList.
+type CascadeEntry struct {
+ ID string
+ Direction string
+ FoundryA string
+ LayerA string
+ FoundryB string
+ LayerB string
+}
+
+// ParseCfgParam parses the compact cfg URL parameter into a slice of
+// CascadeEntry structs. Empty override fields are merged with YAML
+// defaults from the matching MappingList.
+//
+// Format: entry (";" entry)*
+//
+// entry = id ":" dir [ ":" foundryA ":" layerA ":" foundryB ":" layerB ]
+//
+// An entry has either 2 fields (all foundry/layer use defaults) or
+// 6 fields (explicit values, empty means use default).
+func ParseCfgParam(raw string, lists []config.MappingList) ([]CascadeEntry, error) {
+ if raw == "" {
+ return nil, nil
+ }
+
+ listsByID := make(map[string]*config.MappingList, len(lists))
+ for i := range lists {
+ listsByID[lists[i].ID] = &lists[i]
+ }
+
+ parts := strings.Split(raw, ";")
+ result := make([]CascadeEntry, 0, len(parts))
+
+ 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)
+ }
+
+ id := fields[0]
+ dir := fields[1]
+
+ if dir != "atob" && dir != "btoa" {
+ return nil, fmt.Errorf("invalid direction %q in entry %q", dir, part)
+ }
+
+ list, ok := listsByID[id]
+ if !ok {
+ return nil, fmt.Errorf("unknown mapping ID %q", id)
+ }
+
+ 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 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)
+ }
+
+ return result, nil
+}
+
+// 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.
+func BuildCfgParam(entries []CascadeEntry) string {
+ if len(entries) == 0 {
+ return ""
+ }
+
+ parts := make([]string, len(entries))
+ for i, e := range entries {
+ if e.FoundryA == "" && e.LayerA == "" && e.FoundryB == "" && e.LayerB == "" {
+ parts[i] = e.ID + ":" + e.Direction
+ } else {
+ parts[i] = e.ID + ":" + e.Direction + ":" + e.FoundryA + ":" + e.LayerA + ":" + e.FoundryB + ":" + e.LayerB
+ }
+ }
+
+ return strings.Join(parts, ";")
+}