blob: 4ba89135dd01dabc2214e746c180184108ef17d2 [file] [log] [blame]
Akron57ee5582025-05-21 15:25:13 +02001package config
2
3import (
Akron7e8da932025-07-01 11:56:46 +02004 "bytes"
Akron57ee5582025-05-21 15:25:13 +02005 "os"
6 "testing"
7
Akron2ef703c2025-07-03 15:57:42 +02008 "github.com/KorAP/Koral-Mapper/ast"
Akrona67de8f2026-02-23 17:54:26 +01009 "github.com/KorAP/Koral-Mapper/parser"
Akron7e8da932025-07-01 11:56:46 +020010 "github.com/rs/zerolog/log"
Akron57ee5582025-05-21 15:25:13 +020011 "github.com/stretchr/testify/assert"
12 "github.com/stretchr/testify/require"
13)
14
15func TestLoadConfig(t *testing.T) {
16 // Create a temporary YAML file
17 content := `
18- id: opennlp-mapper
19 foundryA: opennlp
20 layerA: p
21 foundryB: upos
22 layerB: p
23 mappings:
24 - "[PIDAT] <> [opennlp/p=PIDAT & opennlp/p=AdjType:Pdt]"
25 - "[PAV] <> [ADV & PronType:Dem]"
26
27- id: simple-mapper
28 mappings:
29 - "[A] <> [B]"
30`
31 tmpfile, err := os.CreateTemp("", "config-*.yaml")
32 require.NoError(t, err)
33 defer os.Remove(tmpfile.Name())
34
35 _, err = tmpfile.WriteString(content)
36 require.NoError(t, err)
37 err = tmpfile.Close()
38 require.NoError(t, err)
39
40 // Test loading the configuration
Akron585f50f2025-07-03 13:55:47 +020041 config, err := LoadFromSources(tmpfile.Name(), nil)
Akron57ee5582025-05-21 15:25:13 +020042 require.NoError(t, err)
43
44 // Verify the configuration
45 require.Len(t, config.Lists, 2)
46
47 // Check first mapping list
48 list1 := config.Lists[0]
49 assert.Equal(t, "opennlp-mapper", list1.ID)
50 assert.Equal(t, "opennlp", list1.FoundryA)
51 assert.Equal(t, "p", list1.LayerA)
52 assert.Equal(t, "upos", list1.FoundryB)
53 assert.Equal(t, "p", list1.LayerB)
54 require.Len(t, list1.Mappings, 2)
55 assert.Equal(t, "[PIDAT] <> [opennlp/p=PIDAT & opennlp/p=AdjType:Pdt]", string(list1.Mappings[0]))
56 assert.Equal(t, "[PAV] <> [ADV & PronType:Dem]", string(list1.Mappings[1]))
57
58 // Check second mapping list
59 list2 := config.Lists[1]
60 assert.Equal(t, "simple-mapper", list2.ID)
61 assert.Empty(t, list2.FoundryA)
62 assert.Empty(t, list2.LayerA)
63 assert.Empty(t, list2.FoundryB)
64 assert.Empty(t, list2.LayerB)
65 require.Len(t, list2.Mappings, 1)
66 assert.Equal(t, "[A] <> [B]", string(list2.Mappings[0]))
67}
68
69func TestParseMappings(t *testing.T) {
70 list := &MappingList{
71 ID: "test-mapper",
72 FoundryA: "opennlp",
73 LayerA: "p",
74 FoundryB: "upos",
75 LayerB: "p",
76 Mappings: []MappingRule{
77 "[PIDAT] <> [opennlp/p=PIDAT & opennlp/p=AdjType:Pdt]",
78 },
79 }
80
81 results, err := list.ParseMappings()
82 require.NoError(t, err)
83 require.Len(t, results, 1)
84
85 // Check the parsed upper pattern
86 upper := results[0].Upper
87 require.NotNil(t, upper)
88 require.IsType(t, &ast.Token{}, upper)
89 upperTerm := upper.Wrap.(*ast.Term)
90 assert.Equal(t, "opennlp", upperTerm.Foundry)
91 assert.Equal(t, "p", upperTerm.Layer)
92 assert.Equal(t, "PIDAT", upperTerm.Key)
93
94 // Check the parsed lower pattern
95 lower := results[0].Lower
96 require.NotNil(t, lower)
97 require.IsType(t, &ast.Token{}, lower)
98 lowerGroup := lower.Wrap.(*ast.TermGroup)
99 require.Len(t, lowerGroup.Operands, 2)
100 assert.Equal(t, ast.AndRelation, lowerGroup.Relation)
101
102 // Check first operand
103 term1 := lowerGroup.Operands[0].(*ast.Term)
104 assert.Equal(t, "opennlp", term1.Foundry)
105 assert.Equal(t, "p", term1.Layer)
106 assert.Equal(t, "PIDAT", term1.Key)
107
108 // Check second operand
109 term2 := lowerGroup.Operands[1].(*ast.Term)
110 assert.Equal(t, "opennlp", term2.Foundry)
111 assert.Equal(t, "p", term2.Layer)
112 assert.Equal(t, "AdjType", term2.Key)
113 assert.Equal(t, "Pdt", term2.Value)
114}
115
116func TestLoadConfigValidation(t *testing.T) {
117 tests := []struct {
118 name string
119 content string
120 wantErr string
121 }{
122 {
123 name: "Missing ID",
124 content: `
125- foundryA: opennlp
126 mappings:
127 - "[A] <> [B]"
128`,
129 wantErr: "mapping list at index 0 is missing an ID",
130 },
131 {
132 name: "Empty mappings",
133 content: `
134- id: test
135 foundryA: opennlp
136 mappings: []
137`,
138 wantErr: "mapping list 'test' has no mapping rules",
139 },
140 {
141 name: "Empty rule",
142 content: `
143- id: test
144 mappings:
145 - ""
146`,
147 wantErr: "mapping list 'test' rule at index 0 is empty",
148 },
149 }
150
151 for _, tt := range tests {
152 t.Run(tt.name, func(t *testing.T) {
153 tmpfile, err := os.CreateTemp("", "config-*.yaml")
154 require.NoError(t, err)
155 defer os.Remove(tmpfile.Name())
156
157 _, err = tmpfile.WriteString(tt.content)
158 require.NoError(t, err)
159 err = tmpfile.Close()
160 require.NoError(t, err)
161
Akron585f50f2025-07-03 13:55:47 +0200162 _, err = LoadFromSources(tmpfile.Name(), nil)
Akron57ee5582025-05-21 15:25:13 +0200163 require.Error(t, err)
164 assert.Contains(t, err.Error(), tt.wantErr)
165 })
166 }
167}
Akrona5d88142025-05-22 14:42:09 +0200168
169func TestLoadConfigEdgeCases(t *testing.T) {
170 tests := []struct {
171 name string
172 content string
173 wantErr string
174 }{
175 {
176 name: "Duplicate mapping list IDs",
177 content: `
178- id: test
179 mappings:
180 - "[A] <> [B]"
181- id: test
182 mappings:
183 - "[C] <> [D]"`,
184 wantErr: "duplicate mapping list ID found: test",
185 },
186 {
187 name: "Invalid YAML syntax",
188 content: `
189- id: test
190 mappings:
191 - [A] <> [B] # Unquoted special characters
192`,
193 wantErr: "yaml",
194 },
195 {
196 name: "Empty file",
197 content: "",
198 wantErr: "EOF",
199 },
200 {
201 name: "Non-list YAML",
202 content: `
203id: test
204mappings:
205 - "[A] <> [B]"`,
Akron813780f2025-06-05 15:44:28 +0200206 wantErr: "no mapping lists found",
Akrona5d88142025-05-22 14:42:09 +0200207 },
208 {
209 name: "Missing required fields",
210 content: `
211- mappings:
212 - "[A] <> [B]"
213- id: test2
214 foundryA: opennlp`,
215 wantErr: "missing an ID",
216 },
217 {
218 name: "Empty mappings list",
219 content: `
220- id: test
221 foundryA: opennlp
222 mappings: []`,
223 wantErr: "has no mapping rules",
224 },
225 {
226 name: "Null values in optional fields",
227 content: `
228- id: test
229 foundryA: null
230 layerA: null
231 foundryB: null
232 layerB: null
233 mappings:
234 - "[A] <> [B]"`,
235 wantErr: "",
236 },
237 {
238 name: "Special characters in IDs",
239 content: `
240- id: "test/special@chars#1"
241 mappings:
242 - "[A] <> [B]"`,
243 wantErr: "",
244 },
245 {
246 name: "Unicode characters in mappings",
247 content: `
248- id: test
249 mappings:
250 - "[ß] <> [ss]"
251 - "[é] <> [e]"`,
252 wantErr: "",
253 },
254 }
255
256 for _, tt := range tests {
257 t.Run(tt.name, func(t *testing.T) {
258 tmpfile, err := os.CreateTemp("", "config-*.yaml")
259 require.NoError(t, err)
260 defer os.Remove(tmpfile.Name())
261
262 _, err = tmpfile.WriteString(tt.content)
263 require.NoError(t, err)
264 err = tmpfile.Close()
265 require.NoError(t, err)
266
Akron585f50f2025-07-03 13:55:47 +0200267 config, err := LoadFromSources(tmpfile.Name(), nil)
Akrona5d88142025-05-22 14:42:09 +0200268 if tt.wantErr != "" {
269 require.Error(t, err)
270 assert.Contains(t, err.Error(), tt.wantErr)
271 return
272 }
273 require.NoError(t, err)
274 require.NotNil(t, config)
275 })
276 }
277}
278
279func TestParseMappingsEdgeCases(t *testing.T) {
280 tests := []struct {
281 name string
282 list *MappingList
283 wantErr bool
284 errCheck func(t *testing.T, err error)
285 }{
286 {
287 name: "Empty mapping rule",
288 list: &MappingList{
289 ID: "test",
290 Mappings: []MappingRule{""},
291 },
292 wantErr: true,
293 errCheck: func(t *testing.T, err error) {
294 assert.Contains(t, err.Error(), "empty")
295 },
296 },
297 {
298 name: "Invalid mapping syntax",
299 list: &MappingList{
300 ID: "test",
301 Mappings: []MappingRule{"[A] -> [B]"},
302 },
303 wantErr: true,
304 errCheck: func(t *testing.T, err error) {
305 assert.Contains(t, err.Error(), "failed to parse")
306 },
307 },
308 {
309 name: "Missing brackets",
310 list: &MappingList{
311 ID: "test",
312 Mappings: []MappingRule{"A <> B"},
313 },
314 wantErr: true,
315 errCheck: func(t *testing.T, err error) {
316 assert.Contains(t, err.Error(), "failed to parse")
317 },
318 },
319 {
320 name: "Complex nested expressions",
321 list: &MappingList{
322 ID: "test",
323 Mappings: []MappingRule{
324 "[A & (B | C) & (D | (E & F))] <> [X & (Y | Z)]",
325 },
326 },
327 wantErr: false,
328 },
329 {
330 name: "Multiple foundry/layer combinations",
331 list: &MappingList{
332 ID: "test",
333 Mappings: []MappingRule{
334 "[foundry1/layer1=A & foundry2/layer2=B] <> [foundry3/layer3=C]",
335 },
336 },
337 wantErr: false,
338 },
339 {
340 name: "Default foundry/layer override",
341 list: &MappingList{
342 ID: "test",
343 FoundryA: "defaultFoundry",
344 LayerA: "defaultLayer",
345 Mappings: []MappingRule{
346 "[A] <> [B]", // Should use defaults
347 },
348 },
349 wantErr: false,
350 },
351 }
352
353 for _, tt := range tests {
354 t.Run(tt.name, func(t *testing.T) {
355 results, err := tt.list.ParseMappings()
356 if tt.wantErr {
357 require.Error(t, err)
358 if tt.errCheck != nil {
359 tt.errCheck(t, err)
360 }
361 return
362 }
363 require.NoError(t, err)
364 require.NotNil(t, results)
365 })
366 }
367}
Akroncc25e932025-06-02 19:39:43 +0200368
369func TestUserProvidedMappingRules(t *testing.T) {
370 // Test the exact YAML mapping rules provided by the user
371 content := `
372- id: stts-ud
373 foundryA: opennlp
374 layerA: p
375 foundryB: upos
376 layerB: p
377 mappings:
378 - "[$\\(] <> [PUNCT & PunctType=Brck]"
379 - "[$,] <> [PUNCT & PunctType=Comm]"
380 - "[$.] <> [PUNCT & PunctType=Peri]"
381 - "[ADJA] <> [ADJ]"
382 - "[ADJD] <> [ADJ & Variant=Short]"
383 - "[ADV] <> [ADV]"
384`
385 tmpfile, err := os.CreateTemp("", "user-config-*.yaml")
386 require.NoError(t, err)
387 defer os.Remove(tmpfile.Name())
388
389 _, err = tmpfile.WriteString(content)
390 require.NoError(t, err)
391 err = tmpfile.Close()
392 require.NoError(t, err)
393
394 // Test loading the configuration
Akron585f50f2025-07-03 13:55:47 +0200395 config, err := LoadFromSources(tmpfile.Name(), nil)
Akroncc25e932025-06-02 19:39:43 +0200396 require.NoError(t, err)
397
398 // Verify the configuration loaded correctly
399 require.Len(t, config.Lists, 1)
400 list := config.Lists[0]
401 assert.Equal(t, "stts-ud", list.ID)
402 assert.Equal(t, "opennlp", list.FoundryA)
403 assert.Equal(t, "p", list.LayerA)
404 assert.Equal(t, "upos", list.FoundryB)
405 assert.Equal(t, "p", list.LayerB)
406 require.Len(t, list.Mappings, 6)
407
408 // First, test individual mappings to isolate the issue
409 t.Run("parenthesis mapping", func(t *testing.T) {
410 singleRule := &MappingList{
411 ID: "test-paren",
412 FoundryA: "opennlp",
413 LayerA: "p",
414 FoundryB: "upos",
415 LayerB: "p",
416 Mappings: []MappingRule{"[$\\(] <> [PUNCT & PunctType=Brck]"},
417 }
418 results, err := singleRule.ParseMappings()
419 require.NoError(t, err)
420 require.Len(t, results, 1)
421
422 upperTerm := results[0].Upper.Wrap.(*ast.Term)
423 assert.Equal(t, "$(", upperTerm.Key)
424 })
425
426 t.Run("comma mapping", func(t *testing.T) {
427 singleRule := &MappingList{
428 ID: "test-comma",
429 FoundryA: "opennlp",
430 LayerA: "p",
431 FoundryB: "upos",
432 LayerB: "p",
433 Mappings: []MappingRule{"[$,] <> [PUNCT & PunctType=Comm]"},
434 }
435 results, err := singleRule.ParseMappings()
436 require.NoError(t, err)
437 require.Len(t, results, 1)
438
439 upperTerm := results[0].Upper.Wrap.(*ast.Term)
440 assert.Equal(t, "$,", upperTerm.Key)
441 })
442
443 t.Run("period mapping", func(t *testing.T) {
444 singleRule := &MappingList{
445 ID: "test-period",
446 FoundryA: "opennlp",
447 LayerA: "p",
448 FoundryB: "upos",
449 LayerB: "p",
450 Mappings: []MappingRule{"[$.] <> [PUNCT & PunctType=Peri]"},
451 }
452 results, err := singleRule.ParseMappings()
453 require.NoError(t, err)
454 require.Len(t, results, 1)
455
456 upperTerm := results[0].Upper.Wrap.(*ast.Term)
457 assert.Equal(t, "$.", upperTerm.Key)
458 })
459
460 // Test that all mapping rules can be parsed successfully
461 results, err := list.ParseMappings()
462 require.NoError(t, err)
463 require.Len(t, results, 6)
464
465 // Verify specific parsing of the special character mapping
466 // The first mapping "[$\\(] <> [PUNCT & PunctType=Brck]" should parse correctly
467 firstMapping := results[0]
468 require.NotNil(t, firstMapping.Upper)
469 upperTerm := firstMapping.Upper.Wrap.(*ast.Term)
470 assert.Equal(t, "$(", upperTerm.Key) // The actual parsed key should be "$("
471 assert.Equal(t, "opennlp", upperTerm.Foundry)
472 assert.Equal(t, "p", upperTerm.Layer)
473
474 require.NotNil(t, firstMapping.Lower)
475 lowerGroup := firstMapping.Lower.Wrap.(*ast.TermGroup)
476 require.Len(t, lowerGroup.Operands, 2)
477 assert.Equal(t, ast.AndRelation, lowerGroup.Relation)
478
479 // Check the PUNCT term
480 punctTerm := lowerGroup.Operands[0].(*ast.Term)
481 assert.Equal(t, "PUNCT", punctTerm.Key)
482 assert.Equal(t, "upos", punctTerm.Foundry)
483 assert.Equal(t, "p", punctTerm.Layer)
484
485 // Check the PunctType term
486 punctTypeTerm := lowerGroup.Operands[1].(*ast.Term)
487 assert.Equal(t, "PunctType", punctTypeTerm.Layer)
488 assert.Equal(t, "Brck", punctTypeTerm.Key)
489 assert.Equal(t, "upos", punctTypeTerm.Foundry)
490
491 // Verify the comma mapping as well
492 secondMapping := results[1]
493 upperTerm2 := secondMapping.Upper.Wrap.(*ast.Term)
494 assert.Equal(t, "$,", upperTerm2.Key)
495
496 // Verify the period mapping
497 thirdMapping := results[2]
498 upperTerm3 := thirdMapping.Upper.Wrap.(*ast.Term)
499 assert.Equal(t, "$.", upperTerm3.Key)
500
501 // Verify basic mappings without special characters
502 fourthMapping := results[3]
503 upperTerm4 := fourthMapping.Upper.Wrap.(*ast.Term)
504 assert.Equal(t, "ADJA", upperTerm4.Key)
505 lowerTerm4 := fourthMapping.Lower.Wrap.(*ast.Term)
506 assert.Equal(t, "ADJ", lowerTerm4.Key)
507}
508
Akron06d21f02025-06-04 14:36:07 +0200509func TestConfigWithSdkAndServer(t *testing.T) {
510 tests := []struct {
511 name string
512 content string
513 expectedSDK string
514 expectedServer string
515 wantErr bool
516 }{
517 {
518 name: "Configuration with SDK and Server values",
519 content: `
520sdk: "https://custom.example.com/sdk.js"
521server: "https://custom.example.com/"
522lists:
523- id: test-mapper
524 foundryA: opennlp
525 layerA: p
526 foundryB: upos
527 layerB: p
528 mappings:
529 - "[A] <> [B]"
530`,
531 expectedSDK: "https://custom.example.com/sdk.js",
532 expectedServer: "https://custom.example.com/",
533 wantErr: false,
534 },
535 {
536 name: "Configuration with only SDK value",
537 content: `
538sdk: "https://custom.example.com/sdk.js"
539lists:
540- id: test-mapper
541 mappings:
542 - "[A] <> [B]"
543`,
544 expectedSDK: "https://custom.example.com/sdk.js",
545 expectedServer: "https://korap.ids-mannheim.de/", // default applied
546 wantErr: false,
547 },
548 {
549 name: "Configuration with only Server value",
550 content: `
551server: "https://custom.example.com/"
552lists:
553- id: test-mapper
554 mappings:
555 - "[A] <> [B]"
556`,
557 expectedSDK: "https://korap.ids-mannheim.de/js/korap-plugin-latest.js", // default applied
558 expectedServer: "https://custom.example.com/",
559 wantErr: false,
560 },
561 {
562 name: "Configuration without SDK and Server (old format with defaults applied)",
563 content: `
564- id: test-mapper
565 mappings:
566 - "[A] <> [B]"
567`,
568 expectedSDK: "https://korap.ids-mannheim.de/js/korap-plugin-latest.js", // default applied
569 expectedServer: "https://korap.ids-mannheim.de/", // default applied
570 wantErr: false,
571 },
572 {
573 name: "Configuration with lists field explicitly",
574 content: `
575sdk: "https://custom.example.com/sdk.js"
576server: "https://custom.example.com/"
577lists:
578- id: test-mapper-1
579 mappings:
580 - "[A] <> [B]"
581- id: test-mapper-2
582 mappings:
583 - "[C] <> [D]"
584`,
585 expectedSDK: "https://custom.example.com/sdk.js",
586 expectedServer: "https://custom.example.com/",
587 wantErr: false,
588 },
589 }
590
591 for _, tt := range tests {
592 t.Run(tt.name, func(t *testing.T) {
593 tmpfile, err := os.CreateTemp("", "config-*.yaml")
594 require.NoError(t, err)
595 defer os.Remove(tmpfile.Name())
596
597 _, err = tmpfile.WriteString(tt.content)
598 require.NoError(t, err)
599 err = tmpfile.Close()
600 require.NoError(t, err)
601
Akron585f50f2025-07-03 13:55:47 +0200602 config, err := LoadFromSources(tmpfile.Name(), nil)
Akron06d21f02025-06-04 14:36:07 +0200603 if tt.wantErr {
604 require.Error(t, err)
605 return
606 }
607
608 require.NoError(t, err)
609 require.NotNil(t, config)
610
611 // Check SDK and Server values
612 assert.Equal(t, tt.expectedSDK, config.SDK)
613 assert.Equal(t, tt.expectedServer, config.Server)
614
615 // Ensure lists are still loaded correctly
616 require.Greater(t, len(config.Lists), 0)
617
618 // Verify first mapping list
619 firstList := config.Lists[0]
620 assert.NotEmpty(t, firstList.ID)
621 assert.Greater(t, len(firstList.Mappings), 0)
622 })
623 }
624}
Akrone1cff7c2025-06-04 18:43:32 +0200625
626func TestLoadFromSources(t *testing.T) {
627 // Create main config file
628 mainConfigContent := `
629sdk: "https://custom.example.com/sdk.js"
630server: "https://custom.example.com/"
631lists:
632- id: main-mapper
633 mappings:
634 - "[A] <> [B]"
635`
636 mainConfigFile, err := os.CreateTemp("", "main-config-*.yaml")
637 require.NoError(t, err)
638 defer os.Remove(mainConfigFile.Name())
639
640 _, err = mainConfigFile.WriteString(mainConfigContent)
641 require.NoError(t, err)
642 err = mainConfigFile.Close()
643 require.NoError(t, err)
644
645 // Create individual mapping files
646 mappingFile1Content := `
647id: mapper-1
648foundryA: opennlp
649layerA: p
650mappings:
651 - "[C] <> [D]"
652`
653 mappingFile1, err := os.CreateTemp("", "mapping1-*.yaml")
654 require.NoError(t, err)
655 defer os.Remove(mappingFile1.Name())
656
657 _, err = mappingFile1.WriteString(mappingFile1Content)
658 require.NoError(t, err)
659 err = mappingFile1.Close()
660 require.NoError(t, err)
661
662 mappingFile2Content := `
663id: mapper-2
664foundryB: upos
665layerB: p
666mappings:
667 - "[E] <> [F]"
668`
669 mappingFile2, err := os.CreateTemp("", "mapping2-*.yaml")
670 require.NoError(t, err)
671 defer os.Remove(mappingFile2.Name())
672
673 _, err = mappingFile2.WriteString(mappingFile2Content)
674 require.NoError(t, err)
675 err = mappingFile2.Close()
676 require.NoError(t, err)
677
678 tests := []struct {
679 name string
680 configFile string
681 mappingFiles []string
682 wantErr bool
683 expectedIDs []string
684 }{
685 {
686 name: "Main config only",
687 configFile: mainConfigFile.Name(),
688 mappingFiles: []string{},
689 wantErr: false,
690 expectedIDs: []string{"main-mapper"},
691 },
692 {
693 name: "Mapping files only",
694 configFile: "",
695 mappingFiles: []string{mappingFile1.Name(), mappingFile2.Name()},
696 wantErr: false,
697 expectedIDs: []string{"mapper-1", "mapper-2"},
698 },
699 {
700 name: "Main config and mapping files",
701 configFile: mainConfigFile.Name(),
702 mappingFiles: []string{mappingFile1.Name(), mappingFile2.Name()},
703 wantErr: false,
704 expectedIDs: []string{"main-mapper", "mapper-1", "mapper-2"},
705 },
706 {
707 name: "No configuration sources",
708 configFile: "",
709 mappingFiles: []string{},
710 wantErr: true,
711 },
712 }
713
714 for _, tt := range tests {
715 t.Run(tt.name, func(t *testing.T) {
716 config, err := LoadFromSources(tt.configFile, tt.mappingFiles)
717 if tt.wantErr {
718 require.Error(t, err)
719 return
720 }
721
722 require.NoError(t, err)
723 require.NotNil(t, config)
724
725 // Check that all expected mapping IDs are present
726 require.Len(t, config.Lists, len(tt.expectedIDs))
727 actualIDs := make([]string, len(config.Lists))
728 for i, list := range config.Lists {
729 actualIDs[i] = list.ID
730 }
731 for _, expectedID := range tt.expectedIDs {
732 assert.Contains(t, actualIDs, expectedID)
733 }
734
735 // Check that SDK and Server are set (either from config or defaults)
736 assert.NotEmpty(t, config.SDK)
Akron43fb1022026-02-20 11:38:49 +0100737 assert.NotEmpty(t, config.Stylesheet)
Akrone1cff7c2025-06-04 18:43:32 +0200738 assert.NotEmpty(t, config.Server)
739 })
740 }
741}
742
743func TestLoadFromSourcesWithDefaults(t *testing.T) {
744 // Test that defaults are applied when loading only mapping files
745 mappingFileContent := `
746id: test-mapper
747mappings:
748 - "[A] <> [B]"
749`
750 mappingFile, err := os.CreateTemp("", "mapping-*.yaml")
751 require.NoError(t, err)
752 defer os.Remove(mappingFile.Name())
753
754 _, err = mappingFile.WriteString(mappingFileContent)
755 require.NoError(t, err)
756 err = mappingFile.Close()
757 require.NoError(t, err)
758
759 config, err := LoadFromSources("", []string{mappingFile.Name()})
760 require.NoError(t, err)
761
762 // Check that defaults are applied
763 assert.Equal(t, defaultSDK, config.SDK)
Akron43fb1022026-02-20 11:38:49 +0100764 assert.Equal(t, defaultStylesheet, config.Stylesheet)
Akrone1cff7c2025-06-04 18:43:32 +0200765 assert.Equal(t, defaultServer, config.Server)
766 require.Len(t, config.Lists, 1)
767 assert.Equal(t, "test-mapper", config.Lists[0].ID)
768}
769
770func TestLoadFromSourcesDuplicateIDs(t *testing.T) {
Akron7e8da932025-07-01 11:56:46 +0200771 // Set up a buffer to capture log output
772 var buf bytes.Buffer
773 originalLogger := log.Logger
774 defer func() {
775 log.Logger = originalLogger
776 }()
777 log.Logger = log.Logger.Output(&buf)
778
Akrone1cff7c2025-06-04 18:43:32 +0200779 // Create config with duplicate IDs across sources
780 configContent := `
781lists:
782- id: duplicate-id
783 mappings:
784 - "[A] <> [B]"
785`
786 configFile, err := os.CreateTemp("", "config-*.yaml")
787 require.NoError(t, err)
788 defer os.Remove(configFile.Name())
789
790 _, err = configFile.WriteString(configContent)
791 require.NoError(t, err)
792 err = configFile.Close()
793 require.NoError(t, err)
794
795 mappingContent := `
796id: duplicate-id
797mappings:
798 - "[C] <> [D]"
799`
800 mappingFile, err := os.CreateTemp("", "mapping-*.yaml")
801 require.NoError(t, err)
802 defer os.Remove(mappingFile.Name())
803
804 _, err = mappingFile.WriteString(mappingContent)
805 require.NoError(t, err)
806 err = mappingFile.Close()
807 require.NoError(t, err)
808
Akron7e8da932025-07-01 11:56:46 +0200809 // The function should now succeed but log the duplicate ID error
810 config, err := LoadFromSources(configFile.Name(), []string{mappingFile.Name()})
811 require.NoError(t, err)
812 require.NotNil(t, config)
813
814 // Check that the duplicate ID error was logged
815 logOutput := buf.String()
816 assert.Contains(t, logOutput, "Duplicate mapping list ID found")
817 assert.Contains(t, logOutput, "duplicate-id")
818
819 // Only the first mapping list (from config file) should be loaded
820 require.Len(t, config.Lists, 1)
821 assert.Equal(t, "duplicate-id", config.Lists[0].ID)
822 // Check that it's the one from the config file (has mapping "[A] <> [B]")
823 assert.Equal(t, "[A] <> [B]", string(config.Lists[0].Mappings[0]))
Akrone1cff7c2025-06-04 18:43:32 +0200824}
Akron813780f2025-06-05 15:44:28 +0200825
826func TestLoadFromSourcesConfigWithOnlyPort(t *testing.T) {
827 // Create config file with only port (no lists)
828 configContent := `
829port: 8080
830loglevel: debug
831`
832 configFile, err := os.CreateTemp("", "config-*.yaml")
833 require.NoError(t, err)
834 defer os.Remove(configFile.Name())
835
836 _, err = configFile.WriteString(configContent)
837 require.NoError(t, err)
838 err = configFile.Close()
839 require.NoError(t, err)
840
841 // Create mapping file
842 mappingContent := `
843id: test-mapper
844mappings:
845 - "[A] <> [B]"
846`
847 mappingFile, err := os.CreateTemp("", "mapping-*.yaml")
848 require.NoError(t, err)
849 defer os.Remove(mappingFile.Name())
850
851 _, err = mappingFile.WriteString(mappingContent)
852 require.NoError(t, err)
853 err = mappingFile.Close()
854 require.NoError(t, err)
855
856 // This should work: config file has only port, mapping file provides the mapping list
857 config, err := LoadFromSources(configFile.Name(), []string{mappingFile.Name()})
858 require.NoError(t, err)
859 require.NotNil(t, config)
860
861 // Check that port and log level from config file are preserved
862 assert.Equal(t, 8080, config.Port)
863 assert.Equal(t, "debug", config.LogLevel)
864
865 // Check that mapping from mapping file is loaded
866 require.Len(t, config.Lists, 1)
867 assert.Equal(t, "test-mapper", config.Lists[0].ID)
868
869 // Check that defaults are applied for other fields
870 assert.Equal(t, defaultSDK, config.SDK)
Akron43fb1022026-02-20 11:38:49 +0100871 assert.Equal(t, defaultStylesheet, config.Stylesheet)
Akron813780f2025-06-05 15:44:28 +0200872 assert.Equal(t, defaultServer, config.Server)
873 assert.Equal(t, defaultServiceURL, config.ServiceURL)
874}
Akron2f93c582026-02-19 16:49:13 +0100875
876func TestCorpusMappingListType(t *testing.T) {
877 content := `
878lists:
879- id: corpus-class-mapping
880 type: corpus
881 desc: Maps textClass values to genre field
882 mappings:
883 - "textClass=novel <> genre=fiction"
884 - "textClass=science <> genre=nonfiction"
885- id: annotation-mapper
886 mappings:
887 - "[A] <> [B]"
888`
889 tmpfile, err := os.CreateTemp("", "config-corpus-*.yaml")
890 require.NoError(t, err)
891 defer os.Remove(tmpfile.Name())
892
893 _, err = tmpfile.WriteString(content)
894 require.NoError(t, err)
895 err = tmpfile.Close()
896 require.NoError(t, err)
897
898 config, err := LoadFromSources(tmpfile.Name(), nil)
899 require.NoError(t, err)
900 require.Len(t, config.Lists, 2)
901
902 assert.Equal(t, "corpus", config.Lists[0].Type)
903 assert.True(t, config.Lists[0].IsCorpus())
904
905 assert.Equal(t, "", config.Lists[1].Type)
906 assert.False(t, config.Lists[1].IsCorpus())
907}
908
909func TestParseCorpusMappings(t *testing.T) {
910 list := &MappingList{
911 ID: "test-corpus",
912 Type: "corpus",
913 Mappings: []MappingRule{
914 "textClass=novel <> genre=fiction",
915 "(textClass=novel & pubDate=2020:geq#date) <> genre=recentfiction",
916 },
917 }
918
919 results, err := list.ParseCorpusMappings()
920 require.NoError(t, err)
921 require.Len(t, results, 2)
922
923 // Verify simple field rule
924 require.NotNil(t, results[0].Upper)
925 require.NotNil(t, results[0].Lower)
926
927 // Verify group rule
928 require.NotNil(t, results[1].Upper)
929 require.NotNil(t, results[1].Lower)
930}
931
932func TestParseCorpusMappingsErrors(t *testing.T) {
933 list := &MappingList{
934 ID: "test-corpus",
935 Type: "corpus",
936 Mappings: []MappingRule{""},
937 }
938
939 _, err := list.ParseCorpusMappings()
940 assert.Error(t, err)
941 assert.Contains(t, err.Error(), "empty corpus mapping rule")
942
943 list2 := &MappingList{
944 ID: "test-corpus",
945 Type: "corpus",
946 Mappings: []MappingRule{"invalid rule without separator"},
947 }
948
949 _, err = list2.ParseCorpusMappings()
950 assert.Error(t, err)
951 assert.Contains(t, err.Error(), "failed to parse corpus mapping rule")
952}
Akrona67de8f2026-02-23 17:54:26 +0100953
954func TestParseCorpusMappingsWithFieldAFieldB(t *testing.T) {
955 list := &MappingList{
956 ID: "test-keyed",
957 Type: "corpus",
958 FieldA: "wikiCat",
959 FieldB: "textClass",
960 Mappings: []MappingRule{
961 "Entertainment <> ((kultur & musik) | (kultur & film))",
962 },
963 }
964
965 results, err := list.ParseCorpusMappings()
966 require.NoError(t, err)
967 require.Len(t, results, 1)
968
969 upper := results[0].Upper.(*parser.CorpusField)
970 assert.Equal(t, "wikiCat", upper.Key)
971 assert.Equal(t, "Entertainment", upper.Value)
972
973 group := results[0].Lower.(*parser.CorpusGroup)
974 assert.Equal(t, "or", group.Operation)
975 require.Len(t, group.Operands, 2)
976
977 and1 := group.Operands[0].(*parser.CorpusGroup)
978 assert.Equal(t, "textClass", and1.Operands[0].(*parser.CorpusField).Key)
979 assert.Equal(t, "kultur", and1.Operands[0].(*parser.CorpusField).Value)
980 assert.Equal(t, "textClass", and1.Operands[1].(*parser.CorpusField).Key)
981 assert.Equal(t, "musik", and1.Operands[1].(*parser.CorpusField).Value)
982}