blob: fa75bd6d6f442ec73e661c0743ae568faad31bb2 [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)
Akrona1a183f2025-05-26 17:47:33 +020014type Direction bool
Akron32d53de2025-05-22 13:45:32 +020015
16const (
Akrona1a183f2025-05-26 17:47:33 +020017 AtoB Direction = true
18 BtoA Direction = false
Akron32d53de2025-05-22 13:45:32 +020019)
20
Akrona1a183f2025-05-26 17:47:33 +020021// String converts the Direction to its string representation
22func (d Direction) String() string {
23 if d {
24 return "atob"
25 }
26 return "btoa"
27}
28
29// ParseDirection converts a string direction to Direction type
30func ParseDirection(dir string) (Direction, error) {
31 switch dir {
32 case "atob":
33 return AtoB, nil
34 case "btoa":
35 return BtoA, nil
36 default:
37 return false, fmt.Errorf("invalid direction: %s", dir)
38 }
39}
40
Akron32d53de2025-05-22 13:45:32 +020041// Mapper handles the application of mapping rules to JSON objects
42type Mapper struct {
43 mappingLists map[string]*config.MappingList
44 parsedRules map[string][]*parser.MappingResult
45}
46
Akrona00d4752025-05-26 17:34:36 +020047// NewMapper creates a new Mapper instance from a list of MappingLists
48func NewMapper(lists []config.MappingList) (*Mapper, error) {
Akron32d53de2025-05-22 13:45:32 +020049 m := &Mapper{
50 mappingLists: make(map[string]*config.MappingList),
51 parsedRules: make(map[string][]*parser.MappingResult),
52 }
53
Akrona00d4752025-05-26 17:34:36 +020054 // Store mapping lists by ID
55 for _, list := range lists {
56 if _, exists := m.mappingLists[list.ID]; exists {
57 return nil, fmt.Errorf("duplicate mapping list ID found: %s", list.ID)
58 }
59
60 // Create a copy of the list to store
61 listCopy := list
62 m.mappingLists[list.ID] = &listCopy
63
64 // Parse the rules immediately
65 parsedRules, err := list.ParseMappings()
Akron32d53de2025-05-22 13:45:32 +020066 if err != nil {
Akrona00d4752025-05-26 17:34:36 +020067 return nil, fmt.Errorf("failed to parse mappings for list %s: %w", list.ID, err)
Akron32d53de2025-05-22 13:45:32 +020068 }
Akrona00d4752025-05-26 17:34:36 +020069 m.parsedRules[list.ID] = parsedRules
Akron32d53de2025-05-22 13:45:32 +020070 }
71
72 return m, nil
73}
74
75// MappingOptions contains the options for applying mappings
76type MappingOptions struct {
77 FoundryA string
78 LayerA string
79 FoundryB string
80 LayerB string
81 Direction Direction
82}
83
Akron7b4984e2025-05-26 19:12:20 +020084// ApplyQueryMappings applies the specified mapping rules to a JSON object
85func (m *Mapper) ApplyQueryMappings(mappingID string, opts MappingOptions, jsonData any) (any, error) {
Akron32d53de2025-05-22 13:45:32 +020086 // Validate mapping ID
87 if _, exists := m.mappingLists[mappingID]; !exists {
88 return nil, fmt.Errorf("mapping list with ID %s not found", mappingID)
89 }
90
Akron32d53de2025-05-22 13:45:32 +020091 // Get the parsed rules
92 rules := m.parsedRules[mappingID]
93
Akron7b4984e2025-05-26 19:12:20 +020094 // Check if we have a wrapper object with a "query" field
95 var queryData any
96 var hasQueryWrapper bool
97
98 if jsonMap, ok := jsonData.(map[string]any); ok {
99 if query, exists := jsonMap["query"]; exists {
100 queryData = query
101 hasQueryWrapper = true
102 }
103 }
104
105 // If no query wrapper was found, use the entire input
106 if !hasQueryWrapper {
107 // If the input itself is not a valid query object, return it as is
108 if !isValidQueryObject(jsonData) {
109 return jsonData, nil
110 }
111 queryData = jsonData
112 } else if queryData == nil || !isValidQueryObject(queryData) {
113 // If we have a query wrapper but the query is nil or not a valid object,
114 // return the original data
115 return jsonData, nil
116 }
117
Akroncc83eb52025-05-27 14:39:12 +0200118 // Store rewrites if they exist
119 var oldRewrites any
120 if queryMap, ok := queryData.(map[string]any); ok {
121 if rewrites, exists := queryMap["rewrites"]; exists {
122 oldRewrites = rewrites
123 delete(queryMap, "rewrites")
124 }
125 }
126
Akron32d53de2025-05-22 13:45:32 +0200127 // Convert input JSON to AST
Akron7b4984e2025-05-26 19:12:20 +0200128 jsonBytes, err := json.Marshal(queryData)
Akron32d53de2025-05-22 13:45:32 +0200129 if err != nil {
130 return nil, fmt.Errorf("failed to marshal input JSON: %w", err)
131 }
132
133 node, err := parser.ParseJSON(jsonBytes)
134 if err != nil {
135 return nil, fmt.Errorf("failed to parse JSON into AST: %w", err)
136 }
137
Akrond5850f82025-05-23 16:44:44 +0200138 // Store whether the input was a Token
139 isToken := false
140 var tokenWrap ast.Node
Akron32d53de2025-05-22 13:45:32 +0200141 if token, ok := node.(*ast.Token); ok {
Akrond5850f82025-05-23 16:44:44 +0200142 isToken = true
143 tokenWrap = token.Wrap
144 node = tokenWrap
Akron32d53de2025-05-22 13:45:32 +0200145 }
146
147 // Apply each rule to the AST
148 for _, rule := range rules {
149 // Create pattern and replacement based on direction
150 var pattern, replacement ast.Node
Akrona1a183f2025-05-26 17:47:33 +0200151 if opts.Direction { // true means AtoB
Akron32d53de2025-05-22 13:45:32 +0200152 pattern = rule.Upper
153 replacement = rule.Lower
154 } else {
155 pattern = rule.Lower
156 replacement = rule.Upper
157 }
158
159 // Extract the inner nodes from the pattern and replacement tokens
160 if token, ok := pattern.(*ast.Token); ok {
161 pattern = token.Wrap
162 }
163 if token, ok := replacement.(*ast.Token); ok {
164 replacement = token.Wrap
165 }
166
167 // Apply foundry and layer overrides
Akrona1a183f2025-05-26 17:47:33 +0200168 if opts.Direction { // true means AtoB
Akron32d53de2025-05-22 13:45:32 +0200169 applyFoundryAndLayerOverrides(pattern, opts.FoundryA, opts.LayerA)
170 applyFoundryAndLayerOverrides(replacement, opts.FoundryB, opts.LayerB)
171 } else {
172 applyFoundryAndLayerOverrides(pattern, opts.FoundryB, opts.LayerB)
173 applyFoundryAndLayerOverrides(replacement, opts.FoundryA, opts.LayerA)
174 }
175
176 // Create matcher and apply replacement
Akrond5850f82025-05-23 16:44:44 +0200177 m, err := matcher.NewMatcher(ast.Pattern{Root: pattern}, ast.Replacement{Root: replacement})
178 if err != nil {
179 return nil, fmt.Errorf("failed to create matcher: %w", err)
180 }
Akron32d53de2025-05-22 13:45:32 +0200181 node = m.Replace(node)
182 }
183
Akrond5850f82025-05-23 16:44:44 +0200184 // Wrap the result in a token if the input was a token
185 var result ast.Node
186 if isToken {
187 result = &ast.Token{Wrap: node}
188 } else {
189 result = node
190 }
Akron32d53de2025-05-22 13:45:32 +0200191
192 // Convert AST back to JSON
193 resultBytes, err := parser.SerializeToJSON(result)
194 if err != nil {
195 return nil, fmt.Errorf("failed to serialize AST to JSON: %w", err)
196 }
197
Akron6f455152025-05-27 09:03:00 +0200198 // Parse the JSON string back into
199 var resultData any
Akron32d53de2025-05-22 13:45:32 +0200200 if err := json.Unmarshal(resultBytes, &resultData); err != nil {
201 return nil, fmt.Errorf("failed to parse result JSON: %w", err)
202 }
203
Akroncc83eb52025-05-27 14:39:12 +0200204 // Restore rewrites if they existed
205 if oldRewrites != nil {
206 if resultMap, ok := resultData.(map[string]any); ok {
207 resultMap["rewrites"] = oldRewrites
208 }
209 }
210
Akron7b4984e2025-05-26 19:12:20 +0200211 // If we had a query wrapper, put the transformed data back in it
212 if hasQueryWrapper {
213 if wrapper, ok := jsonData.(map[string]any); ok {
214 wrapper["query"] = resultData
215 return wrapper, nil
216 }
217 }
218
Akron32d53de2025-05-22 13:45:32 +0200219 return resultData, nil
220}
221
Akron7b4984e2025-05-26 19:12:20 +0200222// isValidQueryObject checks if the query data is a valid object that can be processed
223func isValidQueryObject(data any) bool {
224 // Check if it's a map
225 queryMap, ok := data.(map[string]any)
226 if !ok {
227 return false
228 }
229
230 // Check if it has the required @type field
231 if _, ok := queryMap["@type"]; !ok {
232 return false
233 }
234
235 return true
236}
237
Akron32d53de2025-05-22 13:45:32 +0200238// applyFoundryAndLayerOverrides recursively applies foundry and layer overrides to terms
239func applyFoundryAndLayerOverrides(node ast.Node, foundry, layer string) {
240 if node == nil {
241 return
242 }
243
244 switch n := node.(type) {
245 case *ast.Term:
246 if foundry != "" {
247 n.Foundry = foundry
248 }
249 if layer != "" {
250 n.Layer = layer
251 }
252 case *ast.TermGroup:
253 for _, op := range n.Operands {
254 applyFoundryAndLayerOverrides(op, foundry, layer)
255 }
256 case *ast.Token:
257 if n.Wrap != nil {
258 applyFoundryAndLayerOverrides(n.Wrap, foundry, layer)
259 }
260 case *ast.CatchallNode:
261 if n.Wrap != nil {
262 applyFoundryAndLayerOverrides(n.Wrap, foundry, layer)
263 }
264 for _, op := range n.Operands {
265 applyFoundryAndLayerOverrides(op, foundry, layer)
266 }
267 }
268}