// Copyright 2025 The CUE Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package koala_test import ( "strings" "testing" "github.com/go-quicktest/qt" "cuelang.org/go/cue" "cuelang.org/go/cue/ast/astutil" "cuelang.org/go/cue/cuecontext" "cuelang.org/go/cue/errors" "cuelang.org/go/cue/format" "cuelang.org/go/encoding/xml/koala" ) func TestErrorReporting(t *testing.T) { t.Parallel() tests := []struct { name string inputXML string cueConstraints string expectedError string }{{ name: "Element Text Content Constraint Error", inputXML: ` content `, cueConstraints: `test: { $v: string edge: { $n: string $o: string } container: [...{ $id: string l: [...{ $attr: string }] }] text: { $$: int } }`, expectedError: "test.text.$$: conflicting values int and \"content\" (mismatched types int and string):\n input.xml:10:10\n schema.cue:14:8\n", }, { name: "Attribute Constraint Error", inputXML: ` content `, cueConstraints: `test: { $v: int edge: { $n: string $o: string } container: [...{ $id: string l: [...{ $attr: string }] }] text: { $$: string } }`, 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", }, { name: "Attribute Constraint Error on self-closing element", inputXML: ` content `, cueConstraints: `test: { $v: string edge: { $n: int $o: string } container: [...{ $id: string l: [...{ $attr: string }] }] text: { $$: string } }`, 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", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { t.Parallel() fileName := "input.xml" dec := koala.NewDecoder(fileName, strings.NewReader(test.inputXML)) cueExpr, err := dec.Decode() qt.Assert(t, qt.IsNil(err)) rootCueFile, err := astutil.ToFile(cueExpr) qt.Assert(t, qt.IsNil(err)) c := cuecontext.New() rootCueVal := c.BuildFile(rootCueFile, cue.Filename(fileName)) // compile some CUE into a Value compiledSchema := c.CompileString(test.cueConstraints, cue.Filename("schema.cue")) //unify the compiledSchema against the formattedConfig unified := compiledSchema.Unify(rootCueVal) actualError := "" if err := unified.Validate(cue.Concrete(true)); err != nil { actualError = errors.Details(err, nil) } qt.Assert(t, qt.Equals(actualError, test.expectedError)) }) } } func TestElementDecoding(t *testing.T) { t.Parallel() tests := []struct { name string inputXML string wantCUE string }{{ name: "Simple Elements", inputXML: ` Jani Reminder Don't forget me this weekend! `, wantCUE: `note: { to: $$: " " from: $$: "Jani" heading: $$: "Reminder" body: $$: "Don't forget me this weekend!" } `, }, { name: "Simple self-closing element", inputXML: ` Jani Reminder Don't forget me this weekend! `, wantCUE: `note: { to: {} from: $$: "Jani" heading: $$: "Reminder" body: $$: "Don't forget me this weekend!" } `, }, { name: "Attribute", inputXML: ` Tove Jani Reminder Don't forget me this weekend! `, wantCUE: `note: { $alpha: "abcd" to: $$: "Tove" from: $$: "Jani" heading: $$: "Reminder" body: $$: "Don't forget me this weekend!" } `, }, { name: "Attribute and Element with the same name", inputXML: ` Tove Jani Reminder Don't forget me this weekend! efgh `, wantCUE: `note: { $alpha: "abcd" to: $$: "Tove" from: $$: "Jani" heading: $$: "Reminder" body: $$: "Don't forget me this weekend!" alpha: $$: "efgh" } `, }, { name: "Mapping for content when an attribute exists", inputXML: ` hello `, wantCUE: `note: { $alpha: "abcd" $$: "\n\thello\n" } `, }, { name: "Nested Element", inputXML: ` hello `, wantCUE: `notes: note: { $alpha: "abcd" $$: "hello" } `, }, { name: "Collections", inputXML: ` hello goodbye `, wantCUE: `notes: note: [{ $alpha: "abcd" $$: "hello" }, { $alpha: "abcdef" $$: "goodbye" }] `, }, { name: "Interleaving Element Types", inputXML: ` hello goodbye mybook goodbye direct `, wantCUE: `notes: { note: [{ $alpha: "abcd" $$: "hello" }, { $alpha: "abcdef" $$: "goodbye" }, { $alpha: "ab" $$: "goodbye" }, { $$: "direct" }] book: $$: "mybook" } `, }, { name: "Namespaces", inputXML: ` Apples Bananas `, wantCUE: `"h:table": { "$xmlns:h": "http://www.w3.org/TR/html4/" "h:tr": "h:td": [{ $$: "Apples" }, { $$: "Bananas" }] } `, }, { name: "Attribute namespace prefix", inputXML: ` Apples Bananas `, wantCUE: `"h:table": { "$xmlns:h": "http://www.w3.org/TR/html4/" "$xmlns:f": "http://www.w3.org/TR/html5/" "h:tr": "h:td": [{ "$f:type": "fruit" $$: "Apples" }, { $$: "Bananas" }] } `, }, { name: "Mixed Namespaces", inputXML: ` Apples Bananas e3r `, wantCUE: `"h:table": { "$xmlns:h": "http://www.w3.org/TR/html4/" "$xmlns:r": "d" "h:tr": { "h:td": [{ $$: "Apples" }, { $$: "Bananas" }] "r:blah": $$: "e3r" } } `, }, { name: "Elements with same name but different namespaces", inputXML: ` Apples Bananas e3r `, wantCUE: `"h:table": { "$xmlns:h": "http://www.w3.org/TR/html4/" "$xmlns:r": "d" "h:tr": { "h:td": [{ $$: "Apples" }, { $$: "Bananas" }] "r:td": $$: "e3r" } } `, }, { name: "Collection of elements, where elements have optional properties", inputXML: ` title John Doe title2 Jane Doe Lord of the rings JRR Tolkien Fellowship JRR Tolkien Two Towers JRR Tolkien Return of the King JRR Tolkien `, wantCUE: `books: book: [{ title: $$: "title" author: $$: "John Doe" }, { title: $$: "title2" author: $$: "Jane Doe" }, { title: $$: "Lord of the rings" author: $$: "JRR Tolkien" volume: [{ title: $$: "Fellowship" author: $$: "JRR Tolkien" }, { title: $$: "Two Towers" author: $$: "JRR Tolkien" }, { title: $$: "Return of the King" author: $$: "JRR Tolkien" }] }] `, }, { name: "Carriage Return Filter Test", inputXML: "\r\nhello\r\n", wantCUE: `node: $$: "\nhello\n" `, }, { name: "Spacing either side of xml (including new lines before and after root node)", inputXML: ` Hello World! one level two levels `, wantCUE: `root: { message: $$: "Hello World!" nested: { a1: $$: "one level" a2: b: $$: "two levels" } } `, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { t.Parallel() dec := koala.NewDecoder("input.xml", strings.NewReader(test.inputXML)) cueExpr, err := dec.Decode() qt.Assert(t, qt.IsNil(err)) rootCueFile, err := astutil.ToFile(cueExpr) qt.Assert(t, qt.IsNil(err)) actualCue, err := format.Node(rootCueFile) qt.Assert(t, qt.IsNil(err)) qt.Assert(t, qt.Equals(string(actualCue), test.wantCUE)) }) } } func TestErrors(t *testing.T) { t.Parallel() tests := []struct { name string inputXML string expectedError string }{ { name: "Text after root node followed by subelements", inputXML: ` mixed Jani Reminder Don't forget me this weekend! `, expectedError: `text content within an XML element that has sub-elements is not supported`, }, { name: "Text in middle of subelements", inputXML: ` mixed Jani Reminder Don't forget me this weekend! `, expectedError: `text content within an XML element that has sub-elements is not supported`, }, { name: "Nested mixed content", inputXML: ` Jani Reminder Don't forget me this weekend! `, expectedError: `text content within an XML element that has sub-elements is not supported`, }, { name: "Text before end of root element", inputXML: ` Reminder myText `, expectedError: `text content within an XML element that has sub-elements is not supported`, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { t.Parallel() dec := koala.NewDecoder("input.xml", strings.NewReader(test.inputXML)) _, err := dec.Decode() qt.Assert(t, qt.ErrorMatches(err, test.expectedError)) }) } }