1
2
3
4
5
6 package dev.metaschema.core.metapath.format;
7
8 import static org.junit.jupiter.api.Assertions.assertEquals;
9 import static org.junit.jupiter.api.Assertions.assertNotNull;
10 import static org.mockito.Mockito.doReturn;
11 import static org.mockito.Mockito.mock;
12
13 import org.junit.jupiter.api.BeforeEach;
14 import org.junit.jupiter.api.DisplayName;
15 import org.junit.jupiter.api.Nested;
16 import org.junit.jupiter.api.Test;
17
18 import java.net.URI;
19
20 import dev.metaschema.core.metapath.item.atomic.IStringItem;
21 import dev.metaschema.core.metapath.item.node.IAssemblyInstanceGroupedNodeItem;
22 import dev.metaschema.core.metapath.item.node.IAssemblyNodeItem;
23 import dev.metaschema.core.metapath.item.node.IDocumentNodeItem;
24 import dev.metaschema.core.metapath.item.node.IFieldNodeItem;
25 import dev.metaschema.core.metapath.item.node.IFlagNodeItem;
26 import dev.metaschema.core.metapath.item.node.IModuleNodeItem;
27 import dev.metaschema.core.metapath.item.node.IRootAssemblyNodeItem;
28 import dev.metaschema.core.model.IAssemblyInstance;
29 import dev.metaschema.core.model.IFieldInstance;
30 import dev.metaschema.core.model.XmlGroupAsBehavior;
31 import dev.metaschema.core.qname.IEnhancedQName;
32 import dev.metaschema.core.testsupport.mocking.MockNodeItemFactory;
33 import dev.metaschema.core.util.CollectionUtil;
34
35
36
37
38 class XPathFormatterTest {
39
40 private static final String TEST_NS = "http://example.com/test";
41 private static final String EMPTY_NS = "";
42
43 private XPathFormatter formatter;
44 private MockNodeItemFactory mockFactory;
45
46 @BeforeEach
47 void setUp() {
48 formatter = new XPathFormatter();
49 mockFactory = new MockNodeItemFactory();
50 }
51
52 @Nested
53 @DisplayName("Document Node Formatting")
54 class DocumentNodeTests {
55
56 @Test
57 @DisplayName("formatDocument returns empty string")
58 void testFormatDocumentReturnsEmptyString() {
59 IDocumentNodeItem document = mock(IDocumentNodeItem.class);
60
61 String result = formatter.formatDocument(document);
62
63 assertEquals("", result);
64 }
65 }
66
67 @Nested
68 @DisplayName("Module Node Formatting")
69 class ModuleNodeTests {
70
71 @Test
72 @DisplayName("formatMetaschema returns empty string")
73 void testFormatMetaschemaReturnsEmptyString() {
74 IModuleNodeItem module = mock(IModuleNodeItem.class);
75
76 String result = formatter.formatMetaschema(module);
77
78 assertEquals("", result);
79 }
80 }
81
82 @Nested
83 @DisplayName("Root Assembly Formatting")
84 class RootAssemblyTests {
85
86 @Test
87 @DisplayName("formatRootAssembly with namespace returns EQName format")
88 void testFormatRootAssemblyWithNamespace() {
89 IEnhancedQName qname = IEnhancedQName.of(TEST_NS, "catalog");
90 IRootAssemblyNodeItem root = mock(IRootAssemblyNodeItem.class);
91
92 doReturn(qname).when(root).getQName();
93
94 String result = formatter.formatRootAssembly(root);
95
96 assertEquals("Q{" + TEST_NS + "}catalog", result);
97 }
98
99 @Test
100 @DisplayName("formatRootAssembly without namespace returns local name only")
101 void testFormatRootAssemblyWithoutNamespace() {
102 IEnhancedQName qname = IEnhancedQName.of(EMPTY_NS, "catalog");
103 IRootAssemblyNodeItem root = mock(IRootAssemblyNodeItem.class);
104
105 doReturn(qname).when(root).getQName();
106
107 String result = formatter.formatRootAssembly(root);
108
109 assertEquals("catalog", result);
110 }
111 }
112
113 @Nested
114 @DisplayName("Assembly Formatting")
115 class AssemblyTests {
116
117 @Test
118 @DisplayName("formatAssembly UNGROUPED returns EQName with position")
119 void testFormatAssemblyUngrouped() {
120 IEnhancedQName qname = IEnhancedQName.of(TEST_NS, "control");
121 IAssemblyNodeItem assembly = mock(IAssemblyNodeItem.class);
122 IAssemblyInstance instance = mock(IAssemblyInstance.class);
123
124 doReturn(qname).when(assembly).getQName();
125 doReturn(instance).when(assembly).getInstance();
126 doReturn(2).when(assembly).getPosition();
127 doReturn(XmlGroupAsBehavior.UNGROUPED).when(instance).getXmlGroupAsBehavior();
128
129 String result = formatter.formatAssembly(assembly);
130
131 assertEquals("Q{" + TEST_NS + "}control[2]", result);
132 }
133
134 @Test
135 @DisplayName("formatAssembly GROUPED returns wrapper + EQName with position")
136 void testFormatAssemblyGrouped() {
137 IEnhancedQName elementQname = IEnhancedQName.of(TEST_NS, "control");
138 IEnhancedQName wrapperQname = IEnhancedQName.of(TEST_NS, "controls");
139 IAssemblyNodeItem assembly = mock(IAssemblyNodeItem.class);
140 IAssemblyInstance instance = mock(IAssemblyInstance.class);
141
142 doReturn(elementQname).when(assembly).getQName();
143 doReturn(instance).when(assembly).getInstance();
144 doReturn(1).when(assembly).getPosition();
145 doReturn(XmlGroupAsBehavior.GROUPED).when(instance).getXmlGroupAsBehavior();
146 doReturn(wrapperQname).when(instance).getEffectiveXmlGroupAsQName();
147
148 String result = formatter.formatAssembly(assembly);
149
150 assertEquals("Q{" + TEST_NS + "}controls[1]/Q{" + TEST_NS + "}control[1]", result);
151 }
152
153 @Test
154 @DisplayName("formatAssembly without instance returns EQName with position")
155 void testFormatAssemblyWithoutInstance() {
156 IEnhancedQName qname = IEnhancedQName.of(TEST_NS, "control");
157 IAssemblyNodeItem assembly = mock(IAssemblyNodeItem.class);
158
159 doReturn(qname).when(assembly).getQName();
160 doReturn(null).when(assembly).getInstance();
161 doReturn(1).when(assembly).getPosition();
162
163 String result = formatter.formatAssembly(assembly);
164
165 assertEquals("Q{" + TEST_NS + "}control[1]", result);
166 }
167
168 @Test
169 @DisplayName("formatAssembly without namespace returns local name with position")
170 void testFormatAssemblyWithoutNamespace() {
171 IEnhancedQName qname = IEnhancedQName.of(EMPTY_NS, "control");
172 IAssemblyNodeItem assembly = mock(IAssemblyNodeItem.class);
173 IAssemblyInstance instance = mock(IAssemblyInstance.class);
174
175 doReturn(qname).when(assembly).getQName();
176 doReturn(instance).when(assembly).getInstance();
177 doReturn(3).when(assembly).getPosition();
178 doReturn(XmlGroupAsBehavior.UNGROUPED).when(instance).getXmlGroupAsBehavior();
179
180 String result = formatter.formatAssembly(assembly);
181
182 assertEquals("control[3]", result);
183 }
184
185 @Test
186 @DisplayName("formatAssembly GROUPED with high position keeps wrapper at [1]")
187 void testFormatAssemblyGroupedWrapperAlwaysPositionOne() {
188 IEnhancedQName elementQname = IEnhancedQName.of(TEST_NS, "control");
189 IEnhancedQName wrapperQname = IEnhancedQName.of(TEST_NS, "controls");
190 IAssemblyNodeItem assembly = mock(IAssemblyNodeItem.class);
191 IAssemblyInstance instance = mock(IAssemblyInstance.class);
192
193 doReturn(elementQname).when(assembly).getQName();
194 doReturn(instance).when(assembly).getInstance();
195 doReturn(5).when(assembly).getPosition();
196 doReturn(XmlGroupAsBehavior.GROUPED).when(instance).getXmlGroupAsBehavior();
197 doReturn(wrapperQname).when(instance).getEffectiveXmlGroupAsQName();
198
199 String result = formatter.formatAssembly(assembly);
200
201
202 assertEquals("Q{" + TEST_NS + "}controls[1]/Q{" + TEST_NS + "}control[5]", result);
203 }
204
205 @Test
206 @DisplayName("formatAssembly GROUPED with null wrapper QName falls back to element only")
207 void testFormatAssemblyGroupedNullWrapper() {
208 IEnhancedQName elementQname = IEnhancedQName.of(TEST_NS, "control");
209 IAssemblyNodeItem assembly = mock(IAssemblyNodeItem.class);
210 IAssemblyInstance instance = mock(IAssemblyInstance.class);
211
212 doReturn(elementQname).when(assembly).getQName();
213 doReturn(instance).when(assembly).getInstance();
214 doReturn(1).when(assembly).getPosition();
215 doReturn(XmlGroupAsBehavior.GROUPED).when(instance).getXmlGroupAsBehavior();
216 doReturn(null).when(instance).getEffectiveXmlGroupAsQName();
217
218 String result = formatter.formatAssembly(assembly);
219
220
221 assertEquals("Q{" + TEST_NS + "}control[1]", result);
222 }
223 }
224
225 @Nested
226 @DisplayName("Grouped Assembly Instance Formatting")
227 class GroupedAssemblyInstanceTests {
228
229 @Test
230 @DisplayName("formatAssembly(IAssemblyInstanceGroupedNodeItem) UNGROUPED")
231 void testFormatGroupedAssemblyInstanceUngrouped() {
232 IEnhancedQName qname = IEnhancedQName.of(TEST_NS, "control");
233 IAssemblyInstanceGroupedNodeItem assembly = mock(IAssemblyInstanceGroupedNodeItem.class);
234 IAssemblyInstance instance = mock(IAssemblyInstance.class);
235
236 doReturn(qname).when(assembly).getQName();
237 doReturn(instance).when(assembly).getInstance();
238 doReturn(2).when(assembly).getPosition();
239 doReturn(XmlGroupAsBehavior.UNGROUPED).when(instance).getXmlGroupAsBehavior();
240
241 String result = formatter.formatAssembly(assembly);
242
243 assertEquals("Q{" + TEST_NS + "}control[2]", result);
244 }
245
246 @Test
247 @DisplayName("formatAssembly(IAssemblyInstanceGroupedNodeItem) GROUPED")
248 void testFormatGroupedAssemblyInstanceGrouped() {
249 IEnhancedQName elementQname = IEnhancedQName.of(TEST_NS, "control");
250 IEnhancedQName wrapperQname = IEnhancedQName.of(TEST_NS, "controls");
251 IAssemblyInstanceGroupedNodeItem assembly = mock(IAssemblyInstanceGroupedNodeItem.class);
252 IAssemblyInstance instance = mock(IAssemblyInstance.class);
253
254 doReturn(elementQname).when(assembly).getQName();
255 doReturn(instance).when(assembly).getInstance();
256 doReturn(3).when(assembly).getPosition();
257 doReturn(XmlGroupAsBehavior.GROUPED).when(instance).getXmlGroupAsBehavior();
258 doReturn(wrapperQname).when(instance).getEffectiveXmlGroupAsQName();
259
260 String result = formatter.formatAssembly(assembly);
261
262 assertEquals("Q{" + TEST_NS + "}controls[1]/Q{" + TEST_NS + "}control[3]", result);
263 }
264 }
265
266 @Nested
267 @DisplayName("Field Formatting")
268 class FieldTests {
269
270 @Test
271 @DisplayName("formatField UNGROUPED returns EQName with position")
272 void testFormatFieldUngrouped() {
273 IEnhancedQName qname = IEnhancedQName.of(TEST_NS, "title");
274 IFieldNodeItem field = mock(IFieldNodeItem.class);
275 IFieldInstance instance = mock(IFieldInstance.class);
276
277 doReturn(qname).when(field).getQName();
278 doReturn(instance).when(field).getInstance();
279 doReturn(1).when(field).getPosition();
280 doReturn(XmlGroupAsBehavior.UNGROUPED).when(instance).getXmlGroupAsBehavior();
281
282 String result = formatter.formatField(field);
283
284 assertEquals("Q{" + TEST_NS + "}title[1]", result);
285 }
286
287 @Test
288 @DisplayName("formatField GROUPED returns wrapper + EQName with position")
289 void testFormatFieldGrouped() {
290 IEnhancedQName elementQname = IEnhancedQName.of(TEST_NS, "prop");
291 IEnhancedQName wrapperQname = IEnhancedQName.of(TEST_NS, "props");
292 IFieldNodeItem field = mock(IFieldNodeItem.class);
293 IFieldInstance instance = mock(IFieldInstance.class);
294
295 doReturn(elementQname).when(field).getQName();
296 doReturn(instance).when(field).getInstance();
297 doReturn(2).when(field).getPosition();
298 doReturn(XmlGroupAsBehavior.GROUPED).when(instance).getXmlGroupAsBehavior();
299 doReturn(wrapperQname).when(instance).getEffectiveXmlGroupAsQName();
300
301 String result = formatter.formatField(field);
302
303 assertEquals("Q{" + TEST_NS + "}props[1]/Q{" + TEST_NS + "}prop[2]", result);
304 }
305
306 @Test
307 @DisplayName("formatField without instance returns EQName with position")
308 void testFormatFieldWithoutInstance() {
309 IEnhancedQName qname = IEnhancedQName.of(TEST_NS, "value");
310 IFieldNodeItem field = mock(IFieldNodeItem.class);
311
312 doReturn(qname).when(field).getQName();
313 doReturn(null).when(field).getInstance();
314 doReturn(1).when(field).getPosition();
315
316 String result = formatter.formatField(field);
317
318 assertEquals("Q{" + TEST_NS + "}value[1]", result);
319 }
320 }
321
322 @Nested
323 @DisplayName("Flag Formatting")
324 class FlagTests {
325
326 @Test
327 @DisplayName("formatFlag with namespace returns @EQName")
328 void testFormatFlagWithNamespace() {
329 IEnhancedQName qname = IEnhancedQName.of(TEST_NS, "id");
330 IFlagNodeItem flag = mock(IFlagNodeItem.class);
331
332 doReturn(qname).when(flag).getQName();
333
334 String result = formatter.formatFlag(flag);
335
336 assertEquals("@Q{" + TEST_NS + "}id", result);
337 }
338
339 @Test
340 @DisplayName("formatFlag without namespace returns @localname")
341 void testFormatFlagWithoutNamespace() {
342 IEnhancedQName qname = IEnhancedQName.of(EMPTY_NS, "id");
343 IFlagNodeItem flag = mock(IFlagNodeItem.class);
344
345 doReturn(qname).when(flag).getQName();
346
347 String result = formatter.formatFlag(flag);
348
349 assertEquals("@id", result);
350 }
351 }
352
353 @Nested
354 @DisplayName("Special Characters and Edge Cases")
355 class SpecialCharacterTests {
356
357 @Test
358 @DisplayName("formatAssembly with hyphenated name")
359 void testFormatAssemblyHyphenatedName() {
360 IEnhancedQName qname = IEnhancedQName.of(TEST_NS, "my-control-element");
361 IAssemblyNodeItem assembly = mock(IAssemblyNodeItem.class);
362 IAssemblyInstance instance = mock(IAssemblyInstance.class);
363
364 doReturn(qname).when(assembly).getQName();
365 doReturn(instance).when(assembly).getInstance();
366 doReturn(1).when(assembly).getPosition();
367 doReturn(XmlGroupAsBehavior.UNGROUPED).when(instance).getXmlGroupAsBehavior();
368
369 String result = formatter.formatAssembly(assembly);
370
371 assertEquals("Q{" + TEST_NS + "}my-control-element[1]", result);
372 }
373
374 @Test
375 @DisplayName("formatAssembly with underscore name")
376 void testFormatAssemblyUnderscoreName() {
377 IEnhancedQName qname = IEnhancedQName.of(TEST_NS, "my_control_element");
378 IAssemblyNodeItem assembly = mock(IAssemblyNodeItem.class);
379 IAssemblyInstance instance = mock(IAssemblyInstance.class);
380
381 doReturn(qname).when(assembly).getQName();
382 doReturn(instance).when(assembly).getInstance();
383 doReturn(1).when(assembly).getPosition();
384 doReturn(XmlGroupAsBehavior.UNGROUPED).when(instance).getXmlGroupAsBehavior();
385
386 String result = formatter.formatAssembly(assembly);
387
388 assertEquals("Q{" + TEST_NS + "}my_control_element[1]", result);
389 }
390
391 @Test
392 @DisplayName("formatFlag with numeric suffix in name")
393 void testFormatFlagNumericSuffix() {
394 IEnhancedQName qname = IEnhancedQName.of(TEST_NS, "id2");
395 IFlagNodeItem flag = mock(IFlagNodeItem.class);
396
397 doReturn(qname).when(flag).getQName();
398
399 String result = formatter.formatFlag(flag);
400
401 assertEquals("@Q{" + TEST_NS + "}id2", result);
402 }
403 }
404
405 @Nested
406 @DisplayName("Multiple Siblings Tests")
407 class MultipleSiblingsTests {
408
409 @Test
410 @DisplayName("Multiple assemblies at different positions")
411 void testMultipleAssembliesAtDifferentPositions() {
412 IEnhancedQName qname = IEnhancedQName.of(TEST_NS, "control");
413
414
415 for (int position = 1; position <= 3; position++) {
416 IAssemblyNodeItem assembly = mock(IAssemblyNodeItem.class);
417 IAssemblyInstance instance = mock(IAssemblyInstance.class);
418
419 doReturn(qname).when(assembly).getQName();
420 doReturn(instance).when(assembly).getInstance();
421 doReturn(position).when(assembly).getPosition();
422 doReturn(XmlGroupAsBehavior.UNGROUPED).when(instance).getXmlGroupAsBehavior();
423
424 String result = formatter.formatAssembly(assembly);
425
426 assertEquals("Q{" + TEST_NS + "}control[" + position + "]", result);
427 }
428 }
429
430 @Test
431 @DisplayName("Multiple fields at different positions")
432 void testMultipleFieldsAtDifferentPositions() {
433 IEnhancedQName qname = IEnhancedQName.of(TEST_NS, "prop");
434
435
436 for (int position = 1; position <= 3; position++) {
437 IFieldNodeItem field = mock(IFieldNodeItem.class);
438 IFieldInstance instance = mock(IFieldInstance.class);
439
440 doReturn(qname).when(field).getQName();
441 doReturn(instance).when(field).getInstance();
442 doReturn(position).when(field).getPosition();
443 doReturn(XmlGroupAsBehavior.UNGROUPED).when(instance).getXmlGroupAsBehavior();
444
445 String result = formatter.formatField(field);
446
447 assertEquals("Q{" + TEST_NS + "}prop[" + position + "]", result);
448 }
449 }
450 }
451
452 @Nested
453 @DisplayName("Full Path Formatting")
454 class FullPathTests {
455
456 @Test
457 @DisplayName("format produces complete XPath from document to flag")
458 void testFormatCompletePath() {
459 IEnhancedQName rootQname = IEnhancedQName.of(TEST_NS, "catalog");
460 IEnhancedQName assemblyQname = IEnhancedQName.of(TEST_NS, "control");
461 IEnhancedQName flagQname = IEnhancedQName.of(TEST_NS, "id");
462
463 IFlagNodeItem flag = mockFactory.flag(flagQname, IStringItem.valueOf("ac-1"));
464 IAssemblyNodeItem assembly = mockFactory.assembly(
465 assemblyQname,
466 CollectionUtil.singletonList(flag),
467 CollectionUtil.emptyList());
468
469
470 IAssemblyInstance assemblyInstance = mock(IAssemblyInstance.class);
471 doReturn(assemblyInstance).when(assembly).getInstance();
472 doReturn(XmlGroupAsBehavior.UNGROUPED).when(assemblyInstance).getXmlGroupAsBehavior();
473
474
475 assertNotNull(mockFactory.document(
476 URI.create("https://example.com/catalog.xml"),
477 rootQname,
478 CollectionUtil.emptyList(),
479 CollectionUtil.singletonList(assembly)));
480
481
482 String result = formatter.format(flag);
483
484
485 assertEquals("/Q{" + TEST_NS + "}catalog/Q{" + TEST_NS + "}control[1]/@Q{" + TEST_NS + "}id", result);
486 }
487
488 @Test
489 @DisplayName("format with nested assemblies produces correct path")
490 void testFormatNestedAssemblies() {
491 IEnhancedQName rootQname = IEnhancedQName.of(TEST_NS, "catalog");
492 IEnhancedQName groupQname = IEnhancedQName.of(TEST_NS, "group");
493 IEnhancedQName controlQname = IEnhancedQName.of(TEST_NS, "control");
494 IEnhancedQName flagQname = IEnhancedQName.of(TEST_NS, "id");
495
496 IFlagNodeItem flag = mockFactory.flag(flagQname, IStringItem.valueOf("ac-1"));
497
498 IAssemblyNodeItem control = mockFactory.assembly(
499 controlQname,
500 CollectionUtil.singletonList(flag),
501 CollectionUtil.emptyList());
502 IAssemblyInstance controlInstance = mock(IAssemblyInstance.class);
503 doReturn(controlInstance).when(control).getInstance();
504 doReturn(XmlGroupAsBehavior.UNGROUPED).when(controlInstance).getXmlGroupAsBehavior();
505
506 IAssemblyNodeItem group = mockFactory.assembly(
507 groupQname,
508 CollectionUtil.emptyList(),
509 CollectionUtil.singletonList(control));
510 IAssemblyInstance groupInstance = mock(IAssemblyInstance.class);
511 doReturn(groupInstance).when(group).getInstance();
512 doReturn(XmlGroupAsBehavior.UNGROUPED).when(groupInstance).getXmlGroupAsBehavior();
513
514
515 assertNotNull(mockFactory.document(
516 URI.create("https://example.com/catalog.xml"),
517 rootQname,
518 CollectionUtil.emptyList(),
519 CollectionUtil.singletonList(group)));
520
521 String result = formatter.format(flag);
522
523 assertEquals(
524 "/Q{" + TEST_NS + "}catalog/Q{" + TEST_NS + "}group[1]/Q{" + TEST_NS + "}control[1]/@Q{" + TEST_NS + "}id",
525 result);
526 }
527
528 @Test
529 @DisplayName("format with GROUPED assembly in path")
530 void testFormatWithGroupedAssemblyInPath() {
531 IEnhancedQName rootQname = IEnhancedQName.of(TEST_NS, "catalog");
532 IEnhancedQName controlQname = IEnhancedQName.of(TEST_NS, "control");
533 IEnhancedQName wrapperQname = IEnhancedQName.of(TEST_NS, "controls");
534 IEnhancedQName flagQname = IEnhancedQName.of(TEST_NS, "id");
535
536 IFlagNodeItem flag = mockFactory.flag(flagQname, IStringItem.valueOf("ac-1"));
537
538 IAssemblyNodeItem control = mockFactory.assembly(
539 controlQname,
540 CollectionUtil.singletonList(flag),
541 CollectionUtil.emptyList());
542 IAssemblyInstance controlInstance = mock(IAssemblyInstance.class);
543 doReturn(controlInstance).when(control).getInstance();
544 doReturn(XmlGroupAsBehavior.GROUPED).when(controlInstance).getXmlGroupAsBehavior();
545 doReturn(wrapperQname).when(controlInstance).getEffectiveXmlGroupAsQName();
546
547
548 assertNotNull(mockFactory.document(
549 URI.create("https://example.com/catalog.xml"),
550 rootQname,
551 CollectionUtil.emptyList(),
552 CollectionUtil.singletonList(control)));
553
554 String result = formatter.format(flag);
555
556 assertEquals(
557 "/Q{" + TEST_NS + "}catalog/Q{" + TEST_NS + "}controls[1]/Q{" + TEST_NS + "}control[1]/@Q{" + TEST_NS + "}id",
558 result);
559 }
560
561 @Test
562 @DisplayName("format document node produces single slash")
563 void testFormatDocumentNode() {
564 IEnhancedQName rootQname = IEnhancedQName.of(TEST_NS, "catalog");
565
566 IDocumentNodeItem document = mockFactory.document(
567 URI.create("https://example.com/catalog.xml"),
568 rootQname,
569 CollectionUtil.emptyList(),
570 CollectionUtil.emptyList());
571
572 String result = formatter.format(document);
573
574
575
576 assertEquals("", result);
577 }
578 }
579 }