blob: 81fddba8bab10f79247ab3021fa545ce48352487 [file] [log] [blame]
Akron4de47a92025-06-27 11:58:11 +02001package mapper // ApplyQueryMappings applies the specified mapping rules to a JSON object
2
3import (
4 "encoding/json"
5 "fmt"
6
7 "github.com/KorAP/KoralPipe-TermMapper/ast"
8 "github.com/KorAP/KoralPipe-TermMapper/matcher"
9 "github.com/KorAP/KoralPipe-TermMapper/parser"
10)
11
12// ApplyQueryMappings applies the specified mapping rules to a JSON object
13func (m *Mapper) ApplyQueryMappings(mappingID string, opts MappingOptions, jsonData any) (any, error) {
14 // Validate mapping ID
15 if _, exists := m.mappingLists[mappingID]; !exists {
16 return nil, fmt.Errorf("mapping list with ID %s not found", mappingID)
17 }
18
19 // Get the parsed rules
20 rules := m.parsedRules[mappingID]
21
22 // Check if we have a wrapper object with a "query" field
23 var queryData any
24 var hasQueryWrapper bool
25
26 if jsonMap, ok := jsonData.(map[string]any); ok {
27 if query, exists := jsonMap["query"]; exists {
28 queryData = query
29 hasQueryWrapper = true
30 }
31 }
32
33 // If no query wrapper was found, use the entire input
34 if !hasQueryWrapper {
35 // If the input itself is not a valid query object, return it as is
36 if !isValidQueryObject(jsonData) {
37 return jsonData, nil
38 }
39 queryData = jsonData
40 } else if queryData == nil || !isValidQueryObject(queryData) {
41 // If we have a query wrapper but the query is nil or not a valid object,
42 // return the original data
43 return jsonData, nil
44 }
45
46 // Store rewrites if they exist
47 var oldRewrites any
48 if queryMap, ok := queryData.(map[string]any); ok {
49 if rewrites, exists := queryMap["rewrites"]; exists {
50 oldRewrites = rewrites
51 delete(queryMap, "rewrites")
52 }
53 }
54
55 // Convert input JSON to AST
56 jsonBytes, err := json.Marshal(queryData)
57 if err != nil {
58 return nil, fmt.Errorf("failed to marshal input JSON: %w", err)
59 }
60
61 node, err := parser.ParseJSON(jsonBytes)
62 if err != nil {
63 return nil, fmt.Errorf("failed to parse JSON into AST: %w", err)
64 }
65
66 // Store whether the input was a Token
67 isToken := false
68 var tokenWrap ast.Node
69 if token, ok := node.(*ast.Token); ok {
70 isToken = true
71 tokenWrap = token.Wrap
72 node = tokenWrap
73 }
74
75 // Store original node for rewrite if needed
76 var originalNode ast.Node
77 if opts.AddRewrites {
78 originalNode = node.Clone()
79 }
80
81 // Pre-check foundry/layer overrides to optimize processing
82 var patternFoundry, patternLayer, replacementFoundry, replacementLayer string
83 if opts.Direction { // true means AtoB
84 patternFoundry, patternLayer = opts.FoundryA, opts.LayerA
85 replacementFoundry, replacementLayer = opts.FoundryB, opts.LayerB
86 } else {
87 patternFoundry, patternLayer = opts.FoundryB, opts.LayerB
88 replacementFoundry, replacementLayer = opts.FoundryA, opts.LayerA
89 }
90
91 // Create a pattern cache key for memoization
92 type patternCacheKey struct {
93 ruleIndex int
94 foundry string
95 layer string
96 isReplacement bool
97 }
98 patternCache := make(map[patternCacheKey]ast.Node)
99
100 // Apply each rule to the AST
101 for i, rule := range rules {
102 // Create pattern and replacement based on direction
103 var pattern, replacement ast.Node
104 if opts.Direction { // true means AtoB
105 pattern = rule.Upper
106 replacement = rule.Lower
107 } else {
108 pattern = rule.Lower
109 replacement = rule.Upper
110 }
111
112 // Extract the inner nodes from the pattern and replacement tokens
113 if token, ok := pattern.(*ast.Token); ok {
114 pattern = token.Wrap
115 }
116 if token, ok := replacement.(*ast.Token); ok {
117 replacement = token.Wrap
118 }
119
120 // First, quickly check if the pattern could match without creating a full matcher
121 // This is a lightweight pre-check to avoid expensive operations
122 if !m.couldPatternMatch(node, pattern) {
123 continue
124 }
125
126 // Get or create pattern with overrides
127 patternKey := patternCacheKey{ruleIndex: i, foundry: patternFoundry, layer: patternLayer, isReplacement: false}
128 processedPattern, exists := patternCache[patternKey]
129 if !exists {
130 // Clone pattern only when needed
131 processedPattern = pattern.Clone()
132 // Apply foundry and layer overrides only if they're non-empty
133 if patternFoundry != "" || patternLayer != "" {
134 ast.ApplyFoundryAndLayerOverrides(processedPattern, patternFoundry, patternLayer)
135 }
136 patternCache[patternKey] = processedPattern
137 }
138
139 // Create a temporary matcher to check for actual matches
140 tempMatcher, err := matcher.NewMatcher(ast.Pattern{Root: processedPattern}, ast.Replacement{Root: &ast.Term{}})
141 if err != nil {
142 return nil, fmt.Errorf("failed to create temporary matcher: %w", err)
143 }
144
145 // Only proceed if there's an actual match
146 if !tempMatcher.Match(node) {
147 continue
148 }
149
150 // Get or create replacement with overrides (lazy evaluation)
151 replacementKey := patternCacheKey{ruleIndex: i, foundry: replacementFoundry, layer: replacementLayer, isReplacement: true}
152 processedReplacement, exists := patternCache[replacementKey]
153 if !exists {
154 // Clone replacement only when we have a match
155 processedReplacement = replacement.Clone()
156 // Apply foundry and layer overrides only if they're non-empty
157 if replacementFoundry != "" || replacementLayer != "" {
158 ast.ApplyFoundryAndLayerOverrides(processedReplacement, replacementFoundry, replacementLayer)
159 }
160 patternCache[replacementKey] = processedReplacement
161 }
162
163 // Create the actual matcher and apply replacement
164 actualMatcher, err := matcher.NewMatcher(ast.Pattern{Root: processedPattern}, ast.Replacement{Root: processedReplacement})
165 if err != nil {
166 return nil, fmt.Errorf("failed to create matcher: %w", err)
167 }
168 node = actualMatcher.Replace(node)
169 }
170
171 // Wrap the result in a token if the input was a token
172 var result ast.Node
173 if isToken {
174 result = &ast.Token{Wrap: node}
175 } else {
176 result = node
177 }
178
179 // Convert AST back to JSON
180 resultBytes, err := parser.SerializeToJSON(result)
181 if err != nil {
182 return nil, fmt.Errorf("failed to serialize AST to JSON: %w", err)
183 }
184
185 // Parse the JSON string back into
186 var resultData any
187 if err := json.Unmarshal(resultBytes, &resultData); err != nil {
188 return nil, fmt.Errorf("failed to parse result JSON: %w", err)
189 }
190
191 // Add rewrites if enabled and node was changed
192 if opts.AddRewrites && !ast.NodesEqual(node, originalNode) {
193 // Create rewrite object
194 rewrite := map[string]any{
195 "@type": "koral:rewrite",
196 "editor": "termMapper",
197 }
198
199 // Check if the node types are different (structural change)
200 if originalNode.Type() != node.Type() {
201 // Full node replacement
202 originalBytes, err := parser.SerializeToJSON(originalNode)
203 if err != nil {
204 return nil, fmt.Errorf("failed to serialize original node for rewrite: %w", err)
205 }
206 var originalJSON any
207 if err := json.Unmarshal(originalBytes, &originalJSON); err != nil {
208 return nil, fmt.Errorf("failed to parse original node JSON for rewrite: %w", err)
209 }
210 rewrite["original"] = originalJSON
211 } else if term, ok := originalNode.(*ast.Term); ok && ast.IsTermNode(node) {
212 // Check which attributes changed
213 newTerm := node.(*ast.Term)
214 if term.Foundry != newTerm.Foundry {
215 rewrite["scope"] = "foundry"
216 rewrite["original"] = term.Foundry
217 } else if term.Layer != newTerm.Layer {
218 rewrite["scope"] = "layer"
219 rewrite["original"] = term.Layer
220 } else if term.Key != newTerm.Key {
221 rewrite["scope"] = "key"
222 rewrite["original"] = term.Key
223 } else if term.Value != newTerm.Value {
224 rewrite["scope"] = "value"
225 rewrite["original"] = term.Value
226 } else {
227 // No specific attribute changed, use full node replacement
228 originalBytes, err := parser.SerializeToJSON(originalNode)
229 if err != nil {
230 return nil, fmt.Errorf("failed to serialize original node for rewrite: %w", err)
231 }
232 var originalJSON any
233 if err := json.Unmarshal(originalBytes, &originalJSON); err != nil {
234 return nil, fmt.Errorf("failed to parse original node JSON for rewrite: %w", err)
235 }
236 rewrite["original"] = originalJSON
237 }
238 } else {
239 // Full node replacement
240 originalBytes, err := parser.SerializeToJSON(originalNode)
241 if err != nil {
242 return nil, fmt.Errorf("failed to serialize original node for rewrite: %w", err)
243 }
244 var originalJSON any
245 if err := json.Unmarshal(originalBytes, &originalJSON); err != nil {
246 return nil, fmt.Errorf("failed to parse original node JSON for rewrite: %w", err)
247 }
248 rewrite["original"] = originalJSON
249 }
250
251 // Add rewrite to the node
252 if resultMap, ok := resultData.(map[string]any); ok {
253 if wrapMap, ok := resultMap["wrap"].(map[string]any); ok {
254 rewrites, exists := wrapMap["rewrites"]
255 if !exists {
256 rewrites = []any{}
257 }
258 if rewritesList, ok := rewrites.([]any); ok {
259 wrapMap["rewrites"] = append(rewritesList, rewrite)
260 } else {
261 wrapMap["rewrites"] = []any{rewrite}
262 }
263 }
264 }
265 }
266
267 // Restore rewrites if they existed
268 if oldRewrites != nil {
269 // Process old rewrites through AST to ensure backward compatibility
270 if rewritesList, ok := oldRewrites.([]any); ok {
271 processedRewrites := make([]any, len(rewritesList))
272 for i, rewriteData := range rewritesList {
273 // Marshal and unmarshal each rewrite to apply backward compatibility
274 rewriteBytes, err := json.Marshal(rewriteData)
275 if err != nil {
276 return nil, fmt.Errorf("failed to marshal old rewrite %d: %w", i, err)
277 }
278 var rewrite ast.Rewrite
279 if err := json.Unmarshal(rewriteBytes, &rewrite); err != nil {
280 return nil, fmt.Errorf("failed to unmarshal old rewrite %d: %w", i, err)
281 }
282 // Marshal back to get the transformed version
283 transformedBytes, err := json.Marshal(&rewrite)
284 if err != nil {
285 return nil, fmt.Errorf("failed to marshal transformed rewrite %d: %w", i, err)
286 }
287 var transformedRewrite any
288 if err := json.Unmarshal(transformedBytes, &transformedRewrite); err != nil {
289 return nil, fmt.Errorf("failed to unmarshal transformed rewrite %d: %w", i, err)
290 }
291 processedRewrites[i] = transformedRewrite
292 }
293 if resultMap, ok := resultData.(map[string]any); ok {
294 resultMap["rewrites"] = processedRewrites
295 }
296 } else {
297 // If it's not a list, restore as-is
298 if resultMap, ok := resultData.(map[string]any); ok {
299 resultMap["rewrites"] = oldRewrites
300 }
301 }
302 }
303
304 // If we had a query wrapper, put the transformed data back in it
305 if hasQueryWrapper {
306 if wrapper, ok := jsonData.(map[string]any); ok {
307 wrapper["query"] = resultData
308 return wrapper, nil
309 }
310 }
311
312 return resultData, nil
313}
314
315// isValidQueryObject checks if the query data is a valid object that can be processed
316func isValidQueryObject(data any) bool {
317 // Check if it's a map
318 queryMap, ok := data.(map[string]any)
319 if !ok {
320 return false
321 }
322
323 // Check if it has the required @type field
324 if _, ok := queryMap["@type"]; !ok {
325 return false
326 }
327
328 return true
329}
330
331// couldPatternMatch performs a lightweight check to see if a pattern could potentially match a node
332// This is an optimization to avoid expensive operations when there's clearly no match possible
333func (m *Mapper) couldPatternMatch(node, pattern ast.Node) bool {
334 if pattern == nil {
335 return true
336 }
337 if node == nil {
338 return false
339 }
340
341 // Handle Token wrappers
342 if token, ok := pattern.(*ast.Token); ok {
343 pattern = token.Wrap
344 }
345 if token, ok := node.(*ast.Token); ok {
346 node = token.Wrap
347 }
348
349 // For simple terms, check basic compatibility
350 if patternTerm, ok := pattern.(*ast.Term); ok {
351 // Check if there's any term in the node structure that could match
352 return m.hasMatchingTerm(node, patternTerm)
353 }
354
355 // For TermGroups, we need to check all possible matches
356 if patternGroup, ok := pattern.(*ast.TermGroup); ok {
357 if patternGroup.Relation == ast.OrRelation {
358 // For OR relations, any operand could match
359 for _, op := range patternGroup.Operands {
360 if m.couldPatternMatch(node, op) {
361 return true
362 }
363 }
364 return false
365 } else {
366 // For AND relations, all operands must have potential matches
367 for _, op := range patternGroup.Operands {
368 if !m.couldPatternMatch(node, op) {
369 return false
370 }
371 }
372 return true
373 }
374 }
375
376 // For other cases, assume they could match (conservative approach)
377 return true
378}
379
380// hasMatchingTerm checks if there's any term in the node structure that could match the pattern term
381func (m *Mapper) hasMatchingTerm(node ast.Node, patternTerm *ast.Term) bool {
382 if node == nil {
383 return false
384 }
385
386 switch n := node.(type) {
387 case *ast.Term:
388 // Check if this term could match the pattern
389 // We only check key as that's the most distinctive attribute
390 return n.Key == patternTerm.Key
391 case *ast.TermGroup:
392 // Check all operands
393 for _, op := range n.Operands {
394 if m.hasMatchingTerm(op, patternTerm) {
395 return true
396 }
397 }
398 return false
399 case *ast.Token:
400 return m.hasMatchingTerm(n.Wrap, patternTerm)
401 case *ast.CatchallNode:
402 if n.Wrap != nil && m.hasMatchingTerm(n.Wrap, patternTerm) {
403 return true
404 }
405 for _, op := range n.Operands {
406 if m.hasMatchingTerm(op, patternTerm) {
407 return true
408 }
409 }
410 return false
411 default:
412 return false
413 }
414}