blob: 424896e0f75e40bd4823bbd1aa60d3c74a0027bd [file] [log] [blame]
package mapper
import (
"encoding/json"
"fmt"
"github.com/KorAP/KoralPipe-TermMapper/ast"
"github.com/KorAP/KoralPipe-TermMapper/config"
"github.com/KorAP/KoralPipe-TermMapper/matcher"
"github.com/KorAP/KoralPipe-TermMapper/parser"
)
// Direction represents the mapping direction (A to B or B to A)
type Direction bool
const (
AtoB Direction = true
BtoA Direction = false
)
// String converts the Direction to its string representation
func (d Direction) String() string {
if d {
return "atob"
}
return "btoa"
}
// ParseDirection converts a string direction to Direction type
func ParseDirection(dir string) (Direction, error) {
switch dir {
case "atob":
return AtoB, nil
case "btoa":
return BtoA, nil
default:
return false, fmt.Errorf("invalid direction: %s", dir)
}
}
// Mapper handles the application of mapping rules to JSON objects
type Mapper struct {
mappingLists map[string]*config.MappingList
parsedRules map[string][]*parser.MappingResult
}
// NewMapper creates a new Mapper instance from a list of MappingLists
func NewMapper(lists []config.MappingList) (*Mapper, error) {
m := &Mapper{
mappingLists: make(map[string]*config.MappingList),
parsedRules: make(map[string][]*parser.MappingResult),
}
// Store mapping lists by ID
for _, list := range lists {
if _, exists := m.mappingLists[list.ID]; exists {
return nil, fmt.Errorf("duplicate mapping list ID found: %s", list.ID)
}
// Create a copy of the list to store
listCopy := list
m.mappingLists[list.ID] = &listCopy
// Parse the rules immediately
parsedRules, err := list.ParseMappings()
if err != nil {
return nil, fmt.Errorf("failed to parse mappings for list %s: %w", list.ID, err)
}
m.parsedRules[list.ID] = parsedRules
}
return m, nil
}
// MappingOptions contains the options for applying mappings
type MappingOptions struct {
FoundryA string
LayerA string
FoundryB string
LayerB string
Direction Direction
AddRewrites bool
}
// 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 {
originalBytes, err := parser.SerializeToJSON(node)
if err != nil {
return nil, fmt.Errorf("failed to serialize original node for rewrite: %w", err)
}
originalNode, err = parser.ParseJSON(originalBytes)
if err != nil {
return nil, fmt.Errorf("failed to parse original node for rewrite: %w", err)
}
}
// Apply each rule to the AST
for _, 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
}
// Create deep copies of pattern and replacement to avoid modifying the original parsed rules
patternBytes, err := parser.SerializeToJSON(pattern)
if err != nil {
return nil, fmt.Errorf("failed to serialize pattern for copying: %w", err)
}
patternCopy, err := parser.ParseJSON(patternBytes)
if err != nil {
return nil, fmt.Errorf("failed to parse pattern copy: %w", err)
}
replacementBytes, err := parser.SerializeToJSON(replacement)
if err != nil {
return nil, fmt.Errorf("failed to serialize replacement for copying: %w", err)
}
replacementCopy, err := parser.ParseJSON(replacementBytes)
if err != nil {
return nil, fmt.Errorf("failed to parse replacement copy: %w", err)
}
// Apply foundry and layer overrides to the copies
if opts.Direction { // true means AtoB
applyFoundryAndLayerOverrides(patternCopy, opts.FoundryA, opts.LayerA)
applyFoundryAndLayerOverrides(replacementCopy, opts.FoundryB, opts.LayerB)
} else {
applyFoundryAndLayerOverrides(patternCopy, opts.FoundryB, opts.LayerB)
applyFoundryAndLayerOverrides(replacementCopy, opts.FoundryA, opts.LayerA)
}
// Create matcher and apply replacement using the copies
m, err := matcher.NewMatcher(ast.Pattern{Root: patternCopy}, ast.Replacement{Root: replacementCopy})
if err != nil {
return nil, fmt.Errorf("failed to create matcher: %w", err)
}
node = m.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
}
// applyFoundryAndLayerOverrides recursively applies foundry and layer overrides to terms
func applyFoundryAndLayerOverrides(node ast.Node, foundry, layer string) {
if node == nil {
return
}
switch n := node.(type) {
case *ast.Term:
if foundry != "" {
n.Foundry = foundry
}
if layer != "" {
n.Layer = layer
}
case *ast.TermGroup:
for _, op := range n.Operands {
applyFoundryAndLayerOverrides(op, foundry, layer)
}
case *ast.Token:
if n.Wrap != nil {
applyFoundryAndLayerOverrides(n.Wrap, foundry, layer)
}
case *ast.CatchallNode:
if n.Wrap != nil {
applyFoundryAndLayerOverrides(n.Wrap, foundry, layer)
}
for _, op := range n.Operands {
applyFoundryAndLayerOverrides(op, foundry, layer)
}
}
}