| package mapper // ApplyQueryMappings applies the specified mapping rules to a JSON object |
| |
| import ( |
| "encoding/json" |
| "fmt" |
| |
| "github.com/KorAP/Koral-Mapper/ast" |
| "github.com/KorAP/Koral-Mapper/matcher" |
| "github.com/KorAP/Koral-Mapper/parser" |
| ) |
| |
| // ApplyQueryMappings applies the specified mapping rules to a JSON object |
| func (m *Mapper) ApplyQueryMappings(mappingID string, opts MappingOptions, jsonData any) (any, error) { |
| // Validate mapping ID |
| if _, exists := m.mappingLists[mappingID]; !exists { |
| return nil, fmt.Errorf("mapping list with ID %s not found", mappingID) |
| } |
| |
| // Get the parsed rules |
| rules := m.parsedRules[mappingID] |
| |
| // Check if we have a wrapper object with a "query" field |
| var queryData any |
| var hasQueryWrapper bool |
| |
| if jsonMap, ok := jsonData.(map[string]any); ok { |
| if query, exists := jsonMap["query"]; exists { |
| queryData = query |
| hasQueryWrapper = true |
| } |
| } |
| |
| // If no query wrapper was found, use the entire input |
| if !hasQueryWrapper { |
| // If the input itself is not a valid query object, return it as is |
| if !isValidQueryObject(jsonData) { |
| return jsonData, nil |
| } |
| queryData = jsonData |
| } else if queryData == nil || !isValidQueryObject(queryData) { |
| // If we have a query wrapper but the query is nil or not a valid object, |
| // return the original data |
| return jsonData, nil |
| } |
| |
| // Store rewrites if they exist |
| var oldRewrites any |
| if queryMap, ok := queryData.(map[string]any); ok { |
| if rewrites, exists := queryMap["rewrites"]; exists { |
| oldRewrites = rewrites |
| delete(queryMap, "rewrites") |
| } |
| } |
| |
| // Convert input JSON to AST |
| jsonBytes, err := json.Marshal(queryData) |
| if err != nil { |
| return nil, fmt.Errorf("failed to marshal input JSON: %w", err) |
| } |
| |
| node, err := parser.ParseJSON(jsonBytes) |
| if err != nil { |
| return nil, fmt.Errorf("failed to parse JSON into AST: %w", err) |
| } |
| |
| // Store whether the input was a Token |
| isToken := false |
| var tokenWrap ast.Node |
| if token, ok := node.(*ast.Token); ok { |
| isToken = true |
| tokenWrap = token.Wrap |
| node = tokenWrap |
| } |
| |
| // Store original node for rewrite if needed |
| var originalNode ast.Node |
| if opts.AddRewrites { |
| originalNode = node.Clone() |
| } |
| |
| // Pre-check foundry/layer overrides to optimize processing |
| var patternFoundry, patternLayer, replacementFoundry, replacementLayer string |
| if opts.Direction { // true means AtoB |
| patternFoundry, patternLayer = opts.FoundryA, opts.LayerA |
| replacementFoundry, replacementLayer = opts.FoundryB, opts.LayerB |
| } else { |
| patternFoundry, patternLayer = opts.FoundryB, opts.LayerB |
| replacementFoundry, replacementLayer = opts.FoundryA, opts.LayerA |
| } |
| |
| // Create a pattern cache key for memoization |
| type patternCacheKey struct { |
| ruleIndex int |
| foundry string |
| layer string |
| isReplacement bool |
| } |
| patternCache := make(map[patternCacheKey]ast.Node) |
| |
| // Apply each rule to the AST |
| for i, rule := range rules { |
| // Create pattern and replacement based on direction |
| var pattern, replacement ast.Node |
| if opts.Direction { // true means AtoB |
| pattern = rule.Upper |
| replacement = rule.Lower |
| } else { |
| pattern = rule.Lower |
| replacement = rule.Upper |
| } |
| |
| // Extract the inner nodes from the pattern and replacement tokens |
| if token, ok := pattern.(*ast.Token); ok { |
| pattern = token.Wrap |
| } |
| if token, ok := replacement.(*ast.Token); ok { |
| replacement = token.Wrap |
| } |
| |
| // Get or create pattern with overrides |
| patternKey := patternCacheKey{ruleIndex: i, foundry: patternFoundry, layer: patternLayer, isReplacement: false} |
| processedPattern, exists := patternCache[patternKey] |
| if !exists { |
| // Clone pattern only when needed |
| processedPattern = pattern.Clone() |
| // Apply foundry and layer overrides only if they're non-empty |
| if patternFoundry != "" || patternLayer != "" { |
| ast.ApplyFoundryAndLayerOverrides(processedPattern, patternFoundry, patternLayer) |
| } |
| patternCache[patternKey] = processedPattern |
| } |
| |
| // Create a temporary matcher to check for actual matches |
| tempMatcher, err := matcher.NewMatcher(ast.Pattern{Root: processedPattern}, ast.Replacement{Root: &ast.Term{}}) |
| if err != nil { |
| return nil, fmt.Errorf("failed to create temporary matcher: %w", err) |
| } |
| |
| // Only proceed if there's an actual match |
| if !tempMatcher.Match(node) { |
| continue |
| } |
| |
| // Get or create replacement with overrides (lazy evaluation) |
| replacementKey := patternCacheKey{ruleIndex: i, foundry: replacementFoundry, layer: replacementLayer, isReplacement: true} |
| processedReplacement, exists := patternCache[replacementKey] |
| if !exists { |
| // Clone replacement only when we have a match |
| processedReplacement = replacement.Clone() |
| // Apply foundry and layer overrides only if they're non-empty |
| if replacementFoundry != "" || replacementLayer != "" { |
| ast.ApplyFoundryAndLayerOverrides(processedReplacement, replacementFoundry, replacementLayer) |
| } |
| patternCache[replacementKey] = processedReplacement |
| } |
| |
| // Create the actual matcher and apply replacement |
| actualMatcher, err := matcher.NewMatcher(ast.Pattern{Root: processedPattern}, ast.Replacement{Root: processedReplacement}) |
| if err != nil { |
| return nil, fmt.Errorf("failed to create matcher: %w", err) |
| } |
| node = actualMatcher.Replace(node) |
| } |
| |
| // Wrap the result in a token if the input was a token |
| var result ast.Node |
| if isToken { |
| result = &ast.Token{Wrap: node} |
| } else { |
| result = node |
| } |
| |
| // Convert AST back to JSON |
| resultBytes, err := parser.SerializeToJSON(result) |
| if err != nil { |
| return nil, fmt.Errorf("failed to serialize AST to JSON: %w", err) |
| } |
| |
| // Parse the JSON string back into |
| var resultData any |
| if err := json.Unmarshal(resultBytes, &resultData); err != nil { |
| return nil, fmt.Errorf("failed to parse result JSON: %w", err) |
| } |
| |
| // Add rewrites if enabled and node was changed |
| if opts.AddRewrites && !ast.NodesEqual(node, originalNode) { |
| // Create rewrite object |
| rewrite := map[string]any{ |
| "@type": "koral:rewrite", |
| "editor": "termMapper", |
| } |
| |
| // Check if the node types are different (structural change) |
| if originalNode.Type() != node.Type() { |
| // Full node replacement |
| originalBytes, err := parser.SerializeToJSON(originalNode) |
| if err != nil { |
| return nil, fmt.Errorf("failed to serialize original node for rewrite: %w", err) |
| } |
| var originalJSON any |
| if err := json.Unmarshal(originalBytes, &originalJSON); err != nil { |
| return nil, fmt.Errorf("failed to parse original node JSON for rewrite: %w", err) |
| } |
| rewrite["original"] = originalJSON |
| } else if term, ok := originalNode.(*ast.Term); ok && ast.IsTermNode(node) { |
| // Check which attributes changed |
| newTerm := node.(*ast.Term) |
| if term.Foundry != newTerm.Foundry { |
| rewrite["scope"] = "foundry" |
| rewrite["original"] = term.Foundry |
| } else if term.Layer != newTerm.Layer { |
| rewrite["scope"] = "layer" |
| rewrite["original"] = term.Layer |
| } else if term.Key != newTerm.Key { |
| rewrite["scope"] = "key" |
| rewrite["original"] = term.Key |
| } else if term.Value != newTerm.Value { |
| rewrite["scope"] = "value" |
| rewrite["original"] = term.Value |
| } else { |
| // No specific attribute changed, use full node replacement |
| originalBytes, err := parser.SerializeToJSON(originalNode) |
| if err != nil { |
| return nil, fmt.Errorf("failed to serialize original node for rewrite: %w", err) |
| } |
| var originalJSON any |
| if err := json.Unmarshal(originalBytes, &originalJSON); err != nil { |
| return nil, fmt.Errorf("failed to parse original node JSON for rewrite: %w", err) |
| } |
| rewrite["original"] = originalJSON |
| } |
| } else { |
| // Full node replacement |
| originalBytes, err := parser.SerializeToJSON(originalNode) |
| if err != nil { |
| return nil, fmt.Errorf("failed to serialize original node for rewrite: %w", err) |
| } |
| var originalJSON any |
| if err := json.Unmarshal(originalBytes, &originalJSON); err != nil { |
| return nil, fmt.Errorf("failed to parse original node JSON for rewrite: %w", err) |
| } |
| rewrite["original"] = originalJSON |
| } |
| |
| // Add rewrite to the node |
| if resultMap, ok := resultData.(map[string]any); ok { |
| if wrapMap, ok := resultMap["wrap"].(map[string]any); ok { |
| rewrites, exists := wrapMap["rewrites"] |
| if !exists { |
| rewrites = []any{} |
| } |
| if rewritesList, ok := rewrites.([]any); ok { |
| wrapMap["rewrites"] = append(rewritesList, rewrite) |
| } else { |
| wrapMap["rewrites"] = []any{rewrite} |
| } |
| } |
| } |
| } |
| |
| // Restore rewrites if they existed |
| if oldRewrites != nil { |
| // Process old rewrites through AST to ensure backward compatibility |
| if rewritesList, ok := oldRewrites.([]any); ok { |
| processedRewrites := make([]any, len(rewritesList)) |
| for i, rewriteData := range rewritesList { |
| // Marshal and unmarshal each rewrite to apply backward compatibility |
| rewriteBytes, err := json.Marshal(rewriteData) |
| if err != nil { |
| return nil, fmt.Errorf("failed to marshal old rewrite %d: %w", i, err) |
| } |
| var rewrite ast.Rewrite |
| if err := json.Unmarshal(rewriteBytes, &rewrite); err != nil { |
| return nil, fmt.Errorf("failed to unmarshal old rewrite %d: %w", i, err) |
| } |
| // Marshal back to get the transformed version |
| transformedBytes, err := json.Marshal(&rewrite) |
| if err != nil { |
| return nil, fmt.Errorf("failed to marshal transformed rewrite %d: %w", i, err) |
| } |
| var transformedRewrite any |
| if err := json.Unmarshal(transformedBytes, &transformedRewrite); err != nil { |
| return nil, fmt.Errorf("failed to unmarshal transformed rewrite %d: %w", i, err) |
| } |
| processedRewrites[i] = transformedRewrite |
| } |
| if resultMap, ok := resultData.(map[string]any); ok { |
| resultMap["rewrites"] = processedRewrites |
| } |
| } else { |
| // If it's not a list, restore as-is |
| if resultMap, ok := resultData.(map[string]any); ok { |
| resultMap["rewrites"] = oldRewrites |
| } |
| } |
| } |
| |
| // If we had a query wrapper, put the transformed data back in it |
| if hasQueryWrapper { |
| if wrapper, ok := jsonData.(map[string]any); ok { |
| wrapper["query"] = resultData |
| return wrapper, nil |
| } |
| } |
| |
| return resultData, nil |
| } |
| |
| // isValidQueryObject checks if the query data is a valid object that can be processed |
| func isValidQueryObject(data any) bool { |
| // Check if it's a map |
| queryMap, ok := data.(map[string]any) |
| if !ok { |
| return false |
| } |
| |
| // Check if it has the required @type field |
| if _, ok := queryMap["@type"]; !ok { |
| return false |
| } |
| |
| return true |
| } |