blob: 852ed83762b202431916ef688451578c43728100 [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)
14type Direction string
15
16const (
17 AtoB Direction = "atob"
18 BtoA Direction = "btoa"
19)
20
21// Mapper handles the application of mapping rules to JSON objects
22type Mapper struct {
23 mappingLists map[string]*config.MappingList
24 parsedRules map[string][]*parser.MappingResult
25}
26
Akrona00d4752025-05-26 17:34:36 +020027// NewMapper creates a new Mapper instance from a list of MappingLists
28func NewMapper(lists []config.MappingList) (*Mapper, error) {
Akron32d53de2025-05-22 13:45:32 +020029 m := &Mapper{
30 mappingLists: make(map[string]*config.MappingList),
31 parsedRules: make(map[string][]*parser.MappingResult),
32 }
33
Akrona00d4752025-05-26 17:34:36 +020034 // Store mapping lists by ID
35 for _, list := range lists {
36 if _, exists := m.mappingLists[list.ID]; exists {
37 return nil, fmt.Errorf("duplicate mapping list ID found: %s", list.ID)
38 }
39
40 // Create a copy of the list to store
41 listCopy := list
42 m.mappingLists[list.ID] = &listCopy
43
44 // Parse the rules immediately
45 parsedRules, err := list.ParseMappings()
Akron32d53de2025-05-22 13:45:32 +020046 if err != nil {
Akrona00d4752025-05-26 17:34:36 +020047 return nil, fmt.Errorf("failed to parse mappings for list %s: %w", list.ID, err)
Akron32d53de2025-05-22 13:45:32 +020048 }
Akrona00d4752025-05-26 17:34:36 +020049 m.parsedRules[list.ID] = parsedRules
Akron32d53de2025-05-22 13:45:32 +020050 }
51
52 return m, nil
53}
54
55// MappingOptions contains the options for applying mappings
56type MappingOptions struct {
57 FoundryA string
58 LayerA string
59 FoundryB string
60 LayerB string
61 Direction Direction
62}
63
64// ApplyMappings applies the specified mapping rules to a JSON object
Akrond5850f82025-05-23 16:44:44 +020065func (m *Mapper) ApplyMappings(mappingID string, opts MappingOptions, jsonData any) (any, error) {
Akron32d53de2025-05-22 13:45:32 +020066 // Validate mapping ID
67 if _, exists := m.mappingLists[mappingID]; !exists {
68 return nil, fmt.Errorf("mapping list with ID %s not found", mappingID)
69 }
70
71 // Validate direction
72 if opts.Direction != AtoB && opts.Direction != BtoA {
73 return nil, fmt.Errorf("invalid direction: %s", opts.Direction)
74 }
75
76 // Get the parsed rules
77 rules := m.parsedRules[mappingID]
78
79 // Convert input JSON to AST
80 jsonBytes, err := json.Marshal(jsonData)
81 if err != nil {
82 return nil, fmt.Errorf("failed to marshal input JSON: %w", err)
83 }
84
85 node, err := parser.ParseJSON(jsonBytes)
86 if err != nil {
87 return nil, fmt.Errorf("failed to parse JSON into AST: %w", err)
88 }
89
Akrond5850f82025-05-23 16:44:44 +020090 // Store whether the input was a Token
91 isToken := false
92 var tokenWrap ast.Node
Akron32d53de2025-05-22 13:45:32 +020093 if token, ok := node.(*ast.Token); ok {
Akrond5850f82025-05-23 16:44:44 +020094 isToken = true
95 tokenWrap = token.Wrap
96 node = tokenWrap
Akron32d53de2025-05-22 13:45:32 +020097 }
98
99 // Apply each rule to the AST
100 for _, rule := range rules {
101 // Create pattern and replacement based on direction
102 var pattern, replacement ast.Node
103 if opts.Direction == AtoB {
104 pattern = rule.Upper
105 replacement = rule.Lower
106 } else {
107 pattern = rule.Lower
108 replacement = rule.Upper
109 }
110
111 // Extract the inner nodes from the pattern and replacement tokens
112 if token, ok := pattern.(*ast.Token); ok {
113 pattern = token.Wrap
114 }
115 if token, ok := replacement.(*ast.Token); ok {
116 replacement = token.Wrap
117 }
118
119 // Apply foundry and layer overrides
120 if opts.Direction == AtoB {
121 applyFoundryAndLayerOverrides(pattern, opts.FoundryA, opts.LayerA)
122 applyFoundryAndLayerOverrides(replacement, opts.FoundryB, opts.LayerB)
123 } else {
124 applyFoundryAndLayerOverrides(pattern, opts.FoundryB, opts.LayerB)
125 applyFoundryAndLayerOverrides(replacement, opts.FoundryA, opts.LayerA)
126 }
127
128 // Create matcher and apply replacement
Akrond5850f82025-05-23 16:44:44 +0200129 m, err := matcher.NewMatcher(ast.Pattern{Root: pattern}, ast.Replacement{Root: replacement})
130 if err != nil {
131 return nil, fmt.Errorf("failed to create matcher: %w", err)
132 }
Akron32d53de2025-05-22 13:45:32 +0200133 node = m.Replace(node)
134 }
135
Akrond5850f82025-05-23 16:44:44 +0200136 // Wrap the result in a token if the input was a token
137 var result ast.Node
138 if isToken {
139 result = &ast.Token{Wrap: node}
140 } else {
141 result = node
142 }
Akron32d53de2025-05-22 13:45:32 +0200143
144 // Convert AST back to JSON
145 resultBytes, err := parser.SerializeToJSON(result)
146 if err != nil {
147 return nil, fmt.Errorf("failed to serialize AST to JSON: %w", err)
148 }
149
150 // Parse the JSON string back into an interface{}
151 var resultData interface{}
152 if err := json.Unmarshal(resultBytes, &resultData); err != nil {
153 return nil, fmt.Errorf("failed to parse result JSON: %w", err)
154 }
155
156 return resultData, nil
157}
158
159// applyFoundryAndLayerOverrides recursively applies foundry and layer overrides to terms
160func applyFoundryAndLayerOverrides(node ast.Node, foundry, layer string) {
161 if node == nil {
162 return
163 }
164
165 switch n := node.(type) {
166 case *ast.Term:
167 if foundry != "" {
168 n.Foundry = foundry
169 }
170 if layer != "" {
171 n.Layer = layer
172 }
173 case *ast.TermGroup:
174 for _, op := range n.Operands {
175 applyFoundryAndLayerOverrides(op, foundry, layer)
176 }
177 case *ast.Token:
178 if n.Wrap != nil {
179 applyFoundryAndLayerOverrides(n.Wrap, foundry, layer)
180 }
181 case *ast.CatchallNode:
182 if n.Wrap != nil {
183 applyFoundryAndLayerOverrides(n.Wrap, foundry, layer)
184 }
185 for _, op := range n.Operands {
186 applyFoundryAndLayerOverrides(op, foundry, layer)
187 }
188 }
189}