blob: 4980f857380220f55b4c2c0a369811023a038fb3 [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
Akron2ef703c2025-07-03 15:57:42 +02007 "github.com/KorAP/Koral-Mapper/ast"
8 "github.com/KorAP/Koral-Mapper/matcher"
9 "github.com/KorAP/Koral-Mapper/parser"
Akron4de47a92025-06-27 11:58:11 +020010)
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
Akron2f93c582026-02-19 16:49:13 +010019 if m.mappingLists[mappingID].IsCorpus() {
20 return m.applyCorpusQueryMappings(mappingID, opts, jsonData)
21 }
22
Akron4de47a92025-06-27 11:58:11 +020023 // Get the parsed rules
Akron2f93c582026-02-19 16:49:13 +010024 rules := m.parsedQueryRules[mappingID]
Akron4de47a92025-06-27 11:58:11 +020025
26 // Check if we have a wrapper object with a "query" field
27 var queryData any
28 var hasQueryWrapper bool
29
30 if jsonMap, ok := jsonData.(map[string]any); ok {
31 if query, exists := jsonMap["query"]; exists {
32 queryData = query
33 hasQueryWrapper = true
34 }
35 }
36
37 // If no query wrapper was found, use the entire input
38 if !hasQueryWrapper {
39 // If the input itself is not a valid query object, return it as is
40 if !isValidQueryObject(jsonData) {
41 return jsonData, nil
42 }
43 queryData = jsonData
44 } else if queryData == nil || !isValidQueryObject(queryData) {
45 // If we have a query wrapper but the query is nil or not a valid object,
46 // return the original data
47 return jsonData, nil
48 }
49
50 // Store rewrites if they exist
51 var oldRewrites any
52 if queryMap, ok := queryData.(map[string]any); ok {
53 if rewrites, exists := queryMap["rewrites"]; exists {
54 oldRewrites = rewrites
55 delete(queryMap, "rewrites")
56 }
57 }
58
59 // Convert input JSON to AST
60 jsonBytes, err := json.Marshal(queryData)
61 if err != nil {
62 return nil, fmt.Errorf("failed to marshal input JSON: %w", err)
63 }
64
65 node, err := parser.ParseJSON(jsonBytes)
66 if err != nil {
67 return nil, fmt.Errorf("failed to parse JSON into AST: %w", err)
68 }
69
70 // Store whether the input was a Token
71 isToken := false
72 var tokenWrap ast.Node
73 if token, ok := node.(*ast.Token); ok {
74 isToken = true
75 tokenWrap = token.Wrap
76 node = tokenWrap
77 }
78
79 // Store original node for rewrite if needed
80 var originalNode ast.Node
81 if opts.AddRewrites {
82 originalNode = node.Clone()
83 }
84
85 // Pre-check foundry/layer overrides to optimize processing
86 var patternFoundry, patternLayer, replacementFoundry, replacementLayer string
87 if opts.Direction { // true means AtoB
88 patternFoundry, patternLayer = opts.FoundryA, opts.LayerA
89 replacementFoundry, replacementLayer = opts.FoundryB, opts.LayerB
90 } else {
91 patternFoundry, patternLayer = opts.FoundryB, opts.LayerB
92 replacementFoundry, replacementLayer = opts.FoundryA, opts.LayerA
93 }
94
95 // Create a pattern cache key for memoization
96 type patternCacheKey struct {
97 ruleIndex int
98 foundry string
99 layer string
100 isReplacement bool
101 }
102 patternCache := make(map[patternCacheKey]ast.Node)
103
104 // Apply each rule to the AST
105 for i, rule := range rules {
106 // Create pattern and replacement based on direction
107 var pattern, replacement ast.Node
108 if opts.Direction { // true means AtoB
109 pattern = rule.Upper
110 replacement = rule.Lower
111 } else {
112 pattern = rule.Lower
113 replacement = rule.Upper
114 }
115
116 // Extract the inner nodes from the pattern and replacement tokens
117 if token, ok := pattern.(*ast.Token); ok {
118 pattern = token.Wrap
119 }
120 if token, ok := replacement.(*ast.Token); ok {
121 replacement = token.Wrap
122 }
123
Akron4de47a92025-06-27 11:58:11 +0200124 // Get or create pattern with overrides
125 patternKey := patternCacheKey{ruleIndex: i, foundry: patternFoundry, layer: patternLayer, isReplacement: false}
126 processedPattern, exists := patternCache[patternKey]
127 if !exists {
128 // Clone pattern only when needed
129 processedPattern = pattern.Clone()
130 // Apply foundry and layer overrides only if they're non-empty
131 if patternFoundry != "" || patternLayer != "" {
132 ast.ApplyFoundryAndLayerOverrides(processedPattern, patternFoundry, patternLayer)
133 }
134 patternCache[patternKey] = processedPattern
135 }
136
137 // Create a temporary matcher to check for actual matches
138 tempMatcher, err := matcher.NewMatcher(ast.Pattern{Root: processedPattern}, ast.Replacement{Root: &ast.Term{}})
139 if err != nil {
140 return nil, fmt.Errorf("failed to create temporary matcher: %w", err)
141 }
142
143 // Only proceed if there's an actual match
144 if !tempMatcher.Match(node) {
145 continue
146 }
147
148 // Get or create replacement with overrides (lazy evaluation)
149 replacementKey := patternCacheKey{ruleIndex: i, foundry: replacementFoundry, layer: replacementLayer, isReplacement: true}
150 processedReplacement, exists := patternCache[replacementKey]
151 if !exists {
152 // Clone replacement only when we have a match
153 processedReplacement = replacement.Clone()
154 // Apply foundry and layer overrides only if they're non-empty
155 if replacementFoundry != "" || replacementLayer != "" {
156 ast.ApplyFoundryAndLayerOverrides(processedReplacement, replacementFoundry, replacementLayer)
157 }
158 patternCache[replacementKey] = processedReplacement
159 }
160
161 // Create the actual matcher and apply replacement
162 actualMatcher, err := matcher.NewMatcher(ast.Pattern{Root: processedPattern}, ast.Replacement{Root: processedReplacement})
163 if err != nil {
164 return nil, fmt.Errorf("failed to create matcher: %w", err)
165 }
166 node = actualMatcher.Replace(node)
167 }
168
169 // Wrap the result in a token if the input was a token
170 var result ast.Node
171 if isToken {
172 result = &ast.Token{Wrap: node}
173 } else {
174 result = node
175 }
176
177 // Convert AST back to JSON
178 resultBytes, err := parser.SerializeToJSON(result)
179 if err != nil {
180 return nil, fmt.Errorf("failed to serialize AST to JSON: %w", err)
181 }
182
183 // Parse the JSON string back into
184 var resultData any
185 if err := json.Unmarshal(resultBytes, &resultData); err != nil {
186 return nil, fmt.Errorf("failed to parse result JSON: %w", err)
187 }
188
189 // Add rewrites if enabled and node was changed
190 if opts.AddRewrites && !ast.NodesEqual(node, originalNode) {
Akron2f93c582026-02-19 16:49:13 +0100191 rewrite := buildQueryRewrite(originalNode, node)
Akron4de47a92025-06-27 11:58:11 +0200192
193 // Add rewrite to the node
194 if resultMap, ok := resultData.(map[string]any); ok {
195 if wrapMap, ok := resultMap["wrap"].(map[string]any); ok {
196 rewrites, exists := wrapMap["rewrites"]
197 if !exists {
198 rewrites = []any{}
199 }
200 if rewritesList, ok := rewrites.([]any); ok {
201 wrapMap["rewrites"] = append(rewritesList, rewrite)
202 } else {
203 wrapMap["rewrites"] = []any{rewrite}
204 }
205 }
206 }
207 }
208
209 // Restore rewrites if they existed
210 if oldRewrites != nil {
211 // Process old rewrites through AST to ensure backward compatibility
212 if rewritesList, ok := oldRewrites.([]any); ok {
213 processedRewrites := make([]any, len(rewritesList))
214 for i, rewriteData := range rewritesList {
215 // Marshal and unmarshal each rewrite to apply backward compatibility
216 rewriteBytes, err := json.Marshal(rewriteData)
217 if err != nil {
218 return nil, fmt.Errorf("failed to marshal old rewrite %d: %w", i, err)
219 }
220 var rewrite ast.Rewrite
221 if err := json.Unmarshal(rewriteBytes, &rewrite); err != nil {
222 return nil, fmt.Errorf("failed to unmarshal old rewrite %d: %w", i, err)
223 }
224 // Marshal back to get the transformed version
225 transformedBytes, err := json.Marshal(&rewrite)
226 if err != nil {
227 return nil, fmt.Errorf("failed to marshal transformed rewrite %d: %w", i, err)
228 }
229 var transformedRewrite any
230 if err := json.Unmarshal(transformedBytes, &transformedRewrite); err != nil {
231 return nil, fmt.Errorf("failed to unmarshal transformed rewrite %d: %w", i, err)
232 }
233 processedRewrites[i] = transformedRewrite
234 }
235 if resultMap, ok := resultData.(map[string]any); ok {
236 resultMap["rewrites"] = processedRewrites
237 }
238 } else {
239 // If it's not a list, restore as-is
240 if resultMap, ok := resultData.(map[string]any); ok {
241 resultMap["rewrites"] = oldRewrites
242 }
243 }
244 }
245
246 // If we had a query wrapper, put the transformed data back in it
247 if hasQueryWrapper {
248 if wrapper, ok := jsonData.(map[string]any); ok {
249 wrapper["query"] = resultData
250 return wrapper, nil
251 }
252 }
253
254 return resultData, nil
255}
256
Akron2f93c582026-02-19 16:49:13 +0100257// buildQueryRewrite creates a rewrite entry for a query-level transformation
258// by comparing the original and new AST nodes.
259func buildQueryRewrite(originalNode, newNode ast.Node) map[string]any {
260 if term, ok := originalNode.(*ast.Term); ok && ast.IsTermNode(newNode) && originalNode.Type() == newNode.Type() {
261 newTerm := newNode.(*ast.Term)
262 if term.Foundry != newTerm.Foundry {
263 return newRewriteEntry("foundry", term.Foundry)
264 }
265 if term.Layer != newTerm.Layer {
266 return newRewriteEntry("layer", term.Layer)
267 }
268 if term.Key != newTerm.Key {
269 return newRewriteEntry("key", term.Key)
270 }
271 if term.Value != newTerm.Value {
272 return newRewriteEntry("value", term.Value)
273 }
274 }
275
276 originalBytes, err := parser.SerializeToJSON(originalNode)
277 if err != nil {
278 return newRewriteEntry("", nil)
279 }
280 var originalJSON any
281 if err := json.Unmarshal(originalBytes, &originalJSON); err != nil {
282 return newRewriteEntry("", nil)
283 }
284 return newRewriteEntry("", originalJSON)
285}
286
Akron4de47a92025-06-27 11:58:11 +0200287// isValidQueryObject checks if the query data is a valid object that can be processed
288func isValidQueryObject(data any) bool {
289 // Check if it's a map
290 queryMap, ok := data.(map[string]any)
291 if !ok {
292 return false
293 }
294
295 // Check if it has the required @type field
296 if _, ok := queryMap["@type"]; !ok {
297 return false
298 }
299
300 return true
301}