diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 2091ca0..1076194 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -12,5 +12,5 @@ jobs: uses: ballerina-platform/ballerina-standard-library/.github/workflows/release-package-template.yml@main secrets: inherit with: - package-name: persist + package-name: data.xmldata package-org: ballerina diff --git a/README.md b/README.md index 179a6a0..41dfa7d 100644 --- a/README.md +++ b/README.md @@ -31,11 +31,11 @@ public function main() returns error? { io:println(book); } -type Book record {| +type Book record { int id; string title; string author; -|}; +}; ``` ### Converting an external XML document to a Record value @@ -52,11 +52,11 @@ public function main() returns error? { io:println(book); } -type Book record {| +type Book record { int id; string title; string author; -|}; +}; ``` Make sure to handle possible errors that may arise during the file reading or XML to record conversion process. The `check` keyword is utilized to handle these errors, but more sophisticated error handling can be implemented as per your requirements. @@ -69,7 +69,7 @@ Take for instance the following XML snippet: ```xml - 0 + 601970 string string @@ -80,11 +80,11 @@ XML data is inherently hierarchical, forming a tree structure. In the given exam A straightforward record representation of the above XML data is: ```ballerina -type Book record {| +type Book record { int id; string title; string author; -|}; +}; ``` In this representation, the XML data is efficiently translated into a record value. The `book` element is mapped to a record of type `Book`, and the child elements `id`, `title`, and `author` are converted into record fields of types `int` and `string` correspondingly. @@ -99,7 +99,7 @@ Consider the XML snippet: ```xml - 0 + 601970 string string @@ -108,27 +108,27 @@ Consider the XML snippet: The canonical representation of the above XML as a Ballerina record is: ```ballerina -type Book record {| +type Book record { int id; - string 'title\-name'; - string 'author\-name'; -|}; + string title\-name; + string author\-name; +}; ``` -Observe how the XML element names `title-name` and `author-name` are represented using delimited identifiers in Ballerina; the `-` characters in the XML element names are escaped using the `\` character. +Observe how the XML element names `title-name` and `author-name` are represented using delimited identifiers in Ballerina; the `-` characters in the XML element names are escaped using the `\ ` character. Moreover, the `@Name` annotation can be utilized to explicitly specify the name of the record field, providing control over the translation process: ```ballerina import ballerina/data.xmldata; -type Book record {| +type Book record { int id; @xmldata:Name { value: "title-name" } string title; @xmldata:Name { value: "author-name" } string author; -|}; +}; ``` ### XML Attributes @@ -139,7 +139,7 @@ Consider the following XML snippet: ```xml - 0 + 601970 string string @@ -148,16 +148,16 @@ Consider the following XML snippet: The canonical representation of the above XML as a Ballerina record is: ```ballerina -type Book record {| +type Book record { string lang; decimal price; int id; string title; string author; -|}; +}; ``` -Additionally the `@Attribute` annotation can be utilized to explicitly specify the name of the record field, providing control over the translation process. +Additionally, the `@Attribute` annotation can be used to explicitly specify the field as an attribute providing control over the translation process. When element and attribute have same name in the same scope the priority is given to the element unless the expected record field has the `@Attribute` annotation. ### Child Elements @@ -167,7 +167,7 @@ Examine the XML snippet below: ```xml - 0 + 601970 string string @@ -179,16 +179,16 @@ Examine the XML snippet below: The canonical representation of the above XML as a Ballerina record is: ```ballerina -type Book record {| +type Book record { int id; string title; Author author; -|}; +}; -type Author record {| +type Author record { string name; string country; -|}; +}; ``` In this transformation, child elements, like the `author` element containing its own sub-elements, are converted into nested records. This maintains the hierarchical structure of the XML data within the Ballerina type system, enabling intuitive and type-safe data manipulation. @@ -198,14 +198,14 @@ Alternatively, inline type definitions offer a compact method for representing c Consider the subsequent Ballerina record definition, which employs inline type definition for the `author` field: ```ballerina -type Book record {| +type Book record { int id; string title; - record {| + record { string name; string country; - |} author; -|}; + } author; +}; ``` ### XML Text Content @@ -216,7 +216,7 @@ Consider the XML snippet below: ```xml - 0 + 601970 string string true @@ -227,13 +227,13 @@ Consider the XML snippet below: The translation into a Ballerina record would be as follows: ```ballerina -type Book record {| +type Book record { int id; string title; string author; boolean available; decimal price; -|}; +}; ``` In scenarios where the parent XML element of text content also includes attributes, the XML text content can be represented by a `string` type field named `#content` within a record type, with the attributes being mapped to their respective fields. @@ -242,7 +242,7 @@ For instance, examine this XML: ```xml - 0 + 601970 string 10.5 @@ -251,20 +251,18 @@ For instance, examine this XML: The canonical translation of XML to a Ballerina record is as such: ```ballerina -type Book record {| +type Book record { int id; Title title; decimal price; -|}; +}; -type Title record {| +type Title record { string \#content; string lang; -|}; +}; ``` -Modifications to the default behavior for converting numerical values can be achieved by providing `Options` mappings to the respective functions. This enables developers to choose specific data types and exert finer control over the conversion process. - ### XML Namespaces XML namespaces are accommodated by the library, supporting the translation of XML data that contains namespace prefixes. However, the presence of XML namespaces is not mandatory, and the library is capable of processing XML data without namespaces. Should namespaces be present, they will be utilized to resolve the names of XML elements and attributes. @@ -275,7 +273,7 @@ Examine the XML snippet below with default namespaces: ```xml - 0 + 601970 string string @@ -284,11 +282,11 @@ Examine the XML snippet below with default namespaces: The translation into a Ballerina record would be: ```ballerina -type Book record {| +type Book record { int id; string title; string author; -|}; +}; ``` Incorporating namespace validation yields: @@ -299,18 +297,18 @@ import ballerina/data.xmldata; @xmldata:Namespace { uri: "http://example.com/book" } -type Book record {| +type Book record { int id; string title; string author; -|}; +}; ``` Here is the same XML snippet with a namespace prefix: ```xml - 0 + 601970 string string @@ -321,13 +319,59 @@ The translation into a Ballerina record would be: ```ballerina import ballerina/data.xmldata; +@xmldata:Namespace { + uri: "http://example.com/book" +} +type Book record {| + @xmldata:Namespace { + uri: "http://example.com/book" + } + int id; + @xmldata:Namespace { + uri: "http://example.com/book" + } + string title; + @xmldata:Namespace { + uri: "http://example.com/book" + } + string author; +|}; +``` + +Here is the same XML snippet with a namespace prefix: + +```xml + + 601970 + string + string + +``` + +The translation into a Ballerina record would be: + +```ballerina +import ballerina/data.xmldata; + @xmldata:Namespace { uri: "http://example.com/book", prefix: "bk" } type Book record {| + @xmldata:Namespace { + uri: "http://example.com/book", + prefix: "bk" + } int id; + @xmldata:Namespace { + uri: "http://example.com/book", + prefix: "bk" + } string title; + @xmldata:Namespace { + uri: "http://example.com/author", + prefix: "au" + } string author; |}; ``` @@ -342,7 +386,7 @@ Take the following XML snippet as an example: ```xml - 0 + 601970 string string string @@ -353,11 +397,11 @@ Take the following XML snippet as an example: The canonical representation of this XML as a Ballerina record is: ```ballerina -type Book record {| +type Book record { int id; string title; string[] author; -|}; +}; ``` ### Controlling Which Elements to Convert @@ -368,7 +412,7 @@ Take this XML snippet as an example: ```xml - 0 + 601970 string string 10.5 @@ -387,10 +431,10 @@ type Book record {| However, if the rest field is utilized (or if the record type is defined as an open record), all elements in the XML data will be transformed into record fields: ```ballerina -type Book record {| +type Book record { int id; string title; -|}; +}; ``` In this instance, all other elements in the XML data, such as `author` and `price` along with their attributes, will be transformed into `string` type fields with the corresponding element name as the key. diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index 72f618c..713a8ca 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -6,7 +6,7 @@ authors = ["Ballerina"] keywords = ["xml"] repository = "https://github.com/ballerina-platform/module-ballerina-data-xmldata" license = ["Apache-2.0"] -distribution = "2201.8.1" +distribution = "2201.8.5" export = ["data.xmldata"] [[platform.java17.dependency]] diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index eb35607..5e8838a 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -5,7 +5,7 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.8.1" +distribution-version = "2201.8.4" [[package]] org = "ballerina" diff --git a/ballerina/tests/fromXml_test.bal b/ballerina/tests/fromXml_test.bal index 6ed5ca5..59ec2f2 100644 --- a/ballerina/tests/fromXml_test.bal +++ b/ballerina/tests/fromXml_test.bal @@ -13,7 +13,6 @@ // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. - import ballerina/test; type Data record {| @@ -154,9 +153,9 @@ function testXmlStringToRecord3() returns error? { string xmlStr3 = "123"; Data5 rec3 = check fromXmlStringWithType(xmlStr3); test:assertEquals(rec3.A.length(), 3); - test:assertEquals(rec3.A[0].B.get("#content"), "1"); - test:assertEquals(rec3.A[1].B.get("#content"), "2"); - test:assertEquals(rec3.A[2].B.get("#content"), "3"); + test:assertEquals(rec3.A[0].B.get("#content"), 1); + test:assertEquals(rec3.A[1].B.get("#content"), 2); + test:assertEquals(rec3.A[2].B.get("#content"), 3); } @test:Config { @@ -176,9 +175,9 @@ function testXmlToRecord3() returns error? { xml xmlVal3 = xml `123`; Data5 rec3 = check fromXmlWithType(xmlVal3); test:assertEquals(rec3.A.length(), 3); - test:assertEquals(rec3.A[0].B.get("#content"), "1"); - test:assertEquals(rec3.A[1].B.get("#content"), "2"); - test:assertEquals(rec3.A[2].B.get("#content"), "3"); + test:assertEquals(rec3.A[0].B.get("#content"), 1); + test:assertEquals(rec3.A[1].B.get("#content"), 2); + test:assertEquals(rec3.A[2].B.get("#content"), 3); } type Data6 record {| @@ -326,7 +325,7 @@ function testXmlStringToRecord6() returns error? { 2.0 `; Rec1 rec1 = check fromXmlStringWithType(xmlStr1); - test:assertEquals(rec1.A.get("#content"), "1"); + test:assertEquals(rec1.A.get("#content"), 1); test:assertEquals(rec1.B.length(), 2); test:assertEquals(rec1.B[0].get("#content"), 1.0); test:assertEquals(rec1.B[1].get("#content"), 2.0); @@ -342,7 +341,7 @@ function testXmlToRecord6() returns error? { 2.0 `; Rec1 rec1 = check fromXmlWithType(xmlVal1); - test:assertEquals(rec1.A.get("#content"), "1"); + test:assertEquals(rec1.A.get("#content"), 1); test:assertEquals(rec1.B.length(), 2); test:assertEquals(rec1.B[0].get("#content"), 1.0); test:assertEquals(rec1.B[1].get("#content"), 2.0); @@ -419,7 +418,7 @@ function testXmlStringToRecord21() returns error? { string xmlStr1 = "132"; RecRest1 rec1 = check fromXmlStringWithType(xmlStr1); test:assertEquals(rec1.A.C, 1); - test:assertEquals(rec1.A.get("D"), "3"); + test:assertEquals(rec1.A.get("D"), 3); test:assertEquals(rec1.B, 2); } @@ -430,7 +429,7 @@ function testXmlToRecord21() returns error? { xml xmlVal1 = xml `132`; RecRest1 rec1 = check fromXmlWithType(xmlVal1); test:assertEquals(rec1.A.C, 1); - test:assertEquals(rec1.A.get("D"), "3"); + test:assertEquals(rec1.A.get("D"), 3); test:assertEquals(rec1.B, 2); } @@ -452,7 +451,7 @@ function testXmlStringToRecord22() returns error? { RecRest2 rec1 = check fromXmlStringWithType(xmlStr1); test:assertEquals(rec1.A.C, 1); test:assertEquals(rec1.A.D.E, 3); - test:assertEquals(rec1.A.D.get("F"), "4"); + test:assertEquals(rec1.A.D.get("F"), 4); test:assertEquals(rec1.B, 2); } @@ -464,7 +463,7 @@ function testXmlToRecord22() returns error? { RecRest2 rec1 = check fromXmlWithType(xmlVal1); test:assertEquals(rec1.A.C, 1); test:assertEquals(rec1.A.D.E, 3); - test:assertEquals(rec1.A.D.get("F"), "4"); + test:assertEquals(rec1.A.D.get("F"), 4); test:assertEquals(rec1.B, 2); } @@ -487,9 +486,9 @@ function testXmlStringToRecord23() returns error? { test:assertEquals(rec1.A.C, 1); test:assertEquals(rec1.A.D.length(), 2); test:assertEquals(rec1.A.D[0].E, 3); - test:assertEquals(rec1.A.D[0].get("F"), "4"); + test:assertEquals(rec1.A.D[0].get("F"), 4); test:assertEquals(rec1.A.D[1].E, 5); - test:assertEquals(rec1.A.D[1].get("F"), "6"); + test:assertEquals(rec1.A.D[1].get("F"), 6); test:assertEquals(rec1.B, 2); } @@ -502,9 +501,9 @@ function testXmlToRecord23() returns error? { test:assertEquals(rec1.A.C, 1); test:assertEquals(rec1.A.D.length(), 2); test:assertEquals(rec1.A.D[0].E, 3); - test:assertEquals(rec1.A.D[0].get("F"), "4"); + test:assertEquals(rec1.A.D[0].get("F"), 4); test:assertEquals(rec1.A.D[1].E, 5); - test:assertEquals(rec1.A.D[1].get("F"), "6"); + test:assertEquals(rec1.A.D[1].get("F"), 6); test:assertEquals(rec1.B, 2); } @@ -663,9 +662,9 @@ function testXmlStringToRecord28() returns error? { `; record {} rec = check fromXmlStringWithType(xmlStr); - test:assertEquals(rec.get("D"), "4"); - test:assertEquals(rec.get("A"), [{B: "1"}, {B: "2"}, {B: "3"}]); - test:assertEquals(rec.get("C"), {B: "4"}); + test:assertEquals(rec.get("D"), 4); + test:assertEquals(rec.get("A"), [{B: 1}, {B: 2}, {B: 3}]); + test:assertEquals(rec.get("C"), {B: 4}); } @test:Config { @@ -683,9 +682,9 @@ function testXmlToRecord28() returns error? { `; record {} rec = check fromXmlWithType(xmlVal); - test:assertEquals(rec.get("D"), "4"); - test:assertEquals(rec.get("A"), [{B: "1"}, {B: "2"}, {B: "3"}]); - test:assertEquals(rec.get("C"), {B: "4"}); + test:assertEquals(rec.get("D"), 4); + test:assertEquals(rec.get("A"), [{B: 1}, {B: 2}, {B: 3}]); + test:assertEquals(rec.get("C"), {B: 4}); } // test namespace and attributes annotations @@ -1002,7 +1001,7 @@ function testXmlStringToRecord38() returns error? { test:assertEquals(rec.A, "1"); RecAtt4 rec2 = check fromXmlStringWithType(xmlStr); - test:assertEquals(rec2.A.get("#content"), "1"); + test:assertEquals(rec2.A.get("#content"), 1); RecAtt5 rec3 = check fromXmlStringWithType(xmlStr); test:assertEquals(rec3.A, "name"); @@ -1017,7 +1016,7 @@ function testXmlToRecord38() returns error? { test:assertEquals(rec.A, "1"); RecAtt4 rec2 = check fromXmlWithType(xmlVal); - test:assertEquals(rec2.A.get("#content"), "1"); + test:assertEquals(rec2.A.get("#content"), 1); RecAtt5 rec3 = check fromXmlWithType(xmlVal); test:assertEquals(rec3.A, "name"); @@ -1492,7 +1491,7 @@ function testXmlWithAttributesAgainstOpenRecord2() returns error? { }, "price": { "currency": "USD", - "#content": "19.99" + "#content": 19.99 } }, { @@ -1504,7 +1503,7 @@ function testXmlWithAttributesAgainstOpenRecord2() returns error? { }, "price": { "currency": "EUR", - "#content": "29.95" + "#content": 29.95 } } ]); @@ -1541,7 +1540,7 @@ function testXmlWithAttributesAgainstOpenRecord2() returns error? { }, "price": { "currency": "USD", - "#content": "19.99" + "#content": 19.99 } }, { @@ -1553,7 +1552,7 @@ function testXmlWithAttributesAgainstOpenRecord2() returns error? { }, "price": { "currency": "EUR", - "#content": "29.95" + "#content": 29.95 } } ]); @@ -1569,9 +1568,9 @@ function testXmlWithAttributesAgainstOpenRecord3() returns error? { record {} rec5 = check fromXmlStringWithType(xmlStr3); test:assertEquals(rec5.length(), 1); test:assertEquals(rec5.get("A"), [ - {"B": {"value": "name", "#content": "1"}}, - {"B": {"value": "name", "#content": "2"}}, - {"B": {"value": "name", "#content": "3"}} + {"B": {"value": "name", "#content": 1}}, + {"B": {"value": "name", "#content": 2}}, + {"B": {"value": "name", "#content": 3}} ]); xml xmlVal3 = xml ` @@ -1582,9 +1581,9 @@ function testXmlWithAttributesAgainstOpenRecord3() returns error? { record {} rec6 = check fromXmlWithType(xmlVal3); test:assertEquals(rec6.length(), 1); test:assertEquals(rec6.get("A"), [ - {"B": {"value": "name", "#content": "1"}}, - {"B": {"value": "name", "#content": "2"}}, - {"B": {"value": "name", "#content": "3"}} + {"B": {"value": "name", "#content": 1}}, + {"B": {"value": "name", "#content": 2}}, + {"B": {"value": "name", "#content": 3}} ]); } @@ -1620,6 +1619,1140 @@ function testCommentMiddleInContent2() returns error? { test:assertEquals(rec2.A, "John Doe"); } +@test:Config +function testRegexAsFieldTypeWithFromXmlStringWithType() returns error? { + string xmlStr = string ` + 1 + 2 + Code + + Kanth + + `; + record {| + string:RegExp[] A; + string B; + record {| + string name; + |} C; + |} rec1 = check fromXmlStringWithType(xmlStr); + test:assertEquals(rec1.length(), 3); + test:assertEquals(rec1.A, [1, 2]); + test:assertEquals(rec1.B, "Code"); + test:assertEquals(rec1.C.name, "Kanth"); +} + +@test:Config +function testRegexAsFieldTypeWithFromXmlWithType() returns error? { + xml xmlVal = xml ` + 1 + 2 + Code + + Kanth + + `; + record {| + string:RegExp[] A; + string B; + record {| + string name; + |} C; + |} rec1 = check fromXmlWithType(xmlVal); + test:assertEquals(rec1.length(), 3); + test:assertEquals(rec1.A, [1, 2]); + test:assertEquals(rec1.B, "Code"); + test:assertEquals(rec1.C.name, "Kanth"); +} + +@test:Config +function testAnydataAsRestFieldWithFromXmlStringWithType() returns error? { + string xmlStr = string ` + 1 + 2 + Code_1 + Code_2 + `; + record {| + anydata...; + |} rec = check fromXmlStringWithType(xmlStr); + test:assertEquals(rec.length(), 2); + test:assertEquals(rec.get("A"), [1, 2]); + test:assertEquals(rec.get("B"), ["Code_1", "Code_2"]); + + string xmlStr2 = string ` + 1 + 2 + Code_1 + Code_2 + `; + record {| + anydata...; + |} rec2 = check fromXmlStringWithType(xmlStr2); + test:assertEquals(rec2.length(), 2); + test:assertEquals(rec2.get("A"), [{C: 1}, {C: 2}]); + test:assertEquals(rec2.get("B"), [{C: "Code_1"}, {C: "Code_2"}]); + + string xmlStr3 = string ` + + John Doe + + + Kanth Kevin + + `; + record {| + anydata...; + |} rec3 = check fromXmlStringWithType(xmlStr3); + test:assertEquals(rec3.length(), 1); + test:assertEquals(rec3.get("Employee"), [ + { + "Name": "John Doe", + "age": 30 + }, + { + "Name": "Kanth Kevin", + "age": 26 + } + ]); +} + +@test:Config +function testAnydataAsRestFieldWithFromXmlWithType() returns error? { + xml xmlVal = xml ` + 1 + 2 + Code_1 + Code_2 + `; + record {| + anydata...; + |} rec = check fromXmlWithType(xmlVal); + test:assertEquals(rec.length(), 2); + test:assertEquals(rec.get("A"), [1, 2]); + test:assertEquals(rec.get("B"), ["Code_1", "Code_2"]); + + xml xmlVal2 = xml ` + 1 + 2 + Code_1 + Code_2 + `; + record {| + anydata...; + |} rec2 = check fromXmlWithType(xmlVal2); + test:assertEquals(rec2.length(), 2); + test:assertEquals(rec2.get("A"), [{C: 1}, {C: 2}]); + test:assertEquals(rec2.get("B"), [{C: "Code_1"}, {C: "Code_2"}]); + + xml xmlVal3 = xml ` + + John Doe + + + Kanth Kevin + + `; + record {| + anydata...; + |} rec3 = check fromXmlWithType(xmlVal3); + test:assertEquals(rec3.length(), 1); + test:assertEquals(rec3.get("Employee"), [ + { + "Name": "John Doe", + "age": 30 + }, + { + "Name": "Kanth Kevin", + "age": 26 + } + ]); +} + +@test:Config +function testAnydataAsFieldTypeWiThFromXmlStringWithType() returns error? { + string xmlStr = string ` + + John Doe + 30 + + `; + record {| + anydata Employee; + |} rec = check fromXmlStringWithType(xmlStr); + test:assertEquals(rec.length(), 1); + test:assertEquals(rec.Employee, { + "Name": "John Doe", + "Age": 30 + }); + + string xmlStr2 = string ` + + John Doe + 30 + + + Kanth Kevin + 26 + + `; + record {| + anydata Employee; + |} rec2 = check fromXmlStringWithType(xmlStr2); + test:assertEquals(rec2.length(), 1); + test:assertEquals(rec2.Employee, + [ + { + "Name": "John Doe", + "Age": 30 + }, + { + "Name": "Kanth Kevin", + "Age": 26 + } + ] + ); + + string xmlStr3 = string ` + WSO2 + `; + record { + anydata name; + } rec3 = check fromXmlStringWithType(xmlStr3); + test:assertEquals(rec3.length(), 1); + test:assertEquals(rec3.name, "WSO2"); + + string xmlStr4 = string ` + + John Doe + + + Kanth Kevin + + `; + record {| + anydata Employee; + |} rec4 = check fromXmlStringWithType(xmlStr4); + test:assertEquals(rec4.length(), 1); + test:assertEquals(rec4.Employee, + [ + { + "Name": "John Doe", + "age": 30 + }, + { + "Name": "Kanth Kevin", + "age": 26 + } + ] + ); + + string xmlStr5 = string ` + WSO2 + Sanjeeva + Paul Fremantle + `; + record { + anydata name; + anydata founder; + } rec5 = check fromXmlStringWithType(xmlStr5); + test:assertEquals(rec5.length(), 2); + test:assertEquals(rec5.name, "WSO2"); + test:assertEquals(rec5.founder, [ + { + "age": 55, + "#content": "Sanjeeva" + }, + { + "age": 58, + "#content": "Paul Fremantle" + } + ]); + + string xmlStr6 = string ` + + John Doe + + + Kanth Kevin + + `; + record {| + anydata Employee; + |} rec6 = check fromXmlStringWithType(xmlStr6); + test:assertEquals(rec6.length(), 1); + test:assertEquals(rec6, { + Employee: [ + { + "Name": "John Doe", + "age": 30 + }, + { + "Name": "Kanth Kevin", + "age": 26 + } + ] + }); + + string xmlStr7 = string ` + 1 + Code_1 + `; + record {| + anydata A; + anydata B; + |} rec7 = check fromXmlStringWithType(xmlStr7); + test:assertEquals(rec7.length(), 2); + test:assertEquals(rec7.A, 1); + test:assertEquals(rec7.B, "Code_1"); + + string xmlStr8 = string ` + 1 + 2 + Code_1 + Code_2 + `; + record {| + anydata A; + anydata B; + |} rec8 = check fromXmlStringWithType(xmlStr8); + test:assertEquals(rec8.length(), 2); + test:assertEquals(rec8.A, [1, 2]); + test:assertEquals(rec8.B, ["Code_1", "Code_2"]); +} + +@test:Config +function testAnydataAsFieldTypeWiThFromXmlWithType() returns error? { + xml xmlVal = xml ` + + John Doe + 30 + + `; + record {| + anydata Employee; + |} rec = check fromXmlWithType(xmlVal); + test:assertEquals(rec.length(), 1); + test:assertEquals(rec.Employee, { + "Name": "John Doe", + "Age": 30 + }); + + xml xmlVal2 = xml ` + + John Doe + 30 + + + Kanth Kevin + 26 + + `; + record {| + anydata Employee; + |} rec2 = check fromXmlWithType(xmlVal2); + test:assertEquals(rec2.length(), 1); + test:assertEquals(rec2.Employee, + [ + { + "Name": "John Doe", + "Age": 30 + }, + { + "Name": "Kanth Kevin", + "Age": 26 + } + ] + ); + + xml xmlVal3 = xml ` + WSO2 + `; + record { + anydata name; + } rec3 = check fromXmlWithType(xmlVal3); + test:assertEquals(rec3.length(), 1); + test:assertEquals(rec3.name, "WSO2"); + + xml xmlVal4 = xml ` + + John Doe + + + Kanth Kevin + + `; + record {| + anydata Employee; + |} rec4 = check fromXmlWithType(xmlVal4); + test:assertEquals(rec4.length(), 1); + test:assertEquals(rec4.Employee, + [ + { + "Name": "John Doe", + "age": 30 + }, + { + "Name": "Kanth Kevin", + "age": 26 + } + ] + ); + + xml xmlVal5 = xml ` + WSO2 + Sanjeeva + Paul Fremantle + `; + record { + anydata name; + anydata founder; + } rec5 = check fromXmlWithType(xmlVal5); + test:assertEquals(rec5.length(), 2); + test:assertEquals(rec5.name, "WSO2"); + test:assertEquals(rec5.founder, [ + { + "age": 55, + "#content": "Sanjeeva" + }, + { + "age": 58, + "#content": "Paul Fremantle" + } + ]); + + xml xmlVal6 = xml ` + + John Doe + + + Kanth Kevin + + `; + record {| + anydata Employee; + |} rec6 = check fromXmlWithType(xmlVal6); + test:assertEquals(rec6.length(), 1); + test:assertEquals(rec6, { + Employee: [ + { + "Name": "John Doe", + "age": 30 + }, + { + "Name": "Kanth Kevin", + "age": 26 + } + ] + }); + + xml xmlVal7 = xml ` + 1 + Code_1 + `; + record {| + anydata A; + anydata B; + |} rec7 = check fromXmlWithType(xmlVal7); + test:assertEquals(rec7.length(), 2); + test:assertEquals(rec7.A, 1); + test:assertEquals(rec7.B, "Code_1"); + + xml xmlVal8 = xml ` + 1 + 2 + Code_1 + Code_2 + `; + record {| + anydata A; + anydata B; + |} rec8 = check fromXmlWithType(xmlVal8); + test:assertEquals(rec8.length(), 2); + test:assertEquals(rec8.A, [1, 2]); + test:assertEquals(rec8.B, ["Code_1", "Code_2"]); +} + +@test:Config +function testJsonAsFieldTypeWiThFromXmlStringWithType() returns error? { + string xmlStr = string ` + + John Doe + 30 + + `; + record {| + json Employee; + |} rec = check fromXmlStringWithType(xmlStr); + test:assertEquals(rec.length(), 1); + test:assertEquals(rec.Employee, { + "Name": "John Doe", + "Age": 30 + }); + + string xmlStr2 = string ` + + John Doe + 30 + + + Kanth Kevin + 26 + + `; + record {| + json Employee; + |} rec2 = check fromXmlStringWithType(xmlStr2); + test:assertEquals(rec2.length(), 1); + test:assertEquals(rec2.Employee, + [ + { + "Name": "John Doe", + "Age": 30 + }, + { + "Name": "Kanth Kevin", + "Age": 26 + } + ] + ); + + string xmlStr3 = string ` + WSO2 + `; + record { + json name; + } rec3 = check fromXmlStringWithType(xmlStr3); + test:assertEquals(rec3.length(), 1); + test:assertEquals(rec3.name, "WSO2"); + + string xmlStr6 = string ` + + John Doe + + + Kanth Kevin + + `; + record {| + json Employee; + |} rec6 = check fromXmlStringWithType(xmlStr6); + test:assertEquals(rec6.length(), 1); + test:assertEquals(rec6, { + Employee: [ + { + "Name": "John Doe", + "age": 30 + }, + { + "Name": "Kanth Kevin", + "age": 26 + } + ] + }); + + string xmlStr7 = string ` + 1 + Code_1 + `; + record {| + json A; + json B; + |} rec7 = check fromXmlStringWithType(xmlStr7); + test:assertEquals(rec7.length(), 2); + test:assertEquals(rec7.A, 1); + test:assertEquals(rec7.B, "Code_1"); + + string xmlStr8 = string ` + 1 + 2 + Code_1 + Code_2 + `; + record {| + json A; + json B; + |} rec8 = check fromXmlStringWithType(xmlStr8); + test:assertEquals(rec8.length(), 2); + test:assertEquals(rec8.A, [1, 2]); + test:assertEquals(rec8.B, ["Code_1", "Code_2"]); +} + +@test:Config +function testJsonAsFieldTypeWiThFromXmlWithType() returns error? { + xml xmlVal = xml ` + + John Doe + 30 + + `; + record {| + json Employee; + |} rec = check fromXmlWithType(xmlVal); + test:assertEquals(rec.length(), 1); + test:assertEquals(rec.Employee, { + "Name": "John Doe", + "Age": 30 + }); + + xml xmlVal2 = xml ` + + John Doe + 30 + + + Kanth Kevin + 26 + + `; + record {| + json Employee; + |} rec2 = check fromXmlWithType(xmlVal2); + test:assertEquals(rec2.length(), 1); + test:assertEquals(rec2.Employee, + [ + { + "Name": "John Doe", + "Age": 30 + }, + { + "Name": "Kanth Kevin", + "Age": 26 + } + ] + ); + + xml xmlVal3 = xml ` + WSO2 + `; + record { + json name; + } rec3 = check fromXmlWithType(xmlVal3); + test:assertEquals(rec3.length(), 1); + test:assertEquals(rec3.name, "WSO2"); + + xml xmlVal4 = xml ` + + John Doe + + + Kanth Kevin + + `; + record {| + json Employee; + |} rec4 = check fromXmlWithType(xmlVal4); + test:assertEquals(rec4.length(), 1); + test:assertEquals(rec4.Employee, + [ + { + "Name": "John Doe", + "age": 30 + }, + { + "Name": "Kanth Kevin", + "age": 26 + } + ] + ); + + xml xmlVal5 = xml ` + WSO2 + Sanjeeva + Paul Fremantle + `; + record { + json name; + json founder; + } rec5 = check fromXmlWithType(xmlVal5); + test:assertEquals(rec5.length(), 2); + test:assertEquals(rec5.name, "WSO2"); + test:assertEquals(rec5.founder, [ + { + "age": 55, + "#content": "Sanjeeva" + }, + { + "age": 58, + "#content": "Paul Fremantle" + } + ]); + + xml xmlVal6 = xml ` + + John Doe + + + Kanth Kevin + + `; + record {| + json Employee; + |} rec6 = check fromXmlWithType(xmlVal6); + test:assertEquals(rec6.length(), 1); + test:assertEquals(rec6, { + Employee: [ + { + "Name": "John Doe", + "age": 30 + }, + { + "Name": "Kanth Kevin", + "age": 26 + } + ] + }); + + xml xmlVal7 = xml ` + 1 + Code_1 + `; + record {| + json A; + json B; + |} rec7 = check fromXmlWithType(xmlVal7); + test:assertEquals(rec7.length(), 2); + test:assertEquals(rec7.A, 1); + test:assertEquals(rec7.B, "Code_1"); + + xml xmlVal8 = xml ` + 1 + 2 + Code_1 + Code_2 + `; + record {| + json A; + json B; + |} rec8 = check fromXmlWithType(xmlVal8); + test:assertEquals(rec8.length(), 2); + test:assertEquals(rec8.A, [1, 2]); + test:assertEquals(rec8.B, ["Code_1", "Code_2"]); +} + +@test:Config +function testAnydataArrayAsFieldTypeWiThFromXmlStringWithType() returns error? { + string xmlStr = string ` + + John Doe + 30 + + + Kanth Kevin + 26 + + `; + record {| + anydata[] Employee; + |} rec = check fromXmlStringWithType(xmlStr); + test:assertEquals(rec.length(), 1); + test:assertEquals(rec.Employee, [ + { + "Name": "John Doe", + "Age": 30 + }, + { + "Name": "Kanth Kevin", + "Age": 26 + } + ]); + + string xmlStr2 = string ` + WSO2 + Apple + Sri Lanka + India + `; + record { + anydata[] name; + anydata[] location; + } rec2 = check fromXmlStringWithType(xmlStr2); + test:assertEquals(rec2.length(), 2); + test:assertEquals(rec2.name, ["WSO2", "Apple"]); + test:assertEquals(rec2.location, ["Sri Lanka", "India"]); +} + +@test:Config +function testJsonArrayAsFieldTypeWiThFromXmlStringWithType() returns error? { + string xmlStr = string ` + + John Doe + 30 + + + Kanth Kevin + 26 + + `; + record {| + json[] Employee; + |} rec = check fromXmlStringWithType(xmlStr); + test:assertEquals(rec.length(), 1); + test:assertEquals(rec.Employee, [ + { + "Name": "John Doe", + "Age": 30 + }, + { + "Name": "Kanth Kevin", + "Age": 26 + } + ]); + + string xmlStr2 = string ` + WSO2 + Apple + `; + record { + json[] name; + } rec2 = check fromXmlStringWithType(xmlStr2); + test:assertEquals(rec2.length(), 1); + test:assertEquals(rec2.name, ["WSO2", "Apple"]); +} + +@test:Config +function testAnydataArrayAsFieldTypeWiThFromXmlWithType() returns error? { + xml xmlVal = xml ` + + John Doe + 30 + + + Kanth Kevin + 26 + + `; + record {| + anydata[] Employee; + |} rec = check fromXmlWithType(xmlVal); + test:assertEquals(rec.length(), 1); + test:assertEquals(rec.Employee, [ + { + "Name": "John Doe", + "Age": 30 + }, + { + "Name": "Kanth Kevin", + "Age": 26 + } + ]); + + xml xmlVal2 = xml ` + WSO2 + Apple + Sri Lanka + India + `; + record { + anydata[] name; + anydata[] location; + } rec2 = check fromXmlWithType(xmlVal2); + test:assertEquals(rec2.length(), 2); + test:assertEquals(rec2.name, ["WSO2", "Apple"]); + test:assertEquals(rec2.location, ["Sri Lanka", "India"]); +} + +@test:Config +function testJsonArrayAsFieldTypeWiThFromXmlWithType() returns error? { + xml xmlVal = xml ` + + John Doe + 30 + + + Kanth Kevin + 26 + + `; + record {| + json[] Employee; + |} rec = check fromXmlWithType(xmlVal); + test:assertEquals(rec.length(), 1); + test:assertEquals(rec.Employee, [ + { + "Name": "John Doe", + "Age": 30 + }, + { + "Name": "Kanth Kevin", + "Age": 26 + } + ]); + + xml xmlVal2 = xml ` + WSO2 + Apple + Sri Lanka + India + `; + record { + json[] name; + json[] location; + } rec2 = check fromXmlWithType(xmlVal2); + test:assertEquals(rec2.length(), 2); + test:assertEquals(rec2.name, ["WSO2", "Apple"]); + test:assertEquals(rec2.location, ["Sri Lanka", "India"]); +} + +@test:Config +function testMapArrayAsFieldTypeWiThFromXmlStringWithType() returns error? { + string xmlStr = string ` + + John Doe + 30 + + + Kanth Kevin + 26 + + `; + + record {| + map[] Employee; + |} rec = check fromXmlStringWithType(xmlStr); + test:assertEquals(rec.length(), 1); + test:assertEquals(rec.Employee, [ + { + "Name": "John Doe", + "Age": "30" + }, + { + "Name": "Kanth Kevin", + "Age": "26" + } + ]); +} + +@test:Config +function testMapArrayAsFieldTypeWithFromXmlWithType() returns error? { + xml xmlVal = xml ` + + John Doe + 30 + + + Kanth Kevin + 26 + + `; + record {| + map[] Employee; + |} rec = check fromXmlWithType(xmlVal); + test:assertEquals(rec.length(), 1); + test:assertEquals(rec.Employee, [ + { + "Name": "John Doe", + "Age": "30" + }, + { + "Name": "Kanth Kevin", + "Age": "26" + } + ]); +} + +@test:Config +function testAnydataArrayAsRestTypeWithFromJsonStringWithType() returns error? { + string xmlStr = string ` + 1 + 2 + Code_1 + Code_2 + `; + record {| + anydata[]...; + |} rec = check fromXmlStringWithType(xmlStr); + test:assertEquals(rec.length(), 2); + test:assertEquals(rec.get("A"), [1, 2]); + + string xmlStr2 = string ` + + John Doe + 30 + + + Kanth Kevin + 26 + + `; + record {| + anydata[]...; + |} rec2 = check fromXmlStringWithType(xmlStr2); + test:assertEquals(rec2.length(), 1); + test:assertEquals(rec2.get("Employee"), [ + { + "Name": "John Doe", + "Age": 30 + }, + { + "Name": "Kanth Kevin", + "Age": 26 + } + ]); +} + +@test:Config +function testAnydataArrayAsRestTypeWithFromJsonWithType() returns error? { + xml xmlVal = xml ` + 1 + 2 + Code_1 + Code_2 + `; + record {| + anydata[]...; + |} rec = check fromXmlWithType(xmlVal); + test:assertEquals(rec.length(), 2); + test:assertEquals(rec.get("A"), [1, 2]); + + xml xmlVal2 = xml ` + + John Doe + 30 + + + Kanth Kevin + 26 + + `; + record {| + anydata[]...; + |} rec2 = check fromXmlWithType(xmlVal2); + test:assertEquals(rec2.length(), 1); + test:assertEquals(rec2.get("Employee"), [ + { + "Name": "John Doe", + "Age": 30 + }, + { + "Name": "Kanth Kevin", + "Age": 26 + } + ]); +} + +@test:Config +function testRecordAsRestTypeForFromXmlStringWithType() returns error? { + string xmlStr = string ` + A_C_1 + B_C_1 + `; + record {| + record {}...; + |} rec = check fromXmlStringWithType(xmlStr); + test:assertEquals(rec.length(), 2); + test:assertEquals(rec.get("A"), {C: "A_C_1"}); + test:assertEquals(rec.get("B"), {C: "B_C_1"}); +} + +@test:Config +function testRecordArrayAsRestTypeForFromXmlStringWithType() returns error? { + string xmlStr1 = string ` + + A_C_1 + A_D_1 + + + A_C_2 + A_D_2 + + + B_C_1 + B_D_1 + + + B_C_2 + B_D_2 + + `; + record {| + record { + string C; + }[]...; + |} rec1 = check fromXmlStringWithType(xmlStr1); + test:assertEquals(rec1.length(), 2); + test:assertEquals(rec1.get("A"), [{C:"A_C_1", D:"A_D_1"},{C:"A_C_2", D:"A_D_2"}]); + test:assertEquals(rec1.get("B"), [{C:"B_C_1", D:"B_D_1"},{C:"B_C_2", D:"B_D_2"}]); + + string xmlStr2 = string ` + + + 1 + 2 + 2 + 5 + + 3 + + `; + + record {| + string value; + record {| + int[]...; + |}...; + |} rec2 = check fromXmlStringWithType(xmlStr2); + test:assertEquals(rec2.length(), 2); + test:assertEquals(rec2.get("Depth1"), {A: [1, 2], B: [2, 5]}); + test:assertEquals(rec2.get("value"), "3"); + + record {| + int value; + record {| + string[]...; + |}...; + |} rec3 = check fromXmlStringWithType(xmlStr2); + test:assertEquals(rec3.length(), 2); + test:assertEquals(rec3.get("Depth1"), {A: ["1", "2"], B: ["2", "5"]}); + test:assertEquals(rec3.get("value"), 3); +} + +@test:Config +function testAddingContentFieldWhenRestTypeAsExpTypeForFromXmlStringWithType() returns error? { + string xmlStr = string ` + + 2 + 3 + + `; + + record {} rec = check fromXmlStringWithType(xmlStr); + test:assertEquals(rec.length(), 2); + test:assertEquals(rec.get("A"), {\#content: 2, a: "attribute_a"}); + test:assertEquals(rec.get("B"), {C: {\#content: 3, c: "attribute_c"}}); + + record {| + string A; + record {} B; + |} val2 = check fromXmlStringWithType(xmlStr); + test:assertEquals(val2.length(), 2); + test:assertEquals(val2.A, "2"); + test:assertEquals(val2.B, {C: {\#content: 3, c: "attribute_c"}}); +} + +@test:Config +function testAddingContentFieldWhenRestTypeAsExpTypeForFromXmlWithType() returns error? { + xml xmlVal = xml ` + + 2 + 3 + + `; + + record {} rec = check fromXmlWithType(xmlVal); + test:assertEquals(rec.length(), 2); + test:assertEquals(rec.get("A"), {\#content: 2, a: "attribute_a"}); + test:assertEquals(rec.get("B"), {C: {\#content: 3, c: "attribute_c"}}); + + record {| + string A; + record {} B; + |} val2 = check fromXmlWithType(xmlVal); + test:assertEquals(val2.length(), 2); + test:assertEquals(val2.A, "2"); + test:assertEquals(val2.B, {C: {\#content: 3, c: "attribute_c"}}); +} + // Negative cases type DataN1 record {| int A; @@ -1919,3 +3052,36 @@ function testCommentMiddleInContentNegative2() { |}|error rec2 = fromXmlWithType(xmlVal); test:assertEquals((rec2).message(), "invalid type expected 'int' but found 'string'"); } + +@test:Config { + groups: ["fromXml"] +} +function testUnsupportedTypeNegative() { + xml xmlVal1 = xml ` + + 1 + 2 + + `; + record {| + xml[] A; + |}|error err1 = fromXmlWithType(xmlVal1); + test:assertEquals((err1).message(), "unsupported input type"); + + xml xmlVal2 = xml ` + + + 1 + + + `; + record {| + record {|string a;|}|record {|string b;|} A; + |}|error err2 = fromXmlWithType(xmlVal2); + test:assertEquals((err2).message(), "unsupported input type"); + + record {| + record {|string a;|}? A; + |}|error err3 = fromXmlWithType(xmlVal2); + test:assertEquals((err3).message(), "unsupported input type"); +} diff --git a/ballerina/xml_api.bal b/ballerina/xml_api.bal index f1c41c0..ed635a1 100644 --- a/ballerina/xml_api.bal +++ b/ballerina/xml_api.bal @@ -128,25 +128,18 @@ isolated function getModifiedRecord(map mapValue, typedesc<(map allNamespaces = {}; if !isSingleNode(jsonValue) { diff --git a/build-config/resources/Ballerina.toml b/build-config/resources/Ballerina.toml index c8803ee..ea975f8 100644 --- a/build-config/resources/Ballerina.toml +++ b/build-config/resources/Ballerina.toml @@ -6,7 +6,7 @@ authors = ["Ballerina"] keywords = ["xml"] repository = "https://github.com/ballerina-platform/module-ballerina-data-xmldata" license = ["Apache-2.0"] -distribution = "2201.8.1" +distribution = "2201.8.5" export = ["data.xmldata"] [[platform.java17.dependency]] diff --git a/compiler-plugin-test/src/test/java/io/ballerina/stdlib/data/xmldata/compiler/CompilerPluginTest.java b/compiler-plugin-test/src/test/java/io/ballerina/stdlib/data/xmldata/compiler/CompilerPluginTest.java index 2715587..1e098dc 100644 --- a/compiler-plugin-test/src/test/java/io/ballerina/stdlib/data/xmldata/compiler/CompilerPluginTest.java +++ b/compiler-plugin-test/src/test/java/io/ballerina/stdlib/data/xmldata/compiler/CompilerPluginTest.java @@ -104,7 +104,7 @@ public void testUnsupportedTypeNegative1() { @Test public void testUnsupportedTypeNegative2() { DiagnosticResult diagnosticResult = - CompilerPluginTestUtils.loadPackage("sample_package_8").getCompilation().diagnosticResult(); + CompilerPluginTestUtils.loadPackage("sample_package_7").getCompilation().diagnosticResult(); List errorDiagnosticsList = diagnosticResult.diagnostics().stream() .filter(r -> r.diagnosticInfo().severity().equals(DiagnosticSeverity.ERROR)) .collect(Collectors.toList()); @@ -138,4 +138,44 @@ public void testChildRecordWithNameAnnotNegative() { Assert.assertEquals(errorDiagnosticsList.get(0).diagnosticInfo().messageFormat(), "invalid annotation attachment: child record does not allow name annotation"); } + + @Test + public void testDuplicateFieldInInlineRecordsNegative() { + DiagnosticResult diagnosticResult = + CompilerPluginTestUtils.loadPackage("sample_package_8").getCompilation().diagnosticResult(); + List errorDiagnosticsList = diagnosticResult.diagnostics().stream() + .filter(r -> r.diagnosticInfo().severity().equals(DiagnosticSeverity.ERROR)) + .collect(Collectors.toList()); + Assert.assertEquals(errorDiagnosticsList.size(), 4); + Assert.assertEquals(errorDiagnosticsList.get(0).diagnosticInfo().messageFormat(), + "invalid field: duplicate field found"); + Assert.assertEquals(errorDiagnosticsList.get(1).diagnosticInfo().messageFormat(), + "invalid field: duplicate field found"); + Assert.assertEquals(errorDiagnosticsList.get(2).diagnosticInfo().messageFormat(), + "invalid field: duplicate field found"); + Assert.assertEquals(errorDiagnosticsList.get(3).diagnosticInfo().messageFormat(), + "invalid field: duplicate field found"); + } + + @Test + public void testUnionTypeNegative() { + DiagnosticResult diagnosticResult = + CompilerPluginTestUtils.loadPackage("sample_package_9").getCompilation().diagnosticResult(); + List errorDiagnosticsList = diagnosticResult.diagnostics().stream() + .filter(r -> r.diagnosticInfo().severity().equals(DiagnosticSeverity.ERROR)) + .collect(Collectors.toList()); + Assert.assertEquals(errorDiagnosticsList.size(), 6); + Assert.assertEquals(errorDiagnosticsList.get(0).diagnosticInfo().messageFormat(), + "invalid type: expected a record type"); + Assert.assertEquals(errorDiagnosticsList.get(1).diagnosticInfo().messageFormat(), + "invalid field: duplicate field found"); + Assert.assertEquals(errorDiagnosticsList.get(2).diagnosticInfo().messageFormat(), + "invalid field: duplicate field found"); + Assert.assertEquals(errorDiagnosticsList.get(3).diagnosticInfo().messageFormat(), + "invalid type: expected a record type"); + Assert.assertEquals(errorDiagnosticsList.get(4).diagnosticInfo().messageFormat(), + "invalid field: duplicate field found"); + Assert.assertEquals(errorDiagnosticsList.get(5).diagnosticInfo().messageFormat(), + "invalid field: duplicate field found"); + } } diff --git a/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_7/Ballerina.toml b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_7/Ballerina.toml new file mode 100644 index 0000000..c7355a4 --- /dev/null +++ b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_7/Ballerina.toml @@ -0,0 +1,4 @@ +[package] +org = "xmldata_test" +name = "sample_7" +version = "0.1.0" diff --git a/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_7/main.bal b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_7/main.bal new file mode 100644 index 0000000..6ae9fe3 --- /dev/null +++ b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_7/main.bal @@ -0,0 +1,18 @@ +import ballerina/data.xmldata; + +string xmlStr = string `12`; +record {| + string? A; + () B; + ()[] C; + record {||}?[] D; +|} _ = check xmldata:fromXmlStringWithType(xmlStr); + +function testFunction() returns error? { + record {| + string? A; + () B; + ()[] C; + record {||}?[] D; + |} _ = check xmldata:fromXmlStringWithType(xmlStr); +} diff --git a/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_8/main.bal b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_8/main.bal index 6ae9fe3..84d1401 100644 --- a/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_8/main.bal +++ b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_8/main.bal @@ -1,18 +1,49 @@ import ballerina/data.xmldata; string xmlStr = string `12`; + record {| - string? A; - () B; - ()[] C; - record {||}?[] D; + @xmldata:Name { + value: "A" + } + string a; + @xmldata:Name { + value: "A" + } + string b; |} _ = check xmldata:fromXmlStringWithType(xmlStr); +record {| + @xmldata:Name { + value: "A" + } + string a; + @xmldata:Name { + value: "A" + } + string b; +|} _ = {a: "1", b: "2"}; + function testFunction() returns error? { record {| - string? A; - () B; - ()[] C; - record {||}?[] D; + @xmldata:Name { + value: "A" + } + string a; + @xmldata:Name { + value: "A" + } + string b; |} _ = check xmldata:fromXmlStringWithType(xmlStr); + + record {| + @xmldata:Name { + value: "A" + } + string a; + @xmldata:Name { + value: "A" + } + string b; + |} _ = {a: "1", b: "2"}; } diff --git a/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_9/Ballerina.toml b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_9/Ballerina.toml new file mode 100644 index 0000000..5fc2223 --- /dev/null +++ b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_9/Ballerina.toml @@ -0,0 +1,4 @@ +[package] +org = "xmldata_test" +name = "sample_9" +version = "0.1.0" diff --git a/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_9/main.bal b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_9/main.bal new file mode 100644 index 0000000..2d55f07 --- /dev/null +++ b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_9/main.bal @@ -0,0 +1,37 @@ +import ballerina/data.xmldata; + +string xmlStr = string `12`; + +record {| + @xmldata:Name { + value: "B" + } + string A; + string B; +|}|map|error rec1 = xmldata:fromXmlStringWithType(xmlStr); + +record {| + @xmldata:Name { + value: "B" + } + string A; + string B; +|}|map|error rec2 = {A: "1", B: "2"}; + +public function main() { + record {| + @xmldata:Name { + value: "B" + } + string A; + string B; + |}|map|error rec3 = xmldata:fromXmlStringWithType(xmlStr); + + record {| + @xmldata:Name { + value: "B" + } + string A; + string B; + |}|map|error rec4 = {A: "1", B: "2"}; +} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/data/xmldata/compiler/Constants.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/data/xmldata/compiler/Constants.java index 8d7b6b5..40eb3a1 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/data/xmldata/compiler/Constants.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/data/xmldata/compiler/Constants.java @@ -1,5 +1,10 @@ package io.ballerina.stdlib.data.xmldata.compiler; +/** + * Constants for XmlData's compiler plugin. + * + * @since 0.1.0 + */ public class Constants { static final String FROM_XML_STRING_WITH_TYPE = "fromXmlStringWithType"; static final String FROM_XML_WITH_TYPE = "fromXmlWithType"; diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/data/xmldata/compiler/XmldataCodeAnalyzer.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/data/xmldata/compiler/XmldataCodeAnalyzer.java index 05c14e8..caaa442 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/data/xmldata/compiler/XmldataCodeAnalyzer.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/data/xmldata/compiler/XmldataCodeAnalyzer.java @@ -26,6 +26,8 @@ /** * Xmldata Code Analyzer. + * + * @since 0.1.0 */ public class XmldataCodeAnalyzer extends CodeAnalyzer { diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/data/xmldata/compiler/XmldataCompilerPlugin.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/data/xmldata/compiler/XmldataCompilerPlugin.java index 96e452a..bda1807 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/data/xmldata/compiler/XmldataCompilerPlugin.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/data/xmldata/compiler/XmldataCompilerPlugin.java @@ -23,6 +23,8 @@ /** * Compiler plugin for Xmldata's utils functions. + * + * @since 0.1.0 */ public class XmldataCompilerPlugin extends CompilerPlugin { diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/data/xmldata/compiler/XmldataDiagnosticCodes.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/data/xmldata/compiler/XmldataDiagnosticCodes.java index 17f03f6..af70233 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/data/xmldata/compiler/XmldataDiagnosticCodes.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/data/xmldata/compiler/XmldataDiagnosticCodes.java @@ -25,15 +25,17 @@ /** * Enum class to hold xmldata module diagnostic codes. + * + * @since 0.1.0 */ public enum XmldataDiagnosticCodes { - DUPLICATE_FIELD("BDE201", "invalid field: duplicate field found", ERROR), - UNSUPPORTED_UNION_TYPE("BDE202", + DUPLICATE_FIELD("XML_ERROR_201", "invalid field: duplicate field found", ERROR), + UNSUPPORTED_UNION_TYPE("XML_ERROR_202", "unsupported union type: union type does not support multiple non-primitive record types", ERROR), - UNSUPPORTED_TYPE("BDE203", "unsupported type: the record field does not support the expected type", ERROR), - EXPECTED_RECORD_TYPE("BDE204", "invalid type: expected a record type", ERROR), - NAME_ANNOTATION_NOT_ALLOWED("BDE204", + UNSUPPORTED_TYPE("XML_ERROR_203", "unsupported type: the record field does not support the expected type", ERROR), + EXPECTED_RECORD_TYPE("XML_ERROR_204", "invalid type: expected a record type", ERROR), + NAME_ANNOTATION_NOT_ALLOWED("XML_ERROR_205", "invalid annotation attachment: child record does not allow name annotation", WARNING); private final String code; diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/data/xmldata/compiler/XmldataRecordFieldValidator.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/data/xmldata/compiler/XmldataRecordFieldValidator.java index 52521ca..a12f271 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/data/xmldata/compiler/XmldataRecordFieldValidator.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/data/xmldata/compiler/XmldataRecordFieldValidator.java @@ -62,6 +62,11 @@ import java.util.Map; import java.util.Optional; +/** + * Xmldata Record Field Validator. + * + * @since 0.1.0 + */ public class XmldataRecordFieldValidator implements AnalysisTask { private SemanticModel semanticModel; @@ -99,36 +104,92 @@ private void processFunctionDefinitionNode(FunctionDefinitionNode functionDefini } VariableDeclarationNode variableDeclarationNode = (VariableDeclarationNode) node; Optional initializer = variableDeclarationNode.initializer(); - if (initializer.isEmpty() || !isFromXmlFunctionFromXmldata(initializer.get())) { + if (initializer.isEmpty()) { continue; } - Optional symbol = semanticModel.symbol(variableDeclarationNode.typedBindingPattern()); if (symbol.isEmpty()) { continue; } + TypeSymbol typeSymbol = ((VariableSymbol) symbol.get()).typeDescriptor(); - if (typeSymbol.typeKind() == TypeDescKind.TYPE_REFERENCE) { - typeSymbol = ((TypeReferenceTypeSymbol) typeSymbol).typeDescriptor(); - if (typeSymbol.typeKind() != TypeDescKind.RECORD) { - reportDiagnosticInfo(ctx, symbol.get().getLocation(), XmldataDiagnosticCodes.EXPECTED_RECORD_TYPE); - continue; - } - processRecordFieldsType((RecordTypeSymbol) typeSymbol, ctx); - } else if (typeSymbol.typeKind() != TypeDescKind.RECORD) { - reportDiagnosticInfo(ctx, symbol.get().getLocation(), XmldataDiagnosticCodes.EXPECTED_RECORD_TYPE); + if (!isNotFromXmlFunctionFromXmldata(initializer.get())) { + validateAnnotationUsageInAllInlineExpectedTypes(typeSymbol, ctx); continue; } - RecordTypeSymbol recordSymbol = (RecordTypeSymbol) typeSymbol; - validateRecordFields(recordSymbol, ctx); - processRecordFieldsType(recordSymbol, ctx); + validateExpectedType(typeSymbol, symbol.get().getLocation(), ctx); + } + } + + private void validateAnnotationUsageInAllInlineExpectedTypes(TypeSymbol typeSymbol, SyntaxNodeAnalysisContext ctx) { + switch (typeSymbol.typeKind()) { + case RECORD -> validateRecordFieldNames((RecordTypeSymbol) typeSymbol, ctx); + case UNION -> { + for (TypeSymbol memberTSymbol : ((UnionTypeSymbol) typeSymbol).memberTypeDescriptors()) { + validateAnnotationUsageInAllInlineExpectedTypes(memberTSymbol, ctx); + } + } + } + } + + private void validateExpectedType(TypeSymbol typeSymbol, Optional location, + SyntaxNodeAnalysisContext ctx) { + if (isNotValidExpectedType(typeSymbol)) { + reportDiagnosticInfo(ctx, location, XmldataDiagnosticCodes.EXPECTED_RECORD_TYPE); + } + + switch (typeSymbol.typeKind()) { + case RECORD -> { + RecordTypeSymbol recordSymbol = (RecordTypeSymbol) typeSymbol; + validateRecordFieldNames(recordSymbol, ctx); + processRecordFieldsType(recordSymbol, ctx); + } + case TYPE_REFERENCE -> validateExpectedType(((TypeReferenceTypeSymbol) typeSymbol).typeDescriptor(), + location, ctx); + case UNION -> { + int nonErrorTypeCount = 0; + for (TypeSymbol memberTSymbol : ((UnionTypeSymbol) typeSymbol).memberTypeDescriptors()) { + if (memberTSymbol.typeKind() == TypeDescKind.ERROR) { + continue; + } + nonErrorTypeCount++; + validateExpectedType(memberTSymbol, location, ctx); + } + if (nonErrorTypeCount > 1) { + reportDiagnosticInfo(ctx, location, XmldataDiagnosticCodes.EXPECTED_RECORD_TYPE); + } + } } } + private boolean isNotValidExpectedType(TypeSymbol typeSymbol) { + switch (typeSymbol.typeKind()) { + case RECORD -> { + return false; + } + case TYPE_REFERENCE -> { + return isNotValidExpectedType(((TypeReferenceTypeSymbol) typeSymbol).typeDescriptor()); + } + case UNION -> { + for (TypeSymbol memberTSymbol : ((UnionTypeSymbol) typeSymbol).memberTypeDescriptors()) { + if (memberTSymbol.typeKind() == TypeDescKind.ERROR) { + continue; + } + + if (isNotValidExpectedType(memberTSymbol)) { + return true; + } + } + return false; + } + } + return true; + } + private void processModuleVariableDeclarationNode(ModuleVariableDeclarationNode moduleVariableDeclarationNode, SyntaxNodeAnalysisContext ctx) { Optional initializer = moduleVariableDeclarationNode.initializer(); - if (initializer.isEmpty() || !isFromXmlFunctionFromXmldata(initializer.get())) { + if (initializer.isEmpty()) { return; } @@ -137,20 +198,12 @@ private void processModuleVariableDeclarationNode(ModuleVariableDeclarationNode return; } TypeSymbol typeSymbol = ((VariableSymbol) symbol.get()).typeDescriptor(); - if (typeSymbol.typeKind() == TypeDescKind.TYPE_REFERENCE) { - typeSymbol = ((TypeReferenceTypeSymbol) typeSymbol).typeDescriptor(); - if (typeSymbol.typeKind() != TypeDescKind.RECORD) { - reportDiagnosticInfo(ctx, symbol.get().getLocation(), - XmldataDiagnosticCodes.EXPECTED_RECORD_TYPE); - return; - } - processRecordFieldsType((RecordTypeSymbol) typeSymbol, ctx); - } else if (typeSymbol.typeKind() != TypeDescKind.RECORD) { - reportDiagnosticInfo(ctx, symbol.get().getLocation(), XmldataDiagnosticCodes.EXPECTED_RECORD_TYPE); + + if (!isNotFromXmlFunctionFromXmldata(initializer.get())) { + validateAnnotationUsageInAllInlineExpectedTypes(typeSymbol, ctx); return; } - validateRecordFields((RecordTypeSymbol) typeSymbol, ctx); - processRecordFieldsType((RecordTypeSymbol) typeSymbol, ctx); + validateExpectedType(typeSymbol, symbol.get().getLocation(), ctx); } private void processTypeDefinitionNode(TypeDefinitionNode typeDefinitionNode, SyntaxNodeAnalysisContext ctx) { @@ -167,14 +220,14 @@ private void validateRecordTypeDefinition(TypeDefinitionNode typeDefinitionNode, return; } TypeDefinitionSymbol typeDefinitionSymbol = (TypeDefinitionSymbol) symbol.get(); - validateRecordFields((RecordTypeSymbol) typeDefinitionSymbol.typeDescriptor(), ctx); + validateRecordFieldNames((RecordTypeSymbol) typeDefinitionSymbol.typeDescriptor(), ctx); } - private void validateRecordFields(RecordTypeSymbol recordTypeSymbol, SyntaxNodeAnalysisContext ctx) { + private void validateRecordFieldNames(RecordTypeSymbol recordTypeSymbol, SyntaxNodeAnalysisContext ctx) { List fieldMembers = new ArrayList<>(); for (Map.Entry entry : recordTypeSymbol.fieldDescriptors().entrySet()) { - detectDuplicateFields(entry.getKey(), entry.getValue(), fieldMembers, ctx); RecordFieldSymbol fieldSymbol = entry.getValue(); + detectDuplicateFields(entry.getKey(), fieldSymbol, fieldMembers, ctx); if (fieldSymbol.typeDescriptor().typeKind() != TypeDescKind.TYPE_REFERENCE) { continue; } @@ -284,7 +337,11 @@ private QualifiedName getQNameFromAnnotation(String fieldName, String name = fieldName; String prefix = ""; for (AnnotationAttachmentSymbol annotAttSymbol : annotationAttachments) { - Optional nameAnnot = annotAttSymbol.typeDescriptor().getName(); + AnnotationSymbol annotation = annotAttSymbol.typeDescriptor(); + if (!getAnnotModuleName(annotation).contains(Constants.XMLDATA)) { + continue; + } + Optional nameAnnot = annotation.getName(); if (nameAnnot.isEmpty()) { continue; } @@ -293,8 +350,11 @@ private QualifiedName getQNameFromAnnotation(String fieldName, name = ((LinkedHashMap) annotAttSymbol.attachmentValue().orElseThrow().value()) .get("value").toString(); } else if (value.equals(Constants.NAMESPACE)) { - prefix = ((LinkedHashMap) annotAttSymbol.attachmentValue().orElseThrow().value()) - .get("prefix").toString(); + Object temp = ((LinkedHashMap) annotAttSymbol.attachmentValue().orElseThrow().value()) + .get("prefix"); + if (temp != null) { + prefix = temp.toString(); + } uri = ((LinkedHashMap) annotAttSymbol.attachmentValue().orElseThrow().value()) .get("uri").toString(); } @@ -302,7 +362,16 @@ private QualifiedName getQNameFromAnnotation(String fieldName, return new QualifiedName(uri, name, prefix); } - private boolean isFromXmlFunctionFromXmldata(ExpressionNode expressionNode) { + private String getAnnotModuleName(AnnotationSymbol annotation) { + Optional moduleSymbol = annotation.getModule(); + if (moduleSymbol.isEmpty()) { + return ""; + } + Optional moduleName = moduleSymbol.get().getName(); + return moduleName.orElse(""); + } + + private boolean isNotFromXmlFunctionFromXmldata(ExpressionNode expressionNode) { if (expressionNode.kind() == SyntaxKind.CHECK_EXPRESSION) { expressionNode = ((CheckExpressionNode) expressionNode).expression(); } diff --git a/gradle.properties b/gradle.properties index 538c62a..8c79c39 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ org.gradle.caching=true group=io.ballerina.stdlib version=0.1.0-SNAPSHOT -ballerinaLangVersion=2201.8.1 +ballerinaLangVersion=2201.8.5 checkstyleToolVersion=10.12.0 puppycrawlCheckstyleVersion=10.12.0 diff --git a/native/src/main/java/io/ballerina/stdlib/data/xmldata/FromString.java b/native/src/main/java/io/ballerina/stdlib/data/xmldata/FromString.java index fbf73a3..bfb6c51 100644 --- a/native/src/main/java/io/ballerina/stdlib/data/xmldata/FromString.java +++ b/native/src/main/java/io/ballerina/stdlib/data/xmldata/FromString.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). * * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except @@ -20,10 +20,12 @@ import io.ballerina.runtime.api.PredefinedTypes; import io.ballerina.runtime.api.TypeTags; +import io.ballerina.runtime.api.creators.TypeCreator; import io.ballerina.runtime.api.creators.ValueCreator; import io.ballerina.runtime.api.types.ReferenceType; import io.ballerina.runtime.api.types.Type; import io.ballerina.runtime.api.types.UnionType; +import io.ballerina.runtime.api.utils.TypeUtils; import io.ballerina.runtime.api.values.BDecimal; import io.ballerina.runtime.api.values.BError; import io.ballerina.runtime.api.values.BString; @@ -31,16 +33,37 @@ import io.ballerina.stdlib.data.xmldata.utils.DiagnosticErrorCode; import io.ballerina.stdlib.data.xmldata.utils.DiagnosticLog; +import java.util.ArrayList; import java.util.Comparator; import java.util.List; /** * Native implementation of data:fromStringWithType(string). - * + * * @since 0.1.0 */ public class FromString { + private static final List TYPE_PRIORITY_ORDER = List.of( + TypeTags.INT_TAG, + TypeTags.FLOAT_TAG, + TypeTags.DECIMAL_TAG, + TypeTags.NULL_TAG, + TypeTags.BOOLEAN_TAG, + TypeTags.JSON_TAG, + TypeTags.STRING_TAG + ); + + private static final List BASIC_JSON_MEMBER_TYPES = List.of( + PredefinedTypes.TYPE_NULL, + PredefinedTypes.TYPE_BOOLEAN, + PredefinedTypes.TYPE_INT, + PredefinedTypes.TYPE_FLOAT, + PredefinedTypes.TYPE_DECIMAL, + PredefinedTypes.TYPE_STRING + ); + private static final UnionType JSON_TYPE_WITH_BASIC_TYPES = TypeCreator.createUnionType(BASIC_JSON_MEMBER_TYPES); + public static Object fromStringWithType(BString string, BTypedesc typed) { Type expType = typed.getDescribingType(); @@ -51,11 +74,7 @@ public static Object fromStringWithType(BString string, BTypedesc typed) { } } - public static Object fromStringWithTypeInternal(BString string, Type expType) { - return fromStringWithType(string, expType); - } - - private static Object fromStringWithType(BString string, Type expType) { + public static Object fromStringWithType(BString string, Type expType) { String value = string.getValue(); try { switch (expType.getTag()) { @@ -73,6 +92,8 @@ private static Object fromStringWithType(BString string, Type expType) { return stringToNull(value); case TypeTags.UNION_TAG: return stringToUnion(string, (UnionType) expType); + case TypeTags.JSON_TAG: + return stringToUnion(string, JSON_TYPE_WITH_BASIC_TYPES); case TypeTags.TYPE_REFERENCED_TYPE_TAG: return fromStringWithType(string, ((ReferenceType) expType).getReferredType()); default: @@ -97,7 +118,7 @@ private static Double stringToFloat(String value) throws NumberFormatException { private static BDecimal stringToDecimal(String value) throws NumberFormatException { return ValueCreator.createDecimalValue(value); } - + private static Object stringToBoolean(String value) throws NumberFormatException { if ("true".equalsIgnoreCase(value) || "1".equalsIgnoreCase(value)) { return true; @@ -117,16 +138,13 @@ private static Object stringToNull(String value) throws NumberFormatException { } private static Object stringToUnion(BString string, UnionType expType) throws NumberFormatException { - List memberTypes = expType.getMemberTypes(); - memberTypes.sort(Comparator.comparingInt(t -> t.getTag())); - boolean isStringExpType = false; + List memberTypes = new ArrayList<>(expType.getMemberTypes()); + memberTypes.sort(Comparator.comparingInt(t -> TYPE_PRIORITY_ORDER.indexOf( + TypeUtils.getReferredType(t).getTag()))); for (Type memberType : memberTypes) { try { Object result = fromStringWithType(string, memberType); - if (result instanceof BString) { - isStringExpType = true; - continue; - } else if (result instanceof BError) { + if (result instanceof BError) { continue; } return result; @@ -134,10 +152,6 @@ private static Object stringToUnion(BString string, UnionType expType) throws Nu // Skip } } - - if (isStringExpType) { - return string; - } return returnError(string.getValue(), expType.toString()); } diff --git a/native/src/main/java/io/ballerina/stdlib/data/xmldata/utils/Constants.java b/native/src/main/java/io/ballerina/stdlib/data/xmldata/utils/Constants.java index e860fae..257a53e 100644 --- a/native/src/main/java/io/ballerina/stdlib/data/xmldata/utils/Constants.java +++ b/native/src/main/java/io/ballerina/stdlib/data/xmldata/utils/Constants.java @@ -55,4 +55,5 @@ private Constants() {} public static final String RECORD = "record"; public static final String RECORD_OR_MAP = "record or map"; public static final String ANON_TYPE = "$anonType$"; + public static final QualifiedName EXIT_REST_POINT = new QualifiedName("", "$exitRestPoint$", ""); } diff --git a/native/src/main/java/io/ballerina/stdlib/data/xmldata/utils/DataUtils.java b/native/src/main/java/io/ballerina/stdlib/data/xmldata/utils/DataUtils.java index b8946ac..db0c32b 100644 --- a/native/src/main/java/io/ballerina/stdlib/data/xmldata/utils/DataUtils.java +++ b/native/src/main/java/io/ballerina/stdlib/data/xmldata/utils/DataUtils.java @@ -201,9 +201,9 @@ public static Object convertStringToExpType(BString value, Type expType) { Object result; switch (expType.getTag()) { case TypeTags.ANYDATA_TAG, TypeTags.ANY_TAG, TypeTags.JSON_TAG -> - result = FromString.fromStringWithTypeInternal(value, PredefinedTypes.TYPE_STRING); + result = FromString.fromStringWithType(value, PredefinedTypes.TYPE_JSON); case TypeTags.ARRAY_TAG -> result = convertStringToExpType(value, ((ArrayType) expType).getElementType()); - default -> result = FromString.fromStringWithTypeInternal(value, expType); + default -> result = FromString.fromStringWithType(value, expType); } if (result instanceof BError) { @@ -297,6 +297,45 @@ public static void removeExpectedTypeStacks(XmlAnalyzerData analyzerData) { analyzerData.restTypes.pop(); } + public static boolean isAnydataOrJson(int typeTag) { + return typeTag == TypeTags.ANYDATA_TAG || typeTag == TypeTags.JSON_TAG; + } + + public static boolean isSupportedType(Type type) { + switch (type.getTag()) { + case TypeTags.NULL_TAG, TypeTags.INT_TAG, TypeTags.BYTE_TAG, TypeTags.FLOAT_TAG, TypeTags.DECIMAL_TAG, + TypeTags.BOOLEAN_TAG, TypeTags.STRING_TAG, TypeTags.RECORD_TYPE_TAG, TypeTags.MAP_TAG, + TypeTags.JSON_TAG, TypeTags.ANYDATA_TAG -> { + return true; + } + case TypeTags.ARRAY_TAG -> { + return isSupportedType(((ArrayType) type).getElementType()); + } + case TypeTags.UNION_TAG -> { + return isSupportedUnionType((UnionType) type); + } + case TypeTags.TYPE_REFERENCED_TYPE_TAG -> { + return isSupportedType(TypeUtils.getReferredType(type)); + } + } + return false; + } + + private static boolean isSupportedUnionType(UnionType type) { + for (Type memberType : type.getMemberTypes()) { + switch (memberType.getTag()) { + case TypeTags.RECORD_TYPE_TAG, TypeTags.OBJECT_TYPE_TAG, TypeTags.MAP_TAG, TypeTags.JSON_TAG, + TypeTags.ANYDATA_TAG, TypeTags.XML_TAG -> { + return false; + } + case TypeTags.UNION_TAG -> { + return !isSupportedUnionType(type); + } + } + } + return true; + } + @SuppressWarnings("unchecked") public static Object getModifiedRecord(BMap input, BTypedesc type) { Type describingType = type.getDescribingType(); diff --git a/native/src/main/java/io/ballerina/stdlib/data/xmldata/utils/DiagnosticErrorCode.java b/native/src/main/java/io/ballerina/stdlib/data/xmldata/utils/DiagnosticErrorCode.java index ff1ee55..39fa57c 100644 --- a/native/src/main/java/io/ballerina/stdlib/data/xmldata/utils/DiagnosticErrorCode.java +++ b/native/src/main/java/io/ballerina/stdlib/data/xmldata/utils/DiagnosticErrorCode.java @@ -25,22 +25,22 @@ */ public enum DiagnosticErrorCode { - INVALID_TYPE("BDE_0001", "invalid.type"), - XML_ROOT_MISSING("BDE_0002", "xml.root.missing"), - INVALID_REST_TYPE("BDE_0003", "invalid.rest.type"), - ARRAY_SIZE_MISMATCH("BDE_0004", "array.size.mismatch"), - REQUIRED_FIELD_NOT_PRESENT("BDE_0005", "required.field.not.present"), - REQUIRED_ATTRIBUTE_NOT_PRESENT("BDE_0006", "required.attribute.not.present"), - DUPLICATE_FIELD("BDE_0007", "duplicate.field"), - FOUND_ARRAY_FOR_NON_ARRAY_TYPE("BDE_0008", "found.array.for.non.array.type"), - EXPECTED_ANYDATA_OR_JSON("BDE_0009", "expected.anydata.or.json"), - NAMESPACE_MISMATCH("BDE_0010", "namespace.mismatch"), - TYPE_NAME_MISMATCH_WITH_XML_ELEMENT("BDE_0011", "type.name.mismatch.with.xml.element"), - CAN_NOT_READ_STREAM("BDE_0012", "error.cannot.read.stream"), - CANNOT_CONVERT_TO_EXPECTED_TYPE("BDE_0013", "cannot.convert.to.expected.type"), - UNSUPPORTED_TYPE("BDE_0014", "unsupported.type"), - STREAM_BROKEN("BDE_0015", "stream.broken"), - XML_PARSE_ERROR("BDE_0016", "xml.parse.error"); + INVALID_TYPE("XML_ERROR_001", "invalid.type"), + XML_ROOT_MISSING("XML_ERROR_002", "xml.root.missing"), + INVALID_REST_TYPE("XML_ERROR_003", "invalid.rest.type"), + ARRAY_SIZE_MISMATCH("XML_ERROR_004", "array.size.mismatch"), + REQUIRED_FIELD_NOT_PRESENT("XML_ERROR_005", "required.field.not.present"), + REQUIRED_ATTRIBUTE_NOT_PRESENT("XML_ERROR_006", "required.attribute.not.present"), + DUPLICATE_FIELD("XML_ERROR_007", "duplicate.field"), + FOUND_ARRAY_FOR_NON_ARRAY_TYPE("XML_ERROR_008", "found.array.for.non.array.type"), + EXPECTED_ANYDATA_OR_JSON("XML_ERROR_009", "expected.anydata.or.json"), + NAMESPACE_MISMATCH("XML_ERROR_010", "namespace.mismatch"), + TYPE_NAME_MISMATCH_WITH_XML_ELEMENT("XML_ERROR_011", "type.name.mismatch.with.xml.element"), + CAN_NOT_READ_STREAM("XML_ERROR_012", "error.cannot.read.stream"), + CANNOT_CONVERT_TO_EXPECTED_TYPE("XML_ERROR_013", "cannot.convert.to.expected.type"), + UNSUPPORTED_TYPE("XML_ERROR_014", "unsupported.type"), + STREAM_BROKEN("XML_ERROR_015", "stream.broken"), + XML_PARSE_ERROR("XML_ERROR_016", "xml.parse.error"); String diagnosticId; String messageKey; diff --git a/native/src/main/java/io/ballerina/stdlib/data/xmldata/xml/QualifiedName.java b/native/src/main/java/io/ballerina/stdlib/data/xmldata/xml/QualifiedName.java index c0b26e2..f3a2b35 100644 --- a/native/src/main/java/io/ballerina/stdlib/data/xmldata/xml/QualifiedName.java +++ b/native/src/main/java/io/ballerina/stdlib/data/xmldata/xml/QualifiedName.java @@ -68,12 +68,10 @@ public boolean equals(Object objectToTest) { return true; } - if (objectToTest == null || !(objectToTest instanceof QualifiedName)) { + if (!(objectToTest instanceof QualifiedName qName)) { return false; } - QualifiedName qName = (QualifiedName) objectToTest; - if (qName.namespaceURI.equals(NS_ANNOT_NOT_DEFINED) || namespaceURI.equals(NS_ANNOT_NOT_DEFINED)) { return localPart.equals(qName.localPart); } diff --git a/native/src/main/java/io/ballerina/stdlib/data/xmldata/xml/XmlParser.java b/native/src/main/java/io/ballerina/stdlib/data/xmldata/xml/XmlParser.java index 086c5ac..2ea4771 100644 --- a/native/src/main/java/io/ballerina/stdlib/data/xmldata/xml/XmlParser.java +++ b/native/src/main/java/io/ballerina/stdlib/data/xmldata/xml/XmlParser.java @@ -227,6 +227,7 @@ private void parseRootElement(XMLStreamReader xmlStreamReader, handleAttributes(xmlStreamReader, xmlParserData); } + @SuppressWarnings("unchecked") private void readText(XMLStreamReader xmlStreamReader, boolean isCData, XmlParserData xmlParserData) throws XMLStreamException { @@ -262,7 +263,7 @@ private void readText(XMLStreamReader xmlStreamReader, throw DiagnosticLog.error(DiagnosticErrorCode.INVALID_TYPE, fieldType, PredefinedTypes.TYPE_STRING); } - if (xmlParserData.currentNode.containsKey(bFieldName)) { + if (xmlParserData.currentNode.containsKey(bFieldName) && !DataUtils.isAnydataOrJson(fieldType.getTag())) { if (!DataUtils.isArrayValueAssignable(fieldType.getTag())) { throw DiagnosticLog.error(DiagnosticErrorCode.FOUND_ARRAY_FOR_NON_ARRAY_TYPE, fieldType, fieldName); } @@ -276,13 +277,54 @@ private void readText(XMLStreamReader xmlStreamReader, return; } - if (fieldType.getTag() == TypeTags.RECORD_TYPE_TAG) { - handleContentFieldInRecordType((RecordType) fieldType, bText, xmlParserData); - } else if (fieldType.getTag() == TypeTags.ARRAY_TAG - && ((ArrayType) fieldType).getElementType().getTag() == TypeTags.RECORD_TYPE_TAG) { - handleContentFieldInRecordType((RecordType) ((ArrayType) fieldType).getElementType(), bText, xmlParserData); - } else { - xmlParserData.currentNode.put(bFieldName, convertStringToRestExpType(bText, fieldType)); + switch (fieldType.getTag()) { + case TypeTags.RECORD_TYPE_TAG -> handleContentFieldInRecordType((RecordType) fieldType, bText, + xmlParserData); + case TypeTags.ARRAY_TAG -> + addTextToCurrentNodeIfExpTypeIsArray((ArrayType) fieldType, bFieldName, bText, xmlParserData); + case TypeTags.ANYDATA_TAG, TypeTags.JSON_TAG -> + convertTextAndUpdateCurrentNode(xmlParserData.currentNode, + (BMap) xmlParserData.nodesStack.pop(), + bFieldName, bText, fieldType, xmlParserData); + default -> xmlParserData.currentNode.put(bFieldName, convertStringToRestExpType(bText, fieldType)); + } + } + + private void convertTextAndUpdateCurrentNode(BMap currentNode, + BMap parent, + BString currentFieldName, + BString bText, Type restType, + XmlParserData xmlParserData) { + Object currentElement = currentNode.get(currentFieldName); + Object result = convertStringToRestExpType(bText, restType); + + if (currentElement == null && !currentNode.isEmpty()) { // Add text to the #content field + currentNode.put(StringUtils.fromString(Constants.CONTENT), result); + } else if (parent.get(currentFieldName) instanceof BArray bArray) { + bArray.add(bArray.getLength() - 1, result); + } else { + parent.put(currentFieldName, result); + } + + xmlParserData.currentNode = parent; + xmlParserData.fieldHierarchy.pop(); + xmlParserData.restTypes.pop(); + xmlParserData.attributeHierarchy.pop(); + xmlParserData.recordTypeStack.pop(); + xmlParserData.siblings = xmlParserData.parents.pop(); + } + + @SuppressWarnings("unchecked") + private void addTextToCurrentNodeIfExpTypeIsArray(ArrayType fieldType, BString bFieldName, BString bText, + XmlParserData xmlParserData) { + int elementTypeTag = TypeUtils.getReferredType(fieldType.getElementType()).getTag(); + switch (elementTypeTag) { + case TypeTags.RECORD_TYPE_TAG -> handleContentFieldInRecordType((RecordType) fieldType.getElementType(), + bText, xmlParserData); + case TypeTags.ANYDATA_TAG, TypeTags.JSON_TAG -> { + BArray tempArr = (BArray) ((BMap) xmlParserData.nodesStack.peek()).get(bFieldName); + tempArr.add(tempArr.getLength() - 1, convertStringToRestExpType(bText, fieldType)); + } } } @@ -325,7 +367,7 @@ private Object convertStringToExpType(BString value, Type expType) { if (expType.getTag() == TypeTags.ARRAY_TAG) { expType = ((ArrayType) expType).getElementType(); } - Object result = FromString.fromStringWithTypeInternal(value, expType); + Object result = FromString.fromStringWithType(value, expType); if (result instanceof BError) { throw (BError) result; } @@ -342,18 +384,17 @@ private Object convertStringToRestExpType(BString value, Type expType) { return convertStringToExpType(value, expType); } case TypeTags.ANYDATA_TAG, TypeTags.JSON_TAG -> { - return convertStringToExpType(value, PredefinedTypes.TYPE_STRING); + return convertStringToExpType(value, PredefinedTypes.TYPE_JSON); } case TypeTags.TYPE_REFERENCED_TYPE_TAG -> { - return convertStringToExpType(value, TypeUtils.getReferredType(expType)); + return convertStringToRestExpType(value, TypeUtils.getReferredType(expType)); } } throw DiagnosticLog.error(DiagnosticErrorCode.INVALID_REST_TYPE, expType.getName()); } - private Object buildDocument(XmlParserData xmlParserData) { + private void buildDocument(XmlParserData xmlParserData) { validateRequiredFields(xmlParserData); - return xmlParserData.currentNode; } @SuppressWarnings("unchecked") @@ -407,6 +448,14 @@ private void validateRequiredFields(XmlParserData xmlParserData) { private void readElement(XMLStreamReader xmlStreamReader, XmlParserData xmlParserData) { QualifiedName elemQName = getElementName(xmlStreamReader); + // Assume type of record field `name` is `string[]` and + // relevant source position is `11` + for (QualifiedName key : xmlParserData.fieldHierarchy.peek().keySet()) { + if (key.equals(elemQName)) { + elemQName = key; + break; + } + } Field currentField = xmlParserData.fieldHierarchy.peek().get(elemQName); xmlParserData.currentField = currentField; if (xmlParserData.currentField == null) { @@ -440,16 +489,56 @@ private void readElement(XMLStreamReader xmlStreamReader, XmlParserData xmlParse ValueCreator.createArrayValue(DataUtils.getArrayTypeFromElementType(referredType))); } - if (referredType.getTag() == TypeTags.RECORD_TYPE_TAG) { - updateNextRecord(xmlStreamReader, xmlParserData, fieldName, fieldType, (RecordType) referredType); - } + updateNextArrayMember(xmlStreamReader, xmlParserData, fieldName, fieldType, referredType); } - case TypeTags.MAP_TAG -> { - RecordType recordType = TypeCreator.createRecordType(Constants.ANON_TYPE, fieldType.getPackage(), 0, - new HashMap<>(), ((MapType) fieldType).getConstrainedType(), false, 0); - updateNextRecord(xmlStreamReader, xmlParserData, fieldName, fieldType, recordType); + case TypeTags.MAP_TAG, TypeTags.ANYDATA_TAG, TypeTags.JSON_TAG -> + updateNextMap(xmlParserData, fieldName, fieldType); + } + } + + private void updateNextArrayMember(XMLStreamReader xmlStreamReader, XmlParserData xmlParserData, + String fieldName, Type fieldType, Type type) { + switch (type.getTag()) { + case TypeTags.RECORD_TYPE_TAG -> updateNextRecord(xmlStreamReader, xmlParserData, fieldName, + fieldType, (RecordType) type); + case TypeTags.MAP_TAG, TypeTags.ANYDATA_TAG, TypeTags.JSON_TAG -> + updateNextMap(xmlParserData, fieldName, type); + } + } + + private void updateNextMap(XmlParserData xmlParserData, String fieldName, Type fieldType) { + xmlParserData.parents.push(xmlParserData.siblings); + xmlParserData.siblings = new LinkedHashMap<>(); + BMap nextMapValue = updateNextMapValue(xmlParserData, fieldName, fieldType); + handleAttributesRest(xmlStreamReader, fieldType, nextMapValue); + xmlParserData.currentNode = nextMapValue; + } + + private BMap updateNextMapValue(XmlParserData xmlParserData, String fieldName, Type fieldType) { + BMap nextValue; + if (fieldType.getTag() == TypeTags.MAP_TAG) { + nextValue = ValueCreator.createMapValue((MapType) fieldType); + xmlParserData.restTypes.push(((MapType) fieldType).getConstrainedType()); + } else { + nextValue = ValueCreator.createMapValue(TypeCreator.createMapType(fieldType)); + xmlParserData.restTypes.push(fieldType); + } + + xmlParserData.attributeHierarchy.push(new HashMap<>()); + xmlParserData.fieldHierarchy.push(new HashMap<>()); + xmlParserData.recordTypeStack.push(null); + BMap currentNode = xmlParserData.currentNode; + Object temp = currentNode.get(StringUtils.fromString(fieldName)); + if (temp instanceof BArray) { + int arraySize = ((ArrayType) TypeUtils.getType(temp)).getSize(); + if (arraySize > ((BArray) temp).getLength() || arraySize == -1) { + ((BArray) temp).append(nextValue); } + } else { + currentNode.put(StringUtils.fromString(fieldName), nextValue); } + xmlParserData.nodesStack.push(currentNode); + return nextValue; } private void updateNextRecord(XMLStreamReader xmlStreamReader, XmlParserData xmlParserData, String fieldName, @@ -516,6 +605,11 @@ private Object parseRestField(XmlParserData xmlParserData) { try { boolean readNext = false; while (!xmlParserData.restFieldsPoints.isEmpty()) { + if (xmlParserData.restFieldsPoints.peek() == Constants.EXIT_REST_POINT) { + xmlParserData.restFieldsPoints.pop(); + break; + } + switch (next) { case START_ELEMENT -> currentFieldName = readElementRest(xmlStreamReader, xmlParserData); case END_ELEMENT -> endElementRest(xmlStreamReader, xmlParserData); @@ -544,6 +638,15 @@ private Object parseRestField(XmlParserData xmlParserData) { return xmlParserData.nodesStack.pop(); } + private void updateExpectedTypeStacksOfRestType(Type restType, XmlParserData xmlParserData) { + if (restType.getTag() == TypeTags.ARRAY_TAG) { + updateExpectedTypeStacksOfRestType(((ArrayType) restType).getElementType(), xmlParserData); + } else if (restType.getTag() == TypeTags.ANYDATA_TAG || restType.getTag() == TypeTags.JSON_TAG) { + xmlParserData.fieldHierarchy.push(new HashMap<>()); + xmlParserData.restTypes.push(restType); + } + } + @SuppressWarnings("unchecked") private BString readElementRest(XMLStreamReader xmlStreamReader, XmlParserData xmlParserData) { QualifiedName elemQName = getElementName(xmlStreamReader); @@ -555,16 +658,25 @@ private BString readElementRest(XMLStreamReader xmlStreamReader, XmlParserData x && !xmlParserData.siblings.getOrDefault(lastElement, true)) { xmlParserData.parents.push(xmlParserData.siblings); xmlParserData.siblings = new LinkedHashMap<>(); + updateExpectedTypeStacksOfRestType(restType, xmlParserData); xmlParserData.siblings.put(elemQName, false); - BMap temp = - (BMap) xmlParserData.currentNode.get( - StringUtils.fromString(lastElement.getLocalPart())); BMap next = ValueCreator.createMapValue(DataUtils.getMapTypeFromConstraintType(restType)); - temp.put(currentFieldName, next); - xmlParserData.nodesStack.add(xmlParserData.currentNode); - xmlParserData.currentNode = temp; handleAttributesRest(xmlStreamReader, restType, next); + + Object temp = xmlParserData.currentNode.get( + StringUtils.fromString(lastElement.getLocalPart())); + BMap mapValue; + if (temp instanceof BArray) { + mapValue = ValueCreator.createMapValue(DataUtils.getMapTypeFromConstraintType(restType)); + mapValue.put(currentFieldName, next); + ((BArray) temp).append(mapValue); + } else { + mapValue = (BMap) temp; + mapValue.put(currentFieldName, next); + } + xmlParserData.nodesStack.add(xmlParserData.currentNode); + xmlParserData.currentNode = mapValue; return currentFieldName; } @@ -582,14 +694,15 @@ private BString readElementRest(XMLStreamReader xmlStreamReader, XmlParserData x return currentFieldName; } - xmlParserData.parents.push(xmlParserData.siblings); - xmlParserData.siblings = new LinkedHashMap<>(); Object currentElement = xmlParserData.currentNode.get(currentFieldName); - xmlParserData.nodesStack.add(xmlParserData.currentNode); if (currentElement instanceof BArray) { int elemTypeTag = ((BArray) currentElement).getElementType().getTag(); if (elemTypeTag == TypeTags.ANYDATA_TAG || elemTypeTag == TypeTags.JSON_TAG) { + xmlParserData.nodesStack.add(xmlParserData.currentNode); + xmlParserData.parents.push(xmlParserData.siblings); + xmlParserData.siblings = new LinkedHashMap<>(); + updateExpectedTypeStacksOfRestType(restType, xmlParserData); xmlParserData.currentNode = updateNextArrayMemberForRestType((BArray) currentElement, restType); } return currentFieldName; @@ -605,6 +718,10 @@ private BString readElementRest(XMLStreamReader xmlStreamReader, XmlParserData x int elemTypeTag = tempArray.getElementType().getTag(); if (elemTypeTag == TypeTags.ANYDATA_TAG || elemTypeTag == TypeTags.JSON_TAG) { + xmlParserData.nodesStack.add(xmlParserData.currentNode); + xmlParserData.parents.push(xmlParserData.siblings); + xmlParserData.siblings = new LinkedHashMap<>(); + updateExpectedTypeStacksOfRestType(restType, xmlParserData); xmlParserData.currentNode = updateNextArrayMemberForRestType(tempArray, restType); } return currentFieldName; @@ -630,10 +747,11 @@ private void endElementRest(XMLStreamReader xmlStreamReader, XmlParserData xmlPa xmlParserData.currentNode = (BMap) xmlParserData.nodesStack.pop(); xmlParserData.siblings = xmlParserData.parents.pop(); - if (xmlParserData.siblings.containsKey(elemQName) && xmlParserData.restFieldsPoints.remove(elemQName)) { + if (xmlParserData.siblings.containsKey(elemQName)) { xmlParserData.fieldHierarchy.pop(); xmlParserData.restTypes.pop(); } + xmlParserData.restFieldsPoints.remove(elemQName); xmlParserData.siblings.put(elemQName, true); } @@ -662,19 +780,30 @@ private void readTextRest(XMLStreamReader xmlStreamReader, throw DiagnosticLog.error(DiagnosticErrorCode.INVALID_TYPE, restType, PredefinedTypes.TYPE_STRING); } - Object currentElement = xmlParserData.currentNode.get(currentFieldName); - BMap parent = (BMap) xmlParserData.nodesStack.peek(); + convertTextRestAndUpdateCurrentNodeForRestType(xmlParserData.currentNode, + (BMap) xmlParserData.nodesStack.peek(), currentFieldName, bText, restType); + } + + @SuppressWarnings("unchecked") + private void convertTextRestAndUpdateCurrentNodeForRestType(BMap currentNode, + BMap parent, + BString currentFieldName, + BString bText, Type restType) { + Object currentElement = currentNode.get(currentFieldName); Object result = convertStringToRestExpType(bText, restType); - if (currentElement == null && !xmlParserData.currentNode.isEmpty()) { // Add text to the #content field - xmlParserData.currentNode.put(StringUtils.fromString(Constants.CONTENT), result); - xmlParserData.currentNode = parent; - } else if (currentElement instanceof BArray) { + if (currentElement == null && DataUtils.isAnydataOrJson(restType.getTag()) && + parent != null && parent.get(currentFieldName) instanceof BArray bArray) { + bArray.add(bArray.getLength() - 1, result); + return; + } + + if (currentElement instanceof BArray) { ((BArray) currentElement).append(result); } else if (currentElement instanceof BMap && !((BMap) currentElement).isEmpty()) { ((BMap) currentElement).put(StringUtils.fromString(Constants.CONTENT), result); } else { - xmlParserData.currentNode.put(currentFieldName, result); + currentNode.put(currentFieldName, result); } } @@ -789,6 +918,7 @@ private Optional handleRecordRestType(XmlParserData xmlParserData, XMLSt switch (restType.getTag()) { case TypeTags.RECORD_TYPE_TAG -> { RecordType recordType = (RecordType) restType; + updateStacksWhenRecordAsRestType(elementQName, xmlParserData); xmlParserData.currentNode = updateNextValue(recordType, fieldName, restType, xmlParserData); handleAttributes(xmlStreamReader, xmlParserData); parseRecordRest(fieldName, xmlParserData); @@ -799,6 +929,7 @@ private Optional handleRecordRestType(XmlParserData xmlParserData, XMLSt ArrayType arrayType = (ArrayType) restType; Type elemType = TypeUtils.getReferredType(arrayType.getElementType()); if (elemType.getTag() == TypeTags.RECORD_TYPE_TAG) { + updateStacksWhenRecordAsRestType(elementQName, xmlParserData); // Create an array value since expected type is an array. if (!xmlParserData.currentNode.containsKey(StringUtils.fromString(fieldName))) { xmlParserData.currentNode.put(StringUtils.fromString(fieldName), @@ -816,6 +947,15 @@ private Optional handleRecordRestType(XmlParserData xmlParserData, XMLSt return Optional.empty(); } + private void updateStacksWhenRecordAsRestType(QualifiedName elementQName, XmlParserData xmlParserData) { + if (!xmlParserData.siblings.containsKey(elementQName)) { + xmlParserData.siblings.put(elementQName, false); + } + xmlParserData.restFieldsPoints.push(Constants.EXIT_REST_POINT); + xmlParserData.parents.push(xmlParserData.siblings); + xmlParserData.siblings = new LinkedHashMap<>(); + } + private QualifiedName getElementName(XMLStreamReader xmlStreamReader) { QName qName = xmlStreamReader.getName(); return new QualifiedName(qName.getNamespaceURI(), qName.getLocalPart(), qName.getPrefix()); diff --git a/native/src/main/java/io/ballerina/stdlib/data/xmldata/xml/XmlTraversal.java b/native/src/main/java/io/ballerina/stdlib/data/xmldata/xml/XmlTraversal.java index 0d6afbd..f3d0753 100644 --- a/native/src/main/java/io/ballerina/stdlib/data/xmldata/xml/XmlTraversal.java +++ b/native/src/main/java/io/ballerina/stdlib/data/xmldata/xml/XmlTraversal.java @@ -164,6 +164,10 @@ private void convertElement(BXmlItem xmlItem, XmlAnalyzerData analyzerData) { BMap mapValue = (BMap) currentNode; Type currentFieldType = TypeUtils.getReferredType(currentField.getFieldType()); + if (!DataUtils.isSupportedType(currentFieldType)) { + throw DiagnosticLog.error(DiagnosticErrorCode.UNSUPPORTED_TYPE, currentFieldType); + } + String fieldName = currentField.getFieldName(); BString bCurrentFieldName = StringUtils.fromString(fieldName); switch (currentFieldType.getTag()) { @@ -182,7 +186,8 @@ private void convertElement(BXmlItem xmlItem, XmlAnalyzerData analyzerData) { mapValue.put(bCurrentFieldName, array); } Type elementType = TypeUtils.getReferredType(((ArrayType) currentFieldType).getElementType()); - if (elementType.getTag() == TypeTags.RECORD_TYPE_TAG) { + int elementTypeTag = elementType.getTag(); + if (elementTypeTag == TypeTags.RECORD_TYPE_TAG) { currentNode = updateNextRecord(xmlItem, (RecordType) elementType, fieldName, currentFieldType, mapValue, analyzerData); traverseXml(xmlItem.getChildrenSeq(), currentFieldType, analyzerData); @@ -190,22 +195,52 @@ private void convertElement(BXmlItem xmlItem, XmlAnalyzerData analyzerData) { DataUtils.removeExpectedTypeStacks(analyzerData); currentNode = analyzerData.nodesStack.pop(); return; + } else if (elementTypeTag == TypeTags.MAP_TAG) { + updateNextMap(elementType, analyzerData); + currentNode = updateNextValue(elementType, fieldName, currentFieldType, mapValue, + analyzerData); + traverseXml(xmlItem.getChildrenSeq(), currentFieldType, analyzerData); + DataUtils.validateRequiredFields((BMap) currentNode, analyzerData); + DataUtils.removeExpectedTypeStacks(analyzerData); + currentNode = analyzerData.nodesStack.pop(); + return; + } else if (elementTypeTag == TypeTags.JSON_TAG || elementTypeTag == TypeTags.ANYDATA_TAG) { + updateNextMap(elementType, analyzerData); + convertWithRestType(xmlItem, elementType, analyzerData); + DataUtils.removeExpectedTypeStacks(analyzerData); + return; } } case TypeTags.MAP_TAG -> { - RecordType recordType = TypeCreator.createRecordType("$anonType$", currentFieldType.getPackage(), 0, - new HashMap<>(), ((MapType) currentFieldType).getConstrainedType(), false, 0); - currentNode = updateNextRecord(xmlItem, recordType, fieldName, - currentFieldType, mapValue, analyzerData); - traverseXml(xmlItem.getChildrenSeq(), recordType, analyzerData); + updateNextMap(currentFieldType, analyzerData); + currentNode = updateNextValue(currentFieldType, fieldName, currentFieldType, mapValue, + analyzerData); + traverseXml(xmlItem.getChildrenSeq(), currentFieldType, analyzerData); DataUtils.validateRequiredFields((BMap) currentNode, analyzerData); DataUtils.removeExpectedTypeStacks(analyzerData); currentNode = analyzerData.nodesStack.pop(); + return; + } + case TypeTags.JSON_TAG, TypeTags.ANYDATA_TAG -> { + updateNextMap(currentFieldType, analyzerData); + convertWithRestType(xmlItem, currentFieldType, analyzerData); + DataUtils.removeExpectedTypeStacks(analyzerData); + return; } } traverseXml(xmlItem.getChildrenSeq(), currentFieldType, analyzerData); } + private void updateNextMap(Type fieldType, XmlAnalyzerData analyzerData) { + if (fieldType.getTag() == TypeTags.MAP_TAG) { + analyzerData.restTypes.push(((MapType) fieldType).getConstrainedType()); + } else { + analyzerData.restTypes.push(fieldType); + } + analyzerData.fieldHierarchy.push(new HashMap<>()); + analyzerData.attributeHierarchy.push(new HashMap<>()); + } + private BMap updateNextRecord(BXmlItem xmlItem, RecordType recordType, String fieldName, Type fieldType, BMap currentMapValue, XmlAnalyzerData analyzerData) { @@ -214,14 +249,22 @@ private BMap updateNextRecord(BXmlItem xmlItem, RecordType reco updateNextValue(recordType, fieldName, fieldType, currentMapValue, analyzerData); QName qName = xmlItem.getQName(); DataUtils.validateTypeNamespace(qName.getPrefix(), qName.getNamespaceURI(), recordType); - handleAttributes(xmlItem, nextValue, recordType, analyzerData); + handleAttributes(xmlItem, nextValue, analyzerData); return nextValue; } - private BMap updateNextValue(RecordType recordType, String fieldName, Type fieldType, + private BMap updateNextValue(Type type, String fieldName, Type fieldType, BMap currentMapValue, XmlAnalyzerData analyzerData) { analyzerData.currentField = null; - BMap nextValue = ValueCreator.createRecordValue(recordType); + + BMap nextValue; + switch (type.getTag()) { + case TypeTags.RECORD_TYPE_TAG -> nextValue = ValueCreator.createRecordValue((RecordType) type); + case TypeTags.MAP_TAG -> nextValue = ValueCreator.createMapValue((MapType) type); + case TypeTags.ANYDATA_TAG, TypeTags.JSON_TAG -> + nextValue = ValueCreator.createMapValue(TypeCreator.createMapType(type)); + default -> throw DiagnosticLog.error(DiagnosticErrorCode.UNSUPPORTED_TYPE, type); + } Object temp = currentMapValue.get(StringUtils.fromString(fieldName)); if (temp instanceof BArray) { @@ -288,11 +331,7 @@ private void convertWithRestType(BXmlItem xmlItem, Type restType, XmlAnalyzerDat BMap nextValue = ValueCreator.createMapValue(DataUtils.getMapTypeFromConstraintType(restType)); handleAttributesRest(xmlItem, nextValue, restType); - if (currentElement instanceof BArray) { - arrayValue.append(nextValue); - } else { - mapValue.put(bElementName, nextValue); - } + arrayValue.append(nextValue); if (!nextValue.isEmpty()) { analyzerData.currentField = @@ -313,7 +352,18 @@ private void convertWithRestType(BXmlItem xmlItem, Type restType, XmlAnalyzerDat analyzerData.nodesStack.push(currentNode); currentNode = nextValue; handleAttributesRest(xmlItem, nextValue, restType); - traverseXml(xmlItem.getChildrenSeq(), restType, analyzerData); + + analyzerData.fieldHierarchy.push(new HashMap<>()); + if (restType.getTag() == TypeTags.ARRAY_TAG) { + Type memberType = ((ArrayType) restType).getElementType(); + analyzerData.restTypes.push(memberType); + traverseXml(xmlItem.getChildrenSeq(), memberType, analyzerData); + } else { + analyzerData.restTypes.push(restType); + traverseXml(xmlItem.getChildrenSeq(), restType, analyzerData); + } + analyzerData.fieldHierarchy.pop(); + analyzerData.restTypes.pop(); currentNode = analyzerData.nodesStack.pop(); return; } @@ -456,11 +506,11 @@ private BXml validateRootElement(BXml xml, RecordType recordType, XmlAnalyzerDat // Keep track of fields and attributes DataUtils.updateExpectedTypeStacks(recordType, analyzerData); - handleAttributes(xmlItem, (BMap) currentNode, recordType, analyzerData); + handleAttributes(xmlItem, (BMap) currentNode, analyzerData); return xmlItem.getChildrenSeq(); } - private void handleAttributes(BXmlItem xmlItem, BMap currentNode, RecordType recordType, + private void handleAttributes(BXmlItem xmlItem, BMap currentNode, XmlAnalyzerData analyzerData) { HashSet innerElements = findAllInnerElement(xmlItem); BMap attributeMap = xmlItem.getAttributesMap();