blob: 2747e8604f8ee267fd779c7dab9e7ba476832d8d [file] [log] [blame]
Akron32d53de2025-05-22 13:45:32 +02001package mapper
2
3import (
4 "encoding/json"
5 "fmt"
6
Akronc79d87e2025-05-26 15:03:27 +02007 "github.com/KorAP/KoralPipe-TermMapper2/ast"
8 "github.com/KorAP/KoralPipe-TermMapper2/config"
9 "github.com/KorAP/KoralPipe-TermMapper2/matcher"
10 "github.com/KorAP/KoralPipe-TermMapper2/parser"
Akron32d53de2025-05-22 13:45:32 +020011)
12
13// Direction represents the mapping direction (A to B or B to A)
14type Direction string
15
16const (
17 AtoB Direction = "atob"
18 BtoA Direction = "btoa"
19)
20
21// Mapper handles the application of mapping rules to JSON objects
22type Mapper struct {
23 mappingLists map[string]*config.MappingList
24 parsedRules map[string][]*parser.MappingResult
25}
26
27// NewMapper creates a new Mapper instance
28func NewMapper(configFiles ...string) (*Mapper, error) {
29 m := &Mapper{
30 mappingLists: make(map[string]*config.MappingList),
31 parsedRules: make(map[string][]*parser.MappingResult),
32 }
33
34 // Load and parse all config files
35 for _, file := range configFiles {
36 cfg, err := config.LoadConfig(file)
37 if err != nil {
38 return nil, fmt.Errorf("failed to load config from %s: %w", file, err)
39 }
40
41 // Store mapping lists by ID
42 for _, list := range cfg.Lists {
43 if _, exists := m.mappingLists[list.ID]; exists {
44 return nil, fmt.Errorf("duplicate mapping list ID found: %s", list.ID)
45 }
46
47 // Create a copy of the list to store
48 listCopy := list
49 m.mappingLists[list.ID] = &listCopy
50
51 // Parse the rules immediately
52 parsedRules, err := list.ParseMappings()
53 if err != nil {
54 return nil, fmt.Errorf("failed to parse mappings for list %s: %w", list.ID, err)
55 }
56 m.parsedRules[list.ID] = parsedRules
57 }
58 }
59
60 return m, nil
61}
62
63// MappingOptions contains the options for applying mappings
64type MappingOptions struct {
65 FoundryA string
66 LayerA string
67 FoundryB string
68 LayerB string
69 Direction Direction
70}
71
72// ApplyMappings applies the specified mapping rules to a JSON object
Akrond5850f82025-05-23 16:44:44 +020073func (m *Mapper) ApplyMappings(mappingID string, opts MappingOptions, jsonData any) (any, error) {
Akron32d53de2025-05-22 13:45:32 +020074 // Validate mapping ID
75 if _, exists := m.mappingLists[mappingID]; !exists {
76 return nil, fmt.Errorf("mapping list with ID %s not found", mappingID)
77 }
78
79 // Validate direction
80 if opts.Direction != AtoB && opts.Direction != BtoA {
81 return nil, fmt.Errorf("invalid direction: %s", opts.Direction)
82 }
83
84 // Get the parsed rules
85 rules := m.parsedRules[mappingID]
86
87 // Convert input JSON to AST
88 jsonBytes, err := json.Marshal(jsonData)
89 if err != nil {
90 return nil, fmt.Errorf("failed to marshal input JSON: %w", err)
91 }
92
93 node, err := parser.ParseJSON(jsonBytes)
94 if err != nil {
95 return nil, fmt.Errorf("failed to parse JSON into AST: %w", err)
96 }
97
Akrond5850f82025-05-23 16:44:44 +020098 // Store whether the input was a Token
99 isToken := false
100 var tokenWrap ast.Node
Akron32d53de2025-05-22 13:45:32 +0200101 if token, ok := node.(*ast.Token); ok {
Akrond5850f82025-05-23 16:44:44 +0200102 isToken = true
103 tokenWrap = token.Wrap
104 node = tokenWrap
Akron32d53de2025-05-22 13:45:32 +0200105 }
106
107 // Apply each rule to the AST
108 for _, rule := range rules {
109 // Create pattern and replacement based on direction
110 var pattern, replacement ast.Node
111 if opts.Direction == AtoB {
112 pattern = rule.Upper
113 replacement = rule.Lower
114 } else {
115 pattern = rule.Lower
116 replacement = rule.Upper
117 }
118
119 // Extract the inner nodes from the pattern and replacement tokens
120 if token, ok := pattern.(*ast.Token); ok {
121 pattern = token.Wrap
122 }
123 if token, ok := replacement.(*ast.Token); ok {
124 replacement = token.Wrap
125 }
126
127 // Apply foundry and layer overrides
128 if opts.Direction == AtoB {
129 applyFoundryAndLayerOverrides(pattern, opts.FoundryA, opts.LayerA)
130 applyFoundryAndLayerOverrides(replacement, opts.FoundryB, opts.LayerB)
131 } else {
132 applyFoundryAndLayerOverrides(pattern, opts.FoundryB, opts.LayerB)
133 applyFoundryAndLayerOverrides(replacement, opts.FoundryA, opts.LayerA)
134 }
135
136 // Create matcher and apply replacement
Akrond5850f82025-05-23 16:44:44 +0200137 m, err := matcher.NewMatcher(ast.Pattern{Root: pattern}, ast.Replacement{Root: replacement})
138 if err != nil {
139 return nil, fmt.Errorf("failed to create matcher: %w", err)
140 }
Akron32d53de2025-05-22 13:45:32 +0200141 node = m.Replace(node)
142 }
143
Akrond5850f82025-05-23 16:44:44 +0200144 // Wrap the result in a token if the input was a token
145 var result ast.Node
146 if isToken {
147 result = &ast.Token{Wrap: node}
148 } else {
149 result = node
150 }
Akron32d53de2025-05-22 13:45:32 +0200151
152 // Convert AST back to JSON
153 resultBytes, err := parser.SerializeToJSON(result)
154 if err != nil {
155 return nil, fmt.Errorf("failed to serialize AST to JSON: %w", err)
156 }
157
158 // Parse the JSON string back into an interface{}
159 var resultData interface{}
160 if err := json.Unmarshal(resultBytes, &resultData); err != nil {
161 return nil, fmt.Errorf("failed to parse result JSON: %w", err)
162 }
163
164 return resultData, nil
165}
166
167// applyFoundryAndLayerOverrides recursively applies foundry and layer overrides to terms
168func applyFoundryAndLayerOverrides(node ast.Node, foundry, layer string) {
169 if node == nil {
170 return
171 }
172
173 switch n := node.(type) {
174 case *ast.Term:
175 if foundry != "" {
176 n.Foundry = foundry
177 }
178 if layer != "" {
179 n.Layer = layer
180 }
181 case *ast.TermGroup:
182 for _, op := range n.Operands {
183 applyFoundryAndLayerOverrides(op, foundry, layer)
184 }
185 case *ast.Token:
186 if n.Wrap != nil {
187 applyFoundryAndLayerOverrides(n.Wrap, foundry, layer)
188 }
189 case *ast.CatchallNode:
190 if n.Wrap != nil {
191 applyFoundryAndLayerOverrides(n.Wrap, foundry, layer)
192 }
193 for _, op := range n.Operands {
194 applyFoundryAndLayerOverrides(op, foundry, layer)
195 }
196 }
197}