blob: 424896e0f75e40bd4823bbd1aa60d3c74a0027bd [file] [log] [blame]
Akron32d53de2025-05-22 13:45:32 +02001package mapper
2
3import (
4 "encoding/json"
5 "fmt"
6
Akronfa55bb22025-05-26 15:10:42 +02007 "github.com/KorAP/KoralPipe-TermMapper/ast"
8 "github.com/KorAP/KoralPipe-TermMapper/config"
9 "github.com/KorAP/KoralPipe-TermMapper/matcher"
10 "github.com/KorAP/KoralPipe-TermMapper/parser"
Akron32d53de2025-05-22 13:45:32 +020011)
12
13// Direction represents the mapping direction (A to B or B to A)
Akrona1a183f2025-05-26 17:47:33 +020014type Direction bool
Akron32d53de2025-05-22 13:45:32 +020015
16const (
Akrona1a183f2025-05-26 17:47:33 +020017 AtoB Direction = true
18 BtoA Direction = false
Akron32d53de2025-05-22 13:45:32 +020019)
20
Akrona1a183f2025-05-26 17:47:33 +020021// String converts the Direction to its string representation
22func (d Direction) String() string {
23 if d {
24 return "atob"
25 }
26 return "btoa"
27}
28
29// ParseDirection converts a string direction to Direction type
30func ParseDirection(dir string) (Direction, error) {
31 switch dir {
32 case "atob":
33 return AtoB, nil
34 case "btoa":
35 return BtoA, nil
36 default:
37 return false, fmt.Errorf("invalid direction: %s", dir)
38 }
39}
40
Akron32d53de2025-05-22 13:45:32 +020041// Mapper handles the application of mapping rules to JSON objects
42type Mapper struct {
43 mappingLists map[string]*config.MappingList
44 parsedRules map[string][]*parser.MappingResult
45}
46
Akrona00d4752025-05-26 17:34:36 +020047// NewMapper creates a new Mapper instance from a list of MappingLists
48func NewMapper(lists []config.MappingList) (*Mapper, error) {
Akron32d53de2025-05-22 13:45:32 +020049 m := &Mapper{
50 mappingLists: make(map[string]*config.MappingList),
51 parsedRules: make(map[string][]*parser.MappingResult),
52 }
53
Akrona00d4752025-05-26 17:34:36 +020054 // Store mapping lists by ID
55 for _, list := range lists {
56 if _, exists := m.mappingLists[list.ID]; exists {
57 return nil, fmt.Errorf("duplicate mapping list ID found: %s", list.ID)
58 }
59
60 // Create a copy of the list to store
61 listCopy := list
62 m.mappingLists[list.ID] = &listCopy
63
64 // Parse the rules immediately
65 parsedRules, err := list.ParseMappings()
Akron32d53de2025-05-22 13:45:32 +020066 if err != nil {
Akrona00d4752025-05-26 17:34:36 +020067 return nil, fmt.Errorf("failed to parse mappings for list %s: %w", list.ID, err)
Akron32d53de2025-05-22 13:45:32 +020068 }
Akrona00d4752025-05-26 17:34:36 +020069 m.parsedRules[list.ID] = parsedRules
Akron32d53de2025-05-22 13:45:32 +020070 }
71
72 return m, nil
73}
74
75// MappingOptions contains the options for applying mappings
76type MappingOptions struct {
Akron0d9117c2025-05-27 15:20:21 +020077 FoundryA string
78 LayerA string
79 FoundryB string
80 LayerB string
81 Direction Direction
82 AddRewrites bool
Akron32d53de2025-05-22 13:45:32 +020083}
84
Akron7b4984e2025-05-26 19:12:20 +020085// ApplyQueryMappings applies the specified mapping rules to a JSON object
86func (m *Mapper) ApplyQueryMappings(mappingID string, opts MappingOptions, jsonData any) (any, error) {
Akron32d53de2025-05-22 13:45:32 +020087 // Validate mapping ID
88 if _, exists := m.mappingLists[mappingID]; !exists {
89 return nil, fmt.Errorf("mapping list with ID %s not found", mappingID)
90 }
91
Akron32d53de2025-05-22 13:45:32 +020092 // Get the parsed rules
93 rules := m.parsedRules[mappingID]
94
Akron7b4984e2025-05-26 19:12:20 +020095 // Check if we have a wrapper object with a "query" field
96 var queryData any
97 var hasQueryWrapper bool
98
99 if jsonMap, ok := jsonData.(map[string]any); ok {
100 if query, exists := jsonMap["query"]; exists {
101 queryData = query
102 hasQueryWrapper = true
103 }
104 }
105
106 // If no query wrapper was found, use the entire input
107 if !hasQueryWrapper {
108 // If the input itself is not a valid query object, return it as is
109 if !isValidQueryObject(jsonData) {
110 return jsonData, nil
111 }
112 queryData = jsonData
113 } else if queryData == nil || !isValidQueryObject(queryData) {
114 // If we have a query wrapper but the query is nil or not a valid object,
115 // return the original data
116 return jsonData, nil
117 }
118
Akroncc83eb52025-05-27 14:39:12 +0200119 // Store rewrites if they exist
120 var oldRewrites any
121 if queryMap, ok := queryData.(map[string]any); ok {
122 if rewrites, exists := queryMap["rewrites"]; exists {
123 oldRewrites = rewrites
124 delete(queryMap, "rewrites")
125 }
126 }
127
Akron32d53de2025-05-22 13:45:32 +0200128 // Convert input JSON to AST
Akron7b4984e2025-05-26 19:12:20 +0200129 jsonBytes, err := json.Marshal(queryData)
Akron32d53de2025-05-22 13:45:32 +0200130 if err != nil {
131 return nil, fmt.Errorf("failed to marshal input JSON: %w", err)
132 }
133
134 node, err := parser.ParseJSON(jsonBytes)
135 if err != nil {
136 return nil, fmt.Errorf("failed to parse JSON into AST: %w", err)
137 }
138
Akrond5850f82025-05-23 16:44:44 +0200139 // Store whether the input was a Token
140 isToken := false
141 var tokenWrap ast.Node
Akron32d53de2025-05-22 13:45:32 +0200142 if token, ok := node.(*ast.Token); ok {
Akrond5850f82025-05-23 16:44:44 +0200143 isToken = true
144 tokenWrap = token.Wrap
145 node = tokenWrap
Akron32d53de2025-05-22 13:45:32 +0200146 }
147
Akron0d9117c2025-05-27 15:20:21 +0200148 // Store original node for rewrite if needed
149 var originalNode ast.Node
150 if opts.AddRewrites {
151 originalBytes, err := parser.SerializeToJSON(node)
152 if err != nil {
153 return nil, fmt.Errorf("failed to serialize original node for rewrite: %w", err)
154 }
155 originalNode, err = parser.ParseJSON(originalBytes)
156 if err != nil {
157 return nil, fmt.Errorf("failed to parse original node for rewrite: %w", err)
158 }
159 }
160
Akron32d53de2025-05-22 13:45:32 +0200161 // Apply each rule to the AST
162 for _, rule := range rules {
163 // Create pattern and replacement based on direction
164 var pattern, replacement ast.Node
Akrona1a183f2025-05-26 17:47:33 +0200165 if opts.Direction { // true means AtoB
Akron32d53de2025-05-22 13:45:32 +0200166 pattern = rule.Upper
167 replacement = rule.Lower
168 } else {
169 pattern = rule.Lower
170 replacement = rule.Upper
171 }
172
173 // Extract the inner nodes from the pattern and replacement tokens
174 if token, ok := pattern.(*ast.Token); ok {
175 pattern = token.Wrap
176 }
177 if token, ok := replacement.(*ast.Token); ok {
178 replacement = token.Wrap
179 }
180
Akron8f1970f2025-05-30 12:52:03 +0200181 // Create deep copies of pattern and replacement to avoid modifying the original parsed rules
182 patternBytes, err := parser.SerializeToJSON(pattern)
183 if err != nil {
184 return nil, fmt.Errorf("failed to serialize pattern for copying: %w", err)
185 }
186 patternCopy, err := parser.ParseJSON(patternBytes)
187 if err != nil {
188 return nil, fmt.Errorf("failed to parse pattern copy: %w", err)
Akron32d53de2025-05-22 13:45:32 +0200189 }
190
Akron8f1970f2025-05-30 12:52:03 +0200191 replacementBytes, err := parser.SerializeToJSON(replacement)
192 if err != nil {
193 return nil, fmt.Errorf("failed to serialize replacement for copying: %w", err)
194 }
195 replacementCopy, err := parser.ParseJSON(replacementBytes)
196 if err != nil {
197 return nil, fmt.Errorf("failed to parse replacement copy: %w", err)
198 }
199
200 // Apply foundry and layer overrides to the copies
201 if opts.Direction { // true means AtoB
202 applyFoundryAndLayerOverrides(patternCopy, opts.FoundryA, opts.LayerA)
203 applyFoundryAndLayerOverrides(replacementCopy, opts.FoundryB, opts.LayerB)
204 } else {
205 applyFoundryAndLayerOverrides(patternCopy, opts.FoundryB, opts.LayerB)
206 applyFoundryAndLayerOverrides(replacementCopy, opts.FoundryA, opts.LayerA)
207 }
208
209 // Create matcher and apply replacement using the copies
210 m, err := matcher.NewMatcher(ast.Pattern{Root: patternCopy}, ast.Replacement{Root: replacementCopy})
Akrond5850f82025-05-23 16:44:44 +0200211 if err != nil {
212 return nil, fmt.Errorf("failed to create matcher: %w", err)
213 }
Akron32d53de2025-05-22 13:45:32 +0200214 node = m.Replace(node)
215 }
216
Akrond5850f82025-05-23 16:44:44 +0200217 // Wrap the result in a token if the input was a token
218 var result ast.Node
219 if isToken {
220 result = &ast.Token{Wrap: node}
221 } else {
222 result = node
223 }
Akron32d53de2025-05-22 13:45:32 +0200224
225 // Convert AST back to JSON
226 resultBytes, err := parser.SerializeToJSON(result)
227 if err != nil {
228 return nil, fmt.Errorf("failed to serialize AST to JSON: %w", err)
229 }
230
Akron6f455152025-05-27 09:03:00 +0200231 // Parse the JSON string back into
232 var resultData any
Akron32d53de2025-05-22 13:45:32 +0200233 if err := json.Unmarshal(resultBytes, &resultData); err != nil {
234 return nil, fmt.Errorf("failed to parse result JSON: %w", err)
235 }
236
Akron0d9117c2025-05-27 15:20:21 +0200237 // Add rewrites if enabled and node was changed
238 if opts.AddRewrites && !ast.NodesEqual(node, originalNode) {
239 // Create rewrite object
240 rewrite := map[string]any{
241 "@type": "koral:rewrite",
242 "editor": "termMapper",
243 }
244
Akron8a87d9a2025-05-27 15:30:48 +0200245 // Check if the node types are different (structural change)
246 if originalNode.Type() != node.Type() {
247 // Full node replacement
248 originalBytes, err := parser.SerializeToJSON(originalNode)
249 if err != nil {
250 return nil, fmt.Errorf("failed to serialize original node for rewrite: %w", err)
251 }
252 var originalJSON any
253 if err := json.Unmarshal(originalBytes, &originalJSON); err != nil {
254 return nil, fmt.Errorf("failed to parse original node JSON for rewrite: %w", err)
255 }
256 rewrite["original"] = originalJSON
257 } else if term, ok := originalNode.(*ast.Term); ok && ast.IsTermNode(node) {
258 // Check which attributes changed
259 newTerm := node.(*ast.Term)
260 if term.Foundry != newTerm.Foundry {
261 rewrite["scope"] = "foundry"
262 rewrite["original"] = term.Foundry
263 } else if term.Layer != newTerm.Layer {
264 rewrite["scope"] = "layer"
265 rewrite["original"] = term.Layer
266 } else if term.Key != newTerm.Key {
267 rewrite["scope"] = "key"
268 rewrite["original"] = term.Key
269 } else if term.Value != newTerm.Value {
270 rewrite["scope"] = "value"
271 rewrite["original"] = term.Value
272 } else {
273 // No specific attribute changed, use full node replacement
274 originalBytes, err := parser.SerializeToJSON(originalNode)
275 if err != nil {
276 return nil, fmt.Errorf("failed to serialize original node for rewrite: %w", err)
Akron0d9117c2025-05-27 15:20:21 +0200277 }
Akron8a87d9a2025-05-27 15:30:48 +0200278 var originalJSON any
279 if err := json.Unmarshal(originalBytes, &originalJSON); err != nil {
280 return nil, fmt.Errorf("failed to parse original node JSON for rewrite: %w", err)
Akron0d9117c2025-05-27 15:20:21 +0200281 }
Akron8a87d9a2025-05-27 15:30:48 +0200282 rewrite["original"] = originalJSON
Akron0d9117c2025-05-27 15:20:21 +0200283 }
284 } else {
285 // Full node replacement
286 originalBytes, err := parser.SerializeToJSON(originalNode)
287 if err != nil {
288 return nil, fmt.Errorf("failed to serialize original node for rewrite: %w", err)
289 }
290 var originalJSON any
291 if err := json.Unmarshal(originalBytes, &originalJSON); err != nil {
292 return nil, fmt.Errorf("failed to parse original node JSON for rewrite: %w", err)
293 }
Akron8a87d9a2025-05-27 15:30:48 +0200294 rewrite["original"] = originalJSON
Akron0d9117c2025-05-27 15:20:21 +0200295 }
296
297 // Add rewrite to the node
298 if resultMap, ok := resultData.(map[string]any); ok {
299 if wrapMap, ok := resultMap["wrap"].(map[string]any); ok {
300 rewrites, exists := wrapMap["rewrites"]
301 if !exists {
302 rewrites = []any{}
303 }
304 if rewritesList, ok := rewrites.([]any); ok {
305 wrapMap["rewrites"] = append(rewritesList, rewrite)
306 } else {
307 wrapMap["rewrites"] = []any{rewrite}
308 }
309 }
310 }
311 }
312
Akroncc83eb52025-05-27 14:39:12 +0200313 // Restore rewrites if they existed
314 if oldRewrites != nil {
Akron8f1970f2025-05-30 12:52:03 +0200315 // Process old rewrites through AST to ensure backward compatibility
316 if rewritesList, ok := oldRewrites.([]any); ok {
317 processedRewrites := make([]any, len(rewritesList))
318 for i, rewriteData := range rewritesList {
319 // Marshal and unmarshal each rewrite to apply backward compatibility
320 rewriteBytes, err := json.Marshal(rewriteData)
321 if err != nil {
322 return nil, fmt.Errorf("failed to marshal old rewrite %d: %w", i, err)
323 }
324 var rewrite ast.Rewrite
325 if err := json.Unmarshal(rewriteBytes, &rewrite); err != nil {
326 return nil, fmt.Errorf("failed to unmarshal old rewrite %d: %w", i, err)
327 }
328 // Marshal back to get the transformed version
329 transformedBytes, err := json.Marshal(&rewrite)
330 if err != nil {
331 return nil, fmt.Errorf("failed to marshal transformed rewrite %d: %w", i, err)
332 }
333 var transformedRewrite any
334 if err := json.Unmarshal(transformedBytes, &transformedRewrite); err != nil {
335 return nil, fmt.Errorf("failed to unmarshal transformed rewrite %d: %w", i, err)
336 }
337 processedRewrites[i] = transformedRewrite
338 }
339 if resultMap, ok := resultData.(map[string]any); ok {
340 resultMap["rewrites"] = processedRewrites
341 }
342 } else {
343 // If it's not a list, restore as-is
344 if resultMap, ok := resultData.(map[string]any); ok {
345 resultMap["rewrites"] = oldRewrites
346 }
Akroncc83eb52025-05-27 14:39:12 +0200347 }
348 }
349
Akron7b4984e2025-05-26 19:12:20 +0200350 // If we had a query wrapper, put the transformed data back in it
351 if hasQueryWrapper {
352 if wrapper, ok := jsonData.(map[string]any); ok {
353 wrapper["query"] = resultData
354 return wrapper, nil
355 }
356 }
357
Akron32d53de2025-05-22 13:45:32 +0200358 return resultData, nil
359}
360
Akron7b4984e2025-05-26 19:12:20 +0200361// isValidQueryObject checks if the query data is a valid object that can be processed
362func isValidQueryObject(data any) bool {
363 // Check if it's a map
364 queryMap, ok := data.(map[string]any)
365 if !ok {
366 return false
367 }
368
369 // Check if it has the required @type field
370 if _, ok := queryMap["@type"]; !ok {
371 return false
372 }
373
374 return true
375}
376
Akron32d53de2025-05-22 13:45:32 +0200377// applyFoundryAndLayerOverrides recursively applies foundry and layer overrides to terms
378func applyFoundryAndLayerOverrides(node ast.Node, foundry, layer string) {
379 if node == nil {
380 return
381 }
382
383 switch n := node.(type) {
384 case *ast.Term:
385 if foundry != "" {
386 n.Foundry = foundry
387 }
388 if layer != "" {
389 n.Layer = layer
390 }
391 case *ast.TermGroup:
392 for _, op := range n.Operands {
393 applyFoundryAndLayerOverrides(op, foundry, layer)
394 }
395 case *ast.Token:
396 if n.Wrap != nil {
397 applyFoundryAndLayerOverrides(n.Wrap, foundry, layer)
398 }
399 case *ast.CatchallNode:
400 if n.Wrap != nil {
401 applyFoundryAndLayerOverrides(n.Wrap, foundry, layer)
402 }
403 for _, op := range n.Operands {
404 applyFoundryAndLayerOverrides(op, foundry, layer)
405 }
406 }
407}