1// Copyright 2025 The CUE Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package koala_test
16
17import (
18 "strings"
19 "testing"
20
21 "github.com/go-quicktest/qt"
22
23 "cuelang.org/go/cue"
24 "cuelang.org/go/cue/ast/astutil"
25 "cuelang.org/go/cue/cuecontext"
26 "cuelang.org/go/cue/errors"
27 "cuelang.org/go/cue/format"
28 "cuelang.org/go/encoding/xml/koala"
29)
30
31func TestErrorReporting(t *testing.T) {
32 t.Parallel()
33 tests := []struct {
34 name string
35 inputXML string
36 cueConstraints string
37 expectedError string
38 }{{
39 name: "Element Text Content Constraint Error",
40 inputXML: `<?xml version="1.0" encoding="UTF-8"?>
41 <test v="v2.1">
42 <edge n="2.65" o="3.65"/>
43 <container id="555"/>
44 <container id="777"/>
45 <container id="888" >
46 <l attr="x"/>
47 <l attr="y"/>
48 </container>
49 <text>content</text>
50 </test>`,
51 cueConstraints: `test: {
52 $v: string
53 edge: {
54 $n: string
55 $o: string
56 }
57 container: [...{
58 $id: string
59 l: [...{
60 $attr: string
61 }]
62 }]
63 text: {
64 $$: int
65 }
66 }`,
67 expectedError: "test.text.$$: conflicting values int and \"content\" (mismatched types int and string):\n input.xml:10:10\n schema.cue:14:8\n",
68 }, {
69 name: "Attribute Constraint Error",
70 inputXML: `<?xml version="1.0" encoding="UTF-8"?>
71 <test v="v2.1">
72 <edge n="2.65" o="3.65"/>
73 <container id="555"/>
74 <container id="777"/>
75 <container id="888" >
76 <l attr="x"/>
77 <l attr="y"/>
78 </container>
79 <text>content</text>
80 </test>`,
81 cueConstraints: `test: {
82 $v: int
83 edge: {
84 $n: string
85 $o: string
86 }
87 container: [...{
88 $id: string
89 l: [...{
90 $attr: string
91 }]
92 }]
93 text: {
94 $$: string
95 }
96 }`,
97 expectedError: "test.$v: conflicting values int and \"v2.1\" (mismatched types int and string):\n input.xml:2:3\n schema.cue:2:7\n",
98 },
99 {
100 name: "Attribute Constraint Error on self-closing element",
101 inputXML: `<?xml version="1.0" encoding="UTF-8"?>
102 <test v="v2.1">
103 <edge n="2.65" o="3.65"/>
104 <container id="555"/>
105 <container id="777"/>
106 <container id="888" >
107 <l attr="x"/>
108 <l attr="y"/>
109 </container>
110 <text>content</text>
111 </test>`,
112 cueConstraints: `test: {
113 $v: string
114 edge: {
115 $n: int
116 $o: string
117 }
118 container: [...{
119 $id: string
120 l: [...{
121 $attr: string
122 }]
123 }]
124 text: {
125 $$: string
126 }
127 }`,
128 expectedError: "test.edge.$n: conflicting values int and \"2.65\" (mismatched types int and string):\n input.xml:3:4\n schema.cue:4:8\n",
129 },
130 }
131
132 for _, test := range tests {
133 t.Run(test.name, func(t *testing.T) {
134 t.Parallel()
135
136 fileName := "input.xml"
137 dec := koala.NewDecoder(fileName, strings.NewReader(test.inputXML))
138
139 cueExpr, err := dec.Decode()
140
141 qt.Assert(t, qt.IsNil(err))
142
143 rootCueFile, err := astutil.ToFile(cueExpr)
144 qt.Assert(t, qt.IsNil(err))
145
146 c := cuecontext.New()
147 rootCueVal := c.BuildFile(rootCueFile, cue.Filename(fileName))
148
149 // compile some CUE into a Value
150 compiledSchema := c.CompileString(test.cueConstraints, cue.Filename("schema.cue"))
151
152 //unify the compiledSchema against the formattedConfig
153 unified := compiledSchema.Unify(rootCueVal)
154
155 actualError := ""
156 if err := unified.Validate(cue.Concrete(true)); err != nil {
157 actualError = errors.Details(err, nil)
158 }
159
160 qt.Assert(t, qt.Equals(actualError, test.expectedError))
161 })
162 }
163}
164
165func TestElementDecoding(t *testing.T) {
166 t.Parallel()
167
168 tests := []struct {
169 name string
170 inputXML string
171 wantCUE string
172 }{{
173 name: "Simple Elements",
174 inputXML: `<note>
175 <to> </to>
176 <from>Jani</from>
177 <heading>Reminder</heading>
178 <body>Don't forget me this weekend!</body>
179</note>`,
180 wantCUE: `note: {
181 to: $$: " "
182 from: $$: "Jani"
183 heading: $$: "Reminder"
184 body: $$: "Don't forget me this weekend!"
185}
186`,
187 },
188 {
189 name: "Simple self-closing element",
190 inputXML: `<note>
191 <to/>
192 <from>Jani</from>
193 <heading>Reminder</heading>
194 <body>Don't forget me this weekend!</body>
195</note>`,
196 wantCUE: `note: {
197 to: {}
198 from: $$: "Jani"
199 heading: $$: "Reminder"
200 body: $$: "Don't forget me this weekend!"
201}
202`,
203 },
204 {
205 name: "Attribute",
206 inputXML: `<note alpha="abcd">
207 <to>Tove</to>
208 <from>Jani</from>
209 <heading>Reminder</heading>
210 <body>Don't forget me this weekend!</body>
211</note>`,
212 wantCUE: `note: {
213 $alpha: "abcd"
214 to: $$: "Tove"
215 from: $$: "Jani"
216 heading: $$: "Reminder"
217 body: $$: "Don't forget me this weekend!"
218}
219`,
220 },
221 {
222 name: "Attribute and Element with the same name",
223 inputXML: `<note alpha="abcd">
224 <to>Tove</to>
225 <from>Jani</from>
226 <heading>Reminder</heading>
227 <body>Don't forget me this weekend!</body>
228 <alpha>efgh</alpha>
229</note>`,
230 wantCUE: `note: {
231 $alpha: "abcd"
232 to: $$: "Tove"
233 from: $$: "Jani"
234 heading: $$: "Reminder"
235 body: $$: "Don't forget me this weekend!"
236 alpha: $$: "efgh"
237}
238`,
239 },
240 {
241 name: "Mapping for content when an attribute exists",
242 inputXML: `<note alpha="abcd">
243 hello
244</note>`,
245 wantCUE: `note: {
246 $alpha: "abcd"
247 $$: "\n\thello\n"
248}
249`,
250 },
251 {
252 name: "Nested Element",
253 inputXML: `<notes>
254 <note alpha="abcd">hello</note>
255</notes>`,
256 wantCUE: `notes: note: {
257 $alpha: "abcd"
258 $$: "hello"
259}
260`,
261 },
262 {
263 name: "Collections",
264 inputXML: `<notes>
265 <note alpha="abcd">hello</note>
266 <note alpha="abcdef">goodbye</note>
267</notes>`,
268 wantCUE: `notes: note: [{
269 $alpha: "abcd"
270 $$: "hello"
271}, {
272 $alpha: "abcdef"
273 $$: "goodbye"
274}]
275`,
276 },
277 {
278 name: "Interleaving Element Types",
279 inputXML: `<notes>
280 <note alpha="abcd">hello</note>
281 <note alpha="abcdef">goodbye</note>
282 <book>mybook</book>
283 <note alpha="ab">goodbye</note>
284 <note>direct</note>
285</notes>`,
286 wantCUE: `notes: {
287 note: [{
288 $alpha: "abcd"
289 $$: "hello"
290 }, {
291 $alpha: "abcdef"
292 $$: "goodbye"
293 }, {
294 $alpha: "ab"
295 $$: "goodbye"
296 }, {
297 $$: "direct"
298 }]
299 book: $$: "mybook"
300}
301`,
302 },
303 {
304 name: "Namespaces",
305 inputXML: `<h:table xmlns:h="http://www.w3.org/TR/html4/">
306 <h:tr>
307 <h:td>Apples</h:td>
308 <h:td>Bananas</h:td>
309 </h:tr>
310</h:table>`,
311 wantCUE: `"h:table": {
312 "$xmlns:h": "http://www.w3.org/TR/html4/"
313 "h:tr": "h:td": [{
314 $$: "Apples"
315 }, {
316 $$: "Bananas"
317 }]
318}
319`,
320 },
321 {
322 name: "Attribute namespace prefix",
323 inputXML: `<h:table xmlns:h="http://www.w3.org/TR/html4/" xmlns:f="http://www.w3.org/TR/html5/">
324 <h:tr>
325 <h:td f:type="fruit">Apples</h:td>
326 <h:td>Bananas</h:td>
327 </h:tr>
328</h:table>`,
329 wantCUE: `"h:table": {
330 "$xmlns:h": "http://www.w3.org/TR/html4/"
331 "$xmlns:f": "http://www.w3.org/TR/html5/"
332 "h:tr": "h:td": [{
333 "$f:type": "fruit"
334 $$: "Apples"
335 }, {
336 $$: "Bananas"
337 }]
338}
339`,
340 },
341 {
342 name: "Mixed Namespaces",
343 inputXML: `<h:table xmlns:h="http://www.w3.org/TR/html4/" xmlns:r="d">
344 <h:tr>
345 <h:td>Apples</h:td>
346 <h:td>Bananas</h:td>
347 <r:blah>e3r</r:blah>
348 </h:tr>
349</h:table>`,
350 wantCUE: `"h:table": {
351 "$xmlns:h": "http://www.w3.org/TR/html4/"
352 "$xmlns:r": "d"
353 "h:tr": {
354 "h:td": [{
355 $$: "Apples"
356 }, {
357 $$: "Bananas"
358 }]
359 "r:blah": $$: "e3r"
360 }
361}
362`,
363 },
364 {
365 name: "Elements with same name but different namespaces",
366 inputXML: `<h:table xmlns:h="http://www.w3.org/TR/html4/" xmlns:r="d">
367 <h:tr>
368 <h:td>Apples</h:td>
369 <h:td>Bananas</h:td>
370 <r:td>e3r</r:td>
371 </h:tr>
372</h:table>`,
373 wantCUE: `"h:table": {
374 "$xmlns:h": "http://www.w3.org/TR/html4/"
375 "$xmlns:r": "d"
376 "h:tr": {
377 "h:td": [{
378 $$: "Apples"
379 }, {
380 $$: "Bananas"
381 }]
382 "r:td": $$: "e3r"
383 }
384}
385`,
386 },
387 {
388 name: "Collection of elements, where elements have optional properties",
389 inputXML: `<books>
390 <book>
391 <title>title</title>
392 <author>John Doe</author>
393 </book>
394 <book>
395 <title>title2</title>
396 <author>Jane Doe</author>
397 </book>
398 <book>
399 <title>Lord of the rings</title>
400 <author>JRR Tolkien</author>
401 <volume>
402 <title>Fellowship</title>
403 <author>JRR Tolkien</author>
404 </volume>
405 <volume>
406 <title>Two Towers</title>
407 <author>JRR Tolkien</author>
408 </volume>
409 <volume>
410 <title>Return of the King</title>
411 <author>JRR Tolkien</author>
412 </volume>
413 </book>
414</books>`,
415 wantCUE: `books: book: [{
416 title: $$: "title"
417 author: $$: "John Doe"
418}, {
419 title: $$: "title2"
420 author: $$: "Jane Doe"
421}, {
422 title: $$: "Lord of the rings"
423 author: $$: "JRR Tolkien"
424 volume: [{
425 title: $$: "Fellowship"
426 author: $$: "JRR Tolkien"
427 }, {
428 title: $$: "Two Towers"
429 author: $$: "JRR Tolkien"
430 }, {
431 title: $$: "Return of the King"
432 author: $$: "JRR Tolkien"
433 }]
434}]
435`,
436 },
437 {
438 name: "Carriage Return Filter Test",
439 inputXML: "<node>\r\nhello\r\n</node>",
440 wantCUE: `node: $$: "\nhello\n"
441`,
442 },
443 {
444 name: "Spacing either side of xml (including new lines before and after root node)",
445 inputXML: `
446
447 <root>
448 <message>Hello World!</message>
449 <nested>
450 <a1>one level</a1>
451 <a2>
452 <b>two levels</b>
453 </a2>
454 </nested>
455 </root>
456
457 `,
458 wantCUE: `root: {
459 message: $$: "Hello World!"
460 nested: {
461 a1: $$: "one level"
462 a2: b: $$: "two levels"
463 }
464}
465`,
466 },
467 }
468
469 for _, test := range tests {
470 t.Run(test.name, func(t *testing.T) {
471 t.Parallel()
472
473 dec := koala.NewDecoder("input.xml", strings.NewReader(test.inputXML))
474 cueExpr, err := dec.Decode()
475
476 qt.Assert(t, qt.IsNil(err))
477
478 rootCueFile, err := astutil.ToFile(cueExpr)
479 qt.Assert(t, qt.IsNil(err))
480
481 actualCue, err := format.Node(rootCueFile)
482
483 qt.Assert(t, qt.IsNil(err))
484 qt.Assert(t, qt.Equals(string(actualCue), test.wantCUE))
485 })
486 }
487}
488
489func TestErrors(t *testing.T) {
490 t.Parallel()
491
492 tests := []struct {
493 name string
494 inputXML string
495 expectedError string
496 }{
497 {
498 name: "Text after root node followed by subelements",
499 inputXML: `<note>
500 mixed
501 <from>Jani</from>
502 <heading>Reminder</heading>
503 <body>Don't forget me this weekend!</body>
504 </note>`,
505 expectedError: `text content within an XML element that has sub-elements is not supported`,
506 },
507 {
508 name: "Text in middle of subelements",
509 inputXML: `<note>
510 <to/>
511 mixed
512 <from>Jani</from>
513 <heading>Reminder</heading>
514 <body>Don't forget me this weekend!</body>
515 </note>`,
516 expectedError: `text content within an XML element that has sub-elements is not supported`,
517 },
518 {
519 name: "Nested mixed content",
520 inputXML: `<note>
521 <to/>
522 <from>Jani <subElement/></from>
523 <heading>Reminder</heading>
524 <body>Don't forget me this weekend!</body>
525 </note>`,
526 expectedError: `text content within an XML element that has sub-elements is not supported`,
527 },
528 {
529 name: "Text before end of root element",
530 inputXML: `<note>
531 <to/>
532 <from></from>
533 <heading>Reminder</heading>
534 myText
535 </note>`,
536 expectedError: `text content within an XML element that has sub-elements is not supported`,
537 },
538 }
539
540 for _, test := range tests {
541 t.Run(test.name, func(t *testing.T) {
542 t.Parallel()
543
544 dec := koala.NewDecoder("input.xml", strings.NewReader(test.inputXML))
545 _, err := dec.Decode()
546
547 qt.Assert(t, qt.ErrorMatches(err, test.expectedError))
548 })
549 }
550}