blob: 25b2ad1d11bcf9797dd6641458c25181762c85a3 [file] [log] [blame]
Akronb7e1f352025-05-16 15:45:23 +02001package parser
2
3import (
4 "encoding/json"
5 "testing"
6
Akronfa55bb22025-05-26 15:10:42 +02007 "github.com/KorAP/KoralPipe-TermMapper/ast"
Akronb7e1f352025-05-16 15:45:23 +02008 "github.com/stretchr/testify/assert"
9 "github.com/stretchr/testify/require"
10)
11
Akron56e09e72025-05-22 15:38:35 +020012// normalizeJSON normalizes JSON by parsing and re-marshaling it
13func normalizeJSON(t *testing.T, data json.RawMessage) json.RawMessage {
14 var v interface{}
15 err := json.Unmarshal(data, &v)
16 require.NoError(t, err)
17
18 // Convert to canonical form (sorted keys, no whitespace)
19 normalized, err := json.Marshal(v)
20 require.NoError(t, err)
21 return normalized
22}
23
Akron56e09e72025-05-22 15:38:35 +020024// compareNodes compares two AST nodes, normalizing JSON content in CatchallNodes
25func compareNodes(t *testing.T, expected, actual ast.Node) bool {
26 // If both nodes are CatchallNodes, normalize their JSON content before comparison
27 if expectedCatchall, ok := expected.(*ast.CatchallNode); ok {
28 if actualCatchall, ok := actual.(*ast.CatchallNode); ok {
29 // Compare NodeType
30 if !assert.Equal(t, expectedCatchall.NodeType, actualCatchall.NodeType) {
31 t.Logf("NodeType mismatch: expected '%s', got '%s'", expectedCatchall.NodeType, actualCatchall.NodeType)
32 return false
33 }
34
35 // Normalize and compare RawContent
36 if expectedCatchall.RawContent != nil && actualCatchall.RawContent != nil {
37 expectedNorm := normalizeJSON(t, expectedCatchall.RawContent)
38 actualNorm := normalizeJSON(t, actualCatchall.RawContent)
39 if !assert.Equal(t, string(expectedNorm), string(actualNorm)) {
40 t.Logf("RawContent mismatch:\nExpected: %s\nActual: %s", expectedNorm, actualNorm)
41 return false
42 }
43 } else if !assert.Equal(t, expectedCatchall.RawContent == nil, actualCatchall.RawContent == nil) {
44 t.Log("One node has RawContent while the other doesn't")
45 return false
46 }
47
48 // Compare Operands
49 if !assert.Equal(t, len(expectedCatchall.Operands), len(actualCatchall.Operands)) {
50 t.Logf("Operands length mismatch: expected %d, got %d", len(expectedCatchall.Operands), len(actualCatchall.Operands))
51 return false
52 }
53 for i := range expectedCatchall.Operands {
54 if !compareNodes(t, expectedCatchall.Operands[i], actualCatchall.Operands[i]) {
55 t.Logf("Operand %d mismatch", i)
56 return false
57 }
58 }
59
60 // Compare Wrap
61 if expectedCatchall.Wrap != nil || actualCatchall.Wrap != nil {
62 if !assert.Equal(t, expectedCatchall.Wrap != nil, actualCatchall.Wrap != nil) {
63 t.Log("One node has Wrap while the other doesn't")
64 return false
65 }
66 if expectedCatchall.Wrap != nil {
67 if !compareNodes(t, expectedCatchall.Wrap, actualCatchall.Wrap) {
68 t.Log("Wrap node mismatch")
69 return false
70 }
71 }
72 }
73
74 return true
75 }
76 }
77
78 // For Token nodes, compare their Wrap fields using compareNodes
79 if expectedToken, ok := expected.(*ast.Token); ok {
80 if actualToken, ok := actual.(*ast.Token); ok {
81 if expectedToken.Wrap == nil || actualToken.Wrap == nil {
82 return assert.Equal(t, expectedToken.Wrap == nil, actualToken.Wrap == nil)
83 }
84 return compareNodes(t, expectedToken.Wrap, actualToken.Wrap)
85 }
86 }
87
88 // For TermGroup nodes, compare relation and operands
89 if expectedGroup, ok := expected.(*ast.TermGroup); ok {
90 if actualGroup, ok := actual.(*ast.TermGroup); ok {
91 if !assert.Equal(t, expectedGroup.Relation, actualGroup.Relation) {
92 t.Logf("Relation mismatch: expected '%s', got '%s'", expectedGroup.Relation, actualGroup.Relation)
93 return false
94 }
95 if !assert.Equal(t, len(expectedGroup.Operands), len(actualGroup.Operands)) {
96 t.Logf("Operands length mismatch: expected %d, got %d", len(expectedGroup.Operands), len(actualGroup.Operands))
97 return false
98 }
99 for i := range expectedGroup.Operands {
100 if !compareNodes(t, expectedGroup.Operands[i], actualGroup.Operands[i]) {
101 t.Logf("Operand %d mismatch", i)
102 return false
103 }
104 }
105 return true
106 }
107 }
108
109 // For Term nodes, compare all fields
110 if expectedTerm, ok := expected.(*ast.Term); ok {
111 if actualTerm, ok := actual.(*ast.Term); ok {
112 equal := assert.Equal(t, expectedTerm.Foundry, actualTerm.Foundry) &&
113 assert.Equal(t, expectedTerm.Key, actualTerm.Key) &&
114 assert.Equal(t, expectedTerm.Layer, actualTerm.Layer) &&
115 assert.Equal(t, expectedTerm.Match, actualTerm.Match) &&
116 assert.Equal(t, expectedTerm.Value, actualTerm.Value)
117 if !equal {
118 t.Logf("Term mismatch:\nExpected: %+v\nActual: %+v", expectedTerm, actualTerm)
119 }
120 return equal
121 }
122 }
123
124 // For other node types or mismatched types, use regular equality comparison
125 equal := assert.Equal(t, expected, actual)
126 if !equal {
127 t.Logf("Node type mismatch:\nExpected type: %T\nActual type: %T", expected, actual)
128 }
129 return equal
130}
131
Akronb7e1f352025-05-16 15:45:23 +0200132func TestParseJSON(t *testing.T) {
133 tests := []struct {
134 name string
135 input string
136 expected ast.Node
137 wantErr bool
138 }{
139 {
140 name: "Parse simple term",
141 input: `{
142 "@type": "koral:term",
143 "foundry": "opennlp",
144 "key": "DET",
145 "layer": "p",
146 "match": "match:eq"
147 }`,
148 expected: &ast.Term{
149 Foundry: "opennlp",
150 Key: "DET",
151 Layer: "p",
152 Match: ast.MatchEqual,
153 },
154 wantErr: false,
155 },
156 {
157 name: "Parse term group with AND relation",
158 input: `{
159 "@type": "koral:termGroup",
160 "operands": [
161 {
162 "@type": "koral:term",
163 "foundry": "opennlp",
164 "key": "DET",
165 "layer": "p",
166 "match": "match:eq"
167 },
168 {
169 "@type": "koral:term",
170 "foundry": "opennlp",
171 "key": "AdjType",
172 "layer": "m",
173 "match": "match:eq",
174 "value": "Pdt"
175 }
176 ],
177 "relation": "relation:and"
178 }`,
179 expected: &ast.TermGroup{
180 Operands: []ast.Node{
181 &ast.Term{
182 Foundry: "opennlp",
183 Key: "DET",
184 Layer: "p",
185 Match: ast.MatchEqual,
186 },
187 &ast.Term{
188 Foundry: "opennlp",
189 Key: "AdjType",
190 Layer: "m",
191 Match: ast.MatchEqual,
192 Value: "Pdt",
193 },
194 },
195 Relation: ast.AndRelation,
196 },
197 wantErr: false,
198 },
199 {
200 name: "Parse token with wrapped term",
201 input: `{
202 "@type": "koral:token",
203 "wrap": {
204 "@type": "koral:term",
205 "foundry": "opennlp",
206 "key": "DET",
207 "layer": "p",
208 "match": "match:eq"
209 }
210 }`,
211 expected: &ast.Token{
212 Wrap: &ast.Term{
213 Foundry: "opennlp",
214 Key: "DET",
215 Layer: "p",
216 Match: ast.MatchEqual,
217 },
218 },
219 wantErr: false,
220 },
221 {
222 name: "Parse complex nested structure",
223 input: `{
224 "@type": "koral:token",
225 "wrap": {
226 "@type": "koral:termGroup",
227 "operands": [
228 {
229 "@type": "koral:term",
230 "foundry": "opennlp",
231 "key": "DET",
232 "layer": "p",
233 "match": "match:eq"
234 },
235 {
236 "@type": "koral:termGroup",
237 "operands": [
238 {
239 "@type": "koral:term",
240 "foundry": "opennlp",
241 "key": "AdjType",
242 "layer": "m",
243 "match": "match:eq",
244 "value": "Pdt"
245 },
246 {
247 "@type": "koral:term",
248 "foundry": "opennlp",
249 "key": "PronType",
250 "layer": "m",
251 "match": "match:ne",
252 "value": "Neg"
253 }
254 ],
255 "relation": "relation:or"
256 }
257 ],
258 "relation": "relation:and"
259 }
260 }`,
261 expected: &ast.Token{
262 Wrap: &ast.TermGroup{
263 Operands: []ast.Node{
264 &ast.Term{
265 Foundry: "opennlp",
266 Key: "DET",
267 Layer: "p",
268 Match: ast.MatchEqual,
269 },
270 &ast.TermGroup{
271 Operands: []ast.Node{
272 &ast.Term{
273 Foundry: "opennlp",
274 Key: "AdjType",
275 Layer: "m",
276 Match: ast.MatchEqual,
277 Value: "Pdt",
278 },
279 &ast.Term{
280 Foundry: "opennlp",
281 Key: "PronType",
282 Layer: "m",
283 Match: ast.MatchNotEqual,
284 Value: "Neg",
285 },
286 },
287 Relation: ast.OrRelation,
288 },
289 },
290 Relation: ast.AndRelation,
291 },
292 },
293 wantErr: false,
294 },
295 {
Akron1a5fccd2025-05-27 09:54:09 +0200296 name: "Parse token with term and rewrites",
297 input: `{
298 "@type": "koral:token",
299 "wrap": {
300 "@type": "koral:term",
301 "foundry": "opennlp",
302 "key": "Baum",
303 "layer": "orth",
304 "match": "match:eq",
305 "rewrites": [
306 {
307 "@type": "koral:rewrite",
308 "_comment": "Default foundry has been added.",
309 "editor": "Kustvakt",
310 "operation": "operation:injection",
311 "scope": "foundry",
312 "src": "Kustvakt"
313 }
314 ]
315 }
316 }`,
317 expected: &ast.Token{
318 Wrap: &ast.Term{
319 Foundry: "opennlp",
320 Key: "Baum",
321 Layer: "orth",
322 Match: ast.MatchEqual,
323 Rewrites: []ast.Rewrite{
324 {
325 Comment: "Default foundry has been added.",
326 Editor: "Kustvakt",
327 Operation: "operation:injection",
328 Scope: "foundry",
329 Src: "Kustvakt",
330 },
331 },
332 },
333 },
334 wantErr: false,
335 },
336 {
337 name: "Parse term group with rewrites",
338 input: `{
339 "@type": "koral:termGroup",
340 "operands": [
341 {
342 "@type": "koral:term",
343 "foundry": "opennlp",
344 "key": "DET",
345 "layer": "p",
346 "match": "match:eq"
347 },
348 {
349 "@type": "koral:term",
350 "foundry": "opennlp",
351 "key": "AdjType",
352 "layer": "m",
353 "match": "match:eq",
354 "value": "Pdt"
355 }
356 ],
357 "relation": "relation:and",
358 "rewrites": [
359 {
360 "@type": "koral:rewrite",
361 "_comment": "Default foundry has been added.",
362 "editor": "Kustvakt",
363 "operation": "operation:injection",
364 "scope": "foundry",
365 "src": "Kustvakt"
366 }
367 ]
368 }`,
369 expected: &ast.TermGroup{
370 Operands: []ast.Node{
371 &ast.Term{
372 Foundry: "opennlp",
373 Key: "DET",
374 Layer: "p",
375 Match: ast.MatchEqual,
376 },
377 &ast.Term{
378 Foundry: "opennlp",
379 Key: "AdjType",
380 Layer: "m",
381 Match: ast.MatchEqual,
382 Value: "Pdt",
383 },
384 },
385 Relation: ast.AndRelation,
386 Rewrites: []ast.Rewrite{
387 {
388 Comment: "Default foundry has been added.",
389 Editor: "Kustvakt",
390 Operation: "operation:injection",
391 Scope: "foundry",
392 Src: "Kustvakt",
393 },
394 },
395 },
396 wantErr: false,
397 },
398 {
399 name: "Parse term with rewrites",
400 input: `{
401 "@type": "koral:term",
402 "foundry": "opennlp",
403 "key": "DET",
404 "layer": "p",
405 "match": "match:eq",
406 "rewrites": [
407 {
408 "@type": "koral:rewrite",
409 "_comment": "Default foundry has been added.",
410 "editor": "Kustvakt",
411 "operation": "operation:injection",
412 "scope": "foundry",
413 "src": "Kustvakt"
414 }
415 ]
416 }`,
417 expected: &ast.Term{
418 Foundry: "opennlp",
419 Key: "DET",
420 Layer: "p",
421 Match: ast.MatchEqual,
422 Rewrites: []ast.Rewrite{
423 {
424 Comment: "Default foundry has been added.",
425 Editor: "Kustvakt",
426 Operation: "operation:injection",
427 Scope: "foundry",
428 Src: "Kustvakt",
429 },
430 },
431 },
432 wantErr: false,
433 },
434 {
Akronb7e1f352025-05-16 15:45:23 +0200435 name: "Invalid JSON",
436 input: `{"invalid": json`,
437 wantErr: true,
438 },
439 {
440 name: "Empty JSON",
441 input: `{}`,
442 wantErr: true,
443 },
444 {
Akron32958422025-05-16 16:33:05 +0200445 name: "Unknown node type",
Akronb7e1f352025-05-16 15:45:23 +0200446 input: `{
447 "@type": "koral:unknown",
448 "key": "value"
449 }`,
Akron32958422025-05-16 16:33:05 +0200450 expected: &ast.CatchallNode{
451 NodeType: "koral:unknown",
452 RawContent: json.RawMessage(`{"@type":"koral:unknown","key":"value"}`),
453 },
454 wantErr: false,
Akronb7e1f352025-05-16 15:45:23 +0200455 },
456 }
457
458 for _, tt := range tests {
459 t.Run(tt.name, func(t *testing.T) {
460 result, err := ParseJSON([]byte(tt.input))
461 if tt.wantErr {
462 assert.Error(t, err)
463 return
464 }
465
466 require.NoError(t, err)
467 assert.Equal(t, tt.expected, result)
468 })
469 }
470}
471
Akron1a5fccd2025-05-27 09:54:09 +0200472func TestParseJSONErrors(t *testing.T) {
473 tests := []struct {
474 name string
475 input string
476 wantErr bool
477 }{
478 {
479 name: "Empty JSON",
480 input: "{}",
481 wantErr: true,
482 },
483 {
484 name: "Invalid JSON",
485 input: "{",
486 wantErr: true,
487 },
488 {
489 name: "Token without wrap",
490 input: `{
491 "@type": "koral:token"
492 }`,
493 wantErr: true,
494 },
495 {
496 name: "Term without key",
497 input: `{
498 "@type": "koral:term",
499 "foundry": "opennlp",
500 "layer": "p",
501 "match": "match:eq"
502 }`,
503 wantErr: true,
504 },
505 {
506 name: "TermGroup without operands",
507 input: `{
508 "@type": "koral:termGroup",
509 "relation": "relation:and"
510 }`,
511 wantErr: true,
512 },
513 {
514 name: "TermGroup without relation",
515 input: `{
516 "@type": "koral:termGroup",
517 "operands": [
518 {
519 "@type": "koral:term",
520 "key": "DET",
521 "foundry": "opennlp",
522 "layer": "p",
523 "match": "match:eq"
524 }
525 ]
526 }`,
527 wantErr: true,
528 },
529 {
530 name: "Invalid match type",
531 input: `{
532 "@type": "koral:term",
533 "key": "DET",
534 "foundry": "opennlp",
535 "layer": "p",
536 "match": "match:invalid"
537 }`,
538 wantErr: true,
539 },
540 {
541 name: "Invalid relation type",
542 input: `{
543 "@type": "koral:termGroup",
544 "operands": [
545 {
546 "@type": "koral:term",
547 "key": "DET",
548 "foundry": "opennlp",
549 "layer": "p",
550 "match": "match:eq"
551 }
552 ],
553 "relation": "relation:invalid"
554 }`,
555 wantErr: true,
556 },
557 }
558
559 for _, tt := range tests {
560 t.Run(tt.name, func(t *testing.T) {
561 _, err := ParseJSON([]byte(tt.input))
562 if tt.wantErr {
563 assert.Error(t, err)
564 } else {
565 assert.NoError(t, err)
566 }
567 })
568 }
569}
570
Akronb7e1f352025-05-16 15:45:23 +0200571func TestSerializeToJSON(t *testing.T) {
572 tests := []struct {
573 name string
574 input ast.Node
575 expected string
576 wantErr bool
577 }{
578 {
579 name: "Serialize simple term",
580 input: &ast.Term{
581 Foundry: "opennlp",
582 Key: "DET",
583 Layer: "p",
584 Match: ast.MatchEqual,
585 },
586 expected: `{
587 "@type": "koral:term",
588 "foundry": "opennlp",
589 "key": "DET",
590 "layer": "p",
591 "match": "match:eq"
592}`,
593 wantErr: false,
594 },
595 {
596 name: "Serialize term group",
597 input: &ast.TermGroup{
598 Operands: []ast.Node{
599 &ast.Term{
600 Foundry: "opennlp",
601 Key: "DET",
602 Layer: "p",
603 Match: ast.MatchEqual,
604 },
605 &ast.Term{
606 Foundry: "opennlp",
607 Key: "AdjType",
608 Layer: "m",
609 Match: ast.MatchEqual,
610 Value: "Pdt",
611 },
612 },
613 Relation: ast.AndRelation,
614 },
615 expected: `{
616 "@type": "koral:termGroup",
617 "operands": [
618 {
619 "@type": "koral:term",
620 "foundry": "opennlp",
621 "key": "DET",
622 "layer": "p",
623 "match": "match:eq"
624 },
625 {
626 "@type": "koral:term",
627 "foundry": "opennlp",
628 "key": "AdjType",
629 "layer": "m",
630 "match": "match:eq",
631 "value": "Pdt"
632 }
633 ],
634 "relation": "relation:and"
635}`,
636 wantErr: false,
637 },
Akron32958422025-05-16 16:33:05 +0200638 {
639 name: "Serialize unknown node type",
640 input: &ast.CatchallNode{
641 NodeType: "koral:unknown",
642 RawContent: json.RawMessage(`{
643 "@type": "koral:unknown",
644 "key": "value"
645}`),
646 },
647 expected: `{
648 "@type": "koral:unknown",
649 "key": "value"
650}`,
651 wantErr: false,
652 },
Akronb7e1f352025-05-16 15:45:23 +0200653 }
654
655 for _, tt := range tests {
656 t.Run(tt.name, func(t *testing.T) {
Akron87948e82025-05-26 18:19:51 +0200657 result, err := SerializeToJSON(tt.input)
Akronb7e1f352025-05-16 15:45:23 +0200658 if tt.wantErr {
659 assert.Error(t, err)
660 return
661 }
662
663 require.NoError(t, err)
664 // Compare JSON objects instead of raw strings to avoid whitespace issues
Akron56e09e72025-05-22 15:38:35 +0200665 var expected, actual any
Akronb7e1f352025-05-16 15:45:23 +0200666 err = json.Unmarshal([]byte(tt.expected), &expected)
667 require.NoError(t, err)
668 err = json.Unmarshal(result, &actual)
669 require.NoError(t, err)
670 assert.Equal(t, expected, actual)
671 })
672 }
673}
674
675func TestRoundTrip(t *testing.T) {
676 // Test that parsing and then serializing produces equivalent JSON
677 input := `{
678 "@type": "koral:token",
679 "wrap": {
680 "@type": "koral:termGroup",
681 "operands": [
682 {
683 "@type": "koral:term",
684 "foundry": "opennlp",
685 "key": "DET",
686 "layer": "p",
687 "match": "match:eq"
688 },
689 {
690 "@type": "koral:term",
691 "foundry": "opennlp",
692 "key": "AdjType",
693 "layer": "m",
694 "match": "match:eq",
695 "value": "Pdt"
696 }
697 ],
698 "relation": "relation:and"
699 }
700 }`
701
702 // Parse JSON to AST
703 node, err := ParseJSON([]byte(input))
704 require.NoError(t, err)
705
706 // Serialize AST back to JSON
Akron87948e82025-05-26 18:19:51 +0200707 output, err := SerializeToJSON(node)
Akronb7e1f352025-05-16 15:45:23 +0200708 require.NoError(t, err)
709
710 // Compare JSON objects
711 var expected, actual interface{}
712 err = json.Unmarshal([]byte(input), &expected)
713 require.NoError(t, err)
714 err = json.Unmarshal(output, &actual)
715 require.NoError(t, err)
716 assert.Equal(t, expected, actual)
717}
Akron32958422025-05-16 16:33:05 +0200718
719func TestRoundTripUnknownType(t *testing.T) {
720 // Test that parsing and then serializing an unknown node type preserves the structure
721 input := `{
722 "@type": "koral:unknown",
723 "key": "value",
724 "wrap": {
725 "@type": "koral:term",
726 "foundry": "opennlp",
727 "key": "DET",
728 "layer": "p",
729 "match": "match:eq"
730 },
731 "operands": [
732 {
733 "@type": "koral:term",
734 "foundry": "opennlp",
735 "key": "AdjType",
736 "layer": "m",
737 "match": "match:eq",
738 "value": "Pdt"
739 }
740 ]
741 }`
742
743 // Parse JSON to AST
744 node, err := ParseJSON([]byte(input))
745 require.NoError(t, err)
746
747 // Check that it's a CatchallNode
748 catchall, ok := node.(*ast.CatchallNode)
749 require.True(t, ok)
750 assert.Equal(t, "koral:unknown", catchall.NodeType)
751
752 // Check that wrap and operands were parsed
753 require.NotNil(t, catchall.Wrap)
754 require.Len(t, catchall.Operands, 1)
755
756 // Serialize AST back to JSON
Akron87948e82025-05-26 18:19:51 +0200757 output, err := SerializeToJSON(node)
Akron32958422025-05-16 16:33:05 +0200758 require.NoError(t, err)
759
760 // Compare JSON objects
761 var expected, actual interface{}
762 err = json.Unmarshal([]byte(input), &expected)
763 require.NoError(t, err)
764 err = json.Unmarshal(output, &actual)
765 require.NoError(t, err)
766 assert.Equal(t, expected, actual)
767}
Akron56e09e72025-05-22 15:38:35 +0200768
769func TestParseJSONEdgeCases(t *testing.T) {
770 tests := []struct {
771 name string
772 input string
773 expected ast.Node
774 wantErr bool
775 }{
776 {
777 name: "Unknown node type",
778 input: `{
779 "@type": "koral:unknown",
780 "customField": "value",
781 "wrap": {
782 "@type": "koral:term",
783 "key": "DET"
784 }
785 }`,
786 expected: &ast.CatchallNode{
787 NodeType: "koral:unknown",
788 RawContent: json.RawMessage(`{
789 "@type": "koral:unknown",
790 "customField": "value",
791 "wrap": {
792 "@type": "koral:term",
793 "key": "DET"
794 }
795 }`),
796 Wrap: &ast.Term{
797 Key: "DET",
798 Match: ast.MatchEqual,
799 },
800 },
801 wantErr: false,
802 },
803 {
804 name: "Unknown node with operands",
805 input: `{
806 "@type": "koral:unknown",
807 "operands": [
808 {
809 "@type": "koral:term",
810 "key": "DET"
811 },
812 {
813 "@type": "koral:term",
814 "key": "NOUN"
815 }
816 ]
817 }`,
818 expected: &ast.CatchallNode{
819 NodeType: "koral:unknown",
820 RawContent: json.RawMessage(`{
821 "@type": "koral:unknown",
822 "operands": [
823 {
824 "@type": "koral:term",
825 "key": "DET"
826 },
827 {
828 "@type": "koral:term",
829 "key": "NOUN"
830 }
831 ]
832 }`),
833 Operands: []ast.Node{
834 &ast.Term{
835 Key: "DET",
836 Match: ast.MatchEqual,
837 },
838 &ast.Term{
839 Key: "NOUN",
840 Match: ast.MatchEqual,
841 },
842 },
843 },
844 wantErr: false,
845 },
846 {
847 name: "Deeply nested unknown nodes",
848 input: `{
849 "@type": "koral:outer",
850 "wrap": {
851 "@type": "koral:middle",
852 "wrap": {
853 "@type": "koral:inner",
854 "wrap": {
855 "@type": "koral:term",
856 "key": "DET"
857 }
858 }
859 }
860 }`,
861 expected: &ast.CatchallNode{
862 NodeType: "koral:outer",
863 RawContent: json.RawMessage(`{
864 "@type": "koral:outer",
865 "wrap": {
866 "@type": "koral:middle",
867 "wrap": {
868 "@type": "koral:inner",
869 "wrap": {
870 "@type": "koral:term",
871 "key": "DET"
872 }
873 }
874 }
875 }`),
876 Wrap: &ast.CatchallNode{
877 NodeType: "koral:middle",
878 RawContent: json.RawMessage(`{
879 "@type": "koral:middle",
880 "wrap": {
881 "@type": "koral:inner",
882 "wrap": {
883 "@type": "koral:term",
884 "key": "DET"
885 }
886 }
887 }`),
888 Wrap: &ast.CatchallNode{
889 NodeType: "koral:inner",
890 RawContent: json.RawMessage(`{
891 "@type": "koral:inner",
892 "wrap": {
893 "@type": "koral:term",
894 "key": "DET"
895 }
896 }`),
897 Wrap: &ast.Term{
898 Key: "DET",
899 Match: ast.MatchEqual,
900 },
901 },
902 },
903 },
904 wantErr: false,
905 },
906 {
907 name: "Mixed known and unknown nodes",
908 input: `{
909 "@type": "koral:token",
910 "wrap": {
911 "@type": "koral:custom",
912 "customField": "value",
913 "operands": [
914 {
915 "@type": "koral:termGroup",
916 "operands": [
917 {
918 "@type": "koral:term",
919 "key": "DET"
920 }
921 ],
922 "relation": "relation:and"
923 }
924 ]
925 }
926 }`,
927 expected: &ast.Token{
928 Wrap: &ast.CatchallNode{
929 NodeType: "koral:custom",
930 RawContent: json.RawMessage(`{
931 "@type": "koral:custom",
932 "customField": "value",
933 "operands": [
934 {
935 "@type": "koral:termGroup",
936 "operands": [
937 {
938 "@type": "koral:term",
939 "key": "DET"
940 }
941 ],
942 "relation": "relation:and"
943 }
944 ]
945 }`),
946 Operands: []ast.Node{
947 &ast.TermGroup{
948 Operands: []ast.Node{
949 &ast.Term{
950 Key: "DET",
951 Match: ast.MatchEqual,
952 },
953 },
954 Relation: ast.AndRelation,
955 },
956 },
957 },
958 },
959 wantErr: false,
960 },
961 {
Akron56e09e72025-05-22 15:38:35 +0200962 name: "Empty operands in term group",
963 input: `{
964 "@type": "koral:termGroup",
965 "operands": [],
966 "relation": "relation:and"
967 }`,
968 wantErr: true,
969 },
970 {
971 name: "Null values in term",
972 input: `{
973 "@type": "koral:term",
974 "foundry": null,
975 "key": "DET",
976 "layer": null,
977 "match": null,
978 "value": null
979 }`,
980 expected: &ast.Term{
981 Key: "DET",
982 Match: ast.MatchEqual,
983 },
984 wantErr: false,
985 },
986 }
987
988 for _, tt := range tests {
989 t.Run(tt.name, func(t *testing.T) {
990 result, err := ParseJSON([]byte(tt.input))
991 if tt.wantErr {
992 assert.Error(t, err)
993 return
994 }
995 require.NoError(t, err)
996 compareNodes(t, tt.expected, result)
997 })
998 }
999}