blob: 128c063cb6e293911ae9bdd2b7a55169118850fe [file] [log] [blame]
Akron57ee5582025-05-21 15:25:13 +02001package config
2
3import (
4 "fmt"
5 "os"
6
Akronfa55bb22025-05-26 15:10:42 +02007 "github.com/KorAP/KoralPipe-TermMapper/ast"
8 "github.com/KorAP/KoralPipe-TermMapper/parser"
Akron57ee5582025-05-21 15:25:13 +02009 "gopkg.in/yaml.v3"
10)
11
Akron06d21f02025-06-04 14:36:07 +020012const (
13 defaultServer = "https://korap.ids-mannheim.de/"
14 defaultSDK = "https://korap.ids-mannheim.de/js/korap-plugin-latest.js"
15)
16
Akron57ee5582025-05-21 15:25:13 +020017// MappingRule represents a single mapping rule in the configuration
18type MappingRule string
19
20// MappingList represents a list of mapping rules with metadata
21type MappingList struct {
22 ID string `yaml:"id"`
23 FoundryA string `yaml:"foundryA,omitempty"`
24 LayerA string `yaml:"layerA,omitempty"`
25 FoundryB string `yaml:"foundryB,omitempty"`
26 LayerB string `yaml:"layerB,omitempty"`
27 Mappings []MappingRule `yaml:"mappings"`
28}
29
Akron06d21f02025-06-04 14:36:07 +020030// MappingConfig represents the root configuration containing multiple mapping lists
31type MappingConfig struct {
32 SDK string `yaml:"sdk,omitempty"`
33 Server string `yaml:"server,omitempty"`
34 Lists []MappingList `yaml:"lists,omitempty"`
Akron57ee5582025-05-21 15:25:13 +020035}
36
37// LoadConfig loads a YAML configuration file and returns a Config object
Akron06d21f02025-06-04 14:36:07 +020038func LoadConfig(filename string) (*MappingConfig, error) {
Akron57ee5582025-05-21 15:25:13 +020039 data, err := os.ReadFile(filename)
40 if err != nil {
41 return nil, fmt.Errorf("failed to read config file: %w", err)
42 }
43
Akrona5d88142025-05-22 14:42:09 +020044 // Check for empty file
45 if len(data) == 0 {
46 return nil, fmt.Errorf("EOF: config file is empty")
47 }
48
Akron06d21f02025-06-04 14:36:07 +020049 // Try to unmarshal as new format first (object with optional sdk/server and lists)
50 var config MappingConfig
51 if err := yaml.Unmarshal(data, &config); err == nil && len(config.Lists) > 0 {
52 // Successfully parsed as new format with lists field
53 if err := validateMappingLists(config.Lists); err != nil {
54 return nil, err
55 }
56 // Apply defaults if not specified
57 applyDefaults(&config)
58 return &config, nil
59 }
60
61 // Fall back to old format (direct list)
Akron57ee5582025-05-21 15:25:13 +020062 var lists []MappingList
63 if err := yaml.Unmarshal(data, &lists); err != nil {
64 return nil, fmt.Errorf("failed to parse YAML: %w", err)
65 }
66
Akron06d21f02025-06-04 14:36:07 +020067 if err := validateMappingLists(lists); err != nil {
68 return nil, err
69 }
70
71 config = MappingConfig{Lists: lists}
72 // Apply defaults if not specified
73 applyDefaults(&config)
74 return &config, nil
75}
76
77// applyDefaults sets default values for SDK and Server if they are empty
78func applyDefaults(config *MappingConfig) {
79 if config.SDK == "" {
80 config.SDK = defaultSDK
81 }
82 if config.Server == "" {
83 config.Server = defaultServer
84 }
85}
86
87// validateMappingLists validates a slice of mapping lists
88func validateMappingLists(lists []MappingList) error {
Akron57ee5582025-05-21 15:25:13 +020089 // Validate the configuration
Akrona5d88142025-05-22 14:42:09 +020090 seenIDs := make(map[string]bool)
Akron57ee5582025-05-21 15:25:13 +020091 for i, list := range lists {
92 if list.ID == "" {
Akron06d21f02025-06-04 14:36:07 +020093 return fmt.Errorf("mapping list at index %d is missing an ID", i)
Akron57ee5582025-05-21 15:25:13 +020094 }
Akrona5d88142025-05-22 14:42:09 +020095
96 // Check for duplicate IDs
97 if seenIDs[list.ID] {
Akron06d21f02025-06-04 14:36:07 +020098 return fmt.Errorf("duplicate mapping list ID found: %s", list.ID)
Akrona5d88142025-05-22 14:42:09 +020099 }
100 seenIDs[list.ID] = true
101
Akron57ee5582025-05-21 15:25:13 +0200102 if len(list.Mappings) == 0 {
Akron06d21f02025-06-04 14:36:07 +0200103 return fmt.Errorf("mapping list '%s' has no mapping rules", list.ID)
Akron57ee5582025-05-21 15:25:13 +0200104 }
105
106 // Validate each mapping rule
107 for j, rule := range list.Mappings {
108 if rule == "" {
Akron06d21f02025-06-04 14:36:07 +0200109 return fmt.Errorf("mapping list '%s' rule at index %d is empty", list.ID, j)
Akron57ee5582025-05-21 15:25:13 +0200110 }
111 }
112 }
Akron06d21f02025-06-04 14:36:07 +0200113 return nil
Akron57ee5582025-05-21 15:25:13 +0200114}
115
116// ParseMappings parses all mapping rules in a list and returns a slice of parsed rules
117func (list *MappingList) ParseMappings() ([]*parser.MappingResult, error) {
118 // Create a grammar parser with the list's default foundries and layers
119 grammarParser, err := parser.NewGrammarParser("", "")
120 if err != nil {
121 return nil, fmt.Errorf("failed to create grammar parser: %w", err)
122 }
123
124 results := make([]*parser.MappingResult, len(list.Mappings))
125 for i, rule := range list.Mappings {
Akrona5d88142025-05-22 14:42:09 +0200126 // Check for empty rules first
127 if rule == "" {
128 return nil, fmt.Errorf("empty mapping rule at index %d in list '%s'", i, list.ID)
129 }
130
Akron57ee5582025-05-21 15:25:13 +0200131 // Parse the mapping rule
132 result, err := grammarParser.ParseMapping(string(rule))
133 if err != nil {
134 return nil, fmt.Errorf("failed to parse mapping rule %d in list '%s': %w", i, list.ID, err)
135 }
136
137 // Apply default foundries and layers if not specified in the rule
138 if list.FoundryA != "" {
139 applyDefaultFoundryAndLayer(result.Upper.Wrap, list.FoundryA, list.LayerA)
140 }
141 if list.FoundryB != "" {
142 applyDefaultFoundryAndLayer(result.Lower.Wrap, list.FoundryB, list.LayerB)
143 }
144
145 results[i] = result
146 }
147
148 return results, nil
149}
150
151// applyDefaultFoundryAndLayer recursively applies default foundry and layer to terms that don't have them specified
152func applyDefaultFoundryAndLayer(node ast.Node, defaultFoundry, defaultLayer string) {
153 switch n := node.(type) {
154 case *ast.Term:
155 if n.Foundry == "" {
156 n.Foundry = defaultFoundry
157 }
158 if n.Layer == "" {
159 n.Layer = defaultLayer
160 }
161 case *ast.TermGroup:
162 for _, op := range n.Operands {
163 applyDefaultFoundryAndLayer(op, defaultFoundry, defaultLayer)
164 }
165 }
166}