blob: 2da55b89e447de0035b87fa8363bea98f6ab5df8 [file] [log] [blame]
Akronb7e1f352025-05-16 15:45:23 +02001package ast
2
Akron753f49a2025-05-26 16:53:18 +02003// ast is the abstract syntax tree for the query term mapper.
Akronbf5149c2025-05-20 15:53:41 +02004
Akron32958422025-05-16 16:33:05 +02005import (
6 "encoding/json"
7)
8
Akronb7e1f352025-05-16 15:45:23 +02009// NodeType represents the type of a node in the AST
10type NodeType string
11
Akronb7e1f352025-05-16 15:45:23 +020012// RelationType represents the type of relation between nodes
13type RelationType string
14
Akronb7e1f352025-05-16 15:45:23 +020015// MatchType represents the type of match operation
16type MatchType string
17
18const (
Akron753f49a2025-05-26 16:53:18 +020019 TokenNode NodeType = "token"
20 TermGroupNode NodeType = "termGroup"
21 TermNode NodeType = "term"
Akron1a5fccd2025-05-27 09:54:09 +020022 RewriteNode NodeType = "rewrite"
Akron753f49a2025-05-26 16:53:18 +020023 AndRelation RelationType = "and"
24 OrRelation RelationType = "or"
25 MatchEqual MatchType = "eq"
26 MatchNotEqual MatchType = "ne"
Akronb7e1f352025-05-16 15:45:23 +020027)
28
29// Node represents a node in the AST
30type Node interface {
31 Type() NodeType
Akron441bd122025-05-30 14:19:50 +020032 Clone() Node
Akronb7e1f352025-05-16 15:45:23 +020033}
34
Akron1a5fccd2025-05-27 09:54:09 +020035// Rewrite represents a koral:rewrite
36type Rewrite struct {
37 Editor string `json:"editor,omitempty"`
38 Operation string `json:"operation,omitempty"`
39 Scope string `json:"scope,omitempty"`
40 Src string `json:"src,omitempty"`
41 Comment string `json:"_comment,omitempty"`
Akron8f1970f2025-05-30 12:52:03 +020042 Original any `json:"original,omitempty"`
43}
44
45// UnmarshalJSON implements custom JSON unmarshaling for backward compatibility
46func (r *Rewrite) UnmarshalJSON(data []byte) error {
47 // Create a temporary struct to hold all possible fields
48 var temp struct {
49 Type string `json:"@type,omitempty"`
50 Editor string `json:"editor,omitempty"`
51 Source string `json:"source,omitempty"` // legacy field
52 Operation string `json:"operation,omitempty"` // legacy field
53 Scope string `json:"scope,omitempty"`
54 Src string `json:"src,omitempty"`
55 Origin string `json:"origin,omitempty"` // legacy field
56 Original any `json:"original,omitempty"`
57 Comment string `json:"_comment,omitempty"`
58 }
59
60 if err := json.Unmarshal(data, &temp); err != nil {
61 return err
62 }
63
64 // Apply precedence for editor field: editor >> source
65 if temp.Editor != "" {
66 r.Editor = temp.Editor
67 } else if temp.Source != "" {
68 r.Editor = temp.Source
69 }
70
71 // Apply precedence for original/src/origin: original >> src >> origin
72 if temp.Original != nil {
73 r.Original = temp.Original
74 } else if temp.Src != "" {
75 r.Src = temp.Src
76 } else if temp.Origin != "" {
77 r.Src = temp.Origin
78 }
79
80 // Copy other fields
81 r.Operation = temp.Operation
82 r.Scope = temp.Scope
83 r.Comment = temp.Comment
84
85 return nil
Akron1a5fccd2025-05-27 09:54:09 +020086}
87
88func (r *Rewrite) Type() NodeType {
89 return RewriteNode
90}
91
Akron441bd122025-05-30 14:19:50 +020092// Clone creates a deep copy of the Rewrite node
93func (r *Rewrite) Clone() Node {
94 return &Rewrite{
95 Editor: r.Editor,
96 Operation: r.Operation,
97 Scope: r.Scope,
98 Src: r.Src,
99 Comment: r.Comment,
100 Original: r.Original, // Note: this is a shallow copy of the Original field
101 }
102}
103
Akron8f1970f2025-05-30 12:52:03 +0200104// MarshalJSON implements custom JSON marshaling to ensure clean output
105func (r *Rewrite) MarshalJSON() ([]byte, error) {
106 // Create a map with only the modern field names
107 result := make(map[string]any)
108
109 // Always include @type if this is a rewrite
110 result["@type"] = "koral:rewrite"
111
112 if r.Editor != "" {
113 result["editor"] = r.Editor
114 }
115 if r.Operation != "" {
116 result["operation"] = r.Operation
117 }
118 if r.Scope != "" {
119 result["scope"] = r.Scope
120 }
121 if r.Src != "" {
122 result["src"] = r.Src
123 }
124 if r.Comment != "" {
125 result["_comment"] = r.Comment
126 }
127 if r.Original != nil {
128 result["original"] = r.Original
129 }
130
131 return json.Marshal(result)
132}
133
Akron753f49a2025-05-26 16:53:18 +0200134// Token represents a koral:token
Akronb7e1f352025-05-16 15:45:23 +0200135type Token struct {
Akron1a5fccd2025-05-27 09:54:09 +0200136 Wrap Node `json:"wrap"`
137 Rewrites []Rewrite `json:"rewrites,omitempty"`
Akronb7e1f352025-05-16 15:45:23 +0200138}
139
140func (t *Token) Type() NodeType {
141 return TokenNode
142}
143
Akron441bd122025-05-30 14:19:50 +0200144// Clone creates a deep copy of the Token node
145func (t *Token) Clone() Node {
146 var clonedWrap Node
147 if t.Wrap != nil {
148 clonedWrap = t.Wrap.Clone()
149 }
150 tc := &Token{
151 Wrap: clonedWrap,
152 }
153
154 if t.Rewrites != nil {
155 clonedRewrites := make([]Rewrite, len(t.Rewrites))
156 for i, rewrite := range t.Rewrites {
157 clonedRewrites[i] = *rewrite.Clone().(*Rewrite)
158 }
159 tc.Rewrites = clonedRewrites
160 }
161
162 return tc
163}
164
Akron753f49a2025-05-26 16:53:18 +0200165// TermGroup represents a koral:termGroup
Akronb7e1f352025-05-16 15:45:23 +0200166type TermGroup struct {
167 Operands []Node `json:"operands"`
168 Relation RelationType `json:"relation"`
Akron1a5fccd2025-05-27 09:54:09 +0200169 Rewrites []Rewrite `json:"rewrites,omitempty"`
Akronb7e1f352025-05-16 15:45:23 +0200170}
171
172func (tg *TermGroup) Type() NodeType {
173 return TermGroupNode
174}
175
Akron441bd122025-05-30 14:19:50 +0200176// Clone creates a deep copy of the TermGroup node
177func (tg *TermGroup) Clone() Node {
178 clonedOperands := make([]Node, len(tg.Operands))
179 for i, operand := range tg.Operands {
180 clonedOperands[i] = operand.Clone()
181 }
182 tgc := &TermGroup{
183 Operands: clonedOperands,
184 Relation: tg.Relation,
185 }
186 if tg.Rewrites != nil {
187 clonedRewrites := make([]Rewrite, len(tg.Rewrites))
188 for i, rewrite := range tg.Rewrites {
189 clonedRewrites[i] = *rewrite.Clone().(*Rewrite)
190 }
191 tgc.Rewrites = clonedRewrites
192 }
193
194 return tgc
195}
196
Akron753f49a2025-05-26 16:53:18 +0200197// Term represents a koral:term
Akronb7e1f352025-05-16 15:45:23 +0200198type Term struct {
Akron1a5fccd2025-05-27 09:54:09 +0200199 Foundry string `json:"foundry"`
200 Key string `json:"key"`
201 Layer string `json:"layer"`
202 Match MatchType `json:"match"`
203 Value string `json:"value,omitempty"`
204 Rewrites []Rewrite `json:"rewrites,omitempty"`
Akronb7e1f352025-05-16 15:45:23 +0200205}
206
207func (t *Term) Type() NodeType {
208 return TermNode
209}
210
Akron441bd122025-05-30 14:19:50 +0200211// Clone creates a deep copy of the Term node
212func (t *Term) Clone() Node {
213
214 tc := &Term{
215 Foundry: t.Foundry,
216 Key: t.Key,
217 Layer: t.Layer,
218 Match: t.Match,
219 Value: t.Value,
220 }
221
222 if t.Rewrites != nil {
223 clonedRewrites := make([]Rewrite, len(t.Rewrites))
224 for i, rewrite := range t.Rewrites {
225 clonedRewrites[i] = *rewrite.Clone().(*Rewrite)
226 }
227 tc.Rewrites = clonedRewrites
228 }
229 return tc
230}
231
Akronb7e1f352025-05-16 15:45:23 +0200232// Pattern represents a pattern to match in the AST
233type Pattern struct {
234 Root Node
235}
236
237// Replacement represents a replacement pattern
238type Replacement struct {
239 Root Node
240}
Akron32958422025-05-16 16:33:05 +0200241
242// CatchallNode represents any node type not explicitly handled
243type CatchallNode struct {
244 NodeType string // The original @type value
245 RawContent json.RawMessage // The original JSON content
246 Wrap Node // Optional wrapped node
247 Operands []Node // Optional operands
248}
249
250func (c *CatchallNode) Type() NodeType {
251 return NodeType(c.NodeType)
252}
Akron441bd122025-05-30 14:19:50 +0200253
254// Clone creates a deep copy of the CatchallNode
255func (c *CatchallNode) Clone() Node {
256 newNode := &CatchallNode{
257 NodeType: c.NodeType,
258 }
259
260 // Handle RawContent properly - preserve nil if it's nil
261 if c.RawContent != nil {
262 newNode.RawContent = make(json.RawMessage, len(c.RawContent))
263 copy(newNode.RawContent, c.RawContent)
264 }
265
266 if c.Wrap != nil {
267 newNode.Wrap = c.Wrap.Clone()
268 }
269
270 if len(c.Operands) > 0 {
271 newNode.Operands = make([]Node, len(c.Operands))
272 for i, operand := range c.Operands {
273 newNode.Operands[i] = operand.Clone()
274 }
275 }
276
277 return newNode
278}
279
280// ApplyFoundryAndLayerOverrides recursively applies foundry and layer overrides to terms
281func ApplyFoundryAndLayerOverrides(node Node, foundry, layer string) {
282 if node == nil {
283 return
284 }
285
286 switch n := node.(type) {
287 case *Term:
288 if foundry != "" {
289 n.Foundry = foundry
290 }
291 if layer != "" {
292 n.Layer = layer
293 }
294 case *TermGroup:
295 for _, op := range n.Operands {
296 ApplyFoundryAndLayerOverrides(op, foundry, layer)
297 }
298 case *Token:
299 if n.Wrap != nil {
300 ApplyFoundryAndLayerOverrides(n.Wrap, foundry, layer)
301 }
302 case *CatchallNode:
303 if n.Wrap != nil {
304 ApplyFoundryAndLayerOverrides(n.Wrap, foundry, layer)
305 }
306 for _, op := range n.Operands {
307 ApplyFoundryAndLayerOverrides(op, foundry, layer)
308 }
309 }
310}
Akron7c91cde2025-06-24 17:11:22 +0200311
312// RestrictToObligatory takes a replacement node from a mapping rule and reduces the boolean structure
313// to only obligatory operations by removing optional OR-relations and keeping required AND-relations.
314// It also applies foundry and layer overrides like ApplyFoundryAndLayerOverrides().
315// Note: This function is designed for mapping rule replacement nodes and does not handle CatchallNodes.
316// For efficiency, restriction is performed first, then foundry/layer overrides are applied to the smaller result.
317//
318// Examples:
319// - (a & b & c) -> (a & b & c) (kept as is)
320// - (a & b & (c | d) & e) -> (a & b & e) (OR-relation removed)
321// - (a | b) -> nil (completely optional)
322func RestrictToObligatory(node Node, foundry, layer string) Node {
323 if node == nil {
324 return nil
325 }
326
327 // First, clone and restrict to obligatory operations
328 cloned := node.Clone()
329 restricted := restrictToObligatoryRecursive(cloned)
330
331 // Then apply foundry and layer overrides to the smaller, restricted tree
332 if restricted != nil {
333 ApplyFoundryAndLayerOverrides(restricted, foundry, layer)
334 }
335
336 return restricted
337}
338
339// restrictToObligatoryRecursive performs the actual restriction logic
340func restrictToObligatoryRecursive(node Node) Node {
341 if node == nil {
342 return nil
343 }
344
345 switch n := node.(type) {
346 case *Term:
347 // Terms are always obligatory
348 return n
349
350 case *Token:
351 // Process the wrapped node
352 if n.Wrap != nil {
353 restricted := restrictToObligatoryRecursive(n.Wrap)
354 if restricted == nil {
355 return nil
356 }
357 return &Token{
358 Wrap: restricted,
359 Rewrites: n.Rewrites,
360 }
361 }
362 return n
363
364 case *TermGroup:
365 if n.Relation == OrRelation {
366 // OR-relations are optional, so remove them
367 return nil
368 } else if n.Relation == AndRelation {
369 // AND-relations are obligatory, but we need to process operands
370 var obligatoryOperands []Node
371 for _, operand := range n.Operands {
372 restricted := restrictToObligatoryRecursive(operand)
373 if restricted != nil {
374 obligatoryOperands = append(obligatoryOperands, restricted)
375 }
376 }
377
378 // If no operands remain, return nil
379 if len(obligatoryOperands) == 0 {
380 return nil
381 }
382
383 // If only one operand remains, return it directly
384 if len(obligatoryOperands) == 1 {
385 return obligatoryOperands[0]
386 }
387
388 // Return the group with obligatory operands
389 return &TermGroup{
390 Operands: obligatoryOperands,
391 Relation: AndRelation,
392 Rewrites: n.Rewrites,
393 }
394 }
395 }
396
397 // For unknown node types, return as is
398 return node
399}