Add configurable Kalamar integration

Change-Id: Ic07423dd7cc605509a364154bf4f37e4c13dc0d1
diff --git a/config/config.go b/config/config.go
index 6bafaad..128c063 100644
--- a/config/config.go
+++ b/config/config.go
@@ -9,6 +9,11 @@
 	"gopkg.in/yaml.v3"
 )
 
+const (
+	defaultServer = "https://korap.ids-mannheim.de/"
+	defaultSDK    = "https://korap.ids-mannheim.de/js/korap-plugin-latest.js"
+)
+
 // MappingRule represents a single mapping rule in the configuration
 type MappingRule string
 
@@ -22,13 +27,15 @@
 	Mappings []MappingRule `yaml:"mappings"`
 }
 
-// MappingLists represents the root configuration containing multiple mapping lists
-type MappingLists struct {
-	Lists []MappingList
+// MappingConfig represents the root configuration containing multiple mapping lists
+type MappingConfig struct {
+	SDK    string        `yaml:"sdk,omitempty"`
+	Server string        `yaml:"server,omitempty"`
+	Lists  []MappingList `yaml:"lists,omitempty"`
 }
 
 // LoadConfig loads a YAML configuration file and returns a Config object
-func LoadConfig(filename string) (*MappingLists, error) {
+func LoadConfig(filename string) (*MappingConfig, error) {
 	data, err := os.ReadFile(filename)
 	if err != nil {
 		return nil, fmt.Errorf("failed to read config file: %w", err)
@@ -39,37 +46,71 @@
 		return nil, fmt.Errorf("EOF: config file is empty")
 	}
 
+	// Try to unmarshal as new format first (object with optional sdk/server and lists)
+	var config MappingConfig
+	if err := yaml.Unmarshal(data, &config); err == nil && len(config.Lists) > 0 {
+		// Successfully parsed as new format with lists field
+		if err := validateMappingLists(config.Lists); err != nil {
+			return nil, err
+		}
+		// Apply defaults if not specified
+		applyDefaults(&config)
+		return &config, nil
+	}
+
+	// Fall back to old format (direct list)
 	var lists []MappingList
 	if err := yaml.Unmarshal(data, &lists); err != nil {
 		return nil, fmt.Errorf("failed to parse YAML: %w", err)
 	}
 
+	if err := validateMappingLists(lists); err != nil {
+		return nil, err
+	}
+
+	config = MappingConfig{Lists: lists}
+	// Apply defaults if not specified
+	applyDefaults(&config)
+	return &config, nil
+}
+
+// applyDefaults sets default values for SDK and Server if they are empty
+func applyDefaults(config *MappingConfig) {
+	if config.SDK == "" {
+		config.SDK = defaultSDK
+	}
+	if config.Server == "" {
+		config.Server = defaultServer
+	}
+}
+
+// validateMappingLists validates a slice of mapping lists
+func validateMappingLists(lists []MappingList) error {
 	// Validate the configuration
 	seenIDs := make(map[string]bool)
 	for i, list := range lists {
 		if list.ID == "" {
-			return nil, fmt.Errorf("mapping list at index %d is missing an ID", i)
+			return fmt.Errorf("mapping list at index %d is missing an ID", i)
 		}
 
 		// Check for duplicate IDs
 		if seenIDs[list.ID] {
-			return nil, fmt.Errorf("duplicate mapping list ID found: %s", list.ID)
+			return fmt.Errorf("duplicate mapping list ID found: %s", list.ID)
 		}
 		seenIDs[list.ID] = true
 
 		if len(list.Mappings) == 0 {
-			return nil, fmt.Errorf("mapping list '%s' has no mapping rules", list.ID)
+			return fmt.Errorf("mapping list '%s' has no mapping rules", list.ID)
 		}
 
 		// Validate each mapping rule
 		for j, rule := range list.Mappings {
 			if rule == "" {
-				return nil, fmt.Errorf("mapping list '%s' rule at index %d is empty", list.ID, j)
+				return fmt.Errorf("mapping list '%s' rule at index %d is empty", list.ID, j)
 			}
 		}
 	}
-
-	return &MappingLists{Lists: lists}, nil
+	return nil
 }
 
 // ParseMappings parses all mapping rules in a list and returns a slice of parsed rules