cuelang.org/go@v0.13.0/encoding/xml/koala/decode_test.go (about)

     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  
    15  package koala_test
    16  
    17  import (
    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  
    31  func 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  
   165  func 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, format.Simplify())
   482  
   483  			qt.Assert(t, qt.IsNil(err))
   484  			qt.Assert(t, qt.Equals(string(actualCue), test.wantCUE))
   485  		})
   486  	}
   487  }
   488  
   489  func 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  }