diff --git a/README.md b/README.md index bf9e1de..cfe858c 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,78 @@ -Ballerina YAML Data Library -=================== +# Ballerina JSON Data Library + +[![Build](https://github.com/ballerina-platform/module-ballerina-data.yaml/actions/workflows/build-timestamped-master.yml/badge.svg)](https://github.com/ballerina-platform/module-ballerina-data.yaml/actions/workflows/build-timestamped-master.yml) +[![codecov](https://codecov.io/gh/ballerina-platform/module-ballerina-data.yaml/branch/main/graph/badge.svg)](https://codecov.io/gh/ballerina-platform/module-ballerina-data.yaml) +[![Trivy](https://github.com/ballerina-platform/module-ballerina-data.yaml/actions/workflows/trivy-scan.yml/badge.svg)](https://github.com/ballerina-platform/module-ballerina-data.yaml/actions/workflows/trivy-scan.yml) +[![GraalVM Check](https://github.com/ballerina-platform/module-ballerina-data.yaml/actions/workflows/build-with-bal-test-graalvm.yml/badge.svg)](https://github.com/ballerina-platform/module-ballerina-data.yaml/actions/workflows/build-with-bal-test-graalvm.yml) +[![GitHub Last Commit](https://img.shields.io/github/last-commit/ballerina-platform/module-ballerina-data.yaml.svg)](https://github.com/ballerina-platform/module-ballerina-data.yaml/commits/master) +[![Github issues](https://img.shields.io/github/issues/ballerina-platform/ballerina-standard-library/module/data.yaml.svg?label=Open%20Issues)](https://github.com/ballerina-platform/ballerina-standard-library/labels/module%2Fdata.yaml) + +The Ballerina data.yaml library provides robust and flexible functionalities for working with YAML data within +Ballerina applications. +This library enables developers to seamlessly integrate YAML processing capabilities, +ensuring smooth data interchange and configuration management. + +## Key Features + +- **Versatile Input Handling**: Convert YAML input provided as strings, byte arrays, or streams of byte arrays into + Ballerina's anydata sub-types, facilitating flexible data processing. +- **Data Projection**: Efficiently project data from YAML documents and YAML streams, + allowing for precise data extraction and manipulation. +- **Ordered Data Representation**: Employ tuples to preserve the order of elements when dealing with + YAML document streams of unknown order, ensuring the integrity of data sequences. +- **Serialization**: Serialize Ballerina values into YAML-formatted strings, enabling easy generation of YAML content + from Ballerina applications for configuration files, data storage, or data exchange purposes. + +## Usage + +### Converting external YAML document to a record value + +For transforming YAML content from an external source into a record value, +the `parseString`, `parseBytes`, `parseStream` functions can be used. +This external source can be in the form of a string or a byte array/byte-block-stream that houses the YAML data. +This is commonly extracted from files or network sockets. The example below demonstrates the conversion of an +YAML value from an external source into a record value. + +```ballerina +import ballerina/data.yaml; +import ballerina/io; + +type Book record { + string name; + string author; + int year; +}; + +public function main() returns error? { + string jsonContent = check io:fileReadString("path/to/file.yaml"); + Book book = check yaml:parseString(jsonContent); + io:println(book); +} +``` + +Make sure to handle possible errors that may arise during the file reading or YAML to anydata conversion process. +The `check` keyword is utilized to handle these errors, +but more sophisticated error handling can be implemented as per your requirements. + +## Serialize anydata value to YAML + +The serialization of anydata value into YAML-formatted strings can be done in the below way. + +```ballerina +import ballerina/data.yaml; +import ballerina/io; + +public function main() returns error? { + json content = { + "name": "Clean Code", + "author": "Robert C. Martin", + "year": 2008 + }; + string yamlString = check yaml:yaml:toYamlString(content); + io:println(yamlString); +} +``` -The Ballerina YAML Data Library is a comprehensive toolkit designed to facilitate the handling and manipulation of -YAML data within Ballerina applications. ## Issues and projects diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index c4e45c2..4d0695a 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -6,13 +6,13 @@ authors = ["Ballerina"] keywords = ["yaml"] repository = "https://github.com/ballerina-platform/module-ballerina-data.yaml" license = ["Apache-2.0"] -distribution = "2201.8.1" +distribution = "2201.9.0" [platform.java17] graalvmCompatible = true [[platform.java17.dependency]] -groupId = "io.ballerina.stdlib" +groupId = "io.ballerina.lib" artifactId = "yaml-native" version = "0.1.0" path = "../native/build/libs/data.yaml-native-0.1.0-SNAPSHOT.jar" diff --git a/ballerina/CompilerPlugin.toml b/ballerina/CompilerPlugin.toml new file mode 100644 index 0000000..b9f3b3a --- /dev/null +++ b/ballerina/CompilerPlugin.toml @@ -0,0 +1,6 @@ +[plugin] +id = "constraint-compiler-plugin" +class = "io.ballerina.lib.data.yaml.compiler.YamlDataCompilerPlugin" + +[[dependency]] +path = "../compiler-plugin/build/libs/data.yaml-compiler-plugin-0.1.0-SNAPSHOT.jar" diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index 8086c9c..b39c8f1 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -12,12 +12,27 @@ org = "ballerina" name = "data.yaml" version = "0.1.0" dependencies = [ - {org = "ballerina", name = "jballerina.java"} + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "test"} ] modules = [ {org = "ballerina", packageName = "data.yaml", moduleName = "data.yaml"} ] +[[package]] +org = "ballerina" +name = "io" +version = "1.6.0" +scope = "testOnly" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.value"} +] +modules = [ + {org = "ballerina", packageName = "io", moduleName = "io"} +] + [[package]] org = "ballerina" name = "jballerina.java" @@ -26,3 +41,61 @@ modules = [ {org = "ballerina", packageName = "jballerina.java", moduleName = "jballerina.java"} ] +[[package]] +org = "ballerina" +name = "lang.__internal" +version = "0.0.0" +scope = "testOnly" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.object"} +] + +[[package]] +org = "ballerina" +name = "lang.array" +version = "0.0.0" +scope = "testOnly" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.__internal"} +] + +[[package]] +org = "ballerina" +name = "lang.error" +version = "0.0.0" +scope = "testOnly" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.object" +version = "0.0.0" +scope = "testOnly" + +[[package]] +org = "ballerina" +name = "lang.value" +version = "0.0.0" +scope = "testOnly" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "test" +version = "0.0.0" +scope = "testOnly" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.array"}, + {org = "ballerina", name = "lang.error"} +] +modules = [ + {org = "ballerina", packageName = "test", moduleName = "test"} +] + diff --git a/ballerina/Module.md b/ballerina/Module.md new file mode 100644 index 0000000..b09af49 --- /dev/null +++ b/ballerina/Module.md @@ -0,0 +1,67 @@ +# Ballerina YAML Data Library + +The Ballerina data.yaml library provides robust and flexible functionalities for working with YAML data within +Ballerina applications. +This library enables developers to seamlessly integrate YAML processing capabilities, +ensuring smooth data interchange and configuration management. + +## Key Features + +- **Versatile Input Handling**: Convert YAML input provided as strings, byte arrays, or streams of byte arrays into + Ballerina's anydata sub-types, facilitating flexible data processing. +- **Data Projection**: Efficiently project data from YAML documents and YAML streams, + allowing for precise data extraction and manipulation. +- **Ordered Data Representation**: Employ tuples to preserve the order of elements when dealing with + YAML document streams of unknown order, ensuring the integrity of data sequences. +- **Serialization**: Serialize Ballerina values into YAML-formatted strings, enabling easy generation of YAML content + from Ballerina applications for configuration files, data storage, or data exchange purposes. + +## Usage + +### Converting external YAML document to a record value + +For transforming YAML content from an external source into a record value, +the `parseString`, `parseBytes`, `parseStream` functions can be used. +This external source can be in the form of a string or a byte array/byte-block-stream that houses the YAML data. +This is commonly extracted from files or network sockets. The example below demonstrates the conversion of an +YAML value from an external source into a record value. + +```ballerina +import ballerina/data.yaml; +import ballerina/io; + +type Book record { + string name; + string author; + int year; +}; + +public function main() returns error? { + string jsonContent = check io:fileReadString("path/to/file.yaml"); + Book book = check yaml:parseString(jsonContent); + io:println(book); +} +``` + +Make sure to handle possible errors that may arise during the file reading or YAML to anydata conversion process. +The `check` keyword is utilized to handle these errors, +but more sophisticated error handling can be implemented as per your requirements. + +## Serialize anydata value to YAML + +The serialization of anydata value into YAML-formatted strings can be done in the below way. + +```ballerina +import ballerina/data.yaml; +import ballerina/io; + +public function main() returns error? { + json content = { + "name": "Clean Code", + "author": "Robert C. Martin", + "year": 2008 + }; + string yamlString = check yaml:yaml:toYamlString(content); + io:println(yamlString); +} +``` diff --git a/ballerina/Package.md b/ballerina/Package.md index bef64dc..91e943b 100644 --- a/ballerina/Package.md +++ b/ballerina/Package.md @@ -1,5 +1,67 @@ -# module-ballerina-data.yaml -The Ballerina YAML Data Library is a comprehensive toolkit designed to facilitate the handling and manipulation of -YAML data within Ballerina applications. -It streamlines the process of converting YAML data to native Ballerina data types, -enabling developers to work with YAML content seamlessly and efficiently. +# Ballerina YAML Data Library + +The Ballerina data.yaml library provides robust and flexible functionalities for working with YAML data within +Ballerina applications. +This library enables developers to seamlessly integrate YAML processing capabilities, +ensuring smooth data interchange and configuration management. + +## Key Features + +- **Versatile Input Handling**: Convert YAML input provided as strings, byte arrays, or streams of byte arrays into + Ballerina's anydata sub-types, facilitating flexible data processing. +- **Data Projection**: Efficiently project data from YAML documents and YAML streams, + allowing for precise data extraction and manipulation. +- **Ordered Data Representation**: Employ tuples to preserve the order of elements when dealing with + YAML document streams of unknown order, ensuring the integrity of data sequences. +- **Serialization**: Serialize Ballerina values into YAML-formatted strings, enabling easy generation of YAML content + from Ballerina applications for configuration files, data storage, or data exchange purposes. + +## Usage + +### Converting external YAML document to a record value + +For transforming YAML content from an external source into a record value, +the `parseString`, `parseBytes`, `parseStream` functions can be used. +This external source can be in the form of a string or a byte array/byte-block-stream that houses the YAML data. +This is commonly extracted from files or network sockets. The example below demonstrates the conversion of an +YAML value from an external source into a record value. + +```ballerina +import ballerina/data.yaml; +import ballerina/io; + +type Book record { + string name; + string author; + int year; +}; + +public function main() returns error? { + string jsonContent = check io:fileReadString("path/to/file.yaml"); + Book book = check yaml:parseString(jsonContent); + io:println(book); +} +``` + +Make sure to handle possible errors that may arise during the file reading or YAML to anydata conversion process. +The `check` keyword is utilized to handle these errors, +but more sophisticated error handling can be implemented as per your requirements. + +## Serialize anydata value to YAML + +The serialization of anydata value into YAML-formatted strings can be done in the below way. + +```ballerina +import ballerina/data.yaml; +import ballerina/io; + +public function main() returns error? { + json content = { + "name": "Clean Code", + "author": "Robert C. Martin", + "year": 2008 + }; + string yamlString = check yaml:yaml:toYamlString(content); + io:println(yamlString); +} +``` diff --git a/ballerina/build.gradle b/ballerina/build.gradle index 8a0ae96..d523706 100644 --- a/ballerina/build.gradle +++ b/ballerina/build.gradle @@ -17,22 +17,11 @@ import org.apache.tools.ant.taskdefs.condition.Os -buildscript { - repositories { - maven { - url = 'https://maven.pkg.github.com/ballerina-platform/plugin-gradle' - credentials { - username System.getenv("packageUser") - password System.getenv("packagePAT") - } - } - } - dependencies { - classpath "io.ballerina:plugin-gradle:${project.ballerinaGradlePluginVersion}" - } +plugins { + id 'io.ballerina.plugin' } -description = 'Ballerina - Data YAML Module' +description = 'Ballerina - YAML Data Module' def packageName = "data.yaml" def packageOrg = "ballerina" @@ -40,6 +29,8 @@ def packageOrg = "ballerina" def tomlVersion = stripBallerinaExtensionVersion("${project.version}") def ballerinaTomlFilePlaceHolder = new File("${project.rootDir}/build-config/resources/Ballerina.toml") def ballerinaTomlFile = new File("$project.projectDir/Ballerina.toml") +def compilerPluginTomlFilePlaceHolder = new File("${project.rootDir}/build-config/resources/CompilerPlugin.toml") +def compilerPluginTomlFile = new File("${project.projectDir}/CompilerPlugin.toml") def stripBallerinaExtensionVersion(String extVersion) { if (extVersion.matches(project.ext.timestampedVersionRegex)) { @@ -55,8 +46,6 @@ def stripBallerinaExtensionVersion(String extVersion) { } } -apply plugin: 'io.ballerina.plugin' - ballerina { testCoverageParam = "--code-coverage --coverage-format=xml --includes=io.ballerina.lib.data.*:ballerina.*" packageOrganization = packageOrg @@ -69,6 +58,10 @@ task updateTomlFiles { def newConfig = ballerinaTomlFilePlaceHolder.text.replace("@project.version@", project.version) newConfig = newConfig.replace("@toml.version@", tomlVersion) ballerinaTomlFile.text = newConfig + + def newCompilerPluginToml = compilerPluginTomlFilePlaceHolder.text.replace("@project.version@", + project.version) + compilerPluginTomlFile.text = newCompilerPluginToml } } @@ -114,9 +107,11 @@ updateTomlFiles.dependsOn copyStdlibs build.dependsOn "generatePomFileForMavenPublication" build.dependsOn ":${packageName}-native:build" +build.dependsOn ":${packageName}-compiler-plugin:build" build.dependsOn deleteDependencyTomlFiles test.dependsOn ":${packageName}-native:build" +test.dependsOn ":${packageName}-compiler-plugin:build" publish.dependsOn build publishToMavenLocal.dependsOn build diff --git a/ballerina/init.bal b/ballerina/init.bal index e69de29..2cd7379 100644 --- a/ballerina/init.bal +++ b/ballerina/init.bal @@ -0,0 +1,25 @@ +// 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 +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/jballerina.java; + +isolated function init() { + setModule(); +} + +isolated function setModule() = @java:Method { + 'class: "io.ballerina.lib.data.yaml.utils.ModuleUtils" +} external; diff --git a/ballerina/tests/from_json_string_with_union.bal b/ballerina/tests/from_json_string_with_union.bal new file mode 100644 index 0000000..65951a9 --- /dev/null +++ b/ballerina/tests/from_json_string_with_union.bal @@ -0,0 +1,193 @@ +// 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 +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/test; + +type RecA record { + string a; + int|float|string b; +}; + +type Union1 json|RecA|int; + +type Union2 RecA|json|int; + +@test:Config { + groups: ["Union"] +} +isolated function testUnionTypeAsExpectedTypeForParseString1() returns error? { + string jsonStr = string `{ + "a": "1", + "b": 2 + }`; + Union1 val = check parseString(jsonStr); + test:assertTrue(val is json); + test:assertEquals(val, {a: 1, b: 2}); + + Union2 val2 = check parseString(jsonStr); + test:assertTrue(val2 is RecA); + test:assertEquals(val2, {a: "1", b: 2}); +} + +type RecB record {| + Union1 field1; + Union2 field2; +|}; + +@test:Config { + groups: ["Union"] +} +isolated function testUnionTypeAsExpectedTypeForParseString2() returns error? { + string jsonStr = string `{ + "field1": { + "a": "1", + "b": 2 + }, + "field2": { + "a": "3", + "b": 4 + } + }`; + RecB val = check parseString(jsonStr); + test:assertTrue(val.field1 is json); + test:assertEquals(val.field1, {a: 1, b: 2}); + test:assertTrue(val.field2 is RecA); + test:assertEquals(val.field2, {a: "3", b: 4}); +} + +type RecC record { + Union2[] field1; + int|float[] field2; + string field3; +}; + +@test:Config { + groups: ["Union"] +} +isolated function testUnionTypeAsExpectedTypeForParseString3() returns error? { + string jsonStr = string `{ + "field1": [ + { + "a": "1", + "b": 2 + }, + { + "a": "3", + "b": 4 + } + ], + "field2": [1.0, 2.0], + "field3": "test" + }`; + RecC val = check parseString(jsonStr); + test:assertTrue(val.field1 is Union2[]); + test:assertTrue(val.field1[0] is RecA); + test:assertEquals(val.field1, [{a: "1", b: 2}, {a: "3", b: 4}]); + test:assertTrue(val.field2 is float[]); + test:assertEquals(val.field2, [1.0, 2.0]); + test:assertEquals(val.field3, "test"); +} + +type RecD record { + RecB|RecC l; + record { + string|RecA m; + int|float n; + } p; + string q; +}; + +@test:Config { + groups: ["Union"] +} +isolated function testUnionTypeAsExpectedTypeForParseString4() returns error? { + string jsonStr = string `{ + "l": { + "field1": { + "a": "1", + "b": 2 + }, + "field2": { + "a": "3", + "b": 4 + } + }, + "p": { + "m": "5", + "n": 6 + }, + "q": "test" + }`; + RecD val = check parseString(jsonStr); + test:assertTrue(val.l is RecB); + test:assertEquals(val.l.field1, {a: 1, b: 2}); + test:assertEquals(val.l.field2, {a: "3", b: 4}); + test:assertTrue(val.p.m is string); + test:assertEquals(val.p.m, "5"); + test:assertTrue(val.p.n is int); + test:assertEquals(val.p.n, 6); + test:assertEquals(val.q, "test"); +} + +type UnionList1 [int, int, int]|int[]|float[]; + +@test:Config { + groups: ["Union"] +} +isolated function testUnionTypeAsExpectedTypeForParseString5() returns error? { + string jsonStr = string `[1, 2, 3]`; + UnionList1 val = check parseString(jsonStr); + test:assertTrue(val is [int, int, int]); + test:assertEquals(val, [1, 2, 3]); + + string jsonStr2 = string `[1, 2, 3, 4]`; + UnionList1 val2 = check parseString(jsonStr2); + test:assertTrue(val is [int, int, int]); + test:assertEquals(val2, [1, 2, 3]); +} + +type RecE record {| + UnionList1 l; + RecB|RecC m; + float[] n; +|}; + +@test:Config { + groups: ["Union"] +} +isolated function testUnionTypeAsExpectedTypeForParseString6() returns error? { + string jsonStr = string `{ + "l": [1, 2, 3], + "m": { + "field1": { + "a": "1", + "b": 2 + }, + "field2": { + "a": "3", + "b": 4 + } + }, + "n": [1.0, 2.0] + }`; + RecE val = check parseString(jsonStr); + test:assertTrue(val.l is [int, int, int]); + test:assertEquals(val.l, [1, 2, 3]); + test:assertTrue(val.m is RecB); + test:assertEquals(val.m.field1, {a: 1, b: 2}); + test:assertEquals(val.m.field2, {a: "3", b: 4}); + test:assertEquals(val.n, [1.0, 2.0]); +} diff --git a/ballerina/tests/from_string_with_anchors.bal b/ballerina/tests/from_string_with_anchors.bal new file mode 100644 index 0000000..0a2fe0a --- /dev/null +++ b/ballerina/tests/from_string_with_anchors.bal @@ -0,0 +1,89 @@ +// 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 +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/io; +import ballerina/test; + +const ANCHORS_TEST_PATH = FILE_PATH + "anchors/"; + +@test:Config +isolated function testAnchorWithAnydataExpectedType() returns error? { + string content = check io:fileReadString(ANCHORS_TEST_PATH + "anchor_test_1.yaml"); + anydata result = check parseString(content); + final anydata & readonly expectedResult = { + "user_info": { + "name": "John Doe", + "email": "john.doe@example.com", + "roles": ["admin", "editor"] + }, + "server_config": { + "owner": { + "name": "John Doe", + "email": "john.doe@example.com", + "roles": ["admin", "editor"] + }, + "ports": [80, 443], + "host": "localhost", + "database": "my_database" + }, + "web_server": { + "ports": [8080], + "database": {"host": "localhost", "database": "my_database"}, + "roles": ["admin", "editor"] + } + }; + test:assertEquals(result, expectedResult); +} + +@test:Config +isolated function testAnchorWithRecordAsExpectedType() returns error? { + string content = check io:fileReadString(ANCHORS_TEST_PATH + "anchor_test_2.yaml"); + ProductDetails result = check parseString(content); + ProductDetails expectedResult = { + product: {name: "T-Shirt", brand: "Acme Clothing"}, + variation_1: {size: "S", color: "red"}, + variation_2: {size: "M", color: "blue"}, + shopping_cart: { + items: [{size: "S", color: "red"}, {size: "M", color: "blue"}], + total_price: 29.99, + currency: "USD" + } + }; + test:assertEquals(result, expectedResult); +} + +type ProductDetails record { + record { + string name; + string brand; + } product; + record { + string size; + string color; + } variation_1; + record { + string size; + string color; + } variation_2; + record { + record { + string size; + string color; + }[] items; + decimal total_price; + string currency; + } shopping_cart; +}; diff --git a/ballerina/tests/from_string_with_disable_projection.bal b/ballerina/tests/from_string_with_disable_projection.bal new file mode 100644 index 0000000..18c1bd6 --- /dev/null +++ b/ballerina/tests/from_string_with_disable_projection.bal @@ -0,0 +1,154 @@ +// 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 +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/test; + +@test:Config +isolated function testDisableDataProjectionInArrayTypeForParseString() { + string jsonStr1 = string `[1, 2, 3, 4]`; + int[2]|error val1 = parseString(jsonStr1, {allowDataProjection: false}); + test:assertTrue(val1 is error); + test:assertEquals((val1).message(), "array size is not compatible with the expected size"); + + string strVal2 = string `{ + "a": [1, 2, 3, 4, 5] + }`; + record {|int[2] a;|}|error val2 = parseString(strVal2, {allowDataProjection: false}); + test:assertTrue(val2 is error); + test:assertEquals((val2).message(), "array size is not compatible with the expected size"); + + string strVal3 = string `{ + "a": [1, 2, 3, 4, 5], + "b": [1, 2, 3, 4, 5] + }`; + record {|int[2] a; int[3] b;|}|error val3 = parseString(strVal3, {allowDataProjection: false}); + test:assertTrue(val3 is error); + test:assertEquals((val3).message(), "array size is not compatible with the expected size"); + + string strVal4 = string `{ + "employees": [ + { "name": "Prakanth", + "age": 26 + }, + { "name": "Kevin", + "age": 25 + } + ] + }`; + record {|record {|string name; int age;|}[1] employees;|}|error val4 = parseString(strVal4, + {allowDataProjection: false}); + test:assertTrue(val4 is error); + test:assertEquals((val4).message(), "array size is not compatible with the expected size"); + + string strVal5 = string `[1, 2, 3, { "a" : val_a }]`; + int[3]|error val5 = parseString(strVal5, {allowDataProjection: false}); + test:assertTrue(val5 is error); + test:assertEquals((val5).message(), "array size is not compatible with the expected size"); +} + +@test:Config +isolated function testDisableDataProjectionInTupleTypeForParseString() { + string str1 = string `["1", 2, 3, 4, "5", 8]`; + [string, float]|error val1 = parseString(str1, {allowDataProjection: false}); + test:assertTrue(val1 is error); + test:assertEquals((val1).message(), "array size is not compatible with the expected size"); + + string str2 = string `{ + "a": ["1", 2, 3, 4, "5", 8] + }`; + record {|[string, float] a;|}|error val2 = parseString(str2, {allowDataProjection: false}); + test:assertTrue(val2 is error); + test:assertEquals((val2).message(), "array size is not compatible with the expected size"); + + string str3 = string `[1, "4"]`; + [float]|error val3 = parseString(str3, {allowDataProjection: false}); + test:assertTrue(val3 is error); + test:assertEquals((val3).message(), "array size is not compatible with the expected size"); + + string str4 = string `[1, {}]`; + [float]|error val4 = parseString(str4, {allowDataProjection: false}); + test:assertTrue(val4 is error); + test:assertEquals((val4).message(), "array size is not compatible with the expected size"); + + string str5 = string `["1", [], {"name": 1}]`; + [string]|error val5 = parseString(str5, {allowDataProjection: false}); + test:assertTrue(val5 is error); + test:assertEquals((val5).message(), "array size is not compatible with the expected size"); +} + +@test:Config +isolated function testDisableDataProjectionInRecordTypeWithParseString() { + string jsonStr1 = string `{"name": "John", "age": 30, "city": "New York"}`; + record {|string name; string city;|}|error val1 = parseString(jsonStr1, {allowDataProjection: false}); + test:assertTrue(val1 is error); + test:assertEquals((val1).message(), "undefined field 'age'"); + + string jsonStr2 = string `{"name": "John", "age": "30", "city": "New York"}`; + record {|string name; string city;|}|error val2 = parseString(jsonStr2, {allowDataProjection: false}); + test:assertTrue(val2 is error); + test:assertEquals((val2).message(), "undefined field 'age'"); + + string jsonStr3 = string `{ "name": "John", + "company": { + "name": "wso2", + "year": 2024, + "addrees": { + "street": "123", + "city": "Berkeley" + } + }, + "city": "New York" }`; + record {|string name; string city;|}|error val3 = parseString(jsonStr3, {allowDataProjection: false}); + test:assertTrue(val3 is error); + test:assertEquals((val3).message(), "undefined field 'company'"); + + string jsonStr4 = string `{ "name": "John", + "company": [{ + "name": "wso2", + "year": 2024, + "addrees": { + "street": "123", + "city": "Berkeley" + } + }], + "city": "New York" }`; + record {|string name; string city;|}|error val4 = parseString(jsonStr4, {allowDataProjection: false}); + test:assertTrue(val4 is error); + test:assertEquals((val4).message(), "undefined field 'company'"); + + string jsonStr5 = string `{ "name": "John", + "company1": [{ + "name": "wso2", + "year": 2024, + "addrees": { + "street": "123", + "city": "Berkeley" + } + }], + "city": "New York", + "company2": [{ + "name": "amzn", + "year": 2024, + "addrees": { + "street": "123", + "city": "Miami" + } + }] + }`; + record {|string name; string city;|}|error val5 = parseString(jsonStr5, {allowDataProjection: false}); + test:assertTrue(val5 is error); + test:assertEquals((val5).message(), "undefined field 'company1'"); +} diff --git a/ballerina/tests/from_string_with_tag_resolution.bal b/ballerina/tests/from_string_with_tag_resolution.bal new file mode 100644 index 0000000..e9ff582 --- /dev/null +++ b/ballerina/tests/from_string_with_tag_resolution.bal @@ -0,0 +1,99 @@ +// 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 +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/io; +import ballerina/test; + +const TAG_RESOLUTION_TEST_PATH = FILE_PATH + "tags/"; + +@test:Config +isolated function testTagResolutionWithAnydataExpectedType() returns error? { + string content = check io:fileReadString(TAG_RESOLUTION_TEST_PATH + "tag_resolution_test_1.yaml"); + anydata result = check parseString(content, {schema: JSON_SCHEMA}); + final anydata expectedResult = { + "server": { + "host": "192.168.1.100", + "port": 8080, + "environment": "development", + "restart": true, + "start": 0.12, + "retry": false + }, + "logging": {"level": "info", "file": ()}, + "users": [{"name": "John Doe", "roles": ["admin", "editor"]}, {"name": "Jane Smith", "roles": ["user"]}] + }; + test:assertEquals(result, expectedResult); +} + +@test:Config +isolated function testTagResolutionWithAnydataExpectedType2() returns error? { + string content = check io:fileReadString(TAG_RESOLUTION_TEST_PATH + "tag_resolution_test_2.yaml"); + anydata result = check parseString(content); + final anydata expectedResult = { + "server": { + "host": "192.168.1.100", + "ports": [8000, 9000, 9001], + "environment": "development", + "restart": true, + "start": 0.12, + "end": float:Infinity, + "retry": false + }, + "logging": {"level": "info", "file": null}, + "users": [{"name": "John Doe", "roles": ["admin", "editor"]}, {"name": "Jane Smith", "roles": ["user"]}] + }; + test:assertEquals(result, expectedResult); +} + +type ServerData record { + record { + string host; + [int, int, int] ports; + string environment; + boolean restart; + decimal 'start; + string|float end; + boolean 'retry; + } server; + record { + string level; + string? file; + } logging; + record { + string name; + string[] roles; + }[] users; +}; + +@test:Config +isolated function testTagResolutionWithRecordExpectedType() returns error? { + string content = check io:fileReadString(TAG_RESOLUTION_TEST_PATH + "tag_resolution_test_2.yaml"); + ServerData result = check parseString(content); + final ServerData expectedResult = { + server: { + host: "192.168.1.100", + ports: [8000, 9000, 9001], + environment: "development", + restart: true, + 'start: 0.12, + end: float:Infinity, + 'retry: false + }, + logging: {level: "info", file: ()}, + users: [{name: "John Doe", roles: ["admin", "editor"]}, {name: "Jane Smith", roles: ["user"]}] + }; + test:assertEquals(result, expectedResult); +} diff --git a/ballerina/tests/parse_bytes_with_projection_options.bal b/ballerina/tests/parse_bytes_with_projection_options.bal new file mode 100644 index 0000000..940173d --- /dev/null +++ b/ballerina/tests/parse_bytes_with_projection_options.bal @@ -0,0 +1,333 @@ +// 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 +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/io; +import ballerina/test; + +const PROJECTION_OPTIONS_PATH = FILE_PATH + "projection_options/"; + +final Options & readonly options1 = { + allowDataProjection: { + nilAsOptionalField: true, + absentAsNilableType: false, + strictTupleOrder: false + } +}; + +final Options & readonly options2 = { + allowDataProjection: { + nilAsOptionalField: false, + absentAsNilableType: true, + strictTupleOrder: false + } +}; + +final Options & readonly options3 = { + allowDataProjection: { + nilAsOptionalField: true, + absentAsNilableType: true, + strictTupleOrder: false + } +}; + +type Sales record {| + @Name { + value: "sales_data" + } + SalesData[] salesData; + @Name { + value: "total_sales" + } + record {| + @Name { + value: "date_range" + } + string dataRange?; + @Name { + value: "total_revenue" + } + string totalRevenue; + |} totalSales; +|}; + +type SalesData record {| + @Name { + value: "transaction_id" + } + string transactionId; + string date; + @Name { + value: "customer_name" + } + string customerName; + string product; + @Name { + value: "unit_price" + } + string unitPrice; + @Name { + value: "total_price" + } + string totalPrice?; +|}; + +@test:Config { + groups: ["options"] +} +isolated function testNilAsOptionalFieldForParseString() returns error? { + string data = check io:fileReadString(PROJECTION_OPTIONS_PATH + "sales.yaml"); + Sales sales = check parseBytes(data.toBytes(), options1); + test:assertEquals(sales.salesData[0].length(), 5); + test:assertEquals(sales.salesData[0].transactionId, "TXN001"); + test:assertEquals(sales.salesData[0].date, "2024-03-25"); + test:assertEquals(sales.salesData[0].customerName, "ABC Corporation"); + test:assertEquals(sales.salesData[0].product, "InnovateX"); + test:assertEquals(sales.salesData[0].unitPrice, "$499"); + + test:assertEquals(sales.salesData[1].length(), 6); + test:assertEquals(sales.salesData[1].transactionId, "TXN002"); + test:assertEquals(sales.salesData[1].date, "2024-03-25"); + test:assertEquals(sales.salesData[1].customerName, "XYZ Enterprises"); + test:assertEquals(sales.salesData[1].product, "SecureTech"); + test:assertEquals(sales.salesData[1].unitPrice, "$999"); + test:assertEquals(sales.salesData[1].totalPrice, "$4995"); + + test:assertEquals(sales.salesData[2].length(), 5); + test:assertEquals(sales.salesData[2].transactionId, "TXN003"); + test:assertEquals(sales.salesData[2].date, "2024-03-26"); + test:assertEquals(sales.salesData[2].customerName, "123 Inc."); + test:assertEquals(sales.salesData[2].product, "InnovateX"); + test:assertEquals(sales.salesData[2].unitPrice, "$499"); + + test:assertEquals(sales.totalSales.length(), 1); + test:assertEquals(sales.totalSales.totalRevenue, "$21462"); +} + +@test:Config { + groups: ["options"] +} +isolated function testNilAsOptionalFieldForParseStringNegative() returns error? { + string data = check io:fileReadString(PROJECTION_OPTIONS_PATH + "sales.yaml"); + Sales|Error err = parseBytes(data.toBytes()); + test:assertTrue(err is Error); + test:assertEquals((err).message(), "incompatible value 'null' for type 'string' in field 'salesData.totalPrice'"); +} + + +type Response record {| + string status; + record {| + User user; + Post[] posts; + |} data; +|}; + +type User record {| + int id; + string username; + string? email; +|}; + +type Post record {| + int id; + string title; + string? content; +|}; + +@test:Config { + groups: ["options"] +} +isolated function testAbsentAsNilableTypeForParseString() returns error? { + string data = check io:fileReadString(PROJECTION_OPTIONS_PATH + "response.yaml"); + Response response = check parseBytes(data.toBytes(), options2); + test:assertEquals(response.status, "success"); + + test:assertEquals(response.data.user.length(), 3); + test:assertEquals(response.data.user.id, 123); + test:assertEquals(response.data.user.username, "example_user"); + test:assertEquals(response.data.user.email, ()); + + test:assertEquals(response.data.user.length(), 3); + test:assertEquals(response.data.posts[0].id, 1); + test:assertEquals(response.data.posts[0].title, "First Post"); + test:assertEquals(response.data.posts[0].content, "This is the content of the first post."); + + test:assertEquals(response.data.user.length(), 3); + test:assertEquals(response.data.posts[1].id, 2); + test:assertEquals(response.data.posts[1].title, "Second Post"); + test:assertEquals(response.data.posts[1].content, ()); +} + +type Specifications record { + string storage?; + string display?; + string? processor; + string? ram; + string? graphics; + string? camera; + string? battery; + string os?; + string 'type?; + boolean wireless?; + string battery_life?; + boolean noise_cancellation?; + string? color; +}; + +type ProductsItem record { + int id; + string name; + string brand?; + decimal price; + string? description; + Specifications specifications?; +}; + +type Data record { + ProductsItem[] products; +}; + +type ResponseEcom record { + string status; + Data data; +}; + +@test:Config { + groups: ["options"] +} +isolated function testAbsentAsNilableTypeAndAbsentAsNilableTypeForParseString() returns error? { + string data = check io:fileReadString(PROJECTION_OPTIONS_PATH + "product_list_response.yaml"); + ResponseEcom response = check parseBytes(data.toBytes(), options3); + + test:assertEquals(response.status, "success"); + test:assertEquals(response.data.products[0].length(), 6); + test:assertEquals(response.data.products[0].id, 1); + test:assertEquals(response.data.products[0].name, "Laptop"); + test:assertEquals(response.data.products[0].brand, "ExampleBrand"); + test:assertEquals(response.data.products[0].price, 999.99d); + test:assertEquals(response.data.products[0].description, "A powerful laptop for all your computing needs."); + test:assertEquals(response.data.products[0].specifications?.storage, "512GB SSD"); + test:assertEquals(response.data.products[0].specifications?.display, "15.6-inch FHD"); + test:assertEquals(response.data.products[0].specifications?.processor, "Intel Core i7"); + test:assertEquals(response.data.products[0].specifications?.ram, "16GB DDR4"); + test:assertEquals(response.data.products[0].specifications?.graphics, "NVIDIA GeForce GTX 1650"); + test:assertEquals(response.data.products[0].specifications?.camera, ()); + test:assertEquals(response.data.products[0].specifications?.battery, ()); + test:assertEquals(response.data.products[0].specifications?.color, ()); + + test:assertEquals(response.data.products[1].length(), 5); + test:assertEquals(response.data.products[1].id, 2); + test:assertEquals(response.data.products[1].name, "Smartphone"); + test:assertEquals(response.data.products[1].price, 699.99d); + test:assertEquals(response.data.products[1].description, ()); + test:assertEquals(response.data.products[1].specifications?.storage, "256GB"); + test:assertEquals(response.data.products[1].specifications?.display, "6.5-inch AMOLED"); + test:assertEquals(response.data.products[1].specifications?.processor, ()); + test:assertEquals(response.data.products[1].specifications?.ram, ()); + test:assertEquals(response.data.products[1].specifications?.graphics, ()); + test:assertEquals(response.data.products[1].specifications?.camera, "Quad-camera setup"); + test:assertEquals(response.data.products[1].specifications?.battery, "4000mAh"); + test:assertEquals(response.data.products[1].specifications?.color, ()); + + test:assertEquals(response.data.products[2].length(), 6); + test:assertEquals(response.data.products[2].id, 3); + test:assertEquals(response.data.products[2].name, "Headphones"); + test:assertEquals(response.data.products[2].brand, "AudioTech"); + test:assertEquals(response.data.products[2].price, 149.99d); + test:assertEquals(response.data.products[2].description, "Immerse yourself in high-quality sound with these headphones."); + test:assertEquals(response.data.products[2].specifications?.processor, ()); + test:assertEquals(response.data.products[2].specifications?.ram, ()); + test:assertEquals(response.data.products[2].specifications?.graphics, ()); + test:assertEquals(response.data.products[2].specifications?.camera, ()); + test:assertEquals(response.data.products[2].specifications?.battery, ()); + test:assertEquals(response.data.products[2].specifications?.'type, "Over-ear"); + test:assertEquals(response.data.products[2].specifications?.wireless, true); + test:assertEquals(response.data.products[2].specifications?.noise_cancellation, true); + test:assertEquals(response.data.products[2].specifications?.color, "Black"); + + test:assertEquals(response.data.products[3].length(), 5); + test:assertEquals(response.data.products[3].id, 4); + test:assertEquals(response.data.products[3].name, "Wireless Earbuds"); + test:assertEquals(response.data.products[3].brand, "SoundMaster"); + test:assertEquals(response.data.products[3].price, 99.99d); + test:assertEquals(response.data.products[3].description, "Enjoy freedom of movement with these wireless earbuds."); +} + +@test:Config { + groups: ["options"] +} +isolated function testDisableOptionsOfProjectionTypeForParseString1() returns error? { + string data = check io:fileReadString(PROJECTION_OPTIONS_PATH + "sales.yaml"); + Sales|Error err = parseBytes(data.toBytes()); + test:assertTrue(err is Error); + test:assertEquals((err).message(), "incompatible value 'null' for type 'string' in field 'salesData.totalPrice'"); +} + +@test:Config { + groups: ["options"] +} +isolated function testDisableOptionsOfProjectionTypeForParseString2() returns error? { + string data = check io:fileReadString(PROJECTION_OPTIONS_PATH + "response.yaml"); + + Response|Error err = parseBytes(data.toBytes()); + test:assertTrue(err is Error); + test:assertEquals((err).message(), "required field 'email' not present in YAML"); +} + +@test:Config +isolated function testAbsentAsNilableTypeAndAbsentAsNilableTypeForParseString3() returns error? { + record {| + string name; + |}|Error val1 = parseString(string `{"name": null}`, options3); + test:assertTrue(val1 is Error); + test:assertEquals((val1).message(), "incompatible value 'null' for type 'string' in field 'name'"); + + record {| + string? name; + |} val2 = check parseString(string `{"name": null}`, options3); + test:assertEquals(val2.name, ()); + + record {| + string name?; + |} val3 = check parseString(string `{"name": null}`, options3); + test:assertEquals(val3, {}); + + record {| + string? name?; + |} val4 = check parseString(string `{"name": null}`, options3); + test:assertEquals(val4?.name, ()); + + record {| + string name; + |}|Error val5 = parseString(string `{}`, options3); + test:assertTrue(val5 is Error); + test:assertEquals((val5).message(), "required field 'name' not present in YAML"); + + record {| + string? name; + |} val6 = check parseString(string `{}`, options3); + test:assertEquals(val6.name, ()); + + record {| + string name?; + |} val7 = check parseString(string `{}`, options3); + test:assertEquals(val7, {}); + + record {| + string? name?; + |} val8 = check parseString(string `{}`, options3); + test:assertEquals(val8?.name, ()); +} diff --git a/ballerina/tests/parse_string.bal b/ballerina/tests/parse_string.bal new file mode 100644 index 0000000..3796487 --- /dev/null +++ b/ballerina/tests/parse_string.bal @@ -0,0 +1,974 @@ +// 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 +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/io; +import ballerina/test; + +const FILE_PATH = "tests/resources/"; + +@test:Config { + dataProvider: basicTypeDataForParseString +} +isolated function testBasicTypes(string sourceData, typedesc expectedType, anydata expectedResult) +returns error? { + anydata actualResult = check parseString(sourceData, {}, expectedType); + test:assertEquals(actualResult, expectedResult); +} + +function basicTypeDataForParseString() returns [string, typedesc, anydata][] => [ + ["This is a basic string", string, "This is a basic string"], + ["\"This is a double quoted string\"", string, "This is a double quoted string"], + ["'This is a single quoted string'", string, "This is a single quoted string"], + ["|\nThis is a literal\nblock scalar string", string, "This is a literal\nblock scalar string\n"], + [">\nThis is a folded\nblock scalar string", string, "This is a folded block scalar string\n"], + ["|+\nThis is a literal\nblock scalar string", string, "This is a literal\nblock scalar string\n"], + ["|-\nThis is a literal\nblock scalar string", string, "This is a literal\nblock scalar string"], + [">+\nThis is a folded\nblock scalar string", string, "This is a folded block scalar string\n"], + [">-\nThis is a folded\nblock scalar string", string, "This is a folded block scalar string"], + ["123", int, 123], + ["12.23", float, 12.23], + ["12.23", decimal, 12.23d], + ["'escaped single ''quote'", string, "escaped single 'quote"] +]; + +@test:Config { + dataProvider: simpleYampMappingToRecordData +} +isolated function testSimpleYamlMappingStringToRecord(string inputPath) returns error? { + string content = check io:fileReadString(FILE_PATH + inputPath); + SimpleYaml result = check parseString(content); + test:assertEquals(result.name, "Jhon"); + test:assertEquals(result.age, 30); + test:assertEquals(result.description, "This is a multiline\nstring in YAML.\nIt preserves line breaks.\n"); +} + +@test:Config { + dataProvider: simpleYampMappingToRecordData +} +isolated function testSimpleYamlMappingStringToRecordWithProjection(string inputPath) returns error? { + string content = check io:fileReadString(FILE_PATH + inputPath); + record {|int age;|} result = check parseString(content); + test:assertEquals(result.length(), 1); + test:assertEquals(result.age, 30); +} + +function simpleYampMappingToRecordData() returns string[][] => [ + ["simple_yaml_1a.yaml"], + ["simple_yaml_1b.yaml"] +]; + +@test:Config { + dataProvider: simpleYamlSequenceToArrayData +} +isolated function testSimpleYamlSequenceStringToArray(string inputPath) returns error? { + string content = check io:fileReadString(FILE_PATH + inputPath); + + string[] result1 = check parseString(content); + test:assertEquals(result1.length(), 4); + test:assertEquals(result1, ["YAML", "TOML", "JSON", "XML"]); + + string[2] result2 = check parseString(content); + test:assertEquals(result2.length(), 2); + test:assertEquals(result2, ["YAML", "TOML"]); +} + +function simpleYamlSequenceToArrayData() returns string[][] => [ + ["simple_yaml_2a.yaml"], + ["simple_yaml_2b.yaml"] +]; + +@test:Config +isolated function testNestedYamlStringToRecord() returns error? { + string content = check io:fileReadString(FILE_PATH + "nested_1.yaml"); + + record {|string name; Employee[] employees;|} result = check parseString(content); + Employee[] employees = result.employees; + test:assertEquals(result.name, "YAML Test"); + test:assertEquals(employees.length(), 2); + test:assertEquals(employees[0].name, "Alice"); + test:assertEquals(employees[0].age, 30); + test:assertEquals(employees[1].department, "Engineering"); + test:assertEquals(employees[1].projects.length(), 2); + test:assertEquals(employees[0].projects[0].status, "In Progress"); + test:assertEquals(employees[1].projects[1].name, "Project D"); + + OpenRecord result2 = check parseString(content); + test:assertEquals(result2.get("name"), "YAML Test"); + Employee[] employees2 = check result2.get("employees").cloneWithType(); + test:assertEquals(employees2.length(), 2); + test:assertEquals(employees2[0].name, "Alice"); + test:assertEquals(employees2[0].age, 30); + test:assertEquals(employees2[1].department, "Engineering"); + test:assertEquals(employees2[1].projects.length(), 2); + test:assertEquals(employees2[0].projects[0].status, "In Progress"); + test:assertEquals(employees2[1].projects[1].name, "Project D"); +} + +@test:Config +isolated function testNestedYamlStringToRecordWithProjection() returns error? { + string content = check io:fileReadString(FILE_PATH + "nested_1.yaml"); + + record {|record {|string name; Project[1] projects;|}[] employees;|} result = check parseString(content); + test:assertEquals(result.employees.length(), 2); + test:assertEquals(result.employees[0].name, "Alice"); + test:assertEquals(result.employees[0].projects.length(), 1); + test:assertEquals(result.employees[0].projects[0].name, "Project A"); + test:assertEquals(result.employees[0].projects[0].status, "In Progress"); + test:assertEquals(result.employees[1].projects[0].name, "Project C"); + test:assertEquals(result.employees[1].projects[0].status, "Pending"); +} + +@test:Config +isolated function testYamlStringToRecordWithOptionalField() returns error? { + string content = "{name: Alice}"; + + record {|string name; string optinalField?;|} result = check parseString(content); + test:assertEquals(result.length(), 1); + test:assertEquals(result.name, "Alice"); + test:assertEquals(result.optinalField, ()); +} + +@test:Config +isolated function testYamlStringToRecordWithOptionalFieldWithProjection() returns error? { + string content = check io:fileReadString(FILE_PATH + "nested_2.yaml"); + + record {| + string name; + string optinalField?; + record {|string name; string optinalField?;|}[1] projects; + |} result = check parseString(content); + + test:assertEquals(result.length(), 2); + test:assertEquals(result.name, "Alice"); + test:assertEquals(result.optinalField, ()); + test:assertEquals(result.projects.length(), 1); + test:assertEquals(result.projects[0].length(), 1); + test:assertEquals(result.projects[0].name, "Project A"); + test:assertEquals(result.projects[0].optinalField, ()); +} + +@test:Config +isolated function testYamlStringToNestedArray() returns error? { + string content = check io:fileReadString(FILE_PATH + "nested_3.yaml"); + + BookCover[] books = check parseString(content); + test:assertEquals(books.length(), 3); + test:assertEquals(books[0].title, "Book 1"); + test:assertEquals(books[1].pages, 300); + test:assertEquals(books[1].authors.length(), 2); + test:assertEquals(books[1].authors, ["Author X", "Author Y"]); + test:assertEquals(books[2].price, 12.50); +} + +@test:Config +isolated function testYamlStringToNestedArrayWithProjection() returns error? { + string content = check io:fileReadString(FILE_PATH + "nested_3.yaml"); + + record {|string title; string[1] authors;|}[2] books = check parseString(content); + test:assertEquals(books.length(), 2); + test:assertEquals(books[0].title, "Book 1"); + test:assertEquals(books[1].authors.length(), 1); + test:assertEquals(books[1].authors, ["Author X"]); +} + +@test:Config +isolated function testYamlStringToRecordWithMapTypeString() returns error? { + string content = check io:fileReadString(FILE_PATH + "nested_4.yaml"); + + record {|RecordWithMapType[] records;|} records = check parseString(content); + test:assertEquals(records.records.length(), 2); + test:assertEquals(records.records[0].name, "Record 1"); + test:assertEquals(records.records[0].data.length(), 3); + test:assertEquals(records.records[1].data, {keyA: "valueA", keyB: "valueB"}); +} + +@test:Config +isolated function testNestedYamlStringToRecord2() returns error? { + string content = check io:fileReadString(FILE_PATH + "nested_5.yaml"); + + Book book = check parseString(content); + test:assertEquals(book.title, "To Kill a Mockingbird"); + test:assertEquals(book.author.name, "Harper Lee"); + test:assertEquals(book.author.birthdate, "1926-04-28"); + test:assertEquals(book.author.hometown, "Monroeville, Alabama"); + test:assertEquals(book.publisher.name, "J. B. Lippincott & Co."); + test:assertEquals(book.publisher.year, 1960); + test:assertEquals(book.publisher["location"], "Philadelphia"); + test:assertEquals(book["price"], 10.5); + test:assertEquals(book.author["local"], false); +} + +@test:Config +isolated function testYamlStringToRecordWithRestFields() returns error? { + string content = check io:fileReadString(FILE_PATH + "nested_6.yaml"); + + record {|TicketBooking[] bookings;|} bookings = check parseString(content); + test:assertEquals(bookings.bookings.length(), 2); + test:assertEquals(bookings.bookings[0].length(), 3); + test:assertEquals(bookings.bookings[0].event, "RockFest 2024"); + test:assertEquals(bookings.bookings[0].price, 75.00); + test:assertEquals(bookings.bookings[0].attendee.length(), 3); + test:assertEquals(bookings.bookings[0].attendee.get("name"), "John Doe"); +} + +@test:Config +isolated function testNestedYamlStringToMap() returns error? { + string content = check io:fileReadString(FILE_PATH + "nested_7.yaml"); + + map result = check parseString(content); + test:assertEquals(result.length(), 3); + test:assertEquals(result.get("matrix1"), [[1, 2, 3], [4, 5, 6], [7, 8, 9]]); + test:assertEquals(result.get("matrix2"), [[9, 8, 7], [5, 5, 4], [3, 2, 1]]); + test:assertEquals(result.get("matrix3"), [[5, 2, 7], [9, 4, 1], [3, 6, 8]]); + + map result2 = check parseString(content); + test:assertEquals(result2.length(), 3); + test:assertEquals(result2.get("matrix1"), [[1, 2], [4, 5]]); + test:assertEquals(result2.get("matrix2"), [[9, 8], [5, 5]]); + test:assertEquals(result2.get("matrix3"), [[5, 2], [9, 4]]); +} + +@test:Config +isolated function testNestedYamlStringToRecordWithTupleField() returns error? { + string content = check io:fileReadString(FILE_PATH + "nested_8.yaml"); + + LibraryB libraryB = check parseString(content); + test:assertEquals(libraryB.books.length(), 2); + test:assertEquals(libraryB.books[0].title, "The Great Gatsby"); + test:assertEquals(libraryB.books[0].author, "F. Scott Fitzgerald"); + test:assertEquals(libraryB.books[1].title, "The Grapes of Wrath"); + test:assertEquals(libraryB.books[1].author, "John Steinbeck"); + + LibraryC libraryC = check parseString(content); + test:assertEquals(libraryC.books.length(), 3); + test:assertEquals(libraryC.books[0].title, "The Great Gatsby"); + test:assertEquals(libraryC.books[0].author, "F. Scott Fitzgerald"); + test:assertEquals(libraryC.books[1].title, "The Grapes of Wrath"); + test:assertEquals(libraryC.books[1].author, "John Steinbeck"); + test:assertEquals(libraryC.books[2].title, "Binary Echoes: Unraveling the Digital Web"); + test:assertEquals(libraryC.books[2].author, "Alexandra Quinn"); +} + +@test:Config +isolated function testNestedYamlStringToRecordWithRestFields() returns error? { + string content = check io:fileReadString(FILE_PATH + "nested_9.yaml"); + + record {| + record {| + string c; + string d; + |}...; + |} result = check parseString(content); + test:assertEquals(result.length(), 2); + test:assertEquals(result["a"]["c"], "world"); + test:assertEquals(result["a"]["d"], "2"); + test:assertEquals(result["b"]["c"], "world"); + test:assertEquals(result["b"]["d"], "2"); + + record {| + map...; + |} result2 = check parseString(content); + test:assertEquals(result2.length(), 2); + test:assertEquals(result2["a"]["c"], "world"); + test:assertEquals(result2["a"]["d"], "2"); + test:assertEquals(result2["b"]["c"], "world"); + test:assertEquals(result2["b"]["d"], "2"); +} + +@test:Config +isolated function testNestedYamlStringToRecordWithRestFields2() returns error? { + string content = check io:fileReadString(FILE_PATH + "nested_10.yaml"); + + record {| + record {| + string c; + string d; + |}[]...; + |} result = check parseString(content); + test:assertEquals(result.length(), 2); + test:assertEquals(result["a"], [ + { + "c": "world", + "d": "2" + } + ]); + test:assertEquals(result["b"], [ + { + "c": "world", + "d": "2" + } + ]); +} + +@test:Config +isolated function testUnionTypeAsExpTypeForParseString() returns error? { + decimal|float val1 = check parseString("1.0"); + test:assertEquals(val1, 1.0); + + string content = check io:fileReadString(FILE_PATH + "nested_11.yaml"); + + record {| + record {|decimal|int b; record {|string|boolean e;|} d;|} a; + decimal|float c; + |} result = check parseString(content); + test:assertEquals(result.length(), 2); + test:assertEquals(result.a.length(), 2); + test:assertEquals(result.a.b, 1); + test:assertEquals(result.a.d.e, false); + test:assertEquals(result.c, 2.0); +} + +@test:Config +isolated function testAnydataAsExpTypeForParseString1() returns error? { + string content = check io:fileReadString(FILE_PATH + "nested_10.yaml"); + + anydata result = check parseString(content); + test:assertEquals(result, {"a":[{"c":"world","d":2}],"b":[{"c":"world","d":2}]}); +} + +@test:Config +isolated function testAnydataAsExpTypeForParseString2() returns error? { + string content = check io:fileReadString(FILE_PATH + "nested_11.yaml"); + + anydata result = check parseString(content); + test:assertEquals(result, {"a":{"b":1,"d":{"e":false}},"c":2}); +} + +@test:Config +isolated function testAnydataArrayAsExpTypeForParseString() returns error? { + string jsonStr1 = string `[["1"], 2.0]`; + anydata[] val1 = check parseString(jsonStr1); + test:assertEquals(val1, [[1], 2.0]); + + string jsonStr2 = string `[["1", 2], 2.0]`; + anydata[] val2 = check parseString(jsonStr2); + test:assertEquals(val2, [[1, 2], 2.0]); + + string jsonStr3 = string `[["1", 2], [2, "3"]]`; + anydata[] val3 = check parseString(jsonStr3); + test:assertEquals(val3, [[1, 2], [2, 3]]); + + string jsonStr4 = string `{"val" : [[1, 2], "2.0", 3.0, [5, 6]]}`; + record {| + anydata[] val; + |} val4 = check parseString(jsonStr4); + test:assertEquals(val4, {val: [[1, 2], 2.0, 3.0, [5, 6]]}); + + string jsonStr41 = string `{"val1" : [[1, 2], "2.0", 3.0, [5, 6]], "val2" : [[1, 2], "2.0", 3.0, [5, 6]]}`; + record {| + anydata[] val1; + anydata[] val2; + |} val41 = check parseString(jsonStr41); + test:assertEquals(val41, {val1: [[1, 2], 2.0, 3.0, [5, 6]], val2: [[1, 2], 2.0, 3.0, [5, 6]]}); + + string jsonStr5 = string `{"val" : [["1", 2], [2, "3"]]}`; + record {| + anydata[] val; + |} val5 = check parseString(jsonStr5); + test:assertEquals(val5, {val: [[1, 2], [2, 3]]}); + + string jsonStr6 = string `[{"val" : [["1", 2], [2, "3"]]}]`; + [record {|anydata[][] val;|}] val6 = check parseString(jsonStr6); + test:assertEquals(val6, [{val: [[1, 2], [2, 3]]}]); +} + +@test:Config +isolated function testJsonAsExpTypeForParseString() returns error? { + string jsonStr1 = string `1`; + json val1 = check parseString(jsonStr1); + test:assertEquals(val1, 1); + + string jsonStr2 = string `{ + "a": "hello", + "b": 1 + }`; + + json val2 = check parseString(jsonStr2); + test:assertEquals(val2, {"a": "hello", "b": 1}); + + string jsonStr3 = string `{ + "a": { + "b": 1, + "d": { + "e": "hello" + } + }, + "c": 2 + }`; + + json val3 = check parseString(jsonStr3); + test:assertEquals(val3, {"a": {"b": 1, "d": {"e": "hello"}}, "c": 2}); + + string jsonStr4 = string `{ + "a": [{ + "b": 1, + "d": { + "e": "hello" + } + }], + "c": 2 + }`; + + json val4 = check parseString(jsonStr4); + test:assertEquals(val4, {"a": [{"b": 1, "d": {"e": "hello"}}], "c": 2}); + + string str5 = string `[[1], 2]`; + json val5 = check parseString(str5); + test:assertEquals(val5, [[1], 2]); +} + +@test:Config +isolated function testJsonArrayAsExpTypeForParseString() returns error? { + +} + +@test:Config +isolated function testMapAsExpTypeForParseString() returns error? { + string jsonStr1 = string `{ + "a": "hello", + "b": "1" + }`; + + map val1 = check parseString(jsonStr1); + test:assertEquals(val1, {"a": "hello", "b": "1"}); + + string jsonStr2 = string `{ + "a": "hello", + "b": 1, + "c": { + "d": "world", + "e": "2" + } + }`; + record {| + string a; + int b; + map c; + |} val2 = check parseString(jsonStr2); + test:assertEquals(val2.a, "hello"); + test:assertEquals(val2.b, 1); + test:assertEquals(val2.c, {"d": "world", "e": "2"}); + + string jsonStr3 = string `{ + "a": { + "c": "world", + "d": "2" + }, + "b": { + "c": "world", + "d": "2" + } + }`; + + map> val3 = check parseString(jsonStr3); + test:assertEquals(val3, {"a": {"c": "world", "d": "2"}, "b": {"c": "world", "d": "2"}}); + + record {| + map a; + |} val4 = check parseString(jsonStr3); + test:assertEquals(val4.a, {"c": "world", "d": "2"}); + + map val5 = check parseString(jsonStr3); + test:assertEquals(val5, {"a": {"c": "world", "d": "2"}, "b": {"c": "world", "d": "2"}}); + + string jsonStr6 = string `{ + "a": "Kanth", + "b": { + "g": { + "c": "hello", + "d": "1" + }, + "h": { + "c": "world", + "d": "2" + } + } + }`; + record {| + string a; + map> b; + |} val6 = check parseString(jsonStr6); + test:assertEquals(val6.a, "Kanth"); + test:assertEquals(val6.b, {"g": {"c": "hello", "d": "1"}, "h": {"c": "world", "d": "2"}}); +} + +@test:Config +isolated function testProjectionInTupleForParseString() returns Error? { + string str1 = string `["1", 2, "3", 4, 5, 8]`; + [string, float] val1 = check parseString(str1); + test:assertEquals(val1, ["1", 2.0]); + + string str2 = string `{ + "a": ["1", "2", 3, "4", 5, 8] + }`; + record {|[string, string] a;|} val2 = check parseString(str2); + test:assertEquals(val2.a, ["1", "2"]); + + string str3 = string `[1, "4"]`; + [float] val3 = check parseString(str3); + test:assertEquals(val3, [1.0]); + + string str4 = string `["1", {}]`; + [string] val4 = check parseString(str4); + test:assertEquals(val4, ["1"]); + + string str5 = string `[1, [], {"name": 1}]`; + [float] val5 = check parseString(str5); + test:assertEquals(val5, [1.0]); +} + +@test:Config +isolated function testProjectionInArrayForParseString() returns Error? { + string strVal = string `[1, 2, 3, 4, 5]`; + int[] val = check parseString(strVal); + test:assertEquals(val, [1, 2, 3, 4, 5]); + + string strVal2 = string `[1, 2, 3, 4, 5]`; + int[2] val2 = check parseString(strVal2); + test:assertEquals(val2, [1, 2]); + + string strVal3 = string `{ + "a": [1, 2, 3, 4, 5] + }`; + record {|int[2] a;|} val3 = check parseString(strVal3); + test:assertEquals(val3, {a: [1, 2]}); + + string strVal4 = string `{ + "a": [1, 2, 3, 4, 5], + "b": [1, 2, 3, 4, 5] + }`; + record {|int[2] a; int[3] b;|} val4 = check parseString(strVal4); + test:assertEquals(val4, {a: [1, 2], b: [1, 2, 3]}); + + string strVal5 = string `{ + "employees": [ + { "name": "Prakanth", + "age": 26 + }, + { "name": "Kevin", + "age": 25 + } + ] + }`; + record {|record {|string name; int age;|}[1] employees;|} val5 = check parseString(strVal5); + test:assertEquals(val5, {employees: [{name: "Prakanth", age: 26}]}); + + string strVal6 = string `[1, 2, 3, { "a" : val_a }]`; + int[3] val6 = check parseString(strVal6); + test:assertEquals(val6, [1, 2, 3]); +} + +@test:Config +isolated function testProjectionInRecordForParseString() returns error? { + string jsonStr1 = string `{"name": "John", "age": 30, "city": "New York"}`; + record {|string name; string city;|} val1 = check parseString(jsonStr1); + test:assertEquals(val1, {name: "John", city: "New York"}); + + string jsonStr2 = string `{"name": "John", "age": "30", "city": "New York"}`; + record {|string name; string city;|} val2 = check parseString(jsonStr2); + test:assertEquals(val2, {name: "John", city: "New York"}); + + string jsonStr3 = string `{ "name": "John", + "company": { + "name": "wso2", + "year": 2024, + "addrees": { + "street": "123", + "city": "Berkeley" + } + }, + "city": "New York" }`; + record {|string name; string city;|} val3 = check parseString(jsonStr3); + test:assertEquals(val3, {name: "John", city: "New York"}); + + string jsonStr4 = string `{ "name": "John", + "company": [{ + "name": "wso2", + "year": 2024, + "addrees": { + "street": "123", + "city": "Berkeley" + } + }], + "city": "New York" }`; + record {|string name; string city;|} val4 = check parseString(jsonStr4); + test:assertEquals(val4, {name: "John", city: "New York"}); + + string jsonStr5 = string `{ "name": "John", + "company1": [{ + "name": "wso2", + "year": 2024, + "addrees": { + "street": "123", + "city": "Berkeley" + } + }], + "city": "New York", + "company2": [{ + "name": "amzn", + "year": 2024, + "addrees": { + "street": "123", + "city": "Miami" + } + }] + }`; + record {|string name; string city;|} val5 = check parseString(jsonStr5); + test:assertEquals(val5, {name: "John", city: "New York"}); +} + +@test:Config +isolated function testArrayOrTupleCaseForParseString() returns error? { + string jsonStr1 = string `[["1"], 2.0]`; + [[string], float] val1 = check parseString(jsonStr1); + test:assertEquals(val1, [["1"], 2.0]); + + string jsonStr2 = string `[["1", 2], "2.0"]`; + [[string, float], string] val2 = check parseString(jsonStr2); + test:assertEquals(val2, [["1", 2.0], "2.0"]); + + string jsonStr3 = string `[[1, 2], [2, 3]]`; + int[][] val3 = check parseString(jsonStr3); + test:assertEquals(val3, [[1, 2], [2, 3]]); + + string jsonStr4 = string `{"val" : [[1, 2], "2.0", 3.0, ["5", 6]]}`; + record {| + [[int, float], string, float, [string, int]] val; + |} val4 = check parseString(jsonStr4); + test:assertEquals(val4, {val: [[1, 2.0], "2.0", 3.0, ["5", 6]]}); + + string jsonStr41 = string `{"val1" : [[1, 2], "2.0", 3.0, ["5", 6]], "val2" : [[1, 2], "2.0", 3.0, ["5", 6]]}`; + record {| + [[int, float], string, float, [string, int]] val1; + [[float, float], string, float, [string, float]] val2; + |} val41 = check parseString(jsonStr41); + test:assertEquals(val41, {val1: [[1, 2.0], "2.0", 3.0, ["5", 6]], val2: [[1.0, 2.0], "2.0", 3.0, ["5", 6.0]]}); + + string jsonStr5 = string `{"val" : [[1, 2], [2, 3]]}`; + record {| + int[][] val; + |} val5 = check parseString(jsonStr5); + test:assertEquals(val5, {val: [[1, 2], [2, 3]]}); + + string jsonStr6 = string `[{"val" : [[1, 2], [2, 3]]}]`; + [record {|int[][] val;|}] val6 = check parseString(jsonStr6); + test:assertEquals(val6, [{val: [[1, 2], [2, 3]]}]); +} + +@test:Config +isolated function testListFillerValuesWithParseString() returns error? { + int[2] jsonVal1 = check parseString("[1]"); + test:assertEquals(jsonVal1, [1, 0]); + + [int, float, string, boolean] jsonVal2 = check parseString("[1]"); + test:assertEquals(jsonVal2, [1, 0.0, "", false]); + + record {| + float[3] A; + [int, decimal, float, boolean] B; + |} jsonVal3 = check parseString(string `{"A": [1], "B": [1]}`); + test:assertEquals(jsonVal3, {A: [1.0, 0.0, 0.0], B: [1, 0d, 0.0, false]}); +} + +@test:Config +isolated function testSingletonAsExpectedTypeForParseString() returns error? { + "1" val1 = check parseString("\"1\""); + test:assertEquals(val1, "1"); + + Singleton1 val2 = check parseString("1"); + test:assertEquals(val2, 1); + + SingletonUnion val3 = check parseString("1"); + test:assertEquals(val3, 1); + + () val4 = check parseString("null"); + test:assertEquals(val4, ()); + + // string str5 = string `{ + // "value": 1, + // "id": "3" + // }`; + // SingletonInRecord val5 = check parseString(str5); + // test:assertEquals(val5.id, "3"); + // test:assertEquals(val5.value, 1); +} + +@test:Config +function testDuplicateKeyInTheStringSource() returns error? { + +} + +@test:Config +function testNameAnnotationWithParseString() returns error? { + +} + +@test:Config +isolated function testByteAsExpectedTypeForParseString() returns error? { + byte result = check parseString("1"); + test:assertEquals(result, 1); + + [byte, int] result2 = check parseString("[255, 2000]"); + test:assertEquals(result2, [255, 2000]); + + string content = check io:fileReadString(FILE_PATH + "nested_12.yaml"); + + record { + byte id; + string name; + record { + string street; + string city; + byte id; + } address; + } result3 = check parseString(content); + test:assertEquals(result3.length(), 3); + test:assertEquals(result3.id, 1); + test:assertEquals(result3.name, "Anne"); + test:assertEquals(result3.address.length(), 3); + test:assertEquals(result3.address.street, "Main"); + test:assertEquals(result3.address.city, "94"); + test:assertEquals(result3.address.id, 2); +} + +@test:Config +isolated function testSignedInt8AsExpectedTypeForParseString() returns error? { + int:Signed8 val1 = check parseString("-128"); + test:assertEquals(val1, -128); + + int:Signed8 val2 = check parseString("127"); + test:assertEquals(val2, 127); + + [int:Signed8, int] val3 = check parseString("[127, 2000]"); + test:assertEquals(val3, [127, 2000]); + + string content = check io:fileReadString(FILE_PATH + "nested_13.yaml"); + + record { + int:Signed8 id; + string name; + record { + string street; + string city; + int:Signed8 id; + } address; + } val4 = check parseString(content); + test:assertEquals(val4.length(), 3); + test:assertEquals(val4.id, 100); + test:assertEquals(val4.name, "Anne"); + test:assertEquals(val4.address.length(), 3); + test:assertEquals(val4.address.street, "Main"); + test:assertEquals(val4.address.city, "94"); + test:assertEquals(val4.address.id, -2); +} + +@test:Config +isolated function testSignedInt16AsExpectedTypeForParseString() returns error? { + int:Signed16 val1 = check parseString("-32768"); + test:assertEquals(val1, -32768); + + int:Signed16 val2 = check parseString("32767"); + test:assertEquals(val2, 32767); + + [int:Signed16, int] val3 = check parseString("[32767, -324234]"); + test:assertEquals(val3, [32767, -324234]); + + string content = check io:fileReadString(FILE_PATH + "nested_13.yaml"); + + record { + int:Signed16 id; + string name; + record { + string street; + string city; + int:Signed16 id; + } address; + } val4 = check parseString(content); + test:assertEquals(val4.length(), 3); + test:assertEquals(val4.id, 100); + test:assertEquals(val4.name, "Anne"); + test:assertEquals(val4.address.length(), 3); + test:assertEquals(val4.address.street, "Main"); + test:assertEquals(val4.address.city, "94"); + test:assertEquals(val4.address.id, -2); +} + +@test:Config +isolated function testSignedInt32AsExpectedTypeForParseString() returns error? { + int:Signed32 val1 = check parseString("-2147483648"); + test:assertEquals(val1, -2147483648); + + int:Signed32 val2 = check parseString("2147483647"); + test:assertEquals(val2, 2147483647); + + int:Signed32[] val3 = check parseString("[2147483647, -2147483648]"); + test:assertEquals(val3, [2147483647, -2147483648]); + + string content = check io:fileReadString(FILE_PATH + "nested_14.yaml"); + + record { + int:Signed32 id; + string name; + record { + string street; + string city; + int:Signed32 id; + } address; + } val4 = check parseString(content); + test:assertEquals(val4.length(), 3); + test:assertEquals(val4.id, 2147483647); + test:assertEquals(val4.name, "Anne"); + test:assertEquals(val4.address.length(), 3); + test:assertEquals(val4.address.street, "Main"); + test:assertEquals(val4.address.city, "94"); + test:assertEquals(val4.address.id, -2147483648); +} + +@test:Config +isolated function testUnSignedInt8AsExpectedTypeForParseString() returns error? { + int:Unsigned8 val1 = check parseString("255"); + test:assertEquals(val1, 255); + + int:Unsigned8 val2 = check parseString("0"); + test:assertEquals(val2, 0); + + int:Unsigned8[] val3 = check parseString("[0, 255]"); + test:assertEquals(val3, [0, 255]); + + string content = check io:fileReadString(FILE_PATH + "nested_15.yaml"); + + record { + int:Unsigned8 id; + string name; + record { + string street; + string city; + int:Unsigned8 id; + } address; + } val4 = check parseString(content); + test:assertEquals(val4.length(), 3); + test:assertEquals(val4.id, 0); + test:assertEquals(val4.name, "Anne"); + test:assertEquals(val4.address.length(), 3); + test:assertEquals(val4.address.street, "Main"); + test:assertEquals(val4.address.city, "94"); + test:assertEquals(val4.address.id, 255); +} + +@test:Config +isolated function testUnSignedInt16AsExpectedTypeForParseString() returns error? { + int:Unsigned16 val1 = check parseString("65535"); + test:assertEquals(val1, 65535); + + int:Unsigned16 val2 = check parseString("0"); + test:assertEquals(val2, 0); + + int:Unsigned16[] val3 = check parseString("[0, 65535]"); + test:assertEquals(val3, [0, 65535]); + + string content = check io:fileReadString(FILE_PATH + "nested_16.yaml"); + + record { + int:Unsigned16 id; + string name; + record { + string street; + string city; + int:Unsigned16 id; + } address; + } val4 = check parseString(content); + test:assertEquals(val4.length(), 3); + test:assertEquals(val4.id, 0); + test:assertEquals(val4.name, "Anne"); + test:assertEquals(val4.address.length(), 3); + test:assertEquals(val4.address.street, "Main"); + test:assertEquals(val4.address.city, "94"); + test:assertEquals(val4.address.id, 65535); +} + +@test:Config +isolated function testUnSignedInt32AsExpectedTypeForParseString() returns error? { + int:Unsigned32 val1 = check parseString("4294967295"); + test:assertEquals(val1, 4294967295); + + int:Unsigned32 val2 = check parseString("0"); + test:assertEquals(val2, 0); + + int:Unsigned32[] val3 = check parseString("[0, 4294967295]"); + test:assertEquals(val3, [0, 4294967295]); + + string content = check io:fileReadString(FILE_PATH + "nested_17.yaml"); + + record { + int:Unsigned32 id; + string name; + record { + string street; + string city; + int:Unsigned32 id; + } address; + } val4 = check parseString(content); + test:assertEquals(val4.length(), 3); + test:assertEquals(val4.id, 0); + test:assertEquals(val4.name, "Anne"); + test:assertEquals(val4.address.length(), 3); + test:assertEquals(val4.address.street, "Main"); + test:assertEquals(val4.address.city, "94"); + test:assertEquals(val4.address.id, 4294967295); +} + +@test:Config +isolated function testNilableTypeAsFieldTypeForParseString() returns error? { + +} + +@test:Config +isolated function testNilableTypeAsFieldTypeForParseAsType() returns error? { + +} + +@test:Config +isolated function testEscapeCharacterCaseForParseString() returns error? { + string jsonStr1 = string ` + { + "A": "\\A_Field", + "B": "\/B_Field", + "C": "\"C_Field\"", + "D": "\uD83D\uDE01", + "E": "FIELD\nE", + "F": "FIELD\rF", + "G": "FIELD\tG", + "H": ["\\A_Field", "\/B_Field", "\"C_Field\"", "\uD83D\uDE01", "FIELD\nE", "FIELD\rF", "FIELD\tG"] + } + `; + OpenRecord val1 = check parseString(jsonStr1); + + OpenRecord expectedResult = { + "A": "\\\\A_Field", + "B": "/B_Field", + "C": "\"C_Field\"", + "D": "😁", + "E": "FIELD\nE", + "F": "FIELD\rF", + "G": "FIELD\tG", + "H": ["\\\\A_Field", "/B_Field", "\"C_Field\"", "😁", "FIELD\nE", "FIELD\rF", "FIELD\tG"] + }; + + test:assertEquals(val1, expectedResult); +} diff --git a/ballerina/tests/parse_string_negative.bal b/ballerina/tests/parse_string_negative.bal new file mode 100644 index 0000000..7473e93 --- /dev/null +++ b/ballerina/tests/parse_string_negative.bal @@ -0,0 +1,42 @@ +// 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 +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/io; +import ballerina/test; + +const NEGATIVE_TEST_PATH = FILE_PATH + "negative/"; + +@test:Config { + dataProvider: negativeDataProvider +} +isolated function negtiveTests(string path, string expectedErrMsg) returns io:Error? { + string content = check io:fileReadString(NEGATIVE_TEST_PATH + path); + anydata|Error result = parseString(content); + test:assertTrue(result is Error); + test:assertEquals((result).message(), expectedErrMsg); +} + +function negativeDataProvider() returns [string, string][] => [ + ["negative_test_1.yaml", "'non printable character found' at line: '2' column: '13'"], + ["negative_test_2.yaml", "'invalid indentation' at line: '5' column: '5'"], + ["negative_test_3.yaml", "'invalid block header' at line: '1' column: '2'"], + ["negative_test_4.yaml", "'insufficient indentation for a scalar' at line: '3' column: '4'"], + ["negative_test_5.yaml", "'insufficient indentation for a scalar' at line: '4' column: '4'"], + [ + "negative_test_6.yaml", + "'block mapping cannot have the same indent as a block sequence' at line: '3' column: '10'" + ] +]; diff --git a/ballerina/tests/parse_yaml_streams.bal b/ballerina/tests/parse_yaml_streams.bal new file mode 100644 index 0000000..c30b570 --- /dev/null +++ b/ballerina/tests/parse_yaml_streams.bal @@ -0,0 +1,278 @@ +// 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 +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/io; +import ballerina/test; + +const YAML_STREAM_TEST_PATH = FILE_PATH + "streams/"; + +@test:Config +isolated function testYamlStringParsing() returns error? { + stream streamResult = check io:fileReadBlocksAsStream(YAML_STREAM_TEST_PATH + "stream_1.yaml"); + anydata result = check parseStream(streamResult); + test:assertTrue(result is anydata[]); + test:assertEquals((result).length(), 4); +} + +@test:Config +isolated function testYamlStringParsing2() returns error? { + string filePaht = YAML_STREAM_TEST_PATH + "stream_1.yaml"; + stream streamResult = check io:fileReadBlocksAsStream(filePaht); + ExpectedType result = check parseStream(streamResult); + + final ConfigType configMapValue = { + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": {"name": "ballerina-mongodb-configmap"}, + "data": { + "Config.toml": "[ballerina.ballerina_mongodb_kubernetes]" + + "\nmongodbPort = 27017\nmongodbHost = \"mongodb-service\"" + + "\ndbName = \"students\"\n\n[ballerina.log]\nlevel = \"DEBUG\"\n" + } + }; + + test:assertEquals(result.length(), 2); + test:assertEquals(result[1].apiVersion, configMapValue.apiVersion); + test:assertEquals(result[1].kind, configMapValue.kind); + test:assertEquals(result[1].metadata, configMapValue.metadata); + test:assertEquals((result[1]).data, configMapValue.data); +} + +@test:Config +isolated function testYamlStringParsing3() returns error? { + string filePaht = YAML_STREAM_TEST_PATH + "stream_1.yaml"; + stream streamResult = check io:fileReadBlocksAsStream(filePaht); + UnionType[] result = check parseStream(streamResult); + + final ConfigType configMapValue = { + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": {"name": "ballerina-mongodb-configmap"}, + "data": { + "Config.toml": "[ballerina.ballerina_mongodb_kubernetes]" + + "\nmongodbPort = 27017\nmongodbHost = \"mongodb-service\"" + + "\ndbName = \"students\"\n\n[ballerina.log]\nlevel = \"DEBUG\"\n" + } + }; + + test:assertEquals(result.length(), 4); + test:assertEquals(result[1].apiVersion, configMapValue.apiVersion); + test:assertEquals(result[1].kind, configMapValue.kind); + test:assertEquals(result[1].metadata, configMapValue.metadata); + test:assertEquals((result[1]).data, configMapValue.data); +} + +@test:Config +isolated function testYamlStreamPastingWithTupleExpected() returns error? { + string filePaht = YAML_STREAM_TEST_PATH + "stream_1.yaml"; + stream streamResult = check io:fileReadBlocksAsStream(filePaht); + [ServiceType, ConfigType, ConfigType, DeploymentType] result = check parseStream(streamResult); + + final ConfigType configMapValue = { + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": {"name": "ballerina-mongodb-configmap"}, + "data": { + "Config.toml": "[ballerina.ballerina_mongodb_kubernetes]" + + "\nmongodbPort = 27017\nmongodbHost = \"mongodb-service\"" + + "\ndbName = \"students\"\n\n[ballerina.log]\nlevel = \"DEBUG\"\n" + } + }; + + test:assertEquals(result.length(), 4); + test:assertEquals(result[1].apiVersion, configMapValue.apiVersion); + test:assertEquals(result[1].kind, configMapValue.kind); + test:assertEquals(result[1].metadata, configMapValue.metadata); + test:assertEquals(result[1].data, configMapValue.data); +} + +@test:Config +isolated function testYamlStreamPastingWithTupleExpected2() returns error? { + string filePaht = YAML_STREAM_TEST_PATH + "stream_1.yaml"; + stream streamResult = check io:fileReadBlocksAsStream(filePaht); + [DeploymentType, ServiceType, ConfigType] result = check parseStream(streamResult); + + final ConfigType configMapValue = { + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": {"name": "ballerina-mongodb-configmap"}, + "data": { + "Config.toml": "[ballerina.ballerina_mongodb_kubernetes]" + + "\nmongodbPort = 27017\nmongodbHost = \"mongodb-service\"" + + "\ndbName = \"students\"\n\n[ballerina.log]\nlevel = \"DEBUG\"\n" + } + }; + + test:assertEquals(result.length(), 3); + test:assertEquals(result[2].apiVersion, configMapValue.apiVersion); + test:assertEquals(result[2].kind, configMapValue.kind); + test:assertEquals(result[2].metadata, configMapValue.metadata); + test:assertEquals(result[2].data, configMapValue.data); +} + +@test:Config +isolated function testYamlStreamPastingWithTupleExpected3() returns error? { + string filePaht = YAML_STREAM_TEST_PATH + "stream_1.yaml"; + stream streamResult = check io:fileReadBlocksAsStream(filePaht); + [DeploymentType, ServiceType, ConfigType, ConfigType...] result = check parseStream(streamResult); + + final ConfigType configMapValue = { + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": {"name": "ballerina-mongodb-configmap"}, + "data": { + "Config.toml": "[ballerina.ballerina_mongodb_kubernetes]" + + "\nmongodbPort = 27017\nmongodbHost = \"mongodb-service\"" + + "\ndbName = \"students\"\n\n[ballerina.log]\nlevel = \"DEBUG\"\n" + } + }; + + test:assertEquals(result.length(), 4); + test:assertEquals(result[2].apiVersion, configMapValue.apiVersion); + test:assertEquals(result[2].kind, configMapValue.kind); + test:assertEquals(result[2].metadata, configMapValue.metadata); + test:assertEquals(result[2].data, configMapValue.data); +} + +@test:Config +isolated function testYamlStreamPastingWithTupleExpected4() returns error? { + string filePaht = YAML_STREAM_TEST_PATH + "stream_1.yaml"; + stream streamResult = check io:fileReadBlocksAsStream(filePaht); + [ConfigType, ServiceType, ConfigType...] result = check parseStream(streamResult); + + final ConfigType configMapValue = { + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": {"name": "ballerina-mongodb-configmap"}, + "data": { + "Config.toml": "[ballerina.ballerina_mongodb_kubernetes]" + + "\nmongodbPort = 27017\nmongodbHost = \"mongodb-service\"" + + "\ndbName = \"students\"\n\n[ballerina.log]\nlevel = \"DEBUG\"\n" + } + }; + + test:assertEquals(result.length(), 3); + test:assertEquals(result[0].apiVersion, configMapValue.apiVersion); + test:assertEquals(result[0].kind, configMapValue.kind); + test:assertEquals(result[0].metadata, configMapValue.metadata); + test:assertEquals(result[0].data, configMapValue.data); +} + +@test:Config +isolated function testYamlStreamPastingWithTupleExpected5() returns error? { + string filePaht = YAML_STREAM_TEST_PATH + "stream_2.yaml"; + stream streamResult = check io:fileReadBlocksAsStream(filePaht); + [ServiceType, int...] result = check parseStream(streamResult); + + test:assertEquals(result.length(), 4); + test:assertEquals(result[1], 0); + test:assertEquals(result[2], 1); + test:assertEquals(result[3], 2); +} + +@test:Config +isolated function testYamlStreamPastingWithTupleExpected6() returns error? { + string filePaht = YAML_STREAM_TEST_PATH + "stream_3.yaml"; + stream streamResult = check io:fileReadBlocksAsStream(filePaht); + [T1, T1|T2, T2, T3, T3|T2, T2|T3, T1...] result = check parseStream(streamResult); + [T1, T1|T2, T2, T3, T3|T2, T2|T3, T1...] expectedResult = [ + {"p1": "T1_0"}, + {"p1": "T1_1"}, + {"p2": "123", "p3": "string", "p1": "T2_0"}, + {"p2": 123, "p3": true, "p1": "T3_0"}, + {"p2": 123, "p3": true, "p1": "T3_1"}, + {"p2": 123, "p3": false, "p1": "T3_2"}, + {"p1": "T1_6"}, + {"p1": "T1_7"}, + {"p1": "T1_8"}, + {"p1": "T1_9"} + ]; + test:assertEquals(result, expectedResult); +} + +type T1 record {| + string p1; +|}; + +type T2 record {| + *T1; + string p2; + string p3; +|}; + +type T3 record {| + *T1; + int p2; + boolean p3; +|}; + +type ExpectedType UnionType[2]; + +type UnionType ServiceType|ConfigType|DeploymentType; + +type ServiceType record {| + string apiVersion; + string kind; + record { + record { + string app; + } labels; + string name; + } metadata; + record { + record { + string name; + int port; + string protocol; + int targetPort; + int nodePort; + }[] ports; + record { + string app; + } selector; + string 'type; + } spec; +|}; + +type ConfigType record {| + string apiVersion; + string kind; + record {| + string name; + |} metadata; + map data; +|}; + +type DeploymentType record { + string apiVersion; + string kind; + record { + record { + string app; + } labels; + string name; + } metadata; + record { + int replicas; + record { + record { + string app; + } matchLabels; + } selector; + record { + } template; + } spec; +}; diff --git a/ballerina/tests/parser_tests.bal b/ballerina/tests/parser_tests.bal new file mode 100644 index 0000000..c21a5b3 --- /dev/null +++ b/ballerina/tests/parser_tests.bal @@ -0,0 +1,40 @@ +// 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 +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/io; +import ballerina/test; + +const PARSER_TESTS_PATH = FILE_PATH + "parser/"; + +@test:Config { + dataProvider: tagHandleData +} +isolated function testTagHandles(string inputPath, TestCase expectedValue) returns error? { + string filePath = PARSER_TESTS_PATH + inputPath; + string content = check io:fileReadString(filePath); + TestCase actual = check parseString(content); + test:assertEquals(actual, expectedValue); +} + +function tagHandleData() returns [string, TestCase][] => [ + ["tag_handle_1.yaml", {case: "uri_scanner"}], + ["tag_handle_2.yaml", {case: "yaml_version"}], + ["tag_handle_3.yaml", {case: "verbitam"}] +]; + +type TestCase record {| + string case; +|}; diff --git a/ballerina/tests/readonly_intersection_expected_type_test.bal b/ballerina/tests/readonly_intersection_expected_type_test.bal new file mode 100644 index 0000000..a2dbacb --- /dev/null +++ b/ballerina/tests/readonly_intersection_expected_type_test.bal @@ -0,0 +1,258 @@ +// 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 +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/test; + +type intArrayReadonly int[] & readonly; + +type intArray2dReadonly int[][] & readonly; + +type booleanArrayReadonly boolean[] & readonly; + +type type1Readonly [int, boolean, decimal, string] & readonly; + +type type2Readonly map & readonly; + +type type3Readonly map & readonly; + +type type4Readonly map> & readonly; + +type type5Readonly map[] & readonly; + +type mapIntArrayReadonly map & readonly; + +type jsonTypeReadonly json & readonly; + +type int2ArrayReadonly int[2] & readonly; + +type char2ArrayReadonly string:Char[2] & readonly; + +type char2DFixedArrayReadonly string:Char[3][4] & readonly; + +type int2DFixedArrayReadonly int[2][1] & readonly; + +type intTupleReadonly [[int], [int]] & readonly; + +type intTupleRestReadonly [[int], [int]...] & readonly; + +type intStringTupleReadonly [[int], [string]] & readonly; + +type intStringTupleRestReadonly [[int], [string]...] & readonly; + +type NilTypeReadonly () & readonly; + +type BooleanTypeReadonly boolean & readonly; + +type intTypeReadonly int & readonly; + +type floatTypeReadonly float & readonly; + +type decimalTypeReadonly decimal & readonly; + +type stringTypeReadonly string & readonly; + +type charTypeReadonly string:Char & readonly; + +type ByteTypeReadonly byte & readonly; + +type intUnsigned8Readonly int:Unsigned8 & readonly; + +type intSigned8Readonly int:Signed8 & readonly; + +type intUnsigned16Readonly int:Unsigned16 & readonly; + +type intSigned16Readonly int:Signed16 & readonly; + +type intUnsigned32Readonly int:Unsigned32 & readonly; + +type intSigned32Readonly int:Signed32 & readonly; + +type strinttupleReadonly [int, int] & readonly; + +type stringArrReadonly string[] & readonly; + +type tuple1Readonly [[int, string], [boolean, float]] & readonly; + +type tuple2Readonly [[float, string], [boolean, decimal]...] & readonly; + +type stringArrayTypeReadonly string[] & readonly; + +type Rec1ReadOnly Rec1 & readonly; + +type Rec2ReadOnly Rec2 & readonly; + +type Rec3ReadOnly Rec3 & readonly; + +type Rec1 record {| + string name; + int age; + boolean isMarried = true; + float...; +|}; + +type Rec2 record {| + Rec1 student; + string address; + int count; + float weight = 18.3; + boolean...; +|}; + +type Rec3 record {| + Rec1 student; +|}; + +type Rec4 record {| + readonly string department; + intTypeReadonly studentCount; + Rec1ReadOnly[] student; +|}; + +type Rec5 record {| + readonly & int id; + Rec2 & readonly health; +|}; + +type ExpectedTuple [ + intArrayReadonly, + type1Readonly, + intArrayReadonly, + intArray2dReadonly, + type3Readonly, + type4Readonly, + type5Readonly, + mapIntArrayReadonly, + int2ArrayReadonly, + int2DFixedArrayReadonly, + intTupleReadonly, + intTupleRestReadonly, + intTupleRestReadonly, + intStringTupleRestReadonly, + intStringTupleRestReadonly, + intTupleReadonly, + int2DFixedArrayReadonly, + BooleanTypeReadonly, + BooleanTypeReadonly, + intTypeReadonly, + floatTypeReadonly, + decimalTypeReadonly, + stringTypeReadonly, + charTypeReadonly, + ByteTypeReadonly, + intUnsigned8Readonly, + intSigned8Readonly, + intUnsigned16Readonly, + intSigned16Readonly, + intUnsigned32Readonly, + intSigned32Readonly, + NilTypeReadonly, + Rec1ReadOnly, + Rec3ReadOnly, + Rec2ReadOnly, + Rec4, + Rec5 +]; + +ExpectedTuple expectedResults = [ + [1, 2, 3], + [12, true, 123.4, "hello"], + [12, 13], + [[12], [13]], + {id: false, age: true}, + {key1: {id: 12, age: 24}, key2: {id: 12, age: 24}}, + [{id: 12, age: 24}, {id: 12, age: 24}], + {key1: [12, 13], key2: [132, 133]}, + [12], + [[1], [2]], + [[1], [2]], + [[1], [2], [3]], + [[1]], + [[1], ["2"], ["3"]], + [[1]], + [[1], [2]], + [[1], [2]], + true, + false, + 12, + 12.3, + 12.3, + "hello", + "h", + 12, + 13, + 14, + 15, + 16, + 17, + 18, + null, + {name: "John", age: 30, "height": 1.8}, + {student: {name: "John", age: 30, "height": 1.8}}, + {"isSingle": true, address: "this is address", count: 14, student: {name: "John", age: 30, "height": 1.8}}, + {department: "CSE", studentCount: 3, student: [{name: "John", age: 30, "height": 1.8}]}, + {id: 12, health: {student: {name: "John", age: 30, "height": 1.8}, address: "this is address", count: 14}} +]; + +@test:Config { + dataProvider: readonlyIntersectionTestDataForParseString +} +isolated function testReadOnlyIntersectionTypeAsExpTypForParseString(string sourceData, + typedesc expType, anydata expectedData) returns error? { + anydata result = check parseString(sourceData, {}, expType); + test:assertEquals(result, expectedData); +} + +function readonlyIntersectionTestDataForParseString() returns [string, typedesc, anydata][] { + return [ + [string `[1, 2, 3]`, intArrayReadonly, expectedResults[0]], + ["[12, true, 123.4, \"hello\"]", type1Readonly, expectedResults[1]], + ["[12, 13]", intArrayReadonly, expectedResults[2]], + ["[[12], [13]]", intArray2dReadonly, expectedResults[3]], + ["{\"id\": false, \"age\": true}", type3Readonly, expectedResults[4]], + ["{\"key1\": {\"id\": 12, \"age\": 24}, \"key2\": {\"id\": 12, \"age\": 24}}", type4Readonly, expectedResults[5]], + ["[{\"id\": 12, \"age\": 24}, {\"id\": 12, \"age\": 24}]", type5Readonly, expectedResults[6]], + ["{\"key1\": [12, 13], \"key2\": [132, 133]}", mapIntArrayReadonly, expectedResults[7]], + ["[12]", int2ArrayReadonly, expectedResults[8]], + ["[[1],[2]]", int2DFixedArrayReadonly, expectedResults[9]], + ["[[1],[2]]", intTupleReadonly, expectedResults[10]], + ["[[1],[2],[3]]", intTupleRestReadonly, expectedResults[11]], + ["[[1]]", intTupleRestReadonly, expectedResults[12]], + ["[[1],[\"2\"],[\"3\"]]", intStringTupleRestReadonly, expectedResults[13]], + ["[[1]]", intStringTupleRestReadonly, expectedResults[14]], + ["[[1],[2]]", intTupleReadonly, expectedResults[15]], + ["[[1],[2]]", int2DFixedArrayReadonly, expectedResults[16]], + ["true", BooleanTypeReadonly, expectedResults[17]], + ["false", BooleanTypeReadonly, expectedResults[18]], + ["12", intTypeReadonly, expectedResults[19]], + ["12.3", floatTypeReadonly, expectedResults[20]], + ["12.3", decimalTypeReadonly, expectedResults[21]], + ["\"hello\"", stringTypeReadonly, expectedResults[22]], + ["\"h\"", charTypeReadonly, expectedResults[23]], + ["12", ByteTypeReadonly, expectedResults[24]], + ["13", intUnsigned8Readonly, expectedResults[25]], + ["14", intSigned8Readonly, expectedResults[26]], + ["15", intUnsigned16Readonly, expectedResults[27]], + ["16", intSigned16Readonly, expectedResults[28]], + ["17", intUnsigned32Readonly, expectedResults[29]], + ["18", intSigned32Readonly, expectedResults[30]], + ["null", NilTypeReadonly, expectedResults[31]], + [string `{"name": "John", "age": 30, "height": 1.8}`, Rec1ReadOnly, expectedResults[32]], + [string `{"student": {"name": "John", "age": 30, "height": 1.8}}`, Rec3ReadOnly, expectedResults[33]], + [string `{"isSingle": true, "address": "this is address", "count": 14,"student": {"name": "John", "age": 30, "height": 1.8}}`, Rec2ReadOnly, expectedResults[34]], + [string `{"department": "CSE", "studentCount": 3, "student": [{"name": "John", "age": 30, "height": 1.8}]}`, Rec4, expectedResults[35]], + [string `{"id": 12, "health": {"student": {"name": "John", "age": 30, "height": 1.8}, "address": "this is address", "count": 14}}`, Rec5, expectedResults[36]] + ]; +} diff --git a/ballerina/tests/resources/anchors/anchor_test_1.yaml b/ballerina/tests/resources/anchors/anchor_test_1.yaml new file mode 100644 index 0000000..974262b --- /dev/null +++ b/ballerina/tests/resources/anchors/anchor_test_1.yaml @@ -0,0 +1,27 @@ +# Define some data with anchors +user_info: &user + name: John Doe + email: john.doe@example.com + roles: &roles + - admin + - editor + +server_config: + # Use the user information anchor + owner: *user + # Define a sequence of ports + ports: + - 80 + - 443 + host: &host localhost + database: &database my_database + +# Use the database config anchor again +web_server: + # Override a value from the extended config + ports: + - 8080 + database: + host: *host + database: *database + roles: *roles diff --git a/ballerina/tests/resources/anchors/anchor_test_2.yaml b/ballerina/tests/resources/anchors/anchor_test_2.yaml new file mode 100644 index 0000000..625aa34 --- /dev/null +++ b/ballerina/tests/resources/anchors/anchor_test_2.yaml @@ -0,0 +1,23 @@ +# Define a product with nested data +product: + name: T-Shirt + brand: "Acme Clothing" + +# Create variations with aliases +variation_1: &variation_1 + # Add specific details + size: S + color: red + +variation_2: &variation_2 + size: M + color: blue + +# Define a shopping cart with product references +shopping_cart: + items: + - *variation_1 + - *variation_2 + # Additional information about the cart + total_price: 29.99 + currency: USD diff --git a/ballerina/tests/resources/negative/negative_test_1.yaml b/ballerina/tests/resources/negative/negative_test_1.yaml new file mode 100644 index 0000000..4bd2a67 --- /dev/null +++ b/ballerina/tests/resources/negative/negative_test_1.yaml @@ -0,0 +1,3 @@ +value: BETA +name: ALPHA  +id: 12 diff --git a/ballerina/tests/resources/negative/negative_test_2.yaml b/ballerina/tests/resources/negative/negative_test_2.yaml new file mode 100644 index 0000000..8553d8d --- /dev/null +++ b/ballerina/tests/resources/negative/negative_test_2.yaml @@ -0,0 +1,5 @@ +name: John +age: 30 +address: + street: "123 Main Street" + city: "New York" diff --git a/ballerina/tests/resources/negative/negative_test_3.yaml b/ballerina/tests/resources/negative/negative_test_3.yaml new file mode 100644 index 0000000..a42f34e --- /dev/null +++ b/ballerina/tests/resources/negative/negative_test_3.yaml @@ -0,0 +1,2 @@ +|3 +value diff --git a/ballerina/tests/resources/negative/negative_test_4.yaml b/ballerina/tests/resources/negative/negative_test_4.yaml new file mode 100644 index 0000000..e531194 --- /dev/null +++ b/ballerina/tests/resources/negative/negative_test_4.yaml @@ -0,0 +1,3 @@ +| + another value +value diff --git a/ballerina/tests/resources/negative/negative_test_5.yaml b/ballerina/tests/resources/negative/negative_test_5.yaml new file mode 100644 index 0000000..7155958 --- /dev/null +++ b/ballerina/tests/resources/negative/negative_test_5.yaml @@ -0,0 +1,4 @@ +val: | + another value + another value +value \ No newline at end of file diff --git a/ballerina/tests/resources/negative/negative_test_6.yaml b/ballerina/tests/resources/negative/negative_test_6.yaml new file mode 100644 index 0000000..8ae13e2 --- /dev/null +++ b/ballerina/tests/resources/negative/negative_test_6.yaml @@ -0,0 +1,5 @@ +movies: + - title: Inception + director: Christopher Nolan + genre: Sci-Fi + year: 2010 diff --git a/ballerina/tests/resources/nested_1.yaml b/ballerina/tests/resources/nested_1.yaml new file mode 100644 index 0000000..13b6ced --- /dev/null +++ b/ballerina/tests/resources/nested_1.yaml @@ -0,0 +1,17 @@ +name: "YAML Test" +employees: + - name: 'Alice' + age: 30 + department: HR + projects: [ { + name: "Project A", + status: "In Progress"}, + { name: Project B, status: "Completed" } + ] + - name: Bob + age: 35 + department: Engineering + projects: + - name: Project C + status: "Pending" + - { name: Project D, status: "In Progress" } diff --git a/ballerina/tests/resources/nested_10.yaml b/ballerina/tests/resources/nested_10.yaml new file mode 100644 index 0000000..ec0bf40 --- /dev/null +++ b/ballerina/tests/resources/nested_10.yaml @@ -0,0 +1,6 @@ +a: + - c: "world" + d: "2" +b: + - c: "world" + d: "2" diff --git a/ballerina/tests/resources/nested_11.yaml b/ballerina/tests/resources/nested_11.yaml new file mode 100644 index 0000000..714128f --- /dev/null +++ b/ballerina/tests/resources/nested_11.yaml @@ -0,0 +1,5 @@ +a: + b: 1 + d: + e: false +c: 2 diff --git a/ballerina/tests/resources/nested_12.yaml b/ballerina/tests/resources/nested_12.yaml new file mode 100644 index 0000000..a1f7b93 --- /dev/null +++ b/ballerina/tests/resources/nested_12.yaml @@ -0,0 +1,6 @@ +id: 1 +name: Anne +address: + street: Main + city: "94" + id: 2 diff --git a/ballerina/tests/resources/nested_13.yaml b/ballerina/tests/resources/nested_13.yaml new file mode 100644 index 0000000..58deef3 --- /dev/null +++ b/ballerina/tests/resources/nested_13.yaml @@ -0,0 +1,6 @@ +id: 100 +name: Anne +address: + street: Main + city: "94" + id: -2 diff --git a/ballerina/tests/resources/nested_14.yaml b/ballerina/tests/resources/nested_14.yaml new file mode 100644 index 0000000..9049b5a --- /dev/null +++ b/ballerina/tests/resources/nested_14.yaml @@ -0,0 +1,6 @@ +id: 2147483647 +name: Anne +address: + street: Main + city: "94" + id: -2147483648 diff --git a/ballerina/tests/resources/nested_15.yaml b/ballerina/tests/resources/nested_15.yaml new file mode 100644 index 0000000..26ecb2d --- /dev/null +++ b/ballerina/tests/resources/nested_15.yaml @@ -0,0 +1,6 @@ +id: 0 +name: Anne +address: + street: Main + city: "94" + id: 255 diff --git a/ballerina/tests/resources/nested_16.yaml b/ballerina/tests/resources/nested_16.yaml new file mode 100644 index 0000000..e6b6cba --- /dev/null +++ b/ballerina/tests/resources/nested_16.yaml @@ -0,0 +1,6 @@ +id: 0 +name: Anne +address: + street: Main + city: "94" + id: 65535 diff --git a/ballerina/tests/resources/nested_17.yaml b/ballerina/tests/resources/nested_17.yaml new file mode 100644 index 0000000..956f49e --- /dev/null +++ b/ballerina/tests/resources/nested_17.yaml @@ -0,0 +1,6 @@ +id: 0 +name: Anne +address: + street: Main + city: "94" + id: 4294967295 diff --git a/ballerina/tests/resources/nested_2.yaml b/ballerina/tests/resources/nested_2.yaml new file mode 100644 index 0000000..af97b2f --- /dev/null +++ b/ballerina/tests/resources/nested_2.yaml @@ -0,0 +1,8 @@ +name: 'Alice' +age: 30 +department: HR +projects: [ { + name: "Project A", + status: "In Progress"}, + { name: Project B, status: "Completed" } +] diff --git a/ballerina/tests/resources/nested_3.yaml b/ballerina/tests/resources/nested_3.yaml new file mode 100644 index 0000000..bf5a4d8 --- /dev/null +++ b/ballerina/tests/resources/nested_3.yaml @@ -0,0 +1,17 @@ +- title: "Book 1" + pages: 200 + authors: + - Author A + - Author B + price: 15.99 +- title: "Book 2" + pages: 300 + authors: + - Author X + - Author Y + price: 20.99 +- title: "Book 3" + pages: 150 + authors: + - Author M + price: 12.50 diff --git a/ballerina/tests/resources/nested_4.yaml b/ballerina/tests/resources/nested_4.yaml new file mode 100644 index 0000000..bdd80cc --- /dev/null +++ b/ballerina/tests/resources/nested_4.yaml @@ -0,0 +1,10 @@ +records: + - name: "Record 1" + data: + key1: "value1" + key2: "value2" + key3: "value3" + - name: "Record 2" + data: + keyA: "valueA" + keyB: "valueB" diff --git a/ballerina/tests/resources/nested_5.yaml b/ballerina/tests/resources/nested_5.yaml new file mode 100644 index 0000000..6404089 --- /dev/null +++ b/ballerina/tests/resources/nested_5.yaml @@ -0,0 +1,12 @@ +title: "To Kill a Mockingbird" +author: + name: "Harper Lee" + birthdate: "1926-04-28" + hometown: "Monroeville, Alabama" + local: false +price: 10.5 +publisher: + name: "J. B. Lippincott & Co." + year: 1960 + location: "Philadelphia" + month: "4" diff --git a/ballerina/tests/resources/nested_6.yaml b/ballerina/tests/resources/nested_6.yaml new file mode 100644 index 0000000..c86eee4 --- /dev/null +++ b/ballerina/tests/resources/nested_6.yaml @@ -0,0 +1,24 @@ +bookings: + - event: "RockFest 2024" + date: "2024-07-15" + venue: "Central Park Stadium" + location: "New York City, NY" + seat: 5 + price: 75.00 + attendee: + name: "John Doe" + email: "john.doe@example.com" + phone: "555-555-5555" + notes: "Please arrive at least 30 minutes before the concert starts." + + - event: "PopJam 2024" + date: "2024-08-20" + venue: "Downtown Arena" + location: "Los Angeles, CA" + seat: 12 + price: 150.00 + attendee: + name: "Jane Smith" + email: "jane.smith@example.com" + phone: "555-123-4567" + notes: "Concert starts at 8:00 PM. No outside food or drinks allowed." diff --git a/ballerina/tests/resources/nested_7.yaml b/ballerina/tests/resources/nested_7.yaml new file mode 100644 index 0000000..f88ef3f --- /dev/null +++ b/ballerina/tests/resources/nested_7.yaml @@ -0,0 +1,11 @@ +matrix1: + - [1, 2, 3] + - [4, 5, 6] + - [7, 8, 9] + +matrix2: [[9, 8, 7], [5, 5, 4], [3, 2, 1]] + +matrix3: + - [5, 2, 7] + - [9, 4, 1] + - [3, 6, 8] diff --git a/ballerina/tests/resources/nested_8.yaml b/ballerina/tests/resources/nested_8.yaml new file mode 100644 index 0000000..37b9efe --- /dev/null +++ b/ballerina/tests/resources/nested_8.yaml @@ -0,0 +1,7 @@ +books: + - title: "The Great Gatsby" + author: "F. Scott Fitzgerald" + - title: "The Grapes of Wrath" + author: "John Steinbeck" + - title: "Binary Echoes: Unraveling the Digital Web" + author: "Alexandra Quinn" diff --git a/ballerina/tests/resources/nested_9.yaml b/ballerina/tests/resources/nested_9.yaml new file mode 100644 index 0000000..1376cea --- /dev/null +++ b/ballerina/tests/resources/nested_9.yaml @@ -0,0 +1,6 @@ +a: + c: "world" + d: "2" +b: + c: "world" + d: "2" diff --git a/ballerina/tests/resources/parser/tag_handle_1.yaml b/ballerina/tests/resources/parser/tag_handle_1.yaml new file mode 100644 index 0000000..9f466ff --- /dev/null +++ b/ballerina/tests/resources/parser/tag_handle_1.yaml @@ -0,0 +1,3 @@ +%TAG ! tag:example.com,2002: +--- +case: uri_scanner diff --git a/ballerina/tests/resources/parser/tag_handle_2.yaml b/ballerina/tests/resources/parser/tag_handle_2.yaml new file mode 100644 index 0000000..5a7826c --- /dev/null +++ b/ballerina/tests/resources/parser/tag_handle_2.yaml @@ -0,0 +1,3 @@ +%YAML 1.2 +--- +case: "yaml_version" diff --git a/ballerina/tests/resources/parser/tag_handle_3.yaml b/ballerina/tests/resources/parser/tag_handle_3.yaml new file mode 100644 index 0000000..a4a02a1 --- /dev/null +++ b/ballerina/tests/resources/parser/tag_handle_3.yaml @@ -0,0 +1 @@ +case: ! verbitam \ No newline at end of file diff --git a/ballerina/tests/resources/projection_options/product_list_response.yaml b/ballerina/tests/resources/projection_options/product_list_response.yaml new file mode 100644 index 0000000..ff1066a --- /dev/null +++ b/ballerina/tests/resources/projection_options/product_list_response.yaml @@ -0,0 +1,41 @@ +status: "success" +data: + products: + - id: 1 + name: "Laptop" + brand: "ExampleBrand" + price: 999.99 + description: "A powerful laptop for all your computing needs." + specifications: + processor: "Intel Core i7" + ram: "16GB DDR4" + storage: "512GB SSD" + display: "15.6-inch FHD" + graphics: "NVIDIA GeForce GTX 1650" + - id: 2 + name: "Smartphone" + brand: null + price: 699.99 + specifications: + display: "6.5-inch AMOLED" + camera: "Quad-camera setup" + storage: "256GB" + battery: "4000mAh" + os: null + - id: 3 + name: "Headphones" + brand: "AudioTech" + price: 149.99 + description: "Immerse yourself in high-quality sound with these headphones." + specifications: + type: "Over-ear" + wireless: true + battery_life: null + noise_cancellation: true + color: "Black" + - id: 4 + name: "Wireless Earbuds" + brand: "SoundMaster" + price: 99.99 + description: "Enjoy freedom of movement with these wireless earbuds." + specifications: null diff --git a/ballerina/tests/resources/projection_options/response.yaml b/ballerina/tests/resources/projection_options/response.yaml new file mode 100644 index 0000000..0b9b4ff --- /dev/null +++ b/ballerina/tests/resources/projection_options/response.yaml @@ -0,0 +1,20 @@ +{ + "status": "success", + "data": { + "user": { + "id": 123, + "username": "example_user" + }, + "posts": [ + { + "id": 1, + "title": "First Post", + "content": "This is the content of the first post." + }, + { + "id": 2, + "title": "Second Post" + } + ] + } +} diff --git a/ballerina/tests/resources/projection_options/sales.yaml b/ballerina/tests/resources/projection_options/sales.yaml new file mode 100644 index 0000000..82e9342 --- /dev/null +++ b/ballerina/tests/resources/projection_options/sales.yaml @@ -0,0 +1,26 @@ +sales_data: + - transaction_id: "TXN001" + date: "2024-03-25" + customer_name: "ABC Corporation" + product: "InnovateX" + quantity: 10 + unit_price: "$499" + total_price: null + - transaction_id: "TXN002" + date: "2024-03-25" + customer_name: "XYZ Enterprises" + product: "SecureTech" + quantity: 5 + unit_price: "$999" + total_price: "$4995" + - transaction_id: "TXN003" + date: "2024-03-26" + customer_name: "123 Inc." + product: "InnovateX" + quantity: 8 + unit_price: "$499" + total_price: null +total_sales: + date_range: null + total_transactions: 4 + total_revenue: "$21462" diff --git a/ballerina/tests/resources/simple_yaml_1a.yaml b/ballerina/tests/resources/simple_yaml_1a.yaml new file mode 100644 index 0000000..d055bee --- /dev/null +++ b/ballerina/tests/resources/simple_yaml_1a.yaml @@ -0,0 +1,6 @@ +name: "Jhon" +description: | + This is a multiline + string in YAML. + It preserves line breaks. +age: 30 diff --git a/ballerina/tests/resources/simple_yaml_1b.yaml b/ballerina/tests/resources/simple_yaml_1b.yaml new file mode 100644 index 0000000..e4c4558 --- /dev/null +++ b/ballerina/tests/resources/simple_yaml_1b.yaml @@ -0,0 +1,4 @@ +{ +name: "Jhon", +description: "This is a multiline\nstring in YAML.\nIt preserves line breaks.\n", +age: 30 } diff --git a/ballerina/tests/resources/simple_yaml_2a.yaml b/ballerina/tests/resources/simple_yaml_2a.yaml new file mode 100644 index 0000000..1b00c9c --- /dev/null +++ b/ballerina/tests/resources/simple_yaml_2a.yaml @@ -0,0 +1,4 @@ +- YAML +- TOML +- JSON +- XML diff --git a/ballerina/tests/resources/simple_yaml_2b.yaml b/ballerina/tests/resources/simple_yaml_2b.yaml new file mode 100644 index 0000000..75470fe --- /dev/null +++ b/ballerina/tests/resources/simple_yaml_2b.yaml @@ -0,0 +1,3 @@ +[YAML, TOML +,JSON +,XML] diff --git a/ballerina/tests/resources/streams/stream_1.yaml b/ballerina/tests/resources/streams/stream_1.yaml new file mode 100644 index 0000000..ab610ad --- /dev/null +++ b/ballerina/tests/resources/streams/stream_1.yaml @@ -0,0 +1,74 @@ +apiVersion: "v1" +kind: "Service" +metadata: + labels: + app: "ballerina_mongodb_kubernetes" + name: "ballerina-mongo" +spec: + ports: + - name: "port-1-ballerin" + port: 3005 + protocol: "TCP" + targetPort: 3005 + nodePort: 31781 + selector: + app: "ballerina_mongodb_kubernetes" + type: "NodePort" +--- +apiVersion: "v1" +kind: "ConfigMap" +metadata: + name: "ballerina-mongodb-configmap" +data: + Config.toml: "[ballerina.ballerina_mongodb_kubernetes]\nmongodbPort = 27017\n\ + mongodbHost = \"mongodb-service\"\ndbName = \"students\"\n\n[ballerina.log]\n\ + level = \"DEBUG\"\n" +--- +apiVersion: "v1" +kind: "Secret" +metadata: + name: "ballerina-mongodb-secret" +data: + Secret.toml: "W2xha3NoYW53ZWVyYXNpbmdoZS5iYWxsZXJpbmF" +--- +apiVersion: "apps/v1" +kind: "Deployment" +metadata: + labels: + app: "ballerina_mongodb_kubernetes" + name: "ballerina-mongo-deployment" +spec: + replicas: 1 + selector: + matchLabels: + app: "ballerina_mongodb_kubernetes" + template: + metadata: + labels: + app: "ballerina_mongodb_kubernetes" + spec: + containers: + - env: + - name: "BAL_CONFIG_FILES" + value: "/home/ballerina/conf/Config.toml:/home/ballerina/secrets/Secret.toml:" + image: "lw/students:v1.0.0" + imagePullPolicy: Never + name: "ballerina-mongo-deployment" + ports: + - containerPort: 3005 + name: "port-1-ballerin" + protocol: "TCP" + volumeMounts: + - mountPath: "/home/ballerina/secrets/" + name: "ballerina-mongodb-secret-volume" + readOnly: true + - mountPath: "/home/ballerina/conf/" + name: "ballerina-mongodb-configmap-volume" + readOnly: false + volumes: + - name: "ballerina-mongodb-secret-volume" + secret: + secretName: "ballerina-mongodb-secret" + - configMap: + name: "ballerina-mongodb-configmap" + name: "ballerina-mongodb-configmap-volume" diff --git a/ballerina/tests/resources/streams/stream_2.yaml b/ballerina/tests/resources/streams/stream_2.yaml new file mode 100644 index 0000000..a3d4bd8 --- /dev/null +++ b/ballerina/tests/resources/streams/stream_2.yaml @@ -0,0 +1,86 @@ +0 +--- +apiVersion: "v1" +kind: "Service" +metadata: + labels: + app: "ballerina_mongodb_kubernetes" + name: "ballerina-mongo" +spec: + ports: + - name: "port-1-ballerin" + port: 3005 + protocol: "TCP" + targetPort: 3005 + nodePort: 31781 + selector: + app: "ballerina_mongodb_kubernetes" + type: "NodePort" +--- +Sample String +--- +apiVersion: "v1" +kind: "ConfigMap" +metadata: + name: "ballerina-mongodb-configmap" +data: + Config.toml: "[ballerina.ballerina_mongodb_kubernetes]\nmongodbPort = 27017\n\ + mongodbHost = \"mongodb-service\"\ndbName = \"students\"\n\n[ballerina.log]\n\ + level = \"DEBUG\"\n" +--- +true +--- +apiVersion: "v1" +kind: "Secret" +metadata: + name: "ballerina-mongodb-secret" +data: + Secret.toml: "W2xha3NoYW53ZWVyYXNpbmdoZS5iYWxsZXJpbmF" +--- +12.2312 +--- +apiVersion: "apps/v1" +kind: "Deployment" +metadata: + labels: + app: "ballerina_mongodb_kubernetes" + name: "ballerina-mongo-deployment" +spec: + replicas: 1 + selector: + matchLabels: + app: "ballerina_mongodb_kubernetes" + template: + metadata: + labels: + app: "ballerina_mongodb_kubernetes" + spec: + containers: + - env: + - name: "BAL_CONFIG_FILES" + value: "/home/ballerina/conf/Config.toml:/home/ballerina/secrets/Secret.toml:" + image: "lw/students:v1.0.0" + imagePullPolicy: Never + name: "ballerina-mongo-deployment" + ports: + - containerPort: 3005 + name: "port-1-ballerin" + protocol: "TCP" + volumeMounts: + - mountPath: "/home/ballerina/secrets/" + name: "ballerina-mongodb-secret-volume" + readOnly: true + - mountPath: "/home/ballerina/conf/" + name: "ballerina-mongodb-configmap-volume" + readOnly: false + volumes: + - name: "ballerina-mongodb-secret-volume" + secret: + secretName: "ballerina-mongodb-secret" + - configMap: + name: "ballerina-mongodb-configmap" + name: "ballerina-mongodb-configmap-volume" +--- +1 +--- +2 diff --git a/ballerina/tests/resources/streams/stream_3.yaml b/ballerina/tests/resources/streams/stream_3.yaml new file mode 100644 index 0000000..5cef9be --- /dev/null +++ b/ballerina/tests/resources/streams/stream_3.yaml @@ -0,0 +1,32 @@ +p1: "T1_0" +p2: p2 +p3: p3 +--- +p1: "T1_1" +p2: p2 +p3: p3 +--- +p1: "T1_6" +--- +p1: "T1_7" +--- +p1: "T1_8" +p2: p2 +--- +p1: "T3_0" +p2: 123 +p3: true +--- +p1: "T3_1" +p2: 123 +p3: true +--- +p1: "T2_0" +p2: 123 +p3: string +--- +p1: "T3_2" +p2: 123 +p3: false +--- +p1: "T1_9" diff --git a/ballerina/tests/resources/tags/tag_resolution_test_1.yaml b/ballerina/tests/resources/tags/tag_resolution_test_1.yaml new file mode 100644 index 0000000..a56facb --- /dev/null +++ b/ballerina/tests/resources/tags/tag_resolution_test_1.yaml @@ -0,0 +1,29 @@ +# Define a server configuration +!!map +server: + # Hostname or IP address + host: !!str "192.168.1.100" + # Port number + port: !!int 8080 + # Environment (can be overridden by an environment variable) + environment: development + restart: !!bool true + start: !!float 0.12 + retry: !!bool false + +# Define logging configuration +logging: + # Log level (options: debug, info, warn, error) + level: info + # File path for log messages + file: !!null null + +# List of users with their roles +users: + - name: John Doe + roles: !!seq + - admin + - editor + - name: Jane Smith + roles: + - user diff --git a/ballerina/tests/resources/tags/tag_resolution_test_2.yaml b/ballerina/tests/resources/tags/tag_resolution_test_2.yaml new file mode 100644 index 0000000..44685f5 --- /dev/null +++ b/ballerina/tests/resources/tags/tag_resolution_test_2.yaml @@ -0,0 +1,33 @@ +# Define a server configuration +!!map +server: + # Hostname or IP address + host: !!str "192.168.1.100" + # Port number + ports: + - !!int 8000 + - !!int 0o21450 + - !!int 0x2329 + # Environment (can be overridden by an environment variable) + environment: development + restart: !!bool True + start: !!float 0.12 + end: !!float .inf + retry: !!bool FALSE + +# Define logging configuration +logging: + # Log level (options: debug, info, warn, error) + level: info + # File path for log messages + file: !!null ~ + +# List of users with their roles +users: + - name: John Doe + roles: !!seq + - admin + - editor + - name: Jane Smith + roles: + - user diff --git a/ballerina/tests/resources/to-yaml-string/test_1.yaml b/ballerina/tests/resources/to-yaml-string/test_1.yaml new file mode 100644 index 0000000..10b8e6b --- /dev/null +++ b/ballerina/tests/resources/to-yaml-string/test_1.yaml @@ -0,0 +1,34 @@ +library: + name: Central Library + location: + address: 123 Library St + city: Booktown + state: Knowledge + books: + - + title: The Great Gatsby + author: F. Scott Fitzgerald + genres: + - Classic + - Fiction + copiesAvailable: 3 + - + title: 1984 + author: George Orwell + genres: + - Dystopian + - Science Fiction + copiesAvailable: 5 + staff: + - + name: Jane Doe + position: Librarian + contact: + email: jane.doe@library.com + phone: 555-1234 + - + name: John Smith + position: Assistant Librarian + contact: + email: john.smith@library.com + phone: 555-5678 \ No newline at end of file diff --git a/ballerina/tests/resources/to-yaml-string/test_2.yaml b/ballerina/tests/resources/to-yaml-string/test_2.yaml new file mode 100644 index 0000000..ae81a99 --- /dev/null +++ b/ballerina/tests/resources/to-yaml-string/test_2.yaml @@ -0,0 +1 @@ +{library: {name: Central Library, location: {address: 123 Library St, city: Booktown, state: Knowledge}, books: [{title: The Great Gatsby, author: F. Scott Fitzgerald, genres: [Classic, Fiction], copiesAvailable: 3}, {title: 1984, author: George Orwell, genres: [Dystopian, Science Fiction], copiesAvailable: 5}], staff: [{name: Jane Doe, position: Librarian, contact: {email: jane.doe@library.com, phone: 555-1234}}, {name: John Smith, position: Assistant Librarian, contact: {email: john.smith@library.com, phone: 555-5678}}]}} \ No newline at end of file diff --git a/ballerina/tests/resources/to-yaml-string/test_3.yaml b/ballerina/tests/resources/to-yaml-string/test_3.yaml new file mode 100644 index 0000000..ee9bb79 --- /dev/null +++ b/ballerina/tests/resources/to-yaml-string/test_3.yaml @@ -0,0 +1,34 @@ +"library": + "name": "Central Library" + "location": + "address": "123 Library St" + "city": "Booktown" + "state": "Knowledge" + "books": + - + "title": "The Great Gatsby" + "author": "F. Scott Fitzgerald" + "genres": + - "Classic" + - "Fiction" + "copiesAvailable": "3" + - + "title": "1984" + "author": "George Orwell" + "genres": + - "Dystopian" + - "Science Fiction" + "copiesAvailable": "5" + "staff": + - + "name": "Jane Doe" + "position": "Librarian" + "contact": + "email": "jane.doe@library.com" + "phone": "555-1234" + - + "name": "John Smith" + "position": "Assistant Librarian" + "contact": + "email": "john.smith@library.com" + "phone": "555-5678" \ No newline at end of file diff --git a/ballerina/tests/resources/to-yaml-string/test_4.yaml b/ballerina/tests/resources/to-yaml-string/test_4.yaml new file mode 100644 index 0000000..9ca657f --- /dev/null +++ b/ballerina/tests/resources/to-yaml-string/test_4.yaml @@ -0,0 +1,34 @@ +'library': + 'name': 'Central Library' + 'location': + 'address': '123 Library St' + 'city': 'Booktown' + 'state': 'Knowledge' + 'books': + - + 'title': 'The Great Gatsby' + 'author': 'F. Scott Fitzgerald' + 'genres': + - 'Classic' + - 'Fiction' + 'copiesAvailable': '3' + - + 'title': '1984' + 'author': 'George Orwell' + 'genres': + - 'Dystopian' + - 'Science Fiction' + 'copiesAvailable': '5' + 'staff': + - + 'name': 'Jane Doe' + 'position': 'Librarian' + 'contact': + 'email': 'jane.doe@library.com' + 'phone': '555-1234' + - + 'name': 'John Smith' + 'position': 'Assistant Librarian' + 'contact': + 'email': 'john.smith@library.com' + 'phone': '555-5678' \ No newline at end of file diff --git a/ballerina/tests/resources/to-yaml-string/test_5.yaml b/ballerina/tests/resources/to-yaml-string/test_5.yaml new file mode 100644 index 0000000..fbe4065 --- /dev/null +++ b/ballerina/tests/resources/to-yaml-string/test_5.yaml @@ -0,0 +1,34 @@ +!!str library: + !!str name: !!str Central Library + !!str location: + !!str address: !!str 123 Library St + !!str city: !!str Booktown + !!str state: !!str Knowledge + !!str books: !!seq + - + !!str title: !!str The Great Gatsby + !!str author: !!str F. Scott Fitzgerald + !!str genres: !!seq + - !!str Classic + - !!str Fiction + !!str copiesAvailable: !!str 3 + - + !!str title: !!str 1984 + !!str author: !!str George Orwell + !!str genres: !!seq + - !!str Dystopian + - !!str Science Fiction + !!str copiesAvailable: !!str 5 + !!str staff: !!seq + - + !!str name: !!str Jane Doe + !!str position: !!str Librarian + !!str contact: + !!str email: !!str jane.doe@library.com + !!str phone: !!str 555-1234 + - + !!str name: !!str John Smith + !!str position: !!str Assistant Librarian + !!str contact: + !!str email: !!str john.smith@library.com + !!str phone: !!str 555-5678 \ No newline at end of file diff --git a/ballerina/tests/resources/to-yaml-string/test_6.yaml b/ballerina/tests/resources/to-yaml-string/test_6.yaml new file mode 100644 index 0000000..8dd6d93 --- /dev/null +++ b/ballerina/tests/resources/to-yaml-string/test_6.yaml @@ -0,0 +1,34 @@ +!!str "library": + !!str "name": !!str "Central Library" + !!str "location": + !!str "address": !!str "123 Library St" + !!str "city": !!str "Booktown" + !!str "state": !!str "Knowledge" + !!str "books": !!seq + - + !!str "title": !!str "The Great Gatsby" + !!str "author": !!str "F. Scott Fitzgerald" + !!str "genres": !!seq + - !!str "Classic" + - !!str "Fiction" + !!str "copiesAvailable": !!str "3" + - + !!str "title": !!str "1984" + !!str "author": !!str "George Orwell" + !!str "genres": !!seq + - !!str "Dystopian" + - !!str "Science Fiction" + !!str "copiesAvailable": !!str "5" + !!str "staff": !!seq + - + !!str "name": !!str "Jane Doe" + !!str "position": !!str "Librarian" + !!str "contact": + !!str "email": !!str "jane.doe@library.com" + !!str "phone": !!str "555-1234" + - + !!str "name": !!str "John Smith" + !!str "position": !!str "Assistant Librarian" + !!str "contact": + !!str "email": !!str "john.smith@library.com" + !!str "phone": !!str "555-5678" \ No newline at end of file diff --git a/ballerina/tests/resources/to-yaml-string/test_7.yaml b/ballerina/tests/resources/to-yaml-string/test_7.yaml new file mode 100644 index 0000000..469db47 --- /dev/null +++ b/ballerina/tests/resources/to-yaml-string/test_7.yaml @@ -0,0 +1 @@ +[{title: The Great Gatsby, author: F. Scott Fitzgerald, genres: [Classic, Fiction], copiesAvailable: 3}, {title: 1984, author: George Orwell, genres: [Dystopian, Science Fiction], copiesAvailable: 5}, {title: Dune, author: Frank Herbert, genres: [Science Fiction, Adventure], yearPublished: 1965, isAvailableInEbook: true}, {title: And Then There Were None, author: Agatha Christie, genres: [Mystery, Thriller], firstPublished: 1939, isPartOfSeries: true}] \ No newline at end of file diff --git a/ballerina/tests/resources/to-yaml-string/test_8.yaml b/ballerina/tests/resources/to-yaml-string/test_8.yaml new file mode 100644 index 0000000..38ff0e9 --- /dev/null +++ b/ballerina/tests/resources/to-yaml-string/test_8.yaml @@ -0,0 +1,30 @@ +- + title: The Great Gatsby + author: F. Scott Fitzgerald + genres: + - Classic + - Fiction + copiesAvailable: 3 +- + title: 1984 + author: George Orwell + genres: + - Dystopian + - Science Fiction + copiesAvailable: 5 +- + title: Dune + author: Frank Herbert + genres: + - Science Fiction + - Adventure + yearPublished: 1965 + isAvailableInEbook: true +- + title: And Then There Were None + author: Agatha Christie + genres: + - Mystery + - Thriller + firstPublished: 1939 + isPartOfSeries: true \ No newline at end of file diff --git a/ballerina/tests/resources/to-yaml-string/test_9.yaml b/ballerina/tests/resources/to-yaml-string/test_9.yaml new file mode 100644 index 0000000..acdcdc4 --- /dev/null +++ b/ballerina/tests/resources/to-yaml-string/test_9.yaml @@ -0,0 +1,29 @@ +title: The Great Gatsby +author: F. Scott Fitzgerald +genres: +- Classic +- Fiction +copiesAvailable: 3 +--- +title: 1984 +author: George Orwell +genres: +- Dystopian +- Science Fiction +copiesAvailable: 5 +--- +title: Dune +author: Frank Herbert +genres: +- Science Fiction +- Adventure +yearPublished: 1965 +isAvailableInEbook: true +--- +title: And Then There Were None +author: Agatha Christie +genres: +- Mystery +- Thriller +firstPublished: 1939 +isPartOfSeries: true \ No newline at end of file diff --git a/ballerina/tests/test_to_yaml_string.bal b/ballerina/tests/test_to_yaml_string.bal new file mode 100644 index 0000000..b73e3ee --- /dev/null +++ b/ballerina/tests/test_to_yaml_string.bal @@ -0,0 +1,120 @@ +// 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 +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/io; +import ballerina/test; + +const TO_YAML_STRING_DATA = FILE_PATH + "to-yaml-string/"; + +isolated function testToYamlString() returns error? { + string expectedResultPath = TO_YAML_STRING_DATA + "test_1.yaml"; + string value = check toYamlString(j1); + string expectedValue = check io:fileReadString(expectedResultPath); + test:assertEquals(value, expectedValue); +} + +@test:Config { + dataProvider: dataToConvertAnydataValuesToYamlString +} +isolated function testToYamlString1(anydata inputValue, string expectedFile, WriteConfig conf) returns error? { + string expectedResultPath = TO_YAML_STRING_DATA + expectedFile; + string value = check toYamlString(inputValue, conf); + string expectedValue = check io:fileReadString(expectedResultPath); + test:assertEquals(value, expectedValue); +} + +function dataToConvertAnydataValuesToYamlString() returns [anydata, string, WriteConfig][] => [ + [j1, "test_2.yaml", {flowStyle: true}], + [j1, "test_3.yaml", {forceQuotes: true, useSingleQuotes: false}], + [j1, "test_4.yaml", {forceQuotes: true, useSingleQuotes: true}], + [j1, "test_5.yaml", {canonical: true}], + [j1, "test_6.yaml", {canonical: true, forceQuotes: true}], + [j2, "test_7.yaml", {flowStyle: true}], + [j2, "test_8.yaml", {}], + [j2, "test_9.yaml", {isStream: true}] +]; + +final json & readonly j1 = { + "library": { + "name": "Central Library", + "location": { + "address": "123 Library St", + "city": "Booktown", + "state": "Knowledge" + }, + "books": [ + { + "title": "The Great Gatsby", + "author": "F. Scott Fitzgerald", + "genres": ["Classic", "Fiction"], + "copiesAvailable": 3 + }, + { + "title": "1984", + "author": "George Orwell", + "genres": ["Dystopian", "Science Fiction"], + "copiesAvailable": 5 + } + ], + "staff": [ + { + "name": "Jane Doe", + "position": "Librarian", + "contact": { + "email": "jane.doe@library.com", + "phone": "555-1234" + } + }, + { + "name": "John Smith", + "position": "Assistant Librarian", + "contact": { + "email": "john.smith@library.com", + "phone": "555-5678" + } + } + ] + } +}; + +final json & readonly j2 = [ + { + "title": "The Great Gatsby", + "author": "F. Scott Fitzgerald", + "genres": ["Classic", "Fiction"], + "copiesAvailable": 3 + }, + { + "title": "1984", + "author": "George Orwell", + "genres": ["Dystopian", "Science Fiction"], + "copiesAvailable": 5 + }, + { + "title": "Dune", + "author": "Frank Herbert", + "genres": ["Science Fiction", "Adventure"], + "yearPublished": 1965, + "isAvailableInEbook": true + }, + { + "title": "And Then There Were None", + "author": "Agatha Christie", + "genres": ["Mystery", "Thriller"], + "firstPublished": 1939, + "isPartOfSeries": true + } +]; diff --git a/ballerina/tests/types.bal b/ballerina/tests/types.bal new file mode 100644 index 0000000..545c6b0 --- /dev/null +++ b/ballerina/tests/types.bal @@ -0,0 +1,96 @@ +// 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 +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +type OpenRecord record {}; + +type SimpleYaml record {| + string name; + int age; + string description; +|}; + +type Employee record {| + string name; + int age; + string department; + Project[] projects; +|}; + +type Project record {| + string name; + string status; +|}; + +type BookCover record {| + string title; + int pages; + string[] authors; + float price; +|}; + +type RecordWithMapType record {| + string name; + map data; +|}; + +type Author record {| + string name; + string birthdate; + string hometown; + boolean...; +|}; + +type Publisher record {| + string name; + int year; + string...; +|}; + +type Book record {| + string title; + Author author; + Publisher publisher; + float...; +|}; + +type TicketBooking record {| + string event; + float price; + record {|string...;|} attendee; +|}; + +type LibraryB record { + [BookA, BookA] books; +}; + +type LibraryC record {| + [BookA, BookA...] books; +|}; + + +type BookA record {| + string title; + string author; +|}; + +type Singleton1 1; + +type SingletonUnion Singleton1|2|"3"; + +type SingletonInRecord record {| + Singleton1 value; + SingletonUnion id; +|}; diff --git a/ballerina/yaml_api.bal b/ballerina/yaml_api.bal index 8b9513c..d686ce6 100644 --- a/ballerina/yaml_api.bal +++ b/ballerina/yaml_api.bal @@ -16,16 +16,49 @@ import ballerina/jballerina.java; -# Converts YAML string, byte[] or byte-block-stream to subtype of anydata. +# Converts YAML string to subtype of anydata. # -# + s - Source YAML string value or byte[] or byte-block-stream +# + s - Source string value # + options - Options to be used for filtering in the projection # + t - Target type # + return - On success, returns the given target type value, else returns an `yaml:Error` -public isolated function fromYamlStringWithType(string|byte[]|stream s, +public isolated function parseString(string s, Options options = {}, typedesc t = <>) - returns t|Error = @java:Method {'class: "io.ballerina.stdlib.data.yaml.Native"} external; - + returns t|Error = @java:Method {'class: "io.ballerina.lib.data.yaml.Native"} external; + +# Converts YAML byte[] to subtype of anydata. +# +# + s - Source byte[] value +# + options - Options to be used for filtering in the projection +# + t - Target type +# + return - On success, returns the given target type value, else returns an `yaml:Error` +public isolated function parseBytes(byte[] s, + Options options = {}, typedesc t = <>) + returns t|Error = @java:Method {'class: "io.ballerina.lib.data.yaml.Native"} external; + +# Converts YAML byte-block-stream to subtype of anydata. +# +# + s - Source byte-block-stream value +# + options - Options to be used for filtering in the projection +# + t - Target type +# + return - On success, returns the given target type value, else returns an `yaml:Error` +public isolated function parseStream(stream s, + Options options = {}, typedesc t = <>) + returns t|Error = @java:Method {'class: "io.ballerina.lib.data.yaml.Native"} external; + +# Converts anydata YAML value to a string. +# +# + yamlValue - Input yaml value +# + config - Options used to get desired toString representation +# + return - On success, returns to string value, else returns an `yaml:Error` +public isolated function toYamlString(anydata yamlValue, WriteConfig config = {}) returns string|Error { + string[] lines = check toYamlStringArray(yamlValue, config); + return "\n".'join(...lines); +} + +isolated function toYamlStringArray(anydata yamlValue, WriteConfig config = {}) + returns string[]|Error = @java:Method {'class: "io.ballerina.lib.data.yaml.Native"} external; + # Represents the YAML schema available for the parser. # # + FAILSAFE_SCHEMA - Generic schema that works for any YAML document @@ -42,12 +75,52 @@ public enum YAMLSchema { # + schema - field description # + allowAnchorRedefinition - field description # + allowMapEntryRedefinition - field description -public type Options record { +# + allowDataProjection - Enable or disable projection +public type Options record {| YAMLSchema schema = CORE_SCHEMA; boolean allowAnchorRedefinition = true; boolean allowMapEntryRedefinition = false; -}; + record { + # If `true`, nil values will be considered as optional fields in the projection. + boolean nilAsOptionalField = false; + # If `true`, absent fields will be considered as nilable types in the projection. + boolean absentAsNilableType = false; + # If `true`, top level tuple ordering considered strictly. + boolean strictTupleOrder = false; + }|false allowDataProjection = {}; +|}; + +# Configurations for writing a YAML document. +# +# + indentationPolicy - Number of whitespace for an indentation +# + blockLevel - The maximum depth level for a block collection +# + canonical - If set, the tags are written along with the nodes +# + useSingleQuotes - If set, single quotes are used to surround scalars +# + forceQuotes - If set, all the scalars are surrounded by quotes +# + schema - YAML schema used for writing +# + isStream - If set, the parser will write a stream of YAML documents +# + flowStyle - If set, mappings and sequences will output in flow style +public type WriteConfig record {| + int indentationPolicy = 2; + int blockLevel = 1; + boolean canonical = false; + boolean useSingleQuotes = false; + boolean forceQuotes = false; + YAMLSchema schema = CORE_SCHEMA; + boolean isStream = false; + boolean flowStyle = false; +|}; # Represents the error type of the ballerina/data.yaml module. This error type represents any error that can occur # during the execution of data.yaml APIs. public type Error distinct error; + +# Defines the name of the JSON Object key. +# +# + value - The name of the JSON Object key +public type NameConfig record {| + string value; +|}; + +# The annotation is used to overwrite the existing record field name. +public const annotation NameConfig Name on record field; diff --git a/build-config/resources/Ballerina.toml b/build-config/resources/Ballerina.toml index f55ce72..858125b 100644 --- a/build-config/resources/Ballerina.toml +++ b/build-config/resources/Ballerina.toml @@ -6,13 +6,13 @@ authors = ["Ballerina"] keywords = ["yaml"] repository = "https://github.com/ballerina-platform/module-ballerina-data.yaml" license = ["Apache-2.0"] -distribution = "2201.8.1" +distribution = "2201.9.0" [platform.java17] graalvmCompatible = true [[platform.java17.dependency]] -groupId = "io.ballerina.stdlib" +groupId = "io.ballerina.lib" artifactId = "yaml-native" version = "@toml.version@" path = "../native/build/libs/data.yaml-native-@project.version@.jar" diff --git a/build-config/resources/CompilerPlugin.toml b/build-config/resources/CompilerPlugin.toml new file mode 100644 index 0000000..f6ae028 --- /dev/null +++ b/build-config/resources/CompilerPlugin.toml @@ -0,0 +1,6 @@ +[plugin] +id = "constraint-compiler-plugin" +class = "io.ballerina.lib.data.yaml.compiler.YamlDataCompilerPlugin" + +[[dependency]] +path = "../compiler-plugin/build/libs/data.yaml-compiler-plugin-@project.version@.jar" diff --git a/compiler-plugin-test/build.gradle b/compiler-plugin-test/build.gradle new file mode 100644 index 0000000..4cf6c41 --- /dev/null +++ b/compiler-plugin-test/build.gradle @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +plugins { + id 'java' + id 'checkstyle' + id 'com.github.spotbugs' +} + +description = 'Ballerina - YAML Data Compiler Plugin Tests' + +dependencies { + checkstyle project(':checkstyle') + checkstyle "com.puppycrawl.tools:checkstyle:${puppycrawlCheckstyleVersion}" + + implementation project(':data.yaml-compiler-plugin') + + testImplementation group: 'org.ballerinalang', name: 'ballerina-lang', version: "${ballerinaLangVersion}" + testImplementation group: 'org.ballerinalang', name: 'ballerina-tools-api', version: "${ballerinaLangVersion}" + testImplementation group: 'org.ballerinalang', name: 'ballerina-parser', version: "${ballerinaLangVersion}" + testImplementation group: 'org.testng', name: 'testng', version: "${testngVersion}" +} + +tasks.withType(Checkstyle) { + exclude '**/module-info.java' +} + +checkstyle { + toolVersion "${project.puppycrawlCheckstyleVersion}" + configFile rootProject.file("build-config/checkstyle/build/checkstyle.xml") + configProperties = ["suppressionFile" : file("${rootDir}/build-config/checkstyle/build/suppressions.xml")] +} + +checkstyleTest.dependsOn(":checkstyle:downloadCheckstyleRuleFiles") + +spotbugsTest { + effort "max" + reportLevel "low" + reportsDir = file("$project.buildDir/reports/spotbugs") + reports { + html.enabled true + text.enabled = true + } + def excludeFile = file("${project.projectDir}/spotbugs-exclude.xml") + if(excludeFile.exists()) { + excludeFilter = excludeFile + } +} + +spotbugsMain { + enabled false +} + +checkstyleMain { + enabled false +} + +compileJava { + doFirst { + options.compilerArgs = [ + '--module-path', classpath.asPath, + ] + classpath = files() + } +} + +test { + systemProperty "ballerina.offline.flag", "true" + useTestNG() + finalizedBy jacocoTestReport + + testLogging { + exceptionFormat = "full" + afterSuite { desc, result -> + if (!desc.parent) { // will match the outermost suite + def output = "Compiler Plugin Tests: ${result.resultType} (${result.testCount} tests, " + + "${result.successfulTestCount} successes, ${result.failedTestCount} " + + "failures, ${result.skippedTestCount} skipped)" + def startItem = '| ', endItem = ' |' + def repeatLength = startItem.length() + output.length() + endItem.length() + println('\n' + ('-' * repeatLength) + '\n' + startItem + output + endItem + '\n' + ('-' * repeatLength)) + } + } + } +} + +jacocoTestReport { + dependsOn test + reports { + xml.required = true + } + sourceSets project(':data.yaml-compiler-plugin').sourceSets.main +} + +test.dependsOn ":data.yaml-ballerina:build" diff --git a/compiler-plugin-test/src/test/java/io/ballerina/lib/data/yaml/compiler/CompilerPluginTest.java b/compiler-plugin-test/src/test/java/io/ballerina/lib/data/yaml/compiler/CompilerPluginTest.java new file mode 100644 index 0000000..2da3b37 --- /dev/null +++ b/compiler-plugin-test/src/test/java/io/ballerina/lib/data/yaml/compiler/CompilerPluginTest.java @@ -0,0 +1,149 @@ +/* + * 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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.lib.data.yaml.compiler; + +import io.ballerina.projects.DiagnosticResult; +import io.ballerina.tools.diagnostics.Diagnostic; +import io.ballerina.tools.diagnostics.DiagnosticSeverity; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.List; + +/** + * This class includes tests for Ballerina YAML Data compiler plugin. + */ +public class CompilerPluginTest { + + static final String UNSUPPORTED_TYPE = "unsupported type: type is not supported"; + + @Test + public void testInvalidExpectedUnionType1() { + DiagnosticResult diagnosticResult = + CompilerPluginTestUtils.loadPackage("sample_package_1").getCompilation().diagnosticResult(); + List errorDiagnosticsList = diagnosticResult.diagnostics().stream() + .filter(r -> r.diagnosticInfo().severity().equals(DiagnosticSeverity.ERROR)).toList(); + Assert.assertEquals(errorDiagnosticsList.size(), 1); + Assert.assertEquals(errorDiagnosticsList.get(0).diagnosticInfo().messageFormat(), UNSUPPORTED_TYPE); + } + + @Test + public void testInvalidExpectedUnionType2() { + DiagnosticResult diagnosticResult = + CompilerPluginTestUtils.loadPackage("sample_package_2").getCompilation().diagnosticResult(); + List errorDiagnosticsList = diagnosticResult.diagnostics().stream() + .filter(r -> r.diagnosticInfo().severity().equals(DiagnosticSeverity.ERROR)).toList(); + Assert.assertEquals(errorDiagnosticsList.size(), 1); + Assert.assertEquals(errorDiagnosticsList.get(0).diagnosticInfo().messageFormat(), UNSUPPORTED_TYPE); + } + + @Test + public void testInvalidRecordFieldType1() { + DiagnosticResult diagnosticResult = + CompilerPluginTestUtils.loadPackage("sample_package_3").getCompilation().diagnosticResult(); + List errorDiagnosticsList = diagnosticResult.diagnostics().stream() + .filter(r -> r.diagnosticInfo().severity().equals(DiagnosticSeverity.ERROR)).toList(); + Assert.assertEquals(errorDiagnosticsList.size(), 2); + Assert.assertEquals(errorDiagnosticsList.get(0).diagnosticInfo().messageFormat(), UNSUPPORTED_TYPE); + Assert.assertEquals(errorDiagnosticsList.get(0).diagnosticInfo().messageFormat(), UNSUPPORTED_TYPE); + } + + @Test + public void testInvalidRecordFieldType2() { + DiagnosticResult diagnosticResult = + CompilerPluginTestUtils.loadPackage("sample_package_4").getCompilation().diagnosticResult(); + List errorDiagnosticsList = diagnosticResult.diagnostics().stream() + .filter(r -> r.diagnosticInfo().severity().equals(DiagnosticSeverity.ERROR)).toList(); + Assert.assertEquals(errorDiagnosticsList.size(), 2); + Assert.assertEquals(errorDiagnosticsList.get(0).diagnosticInfo().messageFormat(), UNSUPPORTED_TYPE); + Assert.assertEquals(errorDiagnosticsList.get(0).diagnosticInfo().messageFormat(), UNSUPPORTED_TYPE); + } + + @Test + public void testDuplicateField1() { + DiagnosticResult diagnosticResult = + CompilerPluginTestUtils.loadPackage("sample_package_5").getCompilation().diagnosticResult(); + List errorDiagnosticsList = diagnosticResult.diagnostics().stream() + .filter(r -> r.diagnosticInfo().severity().equals(DiagnosticSeverity.ERROR)).toList(); + Assert.assertEquals(errorDiagnosticsList.size(), 1); + Assert.assertEquals(errorDiagnosticsList.get(0).diagnosticInfo().messageFormat(), + "invalid field: duplicate field found"); + } + + @Test + public void testDuplicateField2() { + DiagnosticResult diagnosticResult = + CompilerPluginTestUtils.loadPackage("sample_package_6").getCompilation().diagnosticResult(); + List errorDiagnosticsList = diagnosticResult.diagnostics().stream() + .filter(r -> r.diagnosticInfo().severity().equals(DiagnosticSeverity.ERROR)).toList(); + Assert.assertEquals(errorDiagnosticsList.size(), 2); + Assert.assertEquals(errorDiagnosticsList.get(0).diagnosticInfo().messageFormat(), + "invalid field: duplicate field found"); + Assert.assertEquals(errorDiagnosticsList.get(1).diagnosticInfo().messageFormat(), + "invalid field: duplicate field found"); + } + + @Test + public void testDuplicateFieldInUnion() { + DiagnosticResult diagnosticResult = + CompilerPluginTestUtils.loadPackage("sample_package_9").getCompilation().diagnosticResult(); + List errorDiagnosticsList = diagnosticResult.diagnostics().stream() + .filter(r -> r.diagnosticInfo().severity().equals(DiagnosticSeverity.ERROR)).toList(); + Assert.assertEquals(errorDiagnosticsList.size(), 2); + Assert.assertEquals(errorDiagnosticsList.get(0).diagnosticInfo().messageFormat(), + "invalid field: duplicate field found"); + Assert.assertEquals(errorDiagnosticsList.get(1).diagnosticInfo().messageFormat(), + "invalid field: duplicate field found"); + } + + @Test + public void testComplexUnionTypeAsExpectedType() { + DiagnosticResult diagnosticResult = + CompilerPluginTestUtils.loadPackage("sample_package_7").getCompilation().diagnosticResult(); + List errorDiagnosticsList = diagnosticResult.diagnostics().stream() + .filter(r -> r.diagnosticInfo().severity().equals(DiagnosticSeverity.ERROR)).toList(); + Assert.assertEquals(errorDiagnosticsList.size(), 2); + Assert.assertEquals(errorDiagnosticsList.get(0).diagnosticInfo().messageFormat(), UNSUPPORTED_TYPE); + Assert.assertEquals(errorDiagnosticsList.get(1).diagnosticInfo().messageFormat(), UNSUPPORTED_TYPE); + } + + @Test + public void testComplexUnionTypeAsMemberOfIntersection() { + DiagnosticResult diagnosticResult = + CompilerPluginTestUtils.loadPackage("sample_package_8").getCompilation().diagnosticResult(); + List errorDiagnosticsList = diagnosticResult.diagnostics().stream() + .filter(r -> r.diagnosticInfo().severity().equals(DiagnosticSeverity.ERROR)).toList(); + Assert.assertEquals(errorDiagnosticsList.size(), 1); + Assert.assertEquals(errorDiagnosticsList.get(0).diagnosticInfo().messageFormat(), UNSUPPORTED_TYPE); + } + + @Test + public void testComplexUnionTypeWithUnsupportedTypeAndDuplicateFields() { + DiagnosticResult diagnosticResult = + CompilerPluginTestUtils.loadPackage("sample_package_10").getCompilation().diagnosticResult(); + List errorDiagnosticsList = diagnosticResult.diagnostics().stream() + .filter(r -> r.diagnosticInfo().severity().equals(DiagnosticSeverity.ERROR)).toList(); + Assert.assertEquals(errorDiagnosticsList.size(), 3); + 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(), UNSUPPORTED_TYPE); + } +} diff --git a/compiler-plugin-test/src/test/java/io/ballerina/lib/data/yaml/compiler/CompilerPluginTestUtils.java b/compiler-plugin-test/src/test/java/io/ballerina/lib/data/yaml/compiler/CompilerPluginTestUtils.java new file mode 100644 index 0000000..ec10070 --- /dev/null +++ b/compiler-plugin-test/src/test/java/io/ballerina/lib/data/yaml/compiler/CompilerPluginTestUtils.java @@ -0,0 +1,46 @@ +/* + * 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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.lib.data.yaml.compiler; + +import io.ballerina.projects.Package; +import io.ballerina.projects.ProjectEnvironmentBuilder; +import io.ballerina.projects.directory.BuildProject; +import io.ballerina.projects.environment.Environment; +import io.ballerina.projects.environment.EnvironmentBuilder; + +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * Utility functions related to compiler plugins tests. + */ +public class CompilerPluginTestUtils { + private static final Path RESOURCE_DIRECTORY = Paths.get("src", "test", "resources", "ballerina_sources") + .toAbsolutePath(); + private static final Path DISTRIBUTION_PATH = Paths.get("../", "target", "ballerina-runtime") + .toAbsolutePath(); + + static Package loadPackage(String path) { + Path projectDirPath = RESOURCE_DIRECTORY.resolve(path); + Environment environment = EnvironmentBuilder.getBuilder().setBallerinaHome(DISTRIBUTION_PATH).build(); + ProjectEnvironmentBuilder projectEnvironmentBuilder = ProjectEnvironmentBuilder.getBuilder(environment); + BuildProject project = BuildProject.load(projectEnvironmentBuilder, projectDirPath); + return project.currentPackage(); + } +} diff --git a/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_1/Ballerina.toml b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_1/Ballerina.toml new file mode 100644 index 0000000..035fe72 --- /dev/null +++ b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_1/Ballerina.toml @@ -0,0 +1,4 @@ +[package] +org = "yamldata_test" +name = "sample_1" +version = "0.1.0" diff --git a/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_1/sample.bal b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_1/sample.bal new file mode 100644 index 0000000..6e12520 --- /dev/null +++ b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_1/sample.bal @@ -0,0 +1,21 @@ +// 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 +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/data.yaml; + +public function main() returns error? { + int|table|record {| int b;|} val = check yaml:parseString("1"); +} diff --git a/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_10/Ballerina.toml b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_10/Ballerina.toml new file mode 100644 index 0000000..d6f8340 --- /dev/null +++ b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_10/Ballerina.toml @@ -0,0 +1,4 @@ +[package] +org = "yamldata_test" +name = "sample_10" +version = "0.1.0" diff --git a/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_10/sample.bal b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_10/sample.bal new file mode 100644 index 0000000..67e0098 --- /dev/null +++ b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_10/sample.bal @@ -0,0 +1,49 @@ +// 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 +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/data.yaml; + +type ArrayExpectedType record {| + @yaml:Name { + value: "first_name" + } + string firstName; + + @yaml:Name { + value: "first_name" + } + string lastName; +|}[]; + +type TupleExpectedType [record {| + @yaml:Name { + value: "first_name" + } + string firstName; + + @yaml:Name { + value: "first_name" + } + string lastName; +|}]; + +type ImmutableType readonly & ArrayExpectedType; + +type UnionType ArrayExpectedType|TupleExpectedType|ImmutableType|xml|table; + +function call(string data) returns error? { + UnionType result = check yaml:parseString(data); +} diff --git a/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_2/Ballerina.toml b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_2/Ballerina.toml new file mode 100644 index 0000000..a8f8cf2 --- /dev/null +++ b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_2/Ballerina.toml @@ -0,0 +1,4 @@ +[package] +org = "yamldata_test" +name = "sample_2" +version = "0.1.0" diff --git a/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_2/sample.bal b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_2/sample.bal new file mode 100644 index 0000000..bb86a79 --- /dev/null +++ b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_2/sample.bal @@ -0,0 +1,23 @@ +// 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 +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/data.yaml as yml; + +type Union int|table|record {| int b;|}; + +public function main() returns error? { + Union val = check yml:parseString("1"); +} diff --git a/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_3/Ballerina.toml b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_3/Ballerina.toml new file mode 100644 index 0000000..4bdcfad --- /dev/null +++ b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_3/Ballerina.toml @@ -0,0 +1,4 @@ +[package] +org = "yamldata_test" +name = "sample_3" +version = "0.1.0" diff --git a/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_3/sample.bal b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_3/sample.bal new file mode 100644 index 0000000..11e80a0 --- /dev/null +++ b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_3/sample.bal @@ -0,0 +1,38 @@ +// 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 +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/data.yaml; + +type Person record {| + string? name; + table|map address; + xml|json company; +|}; + +public function main() returns error? { + string str = string `{ + "name": "John", + "address": { + "street": "Main Street", + "country": "USA" + }, + "company": { + "street": "Main Street", + "country": "USA" + } + }`; + Person _ = check yaml:parseString(str); +} diff --git a/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_4/Ballerina.toml b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_4/Ballerina.toml new file mode 100644 index 0000000..78d5620 --- /dev/null +++ b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_4/Ballerina.toml @@ -0,0 +1,4 @@ +[package] +org = "yamldata_test" +name = "sample_4" +version = "0.1.0" diff --git a/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_4/sample.bal b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_4/sample.bal new file mode 100644 index 0000000..eb152c6 --- /dev/null +++ b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_4/sample.bal @@ -0,0 +1,36 @@ +// 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 +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/data.yaml; + +type Person record {| + string? name; + table|map address; + xml|json company; +|}; + +string str = string `{ + "name": "John", + "address": { + "street": "Main Street", + "country": "USA" + }, + "company": { + "street": "Main Street", + "country": "USA" + } + }`; +Person _ = check yaml:parseString(str); diff --git a/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_5/Ballerina.toml b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_5/Ballerina.toml new file mode 100644 index 0000000..351cefc --- /dev/null +++ b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_5/Ballerina.toml @@ -0,0 +1,4 @@ +[package] +org = "yamldata_test" +name = "sample_5" +version = "0.1.0" diff --git a/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_5/sample.bal b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_5/sample.bal new file mode 100644 index 0000000..e27c5a1 --- /dev/null +++ b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_5/sample.bal @@ -0,0 +1,25 @@ +// 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 +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/data.yaml; + +type Data record { + @yaml:Name { + value: "B" + } + string A; + string B; +}; diff --git a/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_6/Ballerina.toml b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_6/Ballerina.toml new file mode 100644 index 0000000..5cbe039 --- /dev/null +++ b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_6/Ballerina.toml @@ -0,0 +1,4 @@ +[package] +org = "yamldata_test" +name = "sample_6" +version = "0.1.0" diff --git a/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_6/sample.bal b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_6/sample.bal new file mode 100644 index 0000000..ff08904 --- /dev/null +++ b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_6/sample.bal @@ -0,0 +1,38 @@ +// 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 +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/data.yaml as yml; + +public function main() returns error? { + record { + @yml:Name { + value: "B" + } + string A; + string B; + } _ = check yml:parseString(string `{ + "A": "Hello", + "B": "World" + }`); + + record { + @yml:Name { + value: "B" + } + string A; + string B; + } _ = {A: "Hello", B: "World"}; +} 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..f1cee21 --- /dev/null +++ b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_7/Ballerina.toml @@ -0,0 +1,4 @@ +[package] +org = "yamldata_test" +name = "sample_7" +version = "0.1.0" diff --git a/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_7/sample.bal b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_7/sample.bal new file mode 100644 index 0000000..2e53e3c --- /dev/null +++ b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_7/sample.bal @@ -0,0 +1,47 @@ +// 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 +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/data.yaml; + +type T1 (map|int|xml)[]; +type T2 record {| + string p1; + table|int p2; +|}; + +public function main() returns error? { + string str1 = string `[ + { + "p1":"v1", + "p2":1 + }, + { + "p1":"v2", + "p2":true + } + ]`; + T1 _ = check yaml:parseString(str1); + + string str2 = string ` + { + "p1":"v1", + "p2": { + "a": 1, + "b": 2 + } + }`; + T2 _ = check yaml:parseString(str2); +} diff --git a/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_8/Ballerina.toml b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_8/Ballerina.toml new file mode 100644 index 0000000..acadf0e --- /dev/null +++ b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_8/Ballerina.toml @@ -0,0 +1,4 @@ +[package] +org = "yamldata_test" +name = "sample_8" +version = "0.1.0" diff --git a/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_8/sample.bal b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_8/sample.bal new file mode 100644 index 0000000..e7e47bb --- /dev/null +++ b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_8/sample.bal @@ -0,0 +1,26 @@ +// 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 +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/data.yaml; + +type UnionType table|record {|string b;|}; + +type IntersectionType UnionType & readonly; + +public function main() returns error? { + string str = string `{"a": 1, "b": "str"}`; + IntersectionType _ = check yaml:parseString(str); +} 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..d393c5a --- /dev/null +++ b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_9/Ballerina.toml @@ -0,0 +1,4 @@ +[package] +org = "yamldata_test" +name = "sample_9" +version = "0.1.0" diff --git a/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_9/sample.bal b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_9/sample.bal new file mode 100644 index 0000000..cc58e60 --- /dev/null +++ b/compiler-plugin-test/src/test/resources/ballerina_sources/sample_package_9/sample.bal @@ -0,0 +1,54 @@ +// 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 +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/data.yaml; + +type ArrayExpectedType record {| + @yaml:Name { + value: "first_name" + } + string firstName; + + @yaml:Name { + value: "first_name" + } + string lastName; +|}[]; + +type TupleExpectedType [record {| + @yaml:Name { + value: "first_name" + } + string firstName; + + @yaml:Name { + value: "first_name" + } + string lastName; +|}]; + +type ImmutableType readonly & ArrayExpectedType; + +type UnionType ArrayExpectedType|TupleExpectedType|ImmutableType; + +function call() returns UnionType { + UnionType callResult = call(); + + return [{ + firstName: "", + lastName: "" + }]; +} diff --git a/compiler-plugin-test/src/test/resources/testng.xml b/compiler-plugin-test/src/test/resources/testng.xml new file mode 100644 index 0000000..05bd536 --- /dev/null +++ b/compiler-plugin-test/src/test/resources/testng.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + diff --git a/compiler-plugin/build.gradle b/compiler-plugin/build.gradle new file mode 100644 index 0000000..cc15f5f --- /dev/null +++ b/compiler-plugin/build.gradle @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +plugins { + id 'java' + id 'checkstyle' + id 'com.github.spotbugs' +} + +description = 'Ballerina - YAML Data Compiler Plugin' + +dependencies { + checkstyle project(':checkstyle') + checkstyle "com.puppycrawl.tools:checkstyle:${puppycrawlCheckstyleVersion}" + + implementation group: 'org.ballerinalang', name: 'ballerina-lang', version: "${ballerinaLangVersion}" + implementation group: 'org.ballerinalang', name: 'ballerina-tools-api', version: "${ballerinaLangVersion}" + implementation group: 'org.ballerinalang', name: 'ballerina-parser', version: "${ballerinaLangVersion}" +} + +def excludePattern = '**/module-info.java' +tasks.withType(Checkstyle) { + exclude excludePattern +} + +checkstyle { + toolVersion "${project.puppycrawlCheckstyleVersion}" + configFile rootProject.file("build-config/checkstyle/build/checkstyle.xml") + configProperties = ["suppressionFile" : file("${rootDir}/build-config/checkstyle/build/suppressions.xml")] +} + +checkstyleMain.dependsOn(":checkstyle:downloadCheckstyleRuleFiles") + +spotbugsMain { + effort "max" + reportLevel "low" + reportsDir = file("$project.buildDir/reports/spotbugs") + reports { + html.enabled true + text.enabled = true + } + def excludeFile = file("${rootDir}/spotbugs-exclude.xml") + if(excludeFile.exists()) { + excludeFilter = excludeFile + } +} + +spotbugsMain { + enabled false +} + +compileJava { + doFirst { + options.compilerArgs = [ + '--module-path', classpath.asPath, + ] + classpath = files() + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/lib/data/yaml/compiler/Constants.java b/compiler-plugin/src/main/java/io/ballerina/lib/data/yaml/compiler/Constants.java new file mode 100644 index 0000000..4e23261 --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/lib/data/yaml/compiler/Constants.java @@ -0,0 +1,36 @@ +/* + * 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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.lib.data.yaml.compiler; + +/** + * Constants for Yaml data compiler plugin. + * + * @since 0.1.0 + */ +public class Constants { + + static final String BALLERINA = "ballerina"; + static final String PARSE_STRING = "parseString"; + static final String PARSE_BYTES = "parseBytes"; + static final String PARSE_STREAM = "parseStream"; + static final String TO_YAML_STRING = "toYamlString"; + static final String NAME = "Name"; + static final String YAML = "yaml"; + static final String DATA_YAML = "data.yaml"; +} diff --git a/compiler-plugin/src/main/java/io/ballerina/lib/data/yaml/compiler/YamlDataCodeAnalyzer.java b/compiler-plugin/src/main/java/io/ballerina/lib/data/yaml/compiler/YamlDataCodeAnalyzer.java new file mode 100644 index 0000000..f86e82d --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/lib/data/yaml/compiler/YamlDataCodeAnalyzer.java @@ -0,0 +1,38 @@ +/* + * 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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.lib.data.yaml.compiler; + +import io.ballerina.compiler.syntax.tree.SyntaxKind; +import io.ballerina.projects.plugins.CodeAnalysisContext; +import io.ballerina.projects.plugins.CodeAnalyzer; + +import java.util.List; + +/** + * Yaml data Code Analyzer. + * + * @since 0.1.0 + */ +public class YamlDataCodeAnalyzer extends CodeAnalyzer { + @Override + public void init(CodeAnalysisContext codeAnalysisContext) { + codeAnalysisContext.addSyntaxNodeAnalysisTask(new YamlDataTypeValidator(), + List.of(SyntaxKind.MODULE_PART)); + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/lib/data/yaml/compiler/YamlDataCompilerPlugin.java b/compiler-plugin/src/main/java/io/ballerina/lib/data/yaml/compiler/YamlDataCompilerPlugin.java new file mode 100644 index 0000000..9590962 --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/lib/data/yaml/compiler/YamlDataCompilerPlugin.java @@ -0,0 +1,35 @@ +/* + * 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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.lib.data.yaml.compiler; + +import io.ballerina.projects.plugins.CompilerPlugin; +import io.ballerina.projects.plugins.CompilerPluginContext; + +/** + * Compiler plugin for Yaml data utils functions. + * + * @since 0.1.0 + */ +public class YamlDataCompilerPlugin extends CompilerPlugin { + + @Override + public void init(CompilerPluginContext compilerPluginContext) { + compilerPluginContext.addCodeAnalyzer(new YamlDataCodeAnalyzer()); + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/lib/data/yaml/compiler/YamlDataDiagnosticCodes.java b/compiler-plugin/src/main/java/io/ballerina/lib/data/yaml/compiler/YamlDataDiagnosticCodes.java new file mode 100644 index 0000000..eef3a6a --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/lib/data/yaml/compiler/YamlDataDiagnosticCodes.java @@ -0,0 +1,55 @@ +/* + * 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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.lib.data.yaml.compiler; + +import io.ballerina.tools.diagnostics.DiagnosticSeverity; + +import static io.ballerina.tools.diagnostics.DiagnosticSeverity.ERROR; + +/** + * Diagnostic codes for Yaml data compiler plugin. + * + * @since 0.1.0 + */ +public enum YamlDataDiagnosticCodes { + DUPLICATE_FIELD("YAML_ERROR_201", "invalid field: duplicate field found", ERROR), + UNSUPPORTED_TYPE("YAML_ERROR_202", "unsupported type: type is not supported", ERROR); + + private final String code; + private final String message; + private final DiagnosticSeverity severity; + + YamlDataDiagnosticCodes(String code, String message, DiagnosticSeverity severity) { + this.code = code; + this.message = message; + this.severity = severity; + } + + public String getCode() { + return code; + } + + public String getMessage() { + return message; + } + + public DiagnosticSeverity getSeverity() { + return severity; + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/lib/data/yaml/compiler/YamlDataTypeValidator.java b/compiler-plugin/src/main/java/io/ballerina/lib/data/yaml/compiler/YamlDataTypeValidator.java new file mode 100644 index 0000000..54b00f4 --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/lib/data/yaml/compiler/YamlDataTypeValidator.java @@ -0,0 +1,331 @@ +/* + * 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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.lib.data.yaml.compiler; + +import io.ballerina.compiler.api.SemanticModel; +import io.ballerina.compiler.api.symbols.AnnotationAttachmentSymbol; +import io.ballerina.compiler.api.symbols.AnnotationSymbol; +import io.ballerina.compiler.api.symbols.ArrayTypeSymbol; +import io.ballerina.compiler.api.symbols.IntersectionTypeSymbol; +import io.ballerina.compiler.api.symbols.ModuleSymbol; +import io.ballerina.compiler.api.symbols.RecordFieldSymbol; +import io.ballerina.compiler.api.symbols.RecordTypeSymbol; +import io.ballerina.compiler.api.symbols.Symbol; +import io.ballerina.compiler.api.symbols.SymbolKind; +import io.ballerina.compiler.api.symbols.TupleTypeSymbol; +import io.ballerina.compiler.api.symbols.TypeDefinitionSymbol; +import io.ballerina.compiler.api.symbols.TypeDescKind; +import io.ballerina.compiler.api.symbols.TypeReferenceTypeSymbol; +import io.ballerina.compiler.api.symbols.TypeSymbol; +import io.ballerina.compiler.api.symbols.UnionTypeSymbol; +import io.ballerina.compiler.api.symbols.VariableSymbol; +import io.ballerina.compiler.syntax.tree.CheckExpressionNode; +import io.ballerina.compiler.syntax.tree.ChildNodeList; +import io.ballerina.compiler.syntax.tree.ExpressionNode; +import io.ballerina.compiler.syntax.tree.FunctionCallExpressionNode; +import io.ballerina.compiler.syntax.tree.FunctionDefinitionNode; +import io.ballerina.compiler.syntax.tree.ImportDeclarationNode; +import io.ballerina.compiler.syntax.tree.ModuleMemberDeclarationNode; +import io.ballerina.compiler.syntax.tree.ModulePartNode; +import io.ballerina.compiler.syntax.tree.ModuleVariableDeclarationNode; +import io.ballerina.compiler.syntax.tree.NameReferenceNode; +import io.ballerina.compiler.syntax.tree.Node; +import io.ballerina.compiler.syntax.tree.NodeList; +import io.ballerina.compiler.syntax.tree.QualifiedNameReferenceNode; +import io.ballerina.compiler.syntax.tree.SyntaxKind; +import io.ballerina.compiler.syntax.tree.TypeDefinitionNode; +import io.ballerina.compiler.syntax.tree.VariableDeclarationNode; +import io.ballerina.projects.plugins.AnalysisTask; +import io.ballerina.projects.plugins.SyntaxNodeAnalysisContext; +import io.ballerina.tools.diagnostics.Diagnostic; +import io.ballerina.tools.diagnostics.DiagnosticFactory; +import io.ballerina.tools.diagnostics.DiagnosticInfo; +import io.ballerina.tools.diagnostics.DiagnosticSeverity; +import io.ballerina.tools.diagnostics.Location; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static io.ballerina.lib.data.yaml.compiler.Constants.BALLERINA; +import static io.ballerina.lib.data.yaml.compiler.Constants.DATA_YAML; +import static io.ballerina.lib.data.yaml.compiler.Constants.YAML; + +/** + * Yaml Data Record Field Validator. + * + * @since 0.1.0 + */ +public class YamlDataTypeValidator implements AnalysisTask { + + private SemanticModel semanticModel; + private final HashMap allDiagnosticInfo = new HashMap<>(); + Location currentLocation; + private String modulePrefix = YAML; + + @Override + public void perform(SyntaxNodeAnalysisContext ctx) { + semanticModel = ctx.semanticModel(); + List diagnostics = semanticModel.diagnostics(); + boolean erroneousCompilation = diagnostics.stream() + .anyMatch(d -> d.diagnosticInfo().severity().equals(DiagnosticSeverity.ERROR)); + if (erroneousCompilation) { + return; + } + + ModulePartNode rootNode = (ModulePartNode) ctx.node(); + for (ImportDeclarationNode importDeclarationNode :rootNode.imports()) { + Optional symbol = semanticModel.symbol(importDeclarationNode); + if (symbol.isPresent() && symbol.get().kind() == SymbolKind.MODULE) { + ModuleSymbol moduleSymbol = (ModuleSymbol) symbol.get(); + if (isYamlImport(moduleSymbol)) { + modulePrefix = moduleSymbol.id().modulePrefix(); + break; + } + } + } + + NodeList members = rootNode.members(); + for (int i = 0; i < members.size(); i++) { + ModuleMemberDeclarationNode member = members.get(i); + switch (member.kind()) { + case FUNCTION_DEFINITION -> processFunctionDefinitionNode((FunctionDefinitionNode) member, ctx); + case MODULE_VAR_DECL -> + processModuleVariableDeclarationNode((ModuleVariableDeclarationNode) member, ctx); + case TYPE_DEFINITION -> + processTypeDefinitionNode((TypeDefinitionNode) member, ctx); + } + } + } + + private void processFunctionDefinitionNode(FunctionDefinitionNode functionDefinitionNode, + SyntaxNodeAnalysisContext ctx) { + ChildNodeList childNodeList = functionDefinitionNode.functionBody().children(); + for (Node node : childNodeList) { + if (node.kind() != SyntaxKind.LOCAL_VAR_DECL) { + continue; + } + VariableDeclarationNode variableDeclarationNode = (VariableDeclarationNode) node; + Optional initializer = variableDeclarationNode.initializer(); + if (initializer.isEmpty()) { + continue; + } + + currentLocation = variableDeclarationNode.typedBindingPattern().typeDescriptor().location(); + Optional symbol = semanticModel.symbol(variableDeclarationNode.typedBindingPattern()); + if (symbol.isEmpty()) { + continue; + } + + TypeSymbol typeSymbol = ((VariableSymbol) symbol.get()).typeDescriptor(); + if (!isParseFunctionOfStringSource(initializer.get())) { + checkTypeAndDetectDuplicateFields(typeSymbol, ctx); + continue; + } + + validateExpectedType(typeSymbol, ctx); + } + } + + private void checkTypeAndDetectDuplicateFields(TypeSymbol typeSymbol, SyntaxNodeAnalysisContext ctx) { + switch (typeSymbol.typeKind()) { + case RECORD -> detectDuplicateFields((RecordTypeSymbol) typeSymbol, ctx); + case ARRAY -> checkTypeAndDetectDuplicateFields(((ArrayTypeSymbol) typeSymbol).memberTypeDescriptor(), ctx); + case TUPLE -> { + for (TypeSymbol memberType : ((TupleTypeSymbol) typeSymbol).memberTypeDescriptors()) { + checkTypeAndDetectDuplicateFields(memberType, ctx); + } + } + case UNION -> { + for (TypeSymbol memberType : ((UnionTypeSymbol) typeSymbol).memberTypeDescriptors()) { + checkTypeAndDetectDuplicateFields(memberType, ctx); + } + } + case TYPE_REFERENCE -> checkTypeAndDetectDuplicateFields( + ((TypeReferenceTypeSymbol) typeSymbol).typeDescriptor(), ctx); + case INTERSECTION -> checkTypeAndDetectDuplicateFields(getRawType(typeSymbol), ctx); + } + } + + private boolean isParseFunctionOfStringSource(ExpressionNode expressionNode) { + if (expressionNode.kind() == SyntaxKind.CHECK_EXPRESSION) { + expressionNode = ((CheckExpressionNode) expressionNode).expression(); + } + + if (expressionNode.kind() != SyntaxKind.FUNCTION_CALL) { + return false; + } + NameReferenceNode nameReferenceNode = ((FunctionCallExpressionNode) expressionNode).functionName(); + if (nameReferenceNode.kind() != SyntaxKind.QUALIFIED_NAME_REFERENCE) { + return false; + } + String prefix = ((QualifiedNameReferenceNode) nameReferenceNode).modulePrefix().text(); + if (!prefix.equals(modulePrefix)) { + return false; + } + String functionName = ((FunctionCallExpressionNode) expressionNode).functionName().toString().trim(); + return functionName.contains(Constants.PARSE_STRING) || functionName.contains(Constants.PARSE_BYTES) + || functionName.contains(Constants.PARSE_STREAM); + } + + private void validateExpectedType(TypeSymbol typeSymbol, SyntaxNodeAnalysisContext ctx) { + typeSymbol.getLocation().ifPresent(location -> currentLocation = location); + switch (typeSymbol.typeKind()) { + case UNION -> validateUnionType((UnionTypeSymbol) typeSymbol, ctx); + case RECORD -> validateRecordType((RecordTypeSymbol) typeSymbol, ctx); + case ARRAY -> validateExpectedType(((ArrayTypeSymbol) typeSymbol).memberTypeDescriptor(), ctx); + case TUPLE -> validateTupleType((TupleTypeSymbol) typeSymbol, ctx); + case TABLE, XML -> reportDiagnosticInfo(ctx, typeSymbol.getLocation(), + YamlDataDiagnosticCodes.UNSUPPORTED_TYPE); + case TYPE_REFERENCE -> validateExpectedType(((TypeReferenceTypeSymbol) typeSymbol).typeDescriptor(), ctx); + case INTERSECTION -> validateExpectedType(getRawType(typeSymbol), ctx); + default -> { } + } + } + + private void validateTupleType(TupleTypeSymbol tupleTypeSymbol, SyntaxNodeAnalysisContext ctx) { + for (TypeSymbol memberType : tupleTypeSymbol.memberTypeDescriptors()) { + validateExpectedType(memberType, ctx); + } + } + + private void validateRecordType(RecordTypeSymbol recordTypeSymbol, SyntaxNodeAnalysisContext ctx) { + detectDuplicateFields(recordTypeSymbol, ctx); + + for (Map.Entry entry : recordTypeSymbol.fieldDescriptors().entrySet()) { + RecordFieldSymbol fieldSymbol = entry.getValue(); + currentLocation = fieldSymbol.getLocation().orElseGet(() -> currentLocation); + validateExpectedType(fieldSymbol.typeDescriptor(), ctx); + } + } + + private void validateUnionType(UnionTypeSymbol unionTypeSymbol, + SyntaxNodeAnalysisContext ctx) { + List memberTypeSymbols = unionTypeSymbol.memberTypeDescriptors(); + for (TypeSymbol memberTypeSymbol : memberTypeSymbols) { + validateExpectedType(getRawType(memberTypeSymbol), ctx); + } + } + + public static TypeSymbol getRawType(TypeSymbol typeDescriptor) { + if (typeDescriptor.typeKind() == TypeDescKind.INTERSECTION) { + return getRawType(((IntersectionTypeSymbol) typeDescriptor).effectiveTypeDescriptor()); + } + if (typeDescriptor.typeKind() == TypeDescKind.TYPE_REFERENCE) { + TypeReferenceTypeSymbol typeRef = (TypeReferenceTypeSymbol) typeDescriptor; + if (typeRef.typeDescriptor().typeKind() == TypeDescKind.INTERSECTION) { + return getRawType(((IntersectionTypeSymbol) typeRef.typeDescriptor()).effectiveTypeDescriptor()); + } + TypeSymbol rawType = typeRef.typeDescriptor(); + if (rawType.typeKind() == TypeDescKind.TYPE_REFERENCE) { + return getRawType(rawType); + } + return rawType; + } + return typeDescriptor; + } + + private void reportDiagnosticInfo(SyntaxNodeAnalysisContext ctx, Optional location, + YamlDataDiagnosticCodes diagnosticsCodes) { + Location pos = location.orElseGet(() -> currentLocation); + DiagnosticInfo diagnosticInfo = new DiagnosticInfo(diagnosticsCodes.getCode(), + diagnosticsCodes.getMessage(), diagnosticsCodes.getSeverity()); + if (allDiagnosticInfo.containsKey(pos) && allDiagnosticInfo.get(pos).equals(diagnosticInfo)) { + return; + } + allDiagnosticInfo.put(pos, diagnosticInfo); + ctx.reportDiagnostic(DiagnosticFactory.createDiagnostic(diagnosticInfo, pos)); + } + + private void processModuleVariableDeclarationNode(ModuleVariableDeclarationNode moduleVariableDeclarationNode, + SyntaxNodeAnalysisContext ctx) { + Optional initializer = moduleVariableDeclarationNode.initializer(); + if (initializer.isEmpty() || !isParseFunctionOfStringSource(initializer.get())) { + return; + } + + Optional symbol = semanticModel.symbol(moduleVariableDeclarationNode.typedBindingPattern()); + if (symbol.isEmpty()) { + return; + } + validateExpectedType(((VariableSymbol) symbol.get()).typeDescriptor(), ctx); + } + + private void processTypeDefinitionNode(TypeDefinitionNode typeDefinitionNode, SyntaxNodeAnalysisContext ctx) { + Node typeDescriptor = typeDefinitionNode.typeDescriptor(); + if (typeDescriptor.kind() != SyntaxKind.RECORD_TYPE_DESC) { + return; + } + validateRecordTypeDefinition(typeDefinitionNode, ctx); + } + + private void validateRecordTypeDefinition(TypeDefinitionNode typeDefinitionNode, SyntaxNodeAnalysisContext ctx) { + Optional symbol = semanticModel.symbol(typeDefinitionNode); + if (symbol.isEmpty()) { + return; + } + TypeDefinitionSymbol typeDefinitionSymbol = (TypeDefinitionSymbol) symbol.get(); + detectDuplicateFields((RecordTypeSymbol) typeDefinitionSymbol.typeDescriptor(), ctx); + } + + private void detectDuplicateFields(RecordTypeSymbol recordTypeSymbol, SyntaxNodeAnalysisContext ctx) { + List fieldMembers = new ArrayList<>(); + for (Map.Entry entry : recordTypeSymbol.fieldDescriptors().entrySet()) { + RecordFieldSymbol fieldSymbol = entry.getValue(); + String name = getNameFromAnnotation(entry.getKey(), fieldSymbol.annotAttachments()); + if (fieldMembers.contains(name)) { + reportDiagnosticInfo(ctx, fieldSymbol.getLocation(), YamlDataDiagnosticCodes.DUPLICATE_FIELD); + return; + } + fieldMembers.add(name); + } + } + + private String getNameFromAnnotation(String fieldName, + List annotationAttachments) { + for (AnnotationAttachmentSymbol annotAttSymbol : annotationAttachments) { + AnnotationSymbol annotation = annotAttSymbol.typeDescriptor(); + if (!getAnnotModuleName(annotation).equals(DATA_YAML)) { + continue; + } + Optional nameAnnot = annotation.getName(); + if (nameAnnot.isEmpty()) { + continue; + } + String value = nameAnnot.get(); + if (value.equals(Constants.NAME)) { + return ((LinkedHashMap) annotAttSymbol.attachmentValue().orElseThrow().value()) + .get("value").toString(); + } + } + return fieldName; + } + + private String getAnnotModuleName(AnnotationSymbol annotation) { + return annotation.getModule().flatMap(Symbol::getName).orElse(""); + } + + public static boolean isYamlImport(ModuleSymbol moduleSymbol) { + return BALLERINA.equals(moduleSymbol.id().orgName()) + && DATA_YAML.equals(moduleSymbol.id().moduleName()); + } +} diff --git a/compiler-plugin/src/main/java/module-info.java b/compiler-plugin/src/main/java/module-info.java new file mode 100644 index 0000000..407689d --- /dev/null +++ b/compiler-plugin/src/main/java/module-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module io.ballerina.lib.data.yaml.compiler { + requires io.ballerina.lang; + requires io.ballerina.tools.api; + requires io.ballerina.parser; +} diff --git a/gradle.properties b/gradle.properties index da791ab..0a2c154 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ org.gradle.caching=true -group=io.ballerina.stdlib +group=io.ballerina.lib version=0.1.0-SNAPSHOT ballerinaLangVersion=2201.9.0 @@ -11,5 +11,10 @@ githubSpotbugsVersion=5.0.14 githubJohnrengelmanShadowVersion=8.1.1 underCouchDownloadVersion=4.0.4 researchgateReleaseVersion=2.8.0 +spotbugsVersion=5.0.14 +shadowJarPluginVersion=8.1.1 +downloadPluginVersion=4.0.4 +releasePluginVersion=2.8.0 ballerinaGradlePluginVersion=2.0.1 + stdlibIoVersion=1.6.0 diff --git a/native/src/main/java/io/ballerina/lib/data/yaml/Native.java b/native/src/main/java/io/ballerina/lib/data/yaml/Native.java new file mode 100644 index 0000000..ac942c9 --- /dev/null +++ b/native/src/main/java/io/ballerina/lib/data/yaml/Native.java @@ -0,0 +1,90 @@ +/* + * 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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.lib.data.yaml; + +import io.ballerina.lib.data.yaml.emitter.Emitter; +import io.ballerina.lib.data.yaml.io.DataReaderTask; +import io.ballerina.lib.data.yaml.io.DataReaderThreadPool; +import io.ballerina.lib.data.yaml.parser.YamlParser; +import io.ballerina.lib.data.yaml.serializer.Serializer; +import io.ballerina.lib.data.yaml.utils.OptionsUtils; +import io.ballerina.runtime.api.Environment; +import io.ballerina.runtime.api.Future; +import io.ballerina.runtime.api.creators.ValueCreator; +import io.ballerina.runtime.api.values.BArray; +import io.ballerina.runtime.api.values.BError; +import io.ballerina.runtime.api.values.BMap; +import io.ballerina.runtime.api.values.BObject; +import io.ballerina.runtime.api.values.BStream; +import io.ballerina.runtime.api.values.BString; +import io.ballerina.runtime.api.values.BTypedesc; + +import java.io.ByteArrayInputStream; +import java.io.InputStreamReader; +import java.io.StringReader; +import java.util.List; + +/** + * This class is used to convert json inform of string, byte[], byte-stream to record or json type. + * + * @since 0.1.0 + */ +public class Native { + + public static Object parseString(BString yaml, BMap options, BTypedesc typed) { + try { + return YamlParser.compose(new StringReader(yaml.getValue()), options, typed.getDescribingType()); + } catch (BError e) { + return e; + } + } + + public static Object parseBytes(BArray yaml, BMap options, BTypedesc typed) { + try { + return YamlParser.compose(new InputStreamReader(new ByteArrayInputStream(yaml.getBytes())), + options, typed.getDescribingType()); + } catch (BError e) { + return e; + } + } + + public static Object parseStream(Environment env, BStream yaml, BMap options, BTypedesc typed) { + final BObject iteratorObj = yaml.getIteratorObj(); + final Future future = env.markAsync(); + DataReaderTask task = new DataReaderTask(env, iteratorObj, future, typed, options); + DataReaderThreadPool.EXECUTOR_SERVICE.submit(task); + return null; + } + + public static Object toYamlStringArray(Object yamlValue, BMap config) { + OptionsUtils.WriteConfig writeConfig = OptionsUtils.resolveWriteOptions(config); + char delimiter = writeConfig.useSingleQuotes() ? '\'' : '"'; + + Serializer.SerializerState serializerState = new Serializer.SerializerState(delimiter, + writeConfig.forceQuotes(), writeConfig.blockLevel(), writeConfig.flowStyle(), writeConfig.isStream() + ); + Serializer.serialize(serializerState, yamlValue); + + Emitter.EmitterState emitterState = new Emitter.EmitterState( + serializerState.getEvents(), writeConfig.indentationPolicy(), writeConfig.canonical() + ); + List content = Emitter.emit(emitterState, writeConfig.isStream()); + return ValueCreator.createArrayValue(content.toArray(new BString[0])); + } +} diff --git a/native/src/main/java/io/ballerina/lib/data/yaml/common/Types.java b/native/src/main/java/io/ballerina/lib/data/yaml/common/Types.java new file mode 100644 index 0000000..d56a5eb --- /dev/null +++ b/native/src/main/java/io/ballerina/lib/data/yaml/common/Types.java @@ -0,0 +1,51 @@ +/* + * 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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.lib.data.yaml.common; + +/** + * Define basic types and schemas in YAML files. + * + * @since 0.1.0 + */ +public class Types { + + public enum Collection { + STREAM, + SEQUENCE, + MAPPING + } + + public enum FailSafeSchema { + MAPPING, + SEQUENCE, + STRING + } + + public enum YAMLSchema { + FAILSAFE_SCHEMA, + JSON_SCHEMA, + CORE_SCHEMA + } + + public enum DocumentType { + ANY_DOCUMENT, + BARE_DOCUMENT, + DIRECTIVE_DOCUMENT + } +} diff --git a/native/src/main/java/io/ballerina/lib/data/yaml/common/YamlEvent.java b/native/src/main/java/io/ballerina/lib/data/yaml/common/YamlEvent.java new file mode 100644 index 0000000..c4d508a --- /dev/null +++ b/native/src/main/java/io/ballerina/lib/data/yaml/common/YamlEvent.java @@ -0,0 +1,199 @@ +/* + * 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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.lib.data.yaml.common; + +import io.ballerina.lib.data.yaml.common.Types.Collection; + +/** + * Parser Events when parsing yaml data. + * + * @since 0.1.0 + */ +public abstract class YamlEvent { + + private final EventKind kind; + private String anchor = null; + private String tag = null; + + public YamlEvent(EventKind kind) { + this.kind = kind; + } + + public EventKind getKind() { + return kind; + } + + public abstract YamlEvent clone(); + + public String getAnchor() { + return anchor; + } + + public void setAnchor(String anchor) { + this.anchor = anchor; + } + + public String getTag() { + return tag; + } + + public void setTag(String tag) { + this.tag = tag; + } + + public static class AliasEvent extends YamlEvent { + + private final String alias; + + public AliasEvent(String alias) { + super(EventKind.ALIAS_EVENT); + this.alias = alias; + } + + public String getAlias() { + return alias; + } + + @Override + public YamlEvent clone() { + AliasEvent aliasEvent = new AliasEvent(alias); + aliasEvent.setAnchor(getAnchor()); + aliasEvent.setTag(getTag()); + return aliasEvent; + } + } + + public static class ScalarEvent extends YamlEvent { + + private final String value; + + public ScalarEvent() { + super(EventKind.SCALAR_EVENT); + this.value = null; + } + + public ScalarEvent(String value) { + super(EventKind.SCALAR_EVENT); + this.value = value; + } + + public String getValue() { + return value; + } + + @Override + public YamlEvent clone() { + ScalarEvent scalarEvent = new ScalarEvent(value); + scalarEvent.setAnchor(getAnchor()); + scalarEvent.setTag(getTag()); + return scalarEvent; + } + } + + public static class StartEvent extends YamlEvent { + + private final Collection startType; + private boolean flowStyle = false; + private boolean implicit = false; + + public StartEvent(Collection startType) { + super(EventKind.START_EVENT); + this.startType = startType; + } + + public StartEvent(Collection startType, boolean flowStyle, boolean implicit) { + super(EventKind.START_EVENT); + this.startType = startType; + this.flowStyle = flowStyle; + this.implicit = implicit; + } + + public Collection getStartType() { + return startType; + } + + public boolean isFlowStyle() { + return flowStyle; + } + + public boolean isImplicit() { + return implicit; + } + + @Override + public YamlEvent clone() { + StartEvent startEvent = new StartEvent(startType, flowStyle, implicit); + startEvent.setAnchor(getAnchor()); + startEvent.setTag(getTag()); + return startEvent; + } + } + + public static class EndEvent extends YamlEvent { + + private final Collection endType; + + public EndEvent(Collection endType) { + super(EventKind.END_EVENT); + this.endType = endType; + } + + public Collection getEndType() { + return endType; + } + + @Override + public YamlEvent clone() { + EndEvent endEvent = new EndEvent(endType); + endEvent.setAnchor(getAnchor()); + endEvent.setTag(getTag()); + return endEvent; + } + } + + public static class DocumentMarkerEvent extends YamlEvent { + + private final boolean explicit; + + public DocumentMarkerEvent(boolean explicit) { + super(EventKind.DOCUMENT_MARKER_EVENT); + this.explicit = explicit; + } + + public boolean isExplicit() { + return explicit; + } + + @Override + public YamlEvent clone() { + DocumentMarkerEvent documentMarkerEvent = new DocumentMarkerEvent(explicit); + documentMarkerEvent.setAnchor(getAnchor()); + documentMarkerEvent.setTag(getTag()); + return documentMarkerEvent; + } + } + + public enum EventKind { + ALIAS_EVENT, + SCALAR_EVENT, + START_EVENT, + END_EVENT, + DOCUMENT_MARKER_EVENT + } +} diff --git a/native/src/main/java/io/ballerina/lib/data/yaml/emitter/Emitter.java b/native/src/main/java/io/ballerina/lib/data/yaml/emitter/Emitter.java new file mode 100644 index 0000000..2dca69a --- /dev/null +++ b/native/src/main/java/io/ballerina/lib/data/yaml/emitter/Emitter.java @@ -0,0 +1,346 @@ +/* + * 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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.lib.data.yaml.emitter; + +import io.ballerina.lib.data.yaml.common.Types; +import io.ballerina.lib.data.yaml.common.YamlEvent; +import io.ballerina.lib.data.yaml.utils.DiagnosticErrorCode; +import io.ballerina.lib.data.yaml.utils.DiagnosticLog; +import io.ballerina.runtime.api.utils.StringUtils; +import io.ballerina.runtime.api.values.BString; + +import java.util.ArrayList; +import java.util.List; + +import static io.ballerina.lib.data.yaml.utils.Constants.DEFAULT_GLOBAL_TAG_HANDLE; +import static io.ballerina.lib.data.yaml.utils.Constants.DEFAULT_LOCAL_TAG_HANDLE; +import static io.ballerina.lib.data.yaml.utils.Constants.END_OF_YAML_DOCUMENT; +import static io.ballerina.lib.data.yaml.utils.Constants.START_OF_YAML_DOCUMENT; + +/** + * Convert Yaml Event stream into list of YAML strings. + * + * @since 0.1.0 + */ +public class Emitter { + + /** + * Represents the variables of the Emitter state. + * + * @since 0.1.0 + */ + public static class EmitterState { + List document; + List documentTags; + // total white spaces for a single indent + final String indent; + // If set, the tag is written explicitly along with the value + final boolean canonical; + boolean lastBareDoc = false; + final List events; + + public EmitterState(List events, int indentationPolicy, boolean canonical) { + this.events = events; + this.canonical = canonical; + this.document = new ArrayList<>(); + this.documentTags = new ArrayList<>(); + this.indent = " ".repeat(indentationPolicy); + } + + public void addLine(String line) { + document.add(StringUtils.fromString(line)); + } + + public List getDocument() { + return getDocument(false); + } + + public List getDocument(boolean isStream) { + List output = new ArrayList<>(document.stream().toList()); + if (!documentTags.isEmpty()) { + output.add(0, START_OF_YAML_DOCUMENT); + if (lastBareDoc) { + output.add(0, END_OF_YAML_DOCUMENT); + lastBareDoc = false; + } + output.add(END_OF_YAML_DOCUMENT); + } else if (isStream && document.size() > 0) { + output.add(0, START_OF_YAML_DOCUMENT); + lastBareDoc = true; + } + + document = new ArrayList<>(); + documentTags = new ArrayList<>(); + return output; + } + } + + public static List emit(EmitterState state, boolean isStream) { + if (isStream) { + List output = new ArrayList<>(); + boolean isFirstEvent = true; + while (!state.events.isEmpty()) { + write(state); + output.addAll(state.getDocument(!isFirstEvent)); + isFirstEvent = false; + } + return output; + } + write(state); + if (!state.events.isEmpty()) { + throw DiagnosticLog.error(DiagnosticErrorCode.ARRAY_SIZE_MISMATCH); + } + return state.getDocument(); + } + + private static void write(EmitterState state) { + YamlEvent event = Utils.getEvent(state); + + if (event.getKind() == YamlEvent.EventKind.START_EVENT) { + YamlEvent.StartEvent startEvent = ((YamlEvent.StartEvent) event); + Types.Collection startType = startEvent.getStartType(); + if (startType == Types.Collection.SEQUENCE) { + if (startEvent.isFlowStyle()) { + state.addLine(writeFlowSequence(state, event.getTag())); + } else { + writeBlockSequence(state, "", event.getTag()); + } + return; + } + if (startType == Types.Collection.MAPPING) { + if (startEvent.isFlowStyle()) { + state.addLine(writeFlowMapping(state, event.getTag())); + } else { + writeBlockMapping(state, ""); + } + return; + } + if (startType == Types.Collection.STREAM) { + state.addLine("---"); + } + } + + if (event.getKind() == YamlEvent.EventKind.SCALAR_EVENT) { + YamlEvent.ScalarEvent scalarEvent = (YamlEvent.ScalarEvent) event; + state.addLine(writeNode(state, scalarEvent.getValue(), event.getTag())); + } + } + + private static String writeNode(EmitterState state, String value, String tag) { + return writeNode(state, value, tag, false); + } + + private static String writeNode(EmitterState state, String value, String tag, boolean tagAsSuffix) { + if (tag == null) { + return value; + } + + if (tag.startsWith(DEFAULT_GLOBAL_TAG_HANDLE)) { + return state.canonical ? Utils.appendTagToValue(tagAsSuffix, "!!" + + tag.substring(DEFAULT_GLOBAL_TAG_HANDLE.length()), value) : value; + } + + if (tag.startsWith(DEFAULT_LOCAL_TAG_HANDLE)) { + return Utils.appendTagToValue(tagAsSuffix, tag, value); + } + return ""; + } + + private static void writeBlockMapping(EmitterState state, String whitespace) { + YamlEvent event = Utils.getEvent(state); + String line; + + while (true) { + line = ""; + if (event.getKind() == YamlEvent.EventKind.END_EVENT) { + YamlEvent.EndEvent endEvent = (YamlEvent.EndEvent) event; + if (endEvent.getEndType() == Types.Collection.MAPPING || + endEvent.getEndType() == Types.Collection.STREAM) { + break; + } + throw DiagnosticLog.error(DiagnosticErrorCode.ARRAY_SIZE_MISMATCH); + } + + if (event.getKind() == YamlEvent.EventKind.SCALAR_EVENT) { + YamlEvent.ScalarEvent scalarEvent = (YamlEvent.ScalarEvent) event; + line += whitespace + writeNode(state, scalarEvent.getValue(), event.getTag()) + ": "; + } + + event = Utils.getEvent(state); + + if (event.getKind() == YamlEvent.EventKind.SCALAR_EVENT) { + YamlEvent.ScalarEvent scalarEvent = (YamlEvent.ScalarEvent) event; + line += writeNode(state, scalarEvent.getValue(), event.getTag()); + state.addLine(line); + } + + if (event.getKind() == YamlEvent.EventKind.START_EVENT) { + YamlEvent.StartEvent startEvent = (YamlEvent.StartEvent) event; + if (startEvent.getStartType() == Types.Collection.SEQUENCE) { + if (startEvent.isFlowStyle()) { + state.addLine(line + writeFlowSequence(state, event.getTag())); + } else { + state.addLine(writeNode(state, line.substring(0, line.length() - 1), event.getTag(), true)); + writeBlockSequence(state, whitespace, event.getTag()); + } + } else if (startEvent.getStartType() == Types.Collection.MAPPING) { + if (startEvent.isFlowStyle()) { + state.addLine(line + writeFlowMapping(state, event.getTag())); + } else { + state.addLine(writeNode(state, line.substring(0, line.length() - 1), event.getTag(), true)); + writeBlockMapping(state, whitespace + state.indent); + } + } + } + event = Utils.getEvent(state); + } + } + + private static String writeFlowMapping(EmitterState state, String tag) { + StringBuilder line = new StringBuilder(writeNode(state, "{", tag)); + YamlEvent event = Utils.getEvent(state); + boolean isFirstValue = true; + + while (true) { + if (event.getKind() == YamlEvent.EventKind.END_EVENT) { + YamlEvent.EndEvent endEvent = (YamlEvent.EndEvent) event; + if (endEvent.getEndType() == Types.Collection.MAPPING) { + break; + } + DiagnosticLog.error(DiagnosticErrorCode.ARRAY_SIZE_MISMATCH); + } + + if (!isFirstValue) { + line.append(", "); + } + + if (event.getKind() == YamlEvent.EventKind.SCALAR_EVENT) { + YamlEvent.ScalarEvent scalarEvent = (YamlEvent.ScalarEvent) event; + line.append(writeNode(state, scalarEvent.getValue(), event.getTag())).append(": "); + } + + event = Utils.getEvent(state); + + if (event.getKind() == YamlEvent.EventKind.SCALAR_EVENT) { + YamlEvent.ScalarEvent scalarEvent = (YamlEvent.ScalarEvent) event; + line.append(writeNode(state, scalarEvent.getValue(), event.getTag())); + } + + if (event.getKind() == YamlEvent.EventKind.START_EVENT) { + YamlEvent.StartEvent startEvent = (YamlEvent.StartEvent) event; + if (startEvent.getStartType() == Types.Collection.SEQUENCE) { + line.append(writeFlowSequence(state, event.getTag())); + } else if (startEvent.getStartType() == Types.Collection.MAPPING) { + line.append(writeFlowMapping(state, event.getTag())); + } + } + + event = Utils.getEvent(state); + isFirstValue = false; + } + + line.append("}"); + return line.toString(); + } + + private static void writeBlockSequence(EmitterState state, String whitespace, String tag) { + YamlEvent event = Utils.getEvent(state); + boolean emptySequence = true; + + while (true) { + if (event.getKind() == YamlEvent.EventKind.END_EVENT) { + YamlEvent.EndEvent endEvent = (YamlEvent.EndEvent) event; + if (endEvent.getEndType() == Types.Collection.MAPPING) { + throw DiagnosticLog.error(DiagnosticErrorCode.ARRAY_SIZE_MISMATCH); + } + if (emptySequence) { + state.addLine(whitespace + writeNode(state, "-", tag, true)); + } + break; + } + + if (event.getKind() == YamlEvent.EventKind.SCALAR_EVENT) { + YamlEvent.ScalarEvent scalarEvent = (YamlEvent.ScalarEvent) event; + state.addLine(whitespace + "- " + writeNode(state, scalarEvent.getValue(), event.getTag())); + } + + if (event.getKind() == YamlEvent.EventKind.START_EVENT) { + YamlEvent.StartEvent startEvent = (YamlEvent.StartEvent) event; + if (startEvent.getStartType() == Types.Collection.SEQUENCE) { + if (startEvent.isFlowStyle()) { + state.addLine(whitespace + "- " + writeFlowSequence(state, event.getTag())); + } else { + state.addLine(whitespace + writeNode(state, "-", event.getTag(), true)); + writeBlockSequence(state, whitespace + state.indent, event.getTag()); + } + } else if (startEvent.getStartType() == Types.Collection.MAPPING) { + if (startEvent.isFlowStyle()) { + state.addLine(whitespace + "- " + writeFlowMapping(state, event.getTag())); + } else { + state.addLine(whitespace + "-"); + writeBlockMapping(state, whitespace + state.indent); + } + } + } + + event = Utils.getEvent(state); + emptySequence = false; + } + } + + private static String writeFlowSequence(EmitterState state, String tag) { + StringBuilder line = new StringBuilder(writeNode(state, "[", tag)); + YamlEvent event = Utils.getEvent(state); + boolean firstValue = true; + + while (true) { + if (event.getKind() == YamlEvent.EventKind.END_EVENT) { + YamlEvent.EndEvent endEvent = (YamlEvent.EndEvent) event; + if (endEvent.getEndType() == Types.Collection.SEQUENCE) { + break; + } + throw DiagnosticLog.error(DiagnosticErrorCode.ARRAY_SIZE_MISMATCH); + } + + if (!firstValue) { + line.append(", "); + } + + if (event.getKind() == YamlEvent.EventKind.SCALAR_EVENT) { + YamlEvent.ScalarEvent scalarEvent = (YamlEvent.ScalarEvent) event; + line.append(writeNode(state, scalarEvent.getValue(), event.getTag())); + } + + if (event.getKind() == YamlEvent.EventKind.START_EVENT) { + YamlEvent.StartEvent startEvent = (YamlEvent.StartEvent) event; + if (startEvent.getStartType() == Types.Collection.SEQUENCE) { + line.append(writeFlowSequence(state, event.getTag())); + } else if (startEvent.getStartType() == Types.Collection.MAPPING) { + line.append(writeFlowMapping(state, event.getTag())); + } + } + + event = Utils.getEvent(state); + firstValue = false; + } + + line.append("]"); + return line.toString(); + } +} diff --git a/native/src/main/java/io/ballerina/lib/data/yaml/emitter/Utils.java b/native/src/main/java/io/ballerina/lib/data/yaml/emitter/Utils.java new file mode 100644 index 0000000..d775c25 --- /dev/null +++ b/native/src/main/java/io/ballerina/lib/data/yaml/emitter/Utils.java @@ -0,0 +1,41 @@ +/* + * 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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.lib.data.yaml.emitter; + +import io.ballerina.lib.data.yaml.common.Types; +import io.ballerina.lib.data.yaml.common.YamlEvent; + +/** + * Holds utilities use to emit YAML strings. + * + * @since 0.1.0 + */ +public class Utils { + + public static YamlEvent getEvent(Emitter.EmitterState state) { + if (state.events.size() < 1) { + return new YamlEvent.EndEvent(Types.Collection.STREAM); + } + return state.events.remove(0); + } + + public static String appendTagToValue(boolean tagAsSuffix, String tag, String value) { + return tagAsSuffix ? value + " " + tag : tag + " " + value; + } +} diff --git a/native/src/main/java/io/ballerina/lib/data/yaml/io/BallerinaByteBlockInputStream.java b/native/src/main/java/io/ballerina/lib/data/yaml/io/BallerinaByteBlockInputStream.java new file mode 100644 index 0000000..9c1100a --- /dev/null +++ b/native/src/main/java/io/ballerina/lib/data/yaml/io/BallerinaByteBlockInputStream.java @@ -0,0 +1,162 @@ +/* + * 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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.lib.data.yaml.io; + +import io.ballerina.lib.data.yaml.utils.DiagnosticLog; +import io.ballerina.runtime.api.Environment; +import io.ballerina.runtime.api.async.Callback; +import io.ballerina.runtime.api.async.StrandMetadata; +import io.ballerina.runtime.api.types.MethodType; +import io.ballerina.runtime.api.types.Type; +import io.ballerina.runtime.api.values.BArray; +import io.ballerina.runtime.api.values.BError; +import io.ballerina.runtime.api.values.BMap; +import io.ballerina.runtime.api.values.BObject; +import io.ballerina.runtime.api.values.BString; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Map; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +public class BallerinaByteBlockInputStream extends InputStream { + + private final BObject iterator; + private final Environment env; + private final String nextMethodName; + private final Type returnType; + private final String strandName; + private final StrandMetadata metadata; + private final Map properties; + private final AtomicBoolean done = new AtomicBoolean(false); + private final MethodType closeMethod; + private final Consumer futureResultConsumer; + + private byte[] currentChunk = new byte[0]; + private int nextChunkIndex = 0; + + public BallerinaByteBlockInputStream(Environment env, BObject iterator, MethodType nextMethod, + MethodType closeMethod, Consumer futureResultConsumer) { + this.env = env; + this.iterator = iterator; + this.nextMethodName = nextMethod.getName(); + this.returnType = nextMethod.getReturnType(); + this.closeMethod = closeMethod; + this.strandName = env.getStrandName().orElse(""); + this.metadata = env.getStrandMetadata(); + this.properties = Map.of(); + this.futureResultConsumer = futureResultConsumer; + } + + @Override + public int read() { + if (done.get()) { + return -1; + } + if (hasBytesInCurrentChunk()) { + return currentChunk[nextChunkIndex++]; + } + // Need to get a new block from the stream, before reading again. + nextChunkIndex = 0; + try { + if (readNextChunk()) { + return read(); + } + } catch (InterruptedException e) { + BError error = DiagnosticLog.getYamlError("Cannot read the stream, interrupted error"); + futureResultConsumer.accept(error); + return -1; + } + return -1; + } + + @Override + public void close() throws IOException { + super.close(); + Semaphore semaphore = new Semaphore(0); + if (closeMethod != null) { + env.getRuntime().invokeMethodAsyncSequentially(iterator, closeMethod.getName(), strandName, metadata, + new Callback() { + @Override + public void notifyFailure(BError bError) { + semaphore.release(); + } + + @Override + public void notifySuccess(Object result) { + semaphore.release(); + } + }, properties, returnType); + } + try { + semaphore.acquire(); + } catch (InterruptedException e) { + throw new IOException("Error while closing the stream", e); + } + } + + private boolean hasBytesInCurrentChunk() { + return currentChunk.length != 0 && nextChunkIndex < currentChunk.length; + } + + private boolean readNextChunk() throws InterruptedException { + Semaphore semaphore = new Semaphore(0); + Callback callback = new Callback() { + + @Override + public void notifyFailure(BError bError) { + // Panic with an error + done.set(true); + futureResultConsumer.accept(bError); + currentChunk = new byte[0]; + semaphore.release(); + // TODO : Should we panic here? + } + + @Override + public void notifySuccess(Object result) { + if (result == null) { + done.set(true); + currentChunk = new byte[0]; + semaphore.release(); + return; + } + if (result instanceof BMap) { + BMap valueRecord = (BMap) result; + final BString value = Arrays.stream(valueRecord.getKeys()).findFirst().get(); + final BArray arrayValue = valueRecord.getArrayValue(value); + currentChunk = arrayValue.getByteArray(); + semaphore.release(); + } else { + // Case where Completes with an error + done.set(true); + semaphore.release(); + } + } + + }; + env.getRuntime().invokeMethodAsyncSequentially(iterator, nextMethodName, strandName, metadata, callback, + properties, returnType); + semaphore.acquire(); + return !done.get(); + } +} diff --git a/native/src/main/java/io/ballerina/lib/data/yaml/io/DataReaderTask.java b/native/src/main/java/io/ballerina/lib/data/yaml/io/DataReaderTask.java new file mode 100644 index 0000000..84ffa00 --- /dev/null +++ b/native/src/main/java/io/ballerina/lib/data/yaml/io/DataReaderTask.java @@ -0,0 +1,107 @@ +/* + * 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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.lib.data.yaml.io; + +import io.ballerina.lib.data.yaml.parser.YamlParser; +import io.ballerina.lib.data.yaml.utils.DiagnosticLog; +import io.ballerina.runtime.api.Environment; +import io.ballerina.runtime.api.Future; +import io.ballerina.runtime.api.types.MethodType; +import io.ballerina.runtime.api.types.ObjectType; +import io.ballerina.runtime.api.utils.TypeUtils; +import io.ballerina.runtime.api.values.BMap; +import io.ballerina.runtime.api.values.BObject; +import io.ballerina.runtime.api.values.BString; +import io.ballerina.runtime.api.values.BTypedesc; + +import java.io.InputStreamReader; +import java.util.function.Consumer; + +public class DataReaderTask implements Runnable { + + private static final String METHOD_NAME_NEXT = "next"; + private static final String METHOD_NAME_CLOSE = "close"; + + private final Environment env; + private final BObject iteratorObj; + private final Future future; + private final BTypedesc typed; + private final BMap options; + + public DataReaderTask(Environment env, BObject iteratorObj, Future future, BTypedesc typed, + BMap options) { + this.env = env; + this.iteratorObj = iteratorObj; + this.future = future; + this.typed = typed; + this.options = options; + } + + static MethodType resolveNextMethod(BObject iterator) { + MethodType method = getMethodType(iterator, METHOD_NAME_NEXT); + if (method != null) { + return method; + } + throw new IllegalStateException("next method not found in the iterator object"); + } + + static MethodType resolveCloseMethod(BObject iterator) { + return getMethodType(iterator, METHOD_NAME_CLOSE); + } + + private static MethodType getMethodType(BObject iterator, String methodNameClose) { + ObjectType objectType = (ObjectType) TypeUtils.getReferredType(iterator.getOriginalType()); + MethodType[] methods = objectType.getMethods(); + // Assumes compile-time validation of the iterator object + for (MethodType method : methods) { + if (method.getName().equals(methodNameClose)) { + return method; + } + } + return null; + } + + @Override + public void run() { + ResultConsumer resultConsumer = new ResultConsumer<>(future); + try (var byteBlockSteam = new BallerinaByteBlockInputStream(env, iteratorObj, resolveNextMethod(iteratorObj), + resolveCloseMethod(iteratorObj), resultConsumer)) { + Object result = YamlParser.compose(new InputStreamReader(byteBlockSteam), + options, typed.getDescribingType()); + future.complete(result); + } catch (Exception e) { + future.complete(DiagnosticLog.getYamlError("Error occurred while reading the stream: " + e.getMessage())); + } + } + + /** + * This class will hold module related utility functions. + * + * @param The type of the result + * @param future The future to complete + * @since 0.1.0 + */ + public record ResultConsumer(Future future) implements Consumer { + + @Override + public void accept(T t) { + future.complete(t); + } + } +} diff --git a/native/src/main/java/io/ballerina/lib/data/yaml/io/DataReaderThreadPool.java b/native/src/main/java/io/ballerina/lib/data/yaml/io/DataReaderThreadPool.java new file mode 100644 index 0000000..df1aee6 --- /dev/null +++ b/native/src/main/java/io/ballerina/lib/data/yaml/io/DataReaderThreadPool.java @@ -0,0 +1,51 @@ +/* + * 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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.lib.data.yaml.io; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class DataReaderThreadPool { + + // TODO : Make this configurable, in Ballerina Library. + private static final int CORE_POOL_SIZE = 0; + private static final int MAX_POOL_SIZE = 50; + private static final long KEEP_ALIVE_TIME = 60L; + private static final String THREAD_NAME = "bal-data-yaml-thread"; + public static final ExecutorService EXECUTOR_SERVICE = new ThreadPoolExecutor(CORE_POOL_SIZE, + MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS, new SynchronousQueue<>(), new DataThreadFactory()); + + /** + * Thread factory for data reader. + * + * @since 0.1.0 + */ + static class DataThreadFactory implements ThreadFactory { + + @Override + public Thread newThread(Runnable runnable) { + Thread ballerinaData = new Thread(runnable); + ballerinaData.setName(THREAD_NAME); + return ballerinaData; + } + } +} diff --git a/native/src/main/java/io/ballerina/lib/data/yaml/lexer/CharacterReader.java b/native/src/main/java/io/ballerina/lib/data/yaml/lexer/CharacterReader.java new file mode 100644 index 0000000..73e273f --- /dev/null +++ b/native/src/main/java/io/ballerina/lib/data/yaml/lexer/CharacterReader.java @@ -0,0 +1,165 @@ +/* + * 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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.lib.data.yaml.lexer; + +import io.ballerina.lib.data.yaml.utils.DiagnosticErrorCode; +import io.ballerina.lib.data.yaml.utils.DiagnosticLog; +import io.ballerina.lib.data.yaml.utils.Error; + +import java.io.IOException; +import java.io.Reader; +import java.util.Arrays; + +/** + * Read and Consume input stream. + * + * @since 0.1.0 + */ +public class CharacterReader { + private final Reader reader; + private final char[] buff; // data chucks are read into this buffer + private int[] dataBuffer; // store the read characters as code points + private int dataBufferSize = 0; // length of the data buffer + private int remainingBufferedSize = 0; + private int pointer = 0; // current position in the data buffer + private boolean eof = false; // flag saying end of the stream reached + private int line = 1; // current line number + private int column = 0; // current column number + + public CharacterReader(Reader reader) { + this.reader = reader; + this.dataBuffer = new int[0]; + this.buff = new char[1024]; + } + + /** + * Peeks the k-th indexed code point. + * + * @param k number of characters to peek + * @return code point at the peek + */ + public int peek(int k) { + if (k >= 0 && checkAndReadData(k) && pointer + k >= 0) { + return dataBuffer[pointer + k]; + } + return -1; + } + + /** + * Moves the internal pointer forward by the specified amount (`k`). + * + * @param k The number of positions to move forward. + */ + public boolean forward(int k) { + int i; + for (i = 0; i < k && checkAndReadData(k); i++) { + int codePoint = dataBuffer[pointer++]; + if (hasNewLine(codePoint)) { + this.remainingBufferedSize -= column + 1; + this.column = 0; + this.line++; + } else if (codePoint != 0xFEFF) { + this.column++; + } + } + return i == 0; + } + + private boolean hasNewLine(int codePoint) { + return codePoint == '\n'; + } + + private boolean checkAndReadData(int k) { + if (!eof && pointer + k >= dataBufferSize) { + readData(); + } + return (pointer + k) < dataBufferSize; + } + + private void readData() { + try { + int size = reader.read(buff); + if (size <= 0) { + this.eof = true; + return; + } + + int cpIndex = dataBufferSize - pointer; + this.dataBuffer = Arrays.copyOfRange(dataBuffer, pointer, dataBufferSize + size); + + for (int i = 0; i < size; cpIndex++) { + int codePoint = Character.codePointAt(buff, i); + dataBuffer[cpIndex] = codePoint; + if (isPrintable(codePoint)) { + i += Character.charCount(codePoint); + } else { + NewLineIndexData newLineIndexData = findLastNewLineIndexAndNewLineCount(); + line += newLineIndexData.newLineCount; + column += newLineIndexData.lastNewLineIndex == -1 ? cpIndex + : cpIndex - newLineIndexData.lastNewLineIndex; + throw new Error.YamlParserException("non printable character found", line, column); + } + } + dataBufferSize = cpIndex; + remainingBufferedSize = dataBufferSize; + pointer = 0; + } catch (Error.YamlParserException e) { + throw DiagnosticLog.error(DiagnosticErrorCode.YAML_PARSER_EXCEPTION, e.getMessage(), line, column); + } catch (IOException e) { + throw DiagnosticLog.error(DiagnosticErrorCode.YAML_READER_FAILURE, e.getMessage()); + } + } + + private record NewLineIndexData(int lastNewLineIndex, int newLineCount) { + }; + + private NewLineIndexData findLastNewLineIndexAndNewLineCount() { + int idx = -1; + int count = 0; + for (int i = 0; i < this.dataBuffer.length; i++) { + if (this.dataBuffer[i] == 10) { + idx = i; + count++; + } + } + return new NewLineIndexData(idx, count); + } + + private static boolean isPrintable(int codePoint) { + return (codePoint >= 32 && codePoint <= 126) || (codePoint >= 160 && codePoint <= 55295) + || (codePoint >= 57344 && codePoint <= 65533) || (codePoint >= 65536 && codePoint <= 1114111) + || codePoint == 9 || codePoint == 10 || codePoint == 13 || codePoint == 133; + } + + public boolean isEof() { + return eof; + } + + public int getLine() { + return line; + } + + public int getColumn() { + return column; + } + + public int getRemainingBufferedSize() { + return remainingBufferedSize; + } +} diff --git a/native/src/main/java/io/ballerina/lib/data/yaml/lexer/IndentUtils.java b/native/src/main/java/io/ballerina/lib/data/yaml/lexer/IndentUtils.java new file mode 100644 index 0000000..35186a2 --- /dev/null +++ b/native/src/main/java/io/ballerina/lib/data/yaml/lexer/IndentUtils.java @@ -0,0 +1,287 @@ +/* + * 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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.lib.data.yaml.lexer; + +import io.ballerina.lib.data.yaml.common.Types.Collection; +import io.ballerina.lib.data.yaml.utils.Error; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * This class will hold utility functions process indentations. + * + * @since 0.1.0 + */ +public class IndentUtils { + + public record Indent(int column, Collection collection) { + } + + public record Indentation(IndentationChange change, + List collection, List tokens) { + + public enum IndentationChange { + INDENT_INCREASE(+1), + INDENT_NO_CHANGE(0), + INDENT_DECREASE(-1); + + IndentationChange(int i) { + } + } + } + + /** Check if the current index have sufficient indent. + * + * @param lexerState - Current lexer state + */ + public static void assertIndent(LexerState lexerState) throws Error.YamlParserException { + assertIndent(lexerState, 0, false); + } + + /** Check if the current index have sufficient indent. + * + * @param lexerState - Current lexer state + * @param offset - Additional white spaces after the parent indent + */ + public static void assertIndent(LexerState lexerState, int offset) throws Error.YamlParserException { + assertIndent(lexerState, offset, false); + } + + /** Check if the current index have sufficient indent. + * + * @param lexerState - Current lexer state + * @param offset - Additional white spaces after the parent indent + * @param captureIndentationBreak The token is allowed as prefix to a mapping key name + */ + public static void assertIndent(LexerState lexerState, int offset, boolean captureIndentationBreak) + throws Error.YamlParserException { + if (lexerState.getColumn() < lexerState.getIndent() + offset) { + if (captureIndentationBreak) { + lexerState.setIndentationBreak(true); + return; + } + throw new Error.YamlParserException("invalid indentation", lexerState.getIndent(), lexerState.getColumn()); + } + } + + public static boolean isTabInIndent(LexerState lexerState, int upperLimit) { + int tabInWhitespace = lexerState.getTabInWhitespace(); + return lexerState.getIndent() > -1 && tabInWhitespace > -1 && tabInWhitespace <= upperLimit; + } + + /** + * Differentiate the planar and anchor keys against the key of a mapping. + * + * @param lexerState Current state of the lexer + * @param outputToken - Planar or anchor key + */ + public static void handleMappingValueIndent(LexerState lexerState, Token.TokenType outputToken) + throws Error.YamlParserException { + handleMappingValueIndent(lexerState, outputToken, null); + } + + /** + * Differentiate the planar and anchor keys against the key of a mapping. + * + * @param lexerState Current state of the lexer + * @param outputToken - Planar or anchor key + * @param scan - Scanner instance use for scanning + */ + public static void handleMappingValueIndent(LexerState lexerState, Token.TokenType outputToken, + Scanner.Scan scan) throws Error.YamlParserException { + lexerState.setIndentationBreak(false); + boolean enforceMapping = lexerState.getEnforceMapping(); + lexerState.setEnforceMapping(false); + + boolean notSufficientIndent = false; + if (scan == null) { + lexerState.forward(); + lexerState.tokenize(outputToken); + notSufficientIndent = lexerState.getColumn() < lexerState.getIndentStartIndex(); + } else { + try { + assertIndent(lexerState, 1); + } catch (Error.YamlParserException ex) { + notSufficientIndent = true; + } + lexerState.updateStartIndex(); + Scanner.iterate(lexerState, scan, outputToken); + } + + if (lexerState.isFlowCollection()) { + return; + } + + // Ignore whitespace until a character is found + int numWhitespace = 0; + while (Utils.WHITE_SPACE_PATTERN.pattern(lexerState.peek(numWhitespace))) { + numWhitespace += 1; + } + + if (notSufficientIndent) { + if (lexerState.peek(numWhitespace) == ':' && !lexerState.isFlowCollection()) { + lexerState.forward(numWhitespace); + lexerState.setIndentation(handleIndent(lexerState, lexerState.getIndentStartIndex())); + return; + } + + throw new Error.YamlParserException("insufficient indentation for a scalar", + lexerState.getLine(), lexerState.getColumn()); + } + + if (lexerState.peek(numWhitespace) == ':' && !lexerState.isFlowCollection()) { + lexerState.forward(numWhitespace); + lexerState.setIndentation(handleIndent(lexerState, lexerState.getIndentStartIndex())); + return; + } + + if (enforceMapping) { + throw new Error.YamlParserException("insufficient indentation for a scalar", + lexerState.getLine(), lexerState.getColumn()); + } + } + + public static Indentation handleIndent(LexerState sm) throws Error.YamlParserException { + return handleIndent(sm, null); + } + + /** + * Validate the indentation of block collections. + */ + public static Indentation handleIndent(LexerState sm, Integer mapIndex) throws Error.YamlParserException { + int startIndex = mapIndex == null ? sm.getColumn() - 1 : mapIndex; + + if (mapIndex != null) { + sm.setKeyDefinedForLine(true); + } + + if (isTabInIndent(sm, startIndex)) { + throw new Error.YamlParserException("cannot have tab as an indentation", sm.getLine(), sm.getColumn()); + } + + Collection collection = mapIndex == null ? Collection.SEQUENCE : Collection.MAPPING; + + if (sm.getIndent() == startIndex) { + + List existingIndentType = sm.getIndents().stream() + .filter(indent -> indent.column() == startIndex) + .map(Indent::collection) + .toList(); + + // The current token is a mapping key and a sequence entry exists for the indent + if (collection == Collection.MAPPING + && existingIndentType.contains(Collection.SEQUENCE)) { + if (existingIndentType.contains(Collection.MAPPING)) { + return new Indentation( + Indentation.IndentationChange.INDENT_DECREASE, + new ArrayList<>(Collections.singleton(sm.getIndents().pop().collection())), + sm.getClonedTokensForMappingValue()); + } else { + throw new Error.YamlParserException("block mapping cannot have the " + + "same indent as a block sequence", sm.getLine(), sm.getColumn()); + } + } + + // The current token is a sequence entry and a mapping key exists for the indent + if (collection == Collection.SEQUENCE + && existingIndentType.contains(Collection.MAPPING)) { + if (existingIndentType.contains(Collection.SEQUENCE)) { + return new Indentation( + Indentation.IndentationChange.INDENT_NO_CHANGE, + new ArrayList<>(), + sm.getClonedTokensForMappingValue()); + } else { + sm.getIndents().push(new Indent(startIndex, Collection.SEQUENCE)); + return new Indentation( + Indentation.IndentationChange.INDENT_INCREASE, + new ArrayList<>(Collections.singleton(Collection.SEQUENCE)), + sm.getClonedTokensForMappingValue()); + } + } + return new Indentation( + Indentation.IndentationChange.INDENT_NO_CHANGE, + new ArrayList<>(), + sm.getClonedTokensForMappingValue()); + } + + if (sm.getIndent() < startIndex) { + sm.getIndents().push(new Indent(startIndex, collection)); + sm.setIndent(startIndex); + return new Indentation( + Indentation.IndentationChange.INDENT_INCREASE, + new ArrayList<>(Collections.singleton(collection)), + sm.getClonedTokensForMappingValue()); + } + + if (sm.getIndent() > startIndex) { + + List existingIndentType = sm.getIndents().stream() + .filter(indent -> indent.column() == startIndex) + .map(Indent::collection) + .toList(); + + // The current token is a mapping key and a sequence entry exists for the indent + if (collection == Collection.MAPPING + && existingIndentType.contains(Collection.SEQUENCE)) { + if (!existingIndentType.contains(Collection.MAPPING)) { + throw new Error.YamlParserException("block mapping cannot have the " + + "same indent as a block sequence", sm.getLine(), sm.getColumn()); + } + } + } + + Indent removedIndent = null; + List returnCollection = new ArrayList<>(); + + int indentsSize = sm.getIndents().size(); + while (sm.getIndent() > startIndex && indentsSize > 0) { + removedIndent = sm.getIndents().pop(); + sm.setIndent(removedIndent.column()); + returnCollection.add(removedIndent.collection()); + --indentsSize; + } + + + if (indentsSize > 0 && removedIndent != null) { + Indent removedSecondIndent = sm.getIndents().pop(); + if (removedSecondIndent.column() == startIndex && collection == Collection.MAPPING) { + returnCollection.add(removedIndent.collection()); + } else { + sm.getIndents().add(removedSecondIndent); + } + } + + int indent = sm.getIndent(); + if (indent == startIndex) { + sm.getIndents().push(new Indent(indent, collection)); + int returnCollectionSize = returnCollection.size(); + if (returnCollectionSize > 1) { + returnCollection.remove(returnCollectionSize - 1); + return new Indentation( + Indentation.IndentationChange.INDENT_DECREASE, + returnCollection, + sm.getClonedTokensForMappingValue()); + } + } + + throw new Error.YamlParserException("invalid indentation", sm.getLine(), sm.getColumn()); + } +} diff --git a/native/src/main/java/io/ballerina/lib/data/yaml/lexer/LexerState.java b/native/src/main/java/io/ballerina/lib/data/yaml/lexer/LexerState.java new file mode 100644 index 0000000..c661ba8 --- /dev/null +++ b/native/src/main/java/io/ballerina/lib/data/yaml/lexer/LexerState.java @@ -0,0 +1,1041 @@ +/* + * 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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.lib.data.yaml.lexer; + +import io.ballerina.lib.data.yaml.utils.Error; + +import java.util.ArrayList; +import java.util.List; +import java.util.Stack; + +import static io.ballerina.lib.data.yaml.lexer.Scanner.COMMENT_SCANNER; +import static io.ballerina.lib.data.yaml.lexer.Scanner.VERBATIM_URI_SCANNER; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.ALIAS; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.ANCHOR; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.CHOMPING_INDICATOR; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.COMMENT; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.DECIMAL; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.DIRECTIVE; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.DIRECTIVE_MARKER; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.DOCUMENT_MARKER; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.DOT; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.DOUBLE_QUOTE_CHAR; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.DOUBLE_QUOTE_DELIMITER; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.EMPTY_LINE; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.EOL; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.FOLDED; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.LITERAL; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.MAPPING_END; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.MAPPING_KEY; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.MAPPING_START; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.MAPPING_VALUE; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.PLANAR_CHAR; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.PRINTABLE_CHAR; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.SEPARATION_IN_LINE; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.SEPARATOR; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.SEQUENCE_END; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.SEQUENCE_ENTRY; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.SEQUENCE_START; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.SINGLE_QUOTE_CHAR; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.SINGLE_QUOTE_DELIMITER; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.TAG; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.TAG_HANDLE; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.TAG_PREFIX; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.TRAILING_COMMENT; +import static io.ballerina.lib.data.yaml.lexer.Utils.DECIMAL_PATTERN; +import static io.ballerina.lib.data.yaml.lexer.Utils.FLOW_INDICATOR_PATTERN; +import static io.ballerina.lib.data.yaml.lexer.Utils.URI_PATTERN; +import static io.ballerina.lib.data.yaml.lexer.Utils.WHITE_SPACE_PATTERN; +import static io.ballerina.lib.data.yaml.lexer.Utils.WORD_PATTERN; +import static io.ballerina.lib.data.yaml.lexer.Utils.checkCharacters; +import static io.ballerina.lib.data.yaml.lexer.Utils.discernPlanarFromIndicator; +import static io.ballerina.lib.data.yaml.lexer.Utils.getWhitespace; + +/** + * State of the YAML Lexer. + * + * @since 0.1.0 + */ +public class LexerState { + + public static final State LEXER_START_STATE = new StartState(); + public static final State LEXER_TAG_HANDLE_STATE = new TagHandleState(); + public static final State LEXER_TAG_PREFIX_STATE = new TagPrefixState(); + public static final State LEXER_NODE_PROPERTY = new NodePropertyState(); + public static final State LEXER_DIRECTIVE = new DirectiveState(); + public static final State LEXER_DOUBLE_QUOTE = new DoubleQuoteState(); + public static final State LEXER_SINGLE_QUOTE = new SingleQuoteState(); + public static final State LEXER_BLOCK_HEADER = new BlockHeaderState(); + public static final State LEXER_LITERAL = new LiteralState(); + public static final State LEXER_RESERVED_DIRECTIVE = new ReservedDirectiveState(); + private State state = LEXER_START_STATE; + private final CharacterReader characterReader; + private Token.TokenType token = null; + private String lexeme = ""; + private String lexemeBuffer = ""; + private IndentUtils.Indentation indentation = null; + private int tabInWhitespace = -1; + private int numOpenedFlowCollections = 0; + private int indent = -1; + private int indentStartIndex = -1; + private boolean indentationBreak = false; + private boolean enforceMapping = false; + private boolean keyDefinedForLine = false; + private Stack indents = new Stack<>(); + private List tokensForMappingValue = new ArrayList<>(); + private boolean isJsonKey = false; + private int mappingKeyColumn = -1; + private int addIndent = 1; + private boolean captureIndent; + private boolean firstLine = true; + private boolean trailingComment = false; + private boolean isNewLine = false; + private boolean allowTokensAsPlanar = false; + private int lastEscapedChar = -1; + private boolean eofStream = false; + + public LexerState(CharacterReader characterReader) { + this.characterReader = characterReader; + } + + public int peek() { + return peek(0); + } + + public int peek(int k) { + return characterReader.peek(k); + } + + public void forward() { + eofStream = characterReader.forward(1); + } + + public void forward(int k) { + eofStream = characterReader.forward(k); + } + + public void updateStartIndex() { + updateStartIndex(null); + } + + public void updateStartIndex(Token.TokenType token) { + if (token != null) { + tokensForMappingValue.add(token); + } + int column = getColumn(); + if (column < indentStartIndex || indentStartIndex < 0) { + indentStartIndex = column; + } + } + + public void resetState() { + addIndent = 1; + captureIndent = false; + enforceMapping = false; + indentStartIndex = -1; + indent = -1; + indents = new Stack<>(); + lexeme = ""; + state = LEXER_START_STATE; + } + + public Token getToken() { + Token.TokenType tokenBuffer = token; + token = Token.TokenType.DUMMY; + String lexemeBuffer = lexeme; + lexeme = ""; + IndentUtils.Indentation indentationBuffer = indentation; + indentation = null; + return new Token(tokenBuffer, lexemeBuffer, indentationBuffer); + } + + public State getState() { + return state; + } + + public void updateLexerState(State state) { + this.state = state; + } + + public void updateFirstTabIndex() { + int column = getColumn(); + if (column < tabInWhitespace || tabInWhitespace < 0) { + tabInWhitespace = column; + } + } + + public void tokenize(Token.TokenType token) { + this.token = token; + } + + public int getColumn() { + return characterReader.getColumn(); + } + + public int getRemainingBufferedSize() { + return characterReader.getRemainingBufferedSize(); + } + + public boolean isFlowCollection() { + return numOpenedFlowCollections > 0; + } + + public int getIndent() { + return indent; + } + + public void setIndent(int indent) { + this.indent = indent; + } + + public void setIndentationBreak(boolean indentationBreak) { + this.indentationBreak = indentationBreak; + } + + public int getTabInWhitespace() { + return tabInWhitespace; + } + + public boolean getEnforceMapping() { + return enforceMapping; + } + + public void setEnforceMapping(boolean enforceMapping) { + this.enforceMapping = enforceMapping; + } + + public int getIndentStartIndex() { + return indentStartIndex; + } + + public void setLexeme(String value) { + lexeme = value; + } + + public void appendToLexeme(String value) { + lexeme += value; + } + + public void setKeyDefinedForLine(boolean keyDefinedForLine) { + this.keyDefinedForLine = keyDefinedForLine; + } + + public Stack getIndents() { + return indents; + } + + public List getClonedTokensForMappingValue() { + return List.copyOf(tokensForMappingValue); + } + + public void setIndentation(IndentUtils.Indentation indentation) { + this.indentation = indentation; + } + + public String getLexeme() { + return lexeme; + } + + public String getLexemeBuffer() { + return lexemeBuffer; + } + + public void setLexemeBuffer(String lexemeBuffer) { + this.lexemeBuffer = lexemeBuffer; + } + + public boolean isIndentationBreak() { + return indentationBreak; + } + + public void setNewLine(boolean newLine) { + isNewLine = newLine; + } + + public boolean isJsonKey() { + return isJsonKey; + } + + public void setJsonKey(boolean jsonKey) { + isJsonKey = jsonKey; + } + + public void setTrailingComment(boolean trailingComment) { + this.trailingComment = trailingComment; + } + + public boolean isFirstLine() { + return firstLine; + } + + public void setFirstLine(boolean firstLine) { + this.firstLine = firstLine; + } + + public void setAllowTokensAsPlanar(boolean allowTokensAsPlanar) { + this.allowTokensAsPlanar = allowTokensAsPlanar; + } + + public boolean isEndOfStream() { + return eofStream && characterReader.isEof(); + } + + public int getLastEscapedChar() { + return lastEscapedChar; + } + + public void setLastEscapedChar(int lastEscapedChar) { + this.lastEscapedChar = lastEscapedChar; + } + + public void setEofStream(boolean eofStream) { + this.eofStream = eofStream; + } + + public int getLine() { + return characterReader.getLine(); + } + + public void updateNewLineProps() { + lastEscapedChar = -1; + indentStartIndex = -1; + tokensForMappingValue = new ArrayList<>(); + tabInWhitespace = -1; + isNewLine = false; + keyDefinedForLine = false; + } + + public interface State { + State transition(LexerState lexerState) throws Error.YamlParserException; + } + + private static class StartState implements State { + + /** + * Scan the lexemes that are without any explicit context. + * + * @param lexerState Current lexer state + * @return Updated state context + */ + @Override + public State transition(LexerState lexerState) throws Error.YamlParserException { + boolean isFirstChar = lexerState.getColumn() == 0; + boolean startsWithWhiteSpace = false; + + if (WHITE_SPACE_PATTERN.pattern(lexerState.peek())) { + Scanner.iterate(lexerState, Scanner.WHITE_SPACE_SCANNER, SEPARATION_IN_LINE); + startsWithWhiteSpace = true; + } + + if (lexerState.isFlowCollection() && isFirstChar && lexerState.peek() != -1) { + IndentUtils.assertIndent(lexerState); + if (IndentUtils.isTabInIndent(lexerState, lexerState.getIndent())) { + throw new Error.YamlParserException("cannot have tabs as an indentation", + lexerState.getLine(), lexerState.getColumn()); + } + } + + if (startsWithWhiteSpace) { + if (lexerState.peek() == -1 && isFirstChar) { + lexerState.tokenize(EMPTY_LINE); + } + return this; + } + + if (Utils.isComment(lexerState)) { + Scanner.iterate(lexerState, COMMENT_SCANNER, COMMENT); + lexerState.tokenize(COMMENT); + return this; + } + + if (Utils.isMarker(lexerState, true)) { + lexerState.forward(); + lexerState.tokenize(DIRECTIVE_MARKER); + return this; + } + + if (Utils.isMarker(lexerState, false)) { + lexerState.forward(); + lexerState.tokenize(DOCUMENT_MARKER); + return this; + } + + switch (lexerState.peek()) { + case '-' -> { + // Scan for planar characters + if (discernPlanarFromIndicator(lexerState)) { + lexerState.updateStartIndex(); + lexerState.forward(); + lexerState.lexeme += "-"; + IndentUtils.handleMappingValueIndent(lexerState, PLANAR_CHAR, Scanner.PLANAR_CHAR_SCANNER); + return this; + } + + if (lexerState.indent < lexerState.getColumn() && lexerState.allowTokensAsPlanar) { + lexerState.lexeme += "-"; + lexerState.forward(); + Scanner.iterate(lexerState, Scanner.PLANAR_CHAR_SCANNER, PLANAR_CHAR); + return this; + } + + // Return block sequence entry + lexerState.forward(); + lexerState.tokenize(SEQUENCE_ENTRY); + lexerState.indentation = IndentUtils.handleIndent(lexerState); + return this; + } + case '*' -> { + lexerState.updateStartIndex(); + lexerState.forward(); + IndentUtils.handleMappingValueIndent(lexerState, ALIAS, Scanner.ANCHOR_NAME_SCANNER); + return this; + } + case '%' -> { // Directive line + if (lexerState.allowTokensAsPlanar) { + IndentUtils.assertIndent(lexerState, 1); + lexerState.lexeme += "%"; + Scanner.iterate(lexerState, Scanner.PLANAR_CHAR_SCANNER, PLANAR_CHAR); + return this; + } + lexerState.forward(); + Scanner.iterate(lexerState, Scanner.PRINTABLE_CHAR_SCANNER, DIRECTIVE); + return this; + } + case '!' -> { // Node tags + // Check if the tag can be considered as a planar + if (lexerState.allowTokensAsPlanar) { + IndentUtils.assertIndent(lexerState, 1); + lexerState.forward(); + lexerState.lexeme += "!"; + Scanner.iterate(lexerState, Scanner.PLANAR_CHAR_SCANNER, PLANAR_CHAR); + return this; + } + + // Process the tag token + IndentUtils.assertIndent(lexerState, 1, true); + lexerState.updateStartIndex(TAG); + switch (lexerState.peek(1)) { + case '<' -> { // Verbatim tag + lexerState.forward(2); + + if (lexerState.peek() == '!' && lexerState.peek(1) == '>') { + throw new Error.YamlParserException("'verbatim tag' is not resolved. " + + "Hence, '!' is invalid", lexerState.getLine(), lexerState.getColumn()); + } + + int peek = lexerState.peek(); + if (peek != -1 && (URI_PATTERN.pattern(peek) || WORD_PATTERN.pattern(peek))) { + Scanner.iterate(lexerState, VERBATIM_URI_SCANNER, TAG, true); + return this; + } else { + throw new Error.YamlParserException("expected a 'uri-char' " + + "after '<' in a 'verbatim tag'", lexerState.getLine(), lexerState.getColumn()); + } + } + case ' ', -1, '\t' -> { // Non-specific tag + lexerState.lexeme = "!"; + lexerState.forward(); + lexerState.tokenize(TAG); + return this; + } + case '!' -> { // Secondary tag handle + lexerState.lexeme = "!!"; + lexerState.forward(2); + lexerState.tokenize(TAG_HANDLE); + return this; + } + default -> { // Check for primary and name tag handles + lexerState.lexeme = "!"; + Scanner.iterate(lexerState, Scanner.DIFF_TAG_HANDLE_SCANNER, TAG_HANDLE, true); + return this; + } + } + } + case '&' -> { + IndentUtils.assertIndent(lexerState, 1); + if (lexerState.allowTokensAsPlanar) { + lexerState.forward(); + lexerState.lexeme += "&"; + Scanner.iterate(lexerState, Scanner.PLANAR_CHAR_SCANNER, PLANAR_CHAR); + return this; + } + IndentUtils.assertIndent(lexerState, 1, true); + lexerState.updateStartIndex(ANCHOR); + lexerState.forward(); + Scanner.iterate(lexerState, Scanner.ANCHOR_NAME_SCANNER, ANCHOR); + return this; + } + case ':' -> { + if (!lexerState.isJsonKey && discernPlanarFromIndicator(lexerState)) { + lexerState.lexeme += ":"; + lexerState.updateStartIndex(); + lexerState.forward(); + IndentUtils.handleMappingValueIndent(lexerState, PLANAR_CHAR, Scanner.PLANAR_CHAR_SCANNER); + return this; + } + + // Capture the for empty key mapping values + if (!lexerState.keyDefinedForLine && !lexerState.isFlowCollection()) { + if (lexerState.mappingKeyColumn != lexerState.getColumn() + && !lexerState.isFlowCollection() && lexerState.mappingKeyColumn > -1) { + throw new Error.YamlParserException("'?' and ':' should have the same indentation", + lexerState.getLine(), lexerState.getColumn()); + } + if (lexerState.mappingKeyColumn == -1) { + lexerState.updateStartIndex(); + lexerState.keyDefinedForLine = true; + lexerState.indentation = IndentUtils.handleIndent(lexerState, lexerState.indentStartIndex); + } + lexerState.mappingKeyColumn = -1; + } + lexerState.forward(); + lexerState.tokenize(MAPPING_VALUE); + return this; + } + case '?' -> { + if (discernPlanarFromIndicator(lexerState)) { + lexerState.lexeme += "?"; + lexerState.updateStartIndex(); + lexerState.forward(); + IndentUtils.handleMappingValueIndent(lexerState, PLANAR_CHAR, Scanner.PLANAR_CHAR_SCANNER); + return this; + } + lexerState.mappingKeyColumn = lexerState.getColumn(); + lexerState.forward(); + lexerState.tokenize(MAPPING_KEY); + + // Capture the for empty key mapping values + if (!lexerState.isFlowCollection()) { + lexerState.indentation = IndentUtils.handleIndent(lexerState, + lexerState.getColumn() - 1); + } + return this; + } + case '\"' -> { // Process double quote flow style value + lexerState.updateStartIndex(); + lexerState.forward(); + lexerState.tokenize(DOUBLE_QUOTE_DELIMITER); + return this; + } + case '\'' -> { + lexerState.updateStartIndex(); + lexerState.forward(); + lexerState.tokenize(SINGLE_QUOTE_DELIMITER); + return this; + } + case ',' -> { + lexerState.forward(); + lexerState.tokenize(SEPARATOR); + return this; + } + case '[' -> { + IndentUtils.assertIndent(lexerState, 1); + lexerState.numOpenedFlowCollections += 1; + lexerState.forward(); + lexerState.tokenize(SEQUENCE_START); + return this; + } + case ']' -> { + lexerState.numOpenedFlowCollections -= 1; + lexerState.forward(); + lexerState.tokenize(SEQUENCE_END); + return this; + } + case '{' -> { + IndentUtils.assertIndent(lexerState, 1); + lexerState.numOpenedFlowCollections += 1; + lexerState.forward(); + lexerState.tokenize(MAPPING_START); + return this; + } + case '}' -> { + lexerState.numOpenedFlowCollections -= 1; + lexerState.forward(); + lexerState.tokenize(MAPPING_END); + return this; + } + case '|' -> { + lexerState.addIndent = 1; + lexerState.captureIndent = true; + lexerState.forward(); + lexerState.tokenize(LITERAL); + return this; + } + case '>' -> { // Block scalars + lexerState.addIndent = 1; + lexerState.captureIndent = true; + lexerState.forward(); + lexerState.tokenize(FOLDED); + return this; + } + } + + if (Utils.isPlainSafe(lexerState)) { + IndentUtils.handleMappingValueIndent(lexerState, PLANAR_CHAR, Scanner.PLANAR_CHAR_SCANNER); + return this; + } + + throw new Error.YamlParserException("invalid yaml document", lexerState.getLine(), lexerState.getColumn()); + } + } + + private static class TagHandleState implements State { + + /** + * Scan the lexemes for YAML tag handle directive. + * + * @param lexerState - Current lexer state + * @return Updated lexer state + */ + @Override + public State transition(LexerState lexerState) throws Error.YamlParserException { + + // Check fo primary, secondary, and named tag handles + if (lexerState.peek() == '!') { + switch (lexerState.peek(1)) { + case ' ', '\t' -> { // Primary tag handle + lexerState.lexeme = "!"; + lexerState.forward(); + lexerState.tokenize(TAG_HANDLE); + return this; + } + case '!' -> { // Secondary tag handle + lexerState.lexeme = "!!"; + lexerState.forward(2); + lexerState.tokenize(TAG_HANDLE); + return this; + } + case -1 -> { + throw new Error.YamlParserException("expected a separation in line after primary tag handle", + lexerState.getLine(), lexerState.getColumn()); + } + default -> { // Check for named tag handles + lexerState.lexeme = "!"; + lexerState.forward(); + Scanner.iterate(lexerState, Scanner.TAG_HANDLE_SCANNER, TAG_HANDLE, true); + return this; + } + } + } + + // Check for separation-in-space before the tag prefix + if (WHITE_SPACE_PATTERN.pattern(lexerState.peek())) { + Scanner.iterate(lexerState, Scanner.WHITE_SPACE_SCANNER, SEPARATION_IN_LINE); + return this; + } + + throw new Error.YamlParserException("expected '!' to start the tag handle", + lexerState.getLine(), lexerState.getColumn()); + } + } + + private static class TagPrefixState implements State { + + /** + * Scan the lexemes for YAML tag prefix directive. + * + * @param lexerState - Current lexer state. + */ + @Override + public State transition(LexerState lexerState) throws Error.YamlParserException { + if (Utils.matchPattern(lexerState, List.of(URI_PATTERN, WORD_PATTERN, new Utils.CharPattern('%')), + List.of(FLOW_INDICATOR_PATTERN))) { + Scanner.iterate(lexerState, Scanner.URI_SCANNER, TAG_PREFIX); + return this; + } + + // Check for tail separation-in-line + if (WHITE_SPACE_PATTERN.pattern(lexerState.peek())) { + Scanner.iterate(lexerState, Scanner.WHITE_SPACE_SCANNER, SEPARATION_IN_LINE); + return this; + } + + throw new Error.YamlParserException("invalid tag prefix character", + lexerState.getLine(), lexerState.getColumn()); + } + } + + private static class NodePropertyState implements State { + + /** + * Scan the lexemes for tag node properties. + * + * @param lexerState - Current lexer state. + */ + @Override + public State transition(LexerState lexerState) throws Error.YamlParserException { + // Scan the anchor node + if (lexerState.peek() == '&') { + lexerState.forward(); + Scanner.iterate(lexerState, Scanner.ANCHOR_NAME_SCANNER, ANCHOR); + return this; + } + + // Match the tag with the tag character pattern + if (Utils.isTagChar(lexerState)) { + Scanner.iterate(lexerState, Scanner.TAG_CHARACTER_SCANNER, TAG); + return this; + } + + // Check for tail separation-in-line + if (WHITE_SPACE_PATTERN.pattern(lexerState.peek())) { + Scanner.iterate(lexerState, Scanner.WHITE_SPACE_SCANNER, SEPARATION_IN_LINE); + return this; + } + + throw new Error.YamlParserException("invalid character tag", lexerState.getLine(), lexerState.getColumn()); + } + } + + private static class DirectiveState implements State { + + /** + * Scan the lexemes for YAML version directive. + * + * @param lexerState - Current lexer state. + */ + @Override + public State transition(LexerState lexerState) throws Error.YamlParserException { + // Check for decimal digits + if (Utils.matchPattern(lexerState, List.of(DECIMAL_PATTERN))) { + Scanner.iterate(lexerState, Scanner.DIGIT_SCANNER, DECIMAL); + return this; + } + + // Check for decimal point + if (lexerState.peek() == '.') { + lexerState.forward(); + lexerState.tokenize(DOT); + return this; + } + + // Check for tail separation-in-line + if (WHITE_SPACE_PATTERN.pattern(lexerState.peek())) { + Scanner.iterate(lexerState, Scanner.WHITE_SPACE_SCANNER, SEPARATION_IN_LINE); + return this; + } + + throw new Error.YamlParserException("invalid version number character", + lexerState.getLine(), lexerState.getColumn()); + } + } + + private static class DoubleQuoteState implements State { + + /** + * Scan the lexemes for double-quoted scalars. + * + * @param lexerState - Current lexer state. + */ + @Override + public State transition(LexerState lexerState) throws Error.YamlParserException { + if (Utils.isMarker(lexerState, true)) { + lexerState.forward(); + lexerState.tokenize(DIRECTIVE_MARKER); + return this; + } + if (Utils.isMarker(lexerState, false)) { + lexerState.forward(); + lexerState.tokenize(DOCUMENT_MARKER); + return this; + } + + // Check for empty lines + if (WHITE_SPACE_PATTERN.pattern(lexerState.peek())) { + String whitespace = getWhitespace(lexerState); + if (lexerState.peek() == -1) { + lexerState.forward(); + lexerState.tokenize(EMPTY_LINE); + return this; + } + if (lexerState.firstLine) { + lexerState.lexeme += whitespace; + } + } + + // Terminating delimiter + if (lexerState.peek() == '\"') { + IndentUtils.handleMappingValueIndent(lexerState, DOUBLE_QUOTE_DELIMITER); + return this; + } + + // Regular double quoted characters + Scanner.iterate(lexerState, Scanner.DOUBLE_QUOTE_CHAR_SCANNER, DOUBLE_QUOTE_CHAR); + return this; + } + } + + private static class SingleQuoteState implements State { + + /** + * Scan the lexemes for single-quoted scalars. + * + * @param lexerState - Current lexer state. + */ + @Override + public State transition(LexerState lexerState) throws Error.YamlParserException { + if (Utils.isMarker(lexerState, true)) { + lexerState.forward(); + lexerState.tokenize(DIRECTIVE_MARKER); + return this; + } + if (Utils.isMarker(lexerState, false)) { + lexerState.forward(); + lexerState.tokenize(DOCUMENT_MARKER); + return this; + } + + // Check for empty lines + if (WHITE_SPACE_PATTERN.pattern(lexerState.peek())) { + String whitespace = getWhitespace(lexerState); + if (lexerState.peek() == -1) { + lexerState.forward(); + lexerState.tokenize(EMPTY_LINE); + return this; + } + if (lexerState.firstLine) { + lexerState.lexeme += whitespace; + } + } + + // Escaped single quote + if (lexerState.peek() == '\'' && lexerState.peek(1) == '\'') { + lexerState.lexeme += "'"; + lexerState.forward(2); + } + + // Terminating delimiter + if (lexerState.peek() == '\'') { + if (lexerState.lexeme.length() > 0) { + lexerState.tokenize(SINGLE_QUOTE_CHAR); + return this; + } + IndentUtils.handleMappingValueIndent(lexerState, SINGLE_QUOTE_DELIMITER); + return this; + } + + // Regular single quoted characters + Scanner.iterate(lexerState, Scanner.SINGLE_QUOTE_CHAR_SCANNER, SINGLE_QUOTE_CHAR); + return this; + } + } + + private static class BlockHeaderState implements State { + + /** + * Scan the lexemes for block header of a block scalar. + * + * @param lexerState - Current lexer state. + */ + @Override + public State transition(LexerState lexerState) throws Error.YamlParserException { + if (lexerState.peek() == ' ') { + Scanner.iterate(lexerState, Scanner.WHITE_SPACE_SCANNER, SEPARATION_IN_LINE); + } + + // Ignore any comments + if (lexerState.peek() == '#' && WHITE_SPACE_PATTERN.pattern(lexerState.peek())) { + lexerState.tokenize(EOL); + return this; + } + + // Check for indentation indicators and adjust the current indent + if (Utils.matchPattern(lexerState, List.of(DECIMAL_PATTERN), List.of(new Utils.CharPattern('0')))) { + lexerState.captureIndent = false; + int numericValue = Character.getNumericValue(lexerState.peek()); + if (numericValue == -1 || numericValue == -2) { + throw new Error.YamlParserException("invalid numeric value", + lexerState.getLine(), lexerState.getColumn()); + } + lexerState.addIndent += numericValue; + lexerState.forward(); + return this.transition(lexerState); + } + + // If the indentation indicator is at the tail + if (lexerState.getColumn() >= lexerState.getRemainingBufferedSize()) { + lexerState.forward(); + lexerState.tokenize(EOL); + return this; + } + + // Check for chomping indicators + if (checkCharacters(lexerState, List.of('+', '-'))) { + lexerState.lexeme += Character.toString(lexerState.peek()); + lexerState.forward(); + lexerState.tokenize(CHOMPING_INDICATOR); + return this; + } + + throw new Error.YamlParserException("invalid block header", lexerState.getLine(), lexerState.getColumn()); + } + } + + private static class LiteralState implements State { + + /** + * Scan the lexemes for block scalar. + * + * @param lexerState - Current lexer state + */ + @Override + public State transition(LexerState lexerState) throws Error.YamlParserException { + boolean hasSufficientIndent = true; + + int limit = lexerState.indent + lexerState.addIndent; + // Check if the line has sufficient indent to be process as a block scalar. + for (int i = 0; i < limit - 1; i++) { + if (lexerState.peek() != ' ') { + hasSufficientIndent = false; + break; + } + lexerState.forward(); + } + + // There is no sufficient indent to consider printable characters + if (!hasSufficientIndent) { + if (Utils.isPlainSafe(lexerState)) { + lexerState.enforceMapping = true; + return LexerState.LEXER_START_STATE.transition(lexerState); + } + + switch (lexerState.peek()) { + case '#' -> { // Generate beginning of the trailing comment + if (!lexerState.trailingComment && lexerState.captureIndent) { + throw new Error.YamlParserException("Block scalars with more-indented leading empty lines" + + "must use an explicit indentation indicator", + lexerState.getLine(), lexerState.getColumn()); + } + + if (lexerState.trailingComment) { + lexerState.tokenize(EOL); + } else { + lexerState.tokenize(TRAILING_COMMENT); + } + return this; + } + case '\'', '\"', '.' -> { // Possible flow scalar + lexerState.enforceMapping = true; + return LexerState.LEXER_START_STATE.transition(lexerState); + } + case ':', '-' -> { + return LexerState.LEXER_START_STATE.transition(lexerState); + } + case -1 -> { // Empty lines are allowed in trailing comments + lexerState.forward(); + lexerState.tokenize(EMPTY_LINE); + return this; + } + default -> { // Other characters are not allowed when the indentation is less + throw new Error.YamlParserException("insufficient indent to process literal characters", + lexerState.getLine(), lexerState.getColumn()); + } + } + } + + if (lexerState.trailingComment) { + while (true) { + switch (lexerState.peek()) { + case ' ' -> { // Ignore whitespace + lexerState.forward(); + } + case '#' -> { // Generate beginning of the trailing comment + lexerState.tokenize(EOL); + return this; + } + case -1 -> { // Empty lines are allowed in trailing comments + lexerState.forward(); + lexerState.tokenize(EMPTY_LINE); + return this; + } + default -> { + throw new Error.YamlParserException("invalid trailing comment", + lexerState.getLine(), lexerState.getColumn()); + } + } + } + } + + // Generate an empty lines that have less index. + if (lexerState.getColumn() >= lexerState.getRemainingBufferedSize()) { + lexerState.forward(); + lexerState.tokenize(EMPTY_LINE); + return this; + } + + // Update the indent to the first line + if (lexerState.captureIndent) { + int additionalIndent = 0; + + while (lexerState.peek() == ' ') { + additionalIndent += 1; + lexerState.forward(); + } + + lexerState.addIndent += additionalIndent; + if (lexerState.getColumn() < lexerState.getRemainingBufferedSize()) { + lexerState.captureIndent = false; + } + } + + if (lexerState.getColumn() >= lexerState.getRemainingBufferedSize()) { + lexerState.forward(); + lexerState.tokenize(EMPTY_LINE); + return this; + } + + // Check for document end markers + if ((lexerState.peek() == '.' && lexerState.peek(1) == '.' && lexerState.peek(2) == '.') + || (lexerState.peek() == '-' && lexerState.peek(1) == '-' && lexerState.peek(2) == '-')) { + return LexerState.LEXER_START_STATE.transition(lexerState); + } + + // Scan printable character + Scanner.iterate(lexerState, Scanner.PRINTABLE_CHAR_WITH_WHITESPACE_SCANNER, PRINTABLE_CHAR); + return this; + } + } + + private static class ReservedDirectiveState implements State { + + /** + * Scan the lexemes for reserved directive. + * + * @param lexerState - Current lexer state + */ + @Override + public State transition(LexerState lexerState) throws Error.YamlParserException { + // Ignore comments + if (Utils.isComment(lexerState)) { + lexerState.tokenize(EOL); + return this; + } + + // Check for separation-in-line + if (WHITE_SPACE_PATTERN.pattern(lexerState.peek())) { + Scanner.iterate(lexerState, Scanner.WHITE_SPACE_SCANNER, SEPARATION_IN_LINE); + return this; + } + + // Scan printable character + Scanner.iterate(lexerState, Scanner.PRINTABLE_CHAR_SCANNER, PRINTABLE_CHAR); + return this; + } + } +} diff --git a/native/src/main/java/io/ballerina/lib/data/yaml/lexer/Scanner.java b/native/src/main/java/io/ballerina/lib/data/yaml/lexer/Scanner.java new file mode 100644 index 0000000..0013249 --- /dev/null +++ b/native/src/main/java/io/ballerina/lib/data/yaml/lexer/Scanner.java @@ -0,0 +1,552 @@ +/* + * 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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.lib.data.yaml.lexer; + +import io.ballerina.lib.data.yaml.utils.Error; + +import java.util.HashMap; +import java.util.HexFormat; +import java.util.List; +import java.util.Map; + +import static io.ballerina.lib.data.yaml.lexer.Utils.PRINTABLE_PATTERN; +import static io.ballerina.lib.data.yaml.lexer.Utils.JSON_PATTERN; +import static io.ballerina.lib.data.yaml.lexer.Utils.BOM_PATTERN; +import static io.ballerina.lib.data.yaml.lexer.Utils.LINE_BREAK_PATTERN; +import static io.ballerina.lib.data.yaml.lexer.Utils.WHITE_SPACE_PATTERN; +import static io.ballerina.lib.data.yaml.lexer.Utils.DECIMAL_PATTERN; +import static io.ballerina.lib.data.yaml.lexer.Utils.HEXA_DECIMAL_PATTERN; +import static io.ballerina.lib.data.yaml.lexer.Utils.WORD_PATTERN; +import static io.ballerina.lib.data.yaml.lexer.Utils.FLOW_INDICATOR_PATTERN; +import static io.ballerina.lib.data.yaml.lexer.Utils.URI_PATTERN; +import static io.ballerina.lib.data.yaml.lexer.Utils.matchPattern; + +/** + * This class will hold utility functions to scan and consume different character patterns. + * + * @since 0.1.0 + */ +public class Scanner { + + public static final Scan ANCHOR_NAME_SCANNER = new AnchorNameScanner(); + public static final Scan PLANAR_CHAR_SCANNER = new PlanarCharScanner(); + public static final Scan URI_SCANNER = new UriScanner(false); + public static final Scan VERBATIM_URI_SCANNER = new UriScanner(true); + public static final Scan PRINTABLE_CHAR_WITH_WHITESPACE_SCANNER = new PrintableCharScanner(true); + public static final Scan PRINTABLE_CHAR_SCANNER = new PrintableCharScanner(false); + public static final Scan DIFF_TAG_HANDLE_SCANNER = new TagHandleScanner(true); + public static final Scan TAG_HANDLE_SCANNER = new TagHandleScanner(false); + public static final Scan WHITE_SPACE_SCANNER = new WhiteSpaceScanner(); + public static final Scan TAG_CHARACTER_SCANNER = new TagCharacterScanner(); + public static final Scan DIGIT_SCANNER = new DigitScanner(); + public static final Scan DOUBLE_QUOTE_CHAR_SCANNER = new DoubleQuoteCharScanner(); + public static final Scan SINGLE_QUOTE_CHAR_SCANNER = new SingleQuoteCharScanner(); + public static final Scan COMMENT_SCANNER = new CommentScanner(); + public static final Map ESCAPED_CHAR_MAP; + + static { + ESCAPED_CHAR_MAP = new HashMap<>(); + ESCAPED_CHAR_MAP.put('0', "\u0000"); + ESCAPED_CHAR_MAP.put('a', "\u0007"); + ESCAPED_CHAR_MAP.put('b', "\u0008"); + ESCAPED_CHAR_MAP.put('t', "\t"); + ESCAPED_CHAR_MAP.put('n', "\n"); + ESCAPED_CHAR_MAP.put('v', "\u000b"); + ESCAPED_CHAR_MAP.put('f', "\u000c"); + ESCAPED_CHAR_MAP.put('r', "\r"); + ESCAPED_CHAR_MAP.put('e', "\u001b"); + ESCAPED_CHAR_MAP.put('\"', "\""); + ESCAPED_CHAR_MAP.put('/', "/"); + ESCAPED_CHAR_MAP.put('\\', "\\\\"); + ESCAPED_CHAR_MAP.put('N', "\u0085"); + ESCAPED_CHAR_MAP.put('_', "\u00a0"); + ESCAPED_CHAR_MAP.put('L', "\u2028"); + ESCAPED_CHAR_MAP.put('P', "\u2029"); + ESCAPED_CHAR_MAP.put(' ', "\u0020"); + ESCAPED_CHAR_MAP.put('\t', "\t"); + } + + /** + * Lookahead the remaining characters in the buffer and capture lexemes for the tagged token. + * + * @param sm Current lexer state + * @param scan Scan instance that is going to scan + * @param token Targeting token upon successful scan + */ + public static void iterate(LexerState sm, Scan scan, Token.TokenType token) throws Error.YamlParserException { + iterate(sm, scan, token, false); + } + + /** + * Lookahead the remaining characters in the buffer and capture lexemes for the tagged token. + * + * @param sm Current lexer state + * @param scan Scan instance that is going to scan + * @param token Targeting token upon successful scan + * @param include True when the last character belongs to the token + */ + public static void iterate(LexerState sm, Scan scan, Token.TokenType token, boolean include) + throws Error.YamlParserException { + while (true) { + if (scan.scan(sm)) { + if (include && sm.peek() != '\n') { + sm.forward(); + sm.tokenize(token); + return; + } + if (include && sm.peek() != '\r' && sm.peek(1) != '\n') { + sm.forward(2); + sm.tokenize(token); + return; + } + sm.tokenize(token); + return; + } + if (sm.peek(1) == -1) { + sm.setEofStream(true); + break; + } + sm.forward(); + } + sm.forward(); + sm.tokenize(token); + } + + interface Scan { + boolean scan(LexerState sm) throws Error.YamlParserException; + } + + public static class SingleQuoteCharScanner implements Scan { + + /** + * Process double quoted scalar values. + */ + @Override + public boolean scan(LexerState sm) { + // Process nb-json characters + if (matchPattern(sm, List.of(JSON_PATTERN), List.of(new Utils.CharPattern('\'')))) { + sm.appendToLexeme(Character.toString(sm.peek())); + return false; + } + + // Terminate when the delimiter is found + if (sm.peek() == '\'') { + if (sm.peek(1) == '\'') { + sm.appendToLexeme("'"); + sm.forward(); + return false; + } + } + + return true; + } + } + + public static class DoubleQuoteCharScanner implements Scan { + + /** + * Process double quoted scalar values. + * + * @param sm - Current lexer state + */ + @Override + public boolean scan(LexerState sm) throws Error.YamlParserException { + // Process nb-json characters + if (matchPattern(sm, List.of(JSON_PATTERN), + List.of(new Utils.CharPattern('\\'), new Utils.CharPattern('\"')))) { + sm.appendToLexeme(Character.toString(sm.peek())); + return false; + } + + // Process escaped characters + if (sm.peek() == '\\') { + sm.forward(); + escapedCharacterScan(sm); + sm.setLastEscapedChar(sm.getLexeme().length() - 1); + return false; + } + + return true; + } + } + + public static class TagCharacterScanner implements Scan { + + /** + * Scan the lexeme for tag characters. + * + * @param sm Current lexer state + * @return false to continue and true to terminate the token. + */ + @Override + public boolean scan(LexerState sm) throws Error.YamlParserException { + // Check for URI character + if (matchPattern(sm, List.of(URI_PATTERN, WORD_PATTERN), + List.of(FLOW_INDICATOR_PATTERN, new Utils.CharPattern('!')))) { + sm.appendToLexeme(Character.toString(sm.peek())); + return false; + } + + // Terminate if a whitespace or a flow indicator is detected + if (matchPattern(sm, List.of(WHITE_SPACE_PATTERN, FLOW_INDICATOR_PATTERN, LINE_BREAK_PATTERN))) { + return true; + } + + // Process the hexadecimal values after '%' + if (sm.peek() == '%') { + scanUnicodeEscapedCharacters(sm, '%', 2); + return false; + } + + throw new Error.YamlParserException("invalid tag character", sm.getLine(), sm.getColumn()); + } + } + + public static class DigitScanner implements Scan { + + /** + * Check for the lexemes to crete an DECIMAL token. + * + * @param sm - Current lexer state + */ + @Override + public boolean scan(LexerState sm) throws Error.YamlParserException { + if (matchPattern(sm, List.of(DECIMAL_PATTERN))) { + sm.appendToLexeme(Character.toString(sm.peek())); + return false; + } + int currentChar = sm.peek(); + if (WHITE_SPACE_PATTERN.pattern(currentChar) || currentChar == '.' + || LINE_BREAK_PATTERN.pattern(currentChar)) { + return true; + } + throw new Error.YamlParserException("invalid digit character", sm.getLine(), sm.getColumn()); + } + } + + public static class WhiteSpaceScanner implements Scan { + /** + * Scan the white spaces for a line-in-separation. + * + * @param sm - Current lexer state + * @return - False to continue. True to terminate the token. + */ + @Override + public boolean scan(LexerState sm) { + int peek = sm.peek(); + if (peek == ' ') { + return false; + } + if (peek == '\t') { + sm.updateFirstTabIndex(); + return false; + } + return true; + } + } + + public static class CommentScanner implements Scan { + /** + * Scan the comment. + * + * @param sm - Current lexer state + * @return - False to continue. True to terminate the token. + */ + @Override + public boolean scan(LexerState sm) { + return !matchPattern(sm, List.of(PRINTABLE_PATTERN), List.of(LINE_BREAK_PATTERN)); + } + } + + public static class TagHandleScanner implements Scan { + private final boolean differentiate; + + public TagHandleScanner(boolean differentiate) { + this.differentiate = differentiate; + } + + /** + * Scan the lexeme for named tag handle. + * + * @param sm - Current lexer state + */ + @Override + public boolean scan(LexerState sm) throws Error.YamlParserException { + + // Scan the word of the name tag. + if (matchPattern(sm, List.of(WORD_PATTERN, URI_PATTERN), + List.of(new Utils.CharPattern('!'), FLOW_INDICATOR_PATTERN), 1)) { + sm.forward(); + sm.appendToLexeme(Character.toString(sm.peek())); + // Store the complete primary tag if another '!' cannot be detected. + if (differentiate && sm.peek(1) == -1) { + sm.setLexemeBuffer(sm.getLexeme().substring(1)); + sm.setLexeme("!"); + return true; + } + return false; + } + + // Scan the end delimiter of the tag. + if (sm.peek(1) == '!') { + sm.forward(); + sm.appendToLexeme("!"); + return true; + } + + // Store the complete primary tag if a white space or a flow indicator is detected. + if (differentiate && matchPattern(sm, List.of(FLOW_INDICATOR_PATTERN, WHITE_SPACE_PATTERN), 1)) { + sm.setLexemeBuffer(sm.getLexeme().substring(1)); + sm.setLexeme("!"); + return true; + } + + // Store the complete primary tag if a hexadecimal escape is detected. + if (differentiate && sm.peek(1) == '%') { + sm.forward(); + scanUnicodeEscapedCharacters(sm, '%', 2); + sm.setLexemeBuffer(sm.getLexeme().substring(1)); + sm.setLexeme("!"); + return true; + } + + throw new Error.YamlParserException("invalid tag handle runtime exception", sm.getLine(), sm.getColumn()); + } + } + + public static class PrintableCharScanner implements Scan { + private final boolean allowWhiteSpace; + + public PrintableCharScanner(boolean allowWhiteSpace) { + this.allowWhiteSpace = allowWhiteSpace; + } + + /** + * Scan the lexeme for printable char. + * + * @param sm - Current lexer state + */ + @Override + public boolean scan(LexerState sm) throws Error.YamlParserException { + + if (allowWhiteSpace) { + if (matchPattern(sm, List.of(LINE_BREAK_PATTERN))) { + return true; + } + } else { + if (matchPattern(sm, List.of(WHITE_SPACE_PATTERN, LINE_BREAK_PATTERN))) { + return true; + } + } + + if (matchPattern(sm, List.of(PRINTABLE_PATTERN), List.of(BOM_PATTERN, LINE_BREAK_PATTERN))) { + sm.appendToLexeme(Character.toString(sm.peek())); + return false; + } + + throw new Error.YamlParserException("invalid printable character", sm.getLine(), sm.getColumn()); + } + } + + public static class UriScanner implements Scan { + private final boolean isVerbatim; + + public UriScanner(boolean isVerbatim) { + this.isVerbatim = isVerbatim; + } + + /** + * Scan the lexeme for URI characters. + * + * @param sm - Current lexer state + */ + @Override + public boolean scan(LexerState sm) throws Error.YamlParserException { + int currentChar = sm.peek(); + + // Check for URI characters + if (matchPattern(sm, List.of(URI_PATTERN, WORD_PATTERN))) { + sm.appendToLexeme(Character.toString(currentChar)); + return false; + } + + // Process the hexadecimal values after '%' + if (currentChar == '%') { + scanUnicodeEscapedCharacters(sm, '%', 2); + return false; + } + + // Ignore the comments + if (matchPattern(sm, List.of(LINE_BREAK_PATTERN, WHITE_SPACE_PATTERN))) { + return true; + } + + // Terminate when '>' is detected for a verbatim tag + if (isVerbatim && currentChar == '>') { + return true; + } + + throw new Error.YamlParserException("invalid URI character", sm.getLine(), sm.getColumn()); + } + } + + public static class PlanarCharScanner implements Scan { + + /** + * Process planar scalar values. + * + * @param sm - Current lexer state + * @return False to continue. True to terminate the token. An error on failure. + */ + @Override + public boolean scan(LexerState sm) throws Error.YamlParserException { + StringBuilder whitespace = new StringBuilder(); + int numWhitespace = 0; + int peekAtIndex = sm.peek(); + while (WHITE_SPACE_PATTERN.pattern(peekAtIndex)) { + whitespace.append(" "); + peekAtIndex = sm.peek(++numWhitespace); + } + + if (peekAtIndex == -1 || LINE_BREAK_PATTERN.pattern(peekAtIndex)) { + return true; + } + + // Terminate when the flow indicators are detected inside flow style collections + if (matchPattern(sm, List.of(FLOW_INDICATOR_PATTERN), numWhitespace) && sm.isFlowCollection()) { + sm.forward(numWhitespace); + return true; + } + + if (matchPattern(sm, List.of(PRINTABLE_PATTERN), List.of(LINE_BREAK_PATTERN, BOM_PATTERN, + WHITE_SPACE_PATTERN, new Utils.CharPattern('#'), new Utils.CharPattern(':')), numWhitespace)) { + sm.forward(numWhitespace); + sm.appendToLexeme(whitespace + Character.toString(peekAtIndex)); + return false; + } + + // Check for comments with a space before it + if (peekAtIndex == '#') { + if (numWhitespace > 0) { + return true; + } + sm.appendToLexeme("#"); + return false; + } + + // Check for mapping value with a space after it + if (peekAtIndex == ':') { + if (!Utils.discernPlanarFromIndicator(sm, numWhitespace + 1)) { + return true; + } + sm.forward(numWhitespace); + sm.appendToLexeme(whitespace + ":"); + return false; + } + + throw new Error.YamlParserException("invalid planar character", sm.getLine(), sm.getColumn()); + } + } + + public static class AnchorNameScanner implements Scan { + + /** + * Scan the lexeme for the anchor name. + * + * @param sm - Current lexer state + * @return False to continue, true to terminate the token + */ + @Override + public boolean scan(LexerState sm) { + if (matchPattern(sm, List.of(PRINTABLE_PATTERN), + List.of(LINE_BREAK_PATTERN, BOM_PATTERN, FLOW_INDICATOR_PATTERN, WHITE_SPACE_PATTERN) + )) { + sm.appendToLexeme(Character.toString(sm.peek())); + return false; + } + return true; + } + } + + private static void scanUnicodeEscapedCharacters(LexerState sm, char escapedChar, int length) + throws Error.YamlParserException { + + StringBuilder unicodeDigits = new StringBuilder(); + // Check if the digits adhere to the hexadecimal code pattern. + for (int i = 0; i < length; i++) { + sm.forward(); + int peek = sm.peek(); + if (HEXA_DECIMAL_PATTERN.pattern(peek)) { + unicodeDigits.append(Character.toString(peek)); + continue; + } + throw new Error.YamlParserException("expected a unicode character after escaped char", + sm.getLine(), sm.getColumn()); + } + + // Check if the lexeme can be converted to hexadecimal + int hexResult = HexFormat.fromHexDigits(unicodeDigits.toString()); + sm.appendToLexeme(Character.toString(hexResult)); + } + + private static void escapedCharacterScan(LexerState sm) throws Error.YamlParserException { + int currentChar = sm.peek(); + + // Process double escape character + if (LINE_BREAK_PATTERN.pattern(currentChar)) { + sm.forward(); + processEscapedWhiteSpaces(sm); + return; + } + + if (currentChar == -1) { + sm.appendToLexeme("\\"); + return; + } + + // Check for predefined escape characters + if (ESCAPED_CHAR_MAP.containsKey((char) currentChar)) { + sm.appendToLexeme(ESCAPED_CHAR_MAP.get((char) currentChar)); + return; + } + + // Check for unicode characters + switch (currentChar) { + case 'x' -> { + scanUnicodeEscapedCharacters(sm, 'x', 2); + return; + } + case 'u' -> { + scanUnicodeEscapedCharacters(sm, 'u', 4); + return; + } + case 'U' -> { + scanUnicodeEscapedCharacters(sm, 'U', 8); + return; + } + } + throw new Error.YamlParserException("invalid escape character", sm.getLine(), sm.getColumn()); + } + + private static void processEscapedWhiteSpaces(LexerState sm) { + while (WHITE_SPACE_PATTERN.pattern(sm.peek(1))) { + sm.forward(); + } + } +} diff --git a/native/src/main/java/io/ballerina/lib/data/yaml/lexer/Token.java b/native/src/main/java/io/ballerina/lib/data/yaml/lexer/Token.java new file mode 100644 index 0000000..5704c33 --- /dev/null +++ b/native/src/main/java/io/ballerina/lib/data/yaml/lexer/Token.java @@ -0,0 +1,94 @@ +/* + * 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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.lib.data.yaml.lexer; + +/** + * Tokens used in YAML parser. + * + * @since 0.1.0 + */ +public class Token { + private final TokenType type; + private String value; + private IndentUtils.Indentation indentation = null; + + public Token(TokenType type) { + this.type = type; + } + public Token(TokenType type, String value) { + this(type); + this.value = value; + } + + public Token(TokenType type, String value, IndentUtils.Indentation indentation) { + this(type, value); + this.indentation = indentation; + } + + public TokenType getType() { + return type; + } + + public String getValue() { + return value; + } + + public IndentUtils.Indentation getIndentation() { + return indentation; + } + + public enum TokenType { + SEQUENCE_ENTRY("-"), + MAPPING_KEY("?"), + MAPPING_VALUE(":"), + SEPARATOR(","), + SEQUENCE_START("["), + SEQUENCE_END("]"), + MAPPING_START("{"), + MAPPING_END("}"), + DIRECTIVE("%"), + ALIAS("*"), + ANCHOR("&"), + TAG_HANDLE(""), + TAG_PREFIX(""), + TAG(""), + DOT("."), + LITERAL("|"), + FOLDED(">"), + DECIMAL(""), + SEPARATION_IN_LINE(""), + DIRECTIVE_MARKER("---"), + DOCUMENT_MARKER("..."), + DOUBLE_QUOTE_DELIMITER("\""), + DOUBLE_QUOTE_CHAR(""), + SINGLE_QUOTE_DELIMITER("'"), + SINGLE_QUOTE_CHAR(""), + PLANAR_CHAR(""), + PRINTABLE_CHAR(""), + INDENTATION_INDICATOR(""), + CHOMPING_INDICATOR(""), + EMPTY_LINE(""), + EOL(""), + COMMENT(""), + TRAILING_COMMENT(""), + DUMMY(""); + TokenType(String s) { + } + } +} diff --git a/native/src/main/java/io/ballerina/lib/data/yaml/lexer/Utils.java b/native/src/main/java/io/ballerina/lib/data/yaml/lexer/Utils.java new file mode 100644 index 0000000..43f7970 --- /dev/null +++ b/native/src/main/java/io/ballerina/lib/data/yaml/lexer/Utils.java @@ -0,0 +1,253 @@ +/* + * 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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.lib.data.yaml.lexer; + +import java.util.List; + +/** + * This class will hold utility functions used in YAML Lexer. + * + * @since 0.1.0 + */ +public class Utils { + + public static final Pattern PRINTABLE_PATTERN = new PrintablePattern(); + public static final Pattern JSON_PATTERN = new JsonPattern(); + public static final Pattern BOM_PATTERN = new BomPattern(); + public static final Pattern LINE_BREAK_PATTERN = new LineBreakPattern(); + public static final Pattern WHITE_SPACE_PATTERN = new WhiteSpacePattern(); + public static final Pattern DECIMAL_PATTERN = new DecimalPattern(); + public static final Pattern HEXA_DECIMAL_PATTERN = new HexaDecimalPattern(); + public static final Pattern WORD_PATTERN = new WordPattern(); + public static final Pattern FLOW_INDICATOR_PATTERN = new FlowIndicatorPattern(); + public static final Pattern INDICATOR_PATTERN = new IndicatorPattern(); + public static final Pattern URI_PATTERN = new UriPattern(); + + public static boolean matchPattern(LexerState sm, List inclusionPatterns) { + return matchPattern(sm, inclusionPatterns, List.of(), 0); + } + + public static boolean matchPattern(LexerState sm, List inclusionPatterns, int offset) { + return matchPattern(sm, inclusionPatterns, List.of(), offset); + } + + public static boolean matchPattern(LexerState sm, List inclusionPatterns, + List exclusionPatterns) { + return matchPattern(sm, inclusionPatterns, exclusionPatterns, 0); + } + + public static boolean matchPattern(LexerState sm, List inclusionPatterns, + List exclusionPatterns, int offset) { + int peek = sm.peek(offset); + // If there is no character to check the pattern, then return false. + if (peek == -1) { + return false; + } + + for (Pattern pattern: exclusionPatterns) { + if (pattern.pattern(peek)) { + return false; + } + } + + for (Pattern pattern: inclusionPatterns) { + if (pattern.pattern(peek)) { + return true; + } + } + + return false; + } + + interface Pattern { + boolean pattern(int codePoint); + } + + public static class CharPattern implements Pattern { + private final char value; + + public CharPattern(char value) { + this.value = value; + } + + @Override + public boolean pattern(int codePoint) { + return value == codePoint; + } + } + + public static class PrintablePattern implements Pattern { + @Override + public boolean pattern(int codePoint) { + return (codePoint >= 32 && codePoint <= 126) + || (codePoint >= 160 && codePoint <= 55295) + || (codePoint >= 57344 && codePoint <= 65533) + || (codePoint >= 65536 && codePoint <= 1114111) + || codePoint == 9 || codePoint == 10 || codePoint == 13 || codePoint == 133; + } + } + + public static class JsonPattern implements Pattern { + + @Override + public boolean pattern(int codePoint) { + return codePoint >= 32 || codePoint == 9; + } + } + + public static class BomPattern implements Pattern { + + @Override + public boolean pattern(int codePoint) { + return codePoint == 65279; + } + } + + public static class LineBreakPattern implements Pattern { + + @Override + public boolean pattern(int codePoint) { + return codePoint == 10 || codePoint == 13; + } + } + + public static class WhiteSpacePattern implements Pattern { + + @Override + public boolean pattern(int codePoint) { + return codePoint == 32 || codePoint == 9; + } + } + + public static class DecimalPattern implements Pattern { + + @Override + public boolean pattern(int codePoint) { + return codePoint >= 48 && codePoint <= 57; // ASCII codes for 0-9 + } + } + + public static class HexaDecimalPattern implements Pattern { + + @Override + public boolean pattern(int codePoint) { + return (codePoint >= 48 && codePoint <= 57) // 0-9 + || (codePoint >= 65 && codePoint <= 70) // A-F + || (codePoint >= 97 && codePoint <= 102); // a-f + } + } + + public static class WordPattern implements Pattern { + + @Override + public boolean pattern(int codePoint) { + return (codePoint >= 97 && codePoint <= 122) + || (codePoint >= 65 && codePoint <= 90) + || DECIMAL_PATTERN.pattern(codePoint) || codePoint == '-'; + } + } + + public static class FlowIndicatorPattern implements Pattern { + + @Override + public boolean pattern(int codePoint) { + return codePoint == ',' || codePoint == '[' || codePoint == ']' || codePoint == '{' || codePoint == '}'; + } + } + + public static class IndicatorPattern implements Pattern { + + @Override + public boolean pattern(int codePoint) { + return "-?:,[]{}#&*!|>'\"%@`".contains(Character.toString(codePoint)); + } + } + + public static class UriPattern implements Pattern { + + @Override + public boolean pattern(int codePoint) { + return "#;/?:@&=+$,_.!~*'()[]".contains(Character.toString(codePoint)); + } + } + + public static boolean isPlainSafe(LexerState sm) { + return matchPattern(sm, List.of(PRINTABLE_PATTERN), + List.of(LINE_BREAK_PATTERN, BOM_PATTERN, WHITE_SPACE_PATTERN, INDICATOR_PATTERN)); + } + + public static boolean isTagChar(LexerState sm) { + return matchPattern(sm, List.of(URI_PATTERN, WORD_PATTERN, new CharPattern('%')), + List.of(new CharPattern('!'), FLOW_INDICATOR_PATTERN)); + } + + public static boolean checkCharacters(LexerState sm, List expectedChars) { + return expectedChars.contains((char) sm.peek()); + } + + public static boolean isComment(LexerState sm) { + return sm.peek() == '#'; + } + + public static boolean isMarker(LexerState sm, boolean directive) { + int directiveCodePoint = directive ? '-' : '.'; + if (sm.peek() == directiveCodePoint && sm.peek(1) == directiveCodePoint + && sm.peek(2) == directiveCodePoint) { + if (WHITE_SPACE_PATTERN.pattern(sm.peek(3)) || LINE_BREAK_PATTERN.pattern(sm.peek(3))) { + sm.forward(2); + return true; + } + if (sm.peek(3) == -1) { + sm.forward(2); + sm.forward(); + return true; + } + } + return false; + } + + public static boolean discernPlanarFromIndicator(LexerState sm) { + return discernPlanarFromIndicator(sm, 1); + } + + public static boolean discernPlanarFromIndicator(LexerState sm, int offset) { + if (sm.isFlowCollection()) { + return matchPattern(sm, List.of(PRINTABLE_PATTERN), + List.of(LINE_BREAK_PATTERN, BOM_PATTERN, WHITE_SPACE_PATTERN, FLOW_INDICATOR_PATTERN), offset); + } + return matchPattern(sm, List.of(PRINTABLE_PATTERN), + List.of(LINE_BREAK_PATTERN, BOM_PATTERN, WHITE_SPACE_PATTERN), offset); + } + + public static String getWhitespace(LexerState sm) { + StringBuilder whitespace = new StringBuilder(); + while (true) { + if (sm.peek() == ' ') { + whitespace.append(" "); + } else if (sm.peek() == '\t') { + sm.updateFirstTabIndex(); + whitespace.append("\t"); + } else { + break; + } + sm.forward(); + } + return whitespace.toString(); + } +} diff --git a/native/src/main/java/io/ballerina/lib/data/yaml/lexer/YamlLexer.java b/native/src/main/java/io/ballerina/lib/data/yaml/lexer/YamlLexer.java new file mode 100644 index 0000000..3fd2117 --- /dev/null +++ b/native/src/main/java/io/ballerina/lib/data/yaml/lexer/YamlLexer.java @@ -0,0 +1,92 @@ +/* + * 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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.lib.data.yaml.lexer; + +import io.ballerina.lib.data.yaml.utils.Error; + +import static io.ballerina.lib.data.yaml.lexer.LexerState.LEXER_DOUBLE_QUOTE; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.EMPTY_LINE; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.TAG; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.EOL; +import static io.ballerina.lib.data.yaml.lexer.Utils.isTagChar; + +/** + * Core logic of the YAML Lexer. + * + * @since 0.1.0 + */ +public class YamlLexer { + + /** + * Generate a Token for the next immediate lexeme. + * + * @param state Current lexer state + * @return Updated lexer state is returned + */ + public static LexerState.State scanTokens(LexerState state) throws Error.YamlParserException { + // Check the lexeme buffer for the lexeme stored by the primary tag + if (state.getLexemeBuffer().length() > 0) { + state.setLexeme(state.getLexemeBuffer()); + state.setLexemeBuffer(""); + + // If lexeme buffer contains only up to a hexadecimal value, + // then check for the remaining content of the primary tag. + if (isTagChar(state)) { + Scanner.iterate(state, Scanner.TAG_CHARACTER_SCANNER, TAG); + return state.getState(); + } + + state.tokenize(TAG); + return state.getState(); + } + + // Generate EOL token at the last index + if (state.isEndOfStream()) { + if (state.isIndentationBreak()) { + throw new Error.YamlParserException("invalid indentation", state.getLine(), state.getColumn()); + } + if (state.getColumn() == 0) { + state.forward(); + state.tokenize(EMPTY_LINE); + } else { + state.forward(); + state.tokenize(EOL); + } + return state.getState(); + } + + // Check for line breaks when reading from string + if (state.peek() == '\n' && state.getState() != LEXER_DOUBLE_QUOTE) { + state.setNewLine(true); + state.forward(); + state.tokenize(EOL); + return state.getState(); + } + + // Check for line breaks when reading from string + if (state.peek() == '\r' && state.peek(1) == '\n' && state.getState() != LEXER_DOUBLE_QUOTE) { + state.setNewLine(true); + state.forward(); + state.tokenize(EOL); + return state.getState(); + } + + return state.getState().transition(state); + } +} diff --git a/native/src/main/java/io/ballerina/lib/data/yaml/parser/Directive.java b/native/src/main/java/io/ballerina/lib/data/yaml/parser/Directive.java new file mode 100644 index 0000000..4d6d889 --- /dev/null +++ b/native/src/main/java/io/ballerina/lib/data/yaml/parser/Directive.java @@ -0,0 +1,120 @@ +/* + * 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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.lib.data.yaml.parser; + + +import io.ballerina.lib.data.yaml.lexer.LexerState; +import io.ballerina.lib.data.yaml.lexer.Token; +import io.ballerina.lib.data.yaml.utils.Error; + +import java.util.List; + +/** + * This class will hold utility functions to process directives. + * + * @since 0.1.0 + */ +public class Directive { + + /** + * Check the grammar productions for YAML directives. + * Update the yamlVersion of the document. + * + * @param state - Current parser state + */ + public static void yamlDirective(ParserState state) throws Error.YamlParserException { + if (state.getYamlVersion() != null) { + throw new Error.YamlParserException("YAML document version is already defined", + state.getLine(), state.getColumn()); + } + + // Expects a separate in line. + YamlParser.getNextToken(state, List.of(Token.TokenType.SEPARATION_IN_LINE)); + state.updateLexerState(LexerState.LEXER_DIRECTIVE); + + // Expect yaml version + YamlParser.getNextToken(state, List.of(Token.TokenType.DECIMAL)); + String lexemeBuffer = state.getCurrentToken().getValue(); + YamlParser.getNextToken(state, List.of(Token.TokenType.DOT)); + lexemeBuffer += "."; + YamlParser.getNextToken(state, List.of(Token.TokenType.DECIMAL)); + lexemeBuffer += state.getCurrentToken().getValue(); + + float yamlVersion = Float.parseFloat(lexemeBuffer); + if (yamlVersion != 1.2) { + if (yamlVersion >= 2.0 || yamlVersion < 1.0) { + throw new Error.YamlParserException("incompatible yaml version for the 1.2 parser", + state.getLine(), state.getColumn()); + } + } + state.setYamlVersion(yamlVersion); + } + + /** + * Check the grammar production for TAG directives. + * Update the tag handle map. + * + * @param state - Current parser state + */ + public static void tagDirective(ParserState state) throws Error.YamlParserException { + YamlParser.getNextToken(state, List.of(Token.TokenType.SEPARATION_IN_LINE)); + + // Expect a tag handle + state.updateLexerState(LexerState.LEXER_TAG_HANDLE_STATE); + YamlParser.getNextToken(state, List.of(Token.TokenType.TAG_HANDLE)); + String tagHandle = state.getCurrentToken().getValue(); + YamlParser.getNextToken(state, List.of(Token.TokenType.SEPARATION_IN_LINE)); + + // Tag handles cannot be redefined + if (state.getCustomTagHandles().containsKey(tagHandle)) { + throw new Error.YamlParserException("duplicate tag handle", state.getLine(), state.getColumn()); + } + + state.updateLexerState(LexerState.LEXER_TAG_PREFIX_STATE); + YamlParser.getNextToken(state, List.of(Token.TokenType.TAG_PREFIX)); + String tagPrefix = state.getCurrentToken().getValue(); + + state.getCustomTagHandles().put(tagHandle, tagPrefix); + } + + /** + * Check the grammar productions for YAML reserved directives. + * Update the reserved directives of the document. + * + * @param state - Current parser state + */ + public static void reservedDirective(ParserState state) throws Error.YamlParserException { + StringBuilder reservedDirective = new StringBuilder(state.getCurrentToken().getValue()); + state.updateLexerState(LexerState.LEXER_RESERVED_DIRECTIVE); + + // Check for the reserved directive parameters + YamlParser.getNextToken(state, true); + while (state.getBufferedToken().getType() == Token.TokenType.SEPARATION_IN_LINE) { + YamlParser.getNextToken(state); + YamlParser.getNextToken(state, true); + if (state.getBufferedToken().getType() != Token.TokenType.PRINTABLE_CHAR) { + break; + } + YamlParser.getNextToken(state); + reservedDirective.append(" ").append(state.getCurrentToken().getValue()); + YamlParser.getNextToken(state, true); + } + state.getReservedDirectives().add(reservedDirective.toString()); + } +} diff --git a/native/src/main/java/io/ballerina/lib/data/yaml/parser/ParserState.java b/native/src/main/java/io/ballerina/lib/data/yaml/parser/ParserState.java new file mode 100644 index 0000000..5b9d03f --- /dev/null +++ b/native/src/main/java/io/ballerina/lib/data/yaml/parser/ParserState.java @@ -0,0 +1,202 @@ +/* + * 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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.lib.data.yaml.parser; + +import io.ballerina.lib.data.yaml.common.Types; +import io.ballerina.lib.data.yaml.common.YamlEvent; +import io.ballerina.lib.data.yaml.lexer.CharacterReader; +import io.ballerina.lib.data.yaml.lexer.LexerState; +import io.ballerina.lib.data.yaml.lexer.Token; +import io.ballerina.lib.data.yaml.utils.Error; + +import java.io.Reader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * State of YAML Parser. + * + * @since 0.1.0 + */ +public class ParserState { + + public static final Token DUMMY_TOKEN = new Token(Token.TokenType.DUMMY); + private final LexerState lexerState; + private Token currentToken = DUMMY_TOKEN; + private Token bufferedToken = DUMMY_TOKEN; + private final List eventBuffer = new ArrayList<>(); + private int lineIndex = -1; + private boolean explicitDoc = false; + private Float yamlVersion = null; + private Map customTagHandles = new HashMap<>(); + private boolean explicitKey = false; + private int lastExplicitKeyLine = -1; + private boolean expectBlockSequenceValue = false; + private boolean tagPropertiesInLine = false; + private boolean indentationProcessed = false; + private int lastKeyLine = -1; + private boolean emptyKey = false; + private final List reservedDirectives = new ArrayList<>(); + + public ParserState(Reader reader) { + this.lexerState = new LexerState(new CharacterReader(reader)); + try { + initLexer(); + } catch (Exception e) { + eventBuffer.add(new YamlEvent.EndEvent(Types.Collection.STREAM)); + } + } + + public int getLineIndex() { + return lineIndex; + } + + public void updateLexerState(LexerState.State state) { + lexerState.updateLexerState(state); + } + + public void setBufferedToken(Token token) { + bufferedToken = token; + } + + public Token getBufferedToken() { + return bufferedToken; + } + + public Token getCurrentToken() { + return currentToken; + } + + public void setCurrentToken(Token currentToken) { + this.currentToken = currentToken; + } + + public LexerState getLexerState() { + return lexerState; + } + + public List getEventBuffer() { + return eventBuffer; + } + + public boolean isExplicitDoc() { + return explicitDoc; + } + + public void setExplicitDoc(boolean explicitDoc) { + this.explicitDoc = explicitDoc; + } + + public Float getYamlVersion() { + return yamlVersion; + } + + public void setYamlVersion(Float yamlVersion) { + this.yamlVersion = yamlVersion; + } + + public Map getCustomTagHandles() { + return customTagHandles; + } + + public void setCustomTagHandles(Map customTagHandles) { + this.customTagHandles = customTagHandles; + } + + public boolean isExplicitKey() { + return explicitKey; + } + + public void setExplicitKey(boolean explicitKey) { + this.explicitKey = explicitKey; + } + + public int getLastExplicitKeyLine() { + return lastExplicitKeyLine; + } + + public void setLastExplicitKeyLine(int lastExplicitKeyLine) { + this.lastExplicitKeyLine = lastExplicitKeyLine; + } + + public boolean isExpectBlockSequenceValue() { + return expectBlockSequenceValue; + } + + public boolean isTagPropertiesInLine() { + return tagPropertiesInLine; + } + + public void setTagPropertiesInLine(boolean tagPropertiesInLine) { + this.tagPropertiesInLine = tagPropertiesInLine; + } + + public void setExpectBlockSequenceValue(boolean expectBlockSequenceValue) { + this.expectBlockSequenceValue = expectBlockSequenceValue; + } + + public boolean isIndentationProcessed() { + return indentationProcessed; + } + + public void setIndentationProcessed(boolean indentationProcessed) { + this.indentationProcessed = indentationProcessed; + } + + public int getLastKeyLine() { + return lastKeyLine; + } + + public void setLastKeyLine(int lastKeyLine) { + this.lastKeyLine = lastKeyLine; + } + + public boolean isEmptyKey() { + return emptyKey; + } + + public void setEmptyKey(boolean emptyKey) { + this.emptyKey = emptyKey; + } + + public List getReservedDirectives() { + return reservedDirectives; + } + + public int getLine() { + return lexerState.getLine(); + } + + public int getColumn() { + return lexerState.getColumn(); + } + + public void initLexer() throws Error.YamlParserException { + lineIndex += 1; + explicitDoc = false; + expectBlockSequenceValue = false; + tagPropertiesInLine = false; + lexerState.updateNewLineProps(); + if (getLexerState().isEndOfStream()) { + throw new Error.YamlParserException("END of stream reached", lexerState.getLine(), lexerState.getColumn()); + } + } +} diff --git a/native/src/main/java/io/ballerina/lib/data/yaml/parser/ParserUtils.java b/native/src/main/java/io/ballerina/lib/data/yaml/parser/ParserUtils.java new file mode 100644 index 0000000..01d3587 --- /dev/null +++ b/native/src/main/java/io/ballerina/lib/data/yaml/parser/ParserUtils.java @@ -0,0 +1,92 @@ +/* + * 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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.lib.data.yaml.parser; + +import io.ballerina.lib.data.yaml.common.YamlEvent; +import io.ballerina.lib.data.yaml.utils.DiagnosticErrorCode; +import io.ballerina.lib.data.yaml.utils.DiagnosticLog; +import io.ballerina.runtime.api.types.Field; +import io.ballerina.runtime.api.types.RecordType; +import io.ballerina.runtime.api.utils.StringUtils; +import io.ballerina.runtime.api.values.BMap; +import io.ballerina.runtime.api.values.BString; + +import java.util.HashMap; +import java.util.Map; + +import static io.ballerina.lib.data.yaml.common.Types.Collection.STREAM; + +/** + * This class will hold utility functions used in parser. + * + * @since 0.1.0 + */ +public class ParserUtils { + + public static final String FIELD = "$field$."; + public static final String FIELD_REGEX = "\\$field\\$\\."; + public static final String NAME = "Name"; + public static final BString VALUE = StringUtils.fromString("value"); + + public static Map getAllFieldsInRecord(RecordType recordType) { + BMap annotations = recordType.getAnnotations(); + Map modifiedNames = new HashMap<>(); + for (BString annotationKey : annotations.getKeys()) { + String keyStr = annotationKey.getValue(); + if (!keyStr.contains(FIELD)) { + continue; + } + String fieldName = keyStr.split(FIELD_REGEX)[1]; + Map fieldAnnotation = (Map) annotations.get(annotationKey); + modifiedNames.put(fieldName, getModifiedName(fieldAnnotation, fieldName)); + } + + Map fields = new HashMap<>(); + Map recordFields = recordType.getFields(); + for (String key : recordFields.keySet()) { + String fieldName = modifiedNames.getOrDefault(key, key); + if (fields.containsKey(fieldName)) { + throw DiagnosticLog.error(DiagnosticErrorCode.DUPLICATE_FIELD, fieldName); + } + fields.put(fieldName, recordFields.get(key)); + } + return fields; + } + + public static String getModifiedName(Map fieldAnnotation, String fieldName) { + for (BString key : fieldAnnotation.keySet()) { + if (key.getValue().endsWith(NAME)) { + return ((Map) fieldAnnotation.get(key)).get(VALUE).toString(); + } + } + return fieldName; + } + + public static boolean isStreamEndEvent(YamlEvent event) { + return event.getKind() == YamlEvent.EventKind.END_EVENT && ((YamlEvent.EndEvent) event).getEndType() == STREAM; + } + + public enum ParserOption { + DEFAULT, + EXPECT_MAP_KEY, + EXPECT_MAP_VALUE, + EXPECT_SEQUENCE_ENTRY, + EXPECT_SEQUENCE_VALUE + } +} diff --git a/native/src/main/java/io/ballerina/lib/data/yaml/parser/Values.java b/native/src/main/java/io/ballerina/lib/data/yaml/parser/Values.java new file mode 100644 index 0000000..1d19b1d --- /dev/null +++ b/native/src/main/java/io/ballerina/lib/data/yaml/parser/Values.java @@ -0,0 +1,677 @@ +/* + * 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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.lib.data.yaml.parser; + +import io.ballerina.lib.data.yaml.common.Types; +import io.ballerina.lib.data.yaml.utils.DiagnosticErrorCode; +import io.ballerina.lib.data.yaml.utils.DiagnosticLog; +import io.ballerina.lib.data.yaml.utils.TagResolutionUtils; +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.flags.SymbolFlags; +import io.ballerina.runtime.api.types.ArrayType; +import io.ballerina.runtime.api.types.Field; +import io.ballerina.runtime.api.types.FiniteType; +import io.ballerina.runtime.api.types.IntersectionType; +import io.ballerina.runtime.api.types.MapType; +import io.ballerina.runtime.api.types.RecordType; +import io.ballerina.runtime.api.types.ReferenceType; +import io.ballerina.runtime.api.types.TupleType; +import io.ballerina.runtime.api.types.Type; +import io.ballerina.runtime.api.types.UnionType; +import io.ballerina.runtime.api.utils.StringUtils; +import io.ballerina.runtime.api.utils.TypeUtils; +import io.ballerina.runtime.api.values.BArray; +import io.ballerina.runtime.api.values.BDecimal; +import io.ballerina.runtime.api.values.BError; +import io.ballerina.runtime.api.values.BMap; +import io.ballerina.runtime.api.values.BString; +import org.ballerinalang.langlib.value.CloneReadOnly; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.Stack; + +import static io.ballerina.lib.data.yaml.parser.ParserUtils.getAllFieldsInRecord; + +/** + * Create BValue for partially parsed YAML inputs. + * + * @since 0.1.0 + */ +public class Values { + 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 final MapType JSON_MAP_TYPE = TypeCreator.createMapType(PredefinedTypes.TYPE_JSON); + public static final MapType ANYDATA_MAP_TYPE = TypeCreator.createMapType(PredefinedTypes.TYPE_ANYDATA); + public static final String NULL_VALUE = "null"; + public static final Integer BBYTE_MIN_VALUE = 0; + public static final Integer BBYTE_MAX_VALUE = 255; + public static final Integer SIGNED32_MAX_VALUE = 2147483647; + public static final Integer SIGNED32_MIN_VALUE = -2147483648; + public static final Integer SIGNED16_MAX_VALUE = 32767; + public static final Integer SIGNED16_MIN_VALUE = -32768; + public static final Integer SIGNED8_MAX_VALUE = 127; + public static final Integer SIGNED8_MIN_VALUE = -128; + public static final Long UNSIGNED32_MAX_VALUE = 4294967295L; + public static final Integer UNSIGNED16_MAX_VALUE = 65535; + public static final Integer UNSIGNED8_MAX_VALUE = 255; + + static BMap initRootMapValue(YamlParser.ComposerState state) { + state.rootValueInitialized = true; + Type expectedType = state.expectedTypes.peek(); + state.parserContexts.push(YamlParser.ParserContext.MAP); + switch (expectedType.getTag()) { + case TypeTags.RECORD_TYPE_TAG -> { + return ValueCreator.createRecordValue(expectedType.getPackage(), expectedType.getName()); + } + case TypeTags.MAP_TAG -> { + return ValueCreator.createMapValue((MapType) expectedType); + } + case TypeTags.JSON_TAG -> { + return ValueCreator.createMapValue(JSON_MAP_TYPE); + } + case TypeTags.ANYDATA_TAG -> { + return ValueCreator.createMapValue(ANYDATA_MAP_TYPE); + } + case TypeTags.UNION_TAG -> { + state.parserContexts.push(YamlParser.ParserContext.MAP); + state.unionDepth++; + state.fieldNameHierarchy.push(new Stack<>()); + return ValueCreator.createMapValue(JSON_MAP_TYPE); + } + default -> throw DiagnosticLog.error(DiagnosticErrorCode.INVALID_TYPE, expectedType, "map type"); + } + } + + static Object initRootArrayValue(YamlParser.ComposerState state) { + state.rootValueInitialized = true; + state.parserContexts.push(YamlParser.ParserContext.ARRAY); + Type expType = state.expectedTypes.peek(); + // In this point we know rhs is json[] or anydata[] hence init index counter. + if (expType.getTag() == TypeTags.JSON_TAG || expType.getTag() == TypeTags.ANYDATA_TAG + || expType.getTag() == TypeTags.UNION_TAG) { + state.arrayIndexes.push(0); + } + return initArrayValue(state, expType); + } + + static BArray initArrayValue(YamlParser.ComposerState state, Type expectedType) { + switch (expectedType.getTag()) { + case TypeTags.TUPLE_TAG -> { + return ValueCreator.createTupleValue((TupleType) expectedType); + } + case TypeTags.ARRAY_TAG -> { + return ValueCreator.createArrayValue((ArrayType) expectedType); + } + case TypeTags.JSON_TAG -> { + return ValueCreator.createArrayValue(PredefinedTypes.TYPE_JSON_ARRAY); + } + case TypeTags.ANYDATA_TAG -> { + return ValueCreator.createArrayValue(PredefinedTypes.TYPE_ANYDATA_ARRAY); + } + case TypeTags.UNION_TAG -> { + state.unionDepth++; + return ValueCreator.createArrayValue(PredefinedTypes.TYPE_JSON_ARRAY); + } + default -> throw DiagnosticLog.error(DiagnosticErrorCode.INVALID_TYPE, expectedType, "list type"); + } + } + + static void handleFieldName(String jsonFieldName, YamlParser.ComposerState state) { + if (state.jsonFieldDepth == 0 && state.unionDepth == 0) { + Field currentField = state.visitedFieldHierarchy.peek().get(jsonFieldName); + if (currentField == null) { + currentField = state.fieldHierarchy.peek().remove(jsonFieldName); + } + state.currentField = currentField; + + Type fieldType; + if (currentField == null) { + fieldType = state.restType.peek(); + } else { + // Replace modified field name with actual field name. + jsonFieldName = currentField.getFieldName(); + fieldType = currentField.getFieldType(); + state.visitedFieldHierarchy.peek().put(jsonFieldName, currentField); + } + state.expectedTypes.push(fieldType); + + if (!state.allowDataProjection && fieldType == null) { + throw DiagnosticLog.error(DiagnosticErrorCode.UNDEFINED_FIELD, jsonFieldName); + } + } else if (state.expectedTypes.peek() == null) { + state.currentField = null; + state.expectedTypes.push(null); + } + state.fieldNameHierarchy.peek().push(jsonFieldName); + } + + static Object convertAndUpdateCurrentValueNode(YamlParser.ComposerState sm, String value, Type type) { + if (sm.nilAsOptionalField && !sm.expectedTypes.peek().isNilable() + && value.equals(NULL_VALUE) + && sm.currentField != null && SymbolFlags.isFlagOn(sm.currentField.getFlags(), SymbolFlags.OPTIONAL)) { + return sm.currentYamlNode; + } + Object currentYaml = sm.currentYamlNode; + Object convertedValue = convertToExpectedType(StringUtils.fromString(value), type, sm.schema); + if (convertedValue instanceof BError) { + if (sm.currentField != null) { + throw DiagnosticLog.error(DiagnosticErrorCode.INCOMPATIBLE_VALUE_FOR_FIELD, value, type, + getCurrentFieldPath(sm)); + } + throw DiagnosticLog.error(DiagnosticErrorCode.INCOMPATIBLE_TYPE, type, value); + } + + return updateCurrentValueNode(sm, currentYaml, convertedValue); + } + + static Object updateCurrentValueNode(YamlParser.ComposerState sm, Object currentYaml, Object convertedValue) { + Type currentJsonNodeType = TypeUtils.getType(currentYaml); + switch (currentJsonNodeType.getTag()) { + case TypeTags.MAP_TAG, TypeTags.RECORD_TYPE_TAG -> { + ((BMap) currentYaml).put(StringUtils.fromString(sm.fieldNameHierarchy.peek().pop()), + convertedValue); + return currentYaml; + } + case TypeTags.ARRAY_TAG -> { + // Handle projection in array. + ArrayType arrayType = (ArrayType) currentJsonNodeType; + if (arrayType.getState() == ArrayType.ArrayState.CLOSED && + arrayType.getSize() <= sm.arrayIndexes.peek()) { + return currentYaml; + } + ((BArray) currentYaml).add(sm.arrayIndexes.peek(), convertedValue); + return currentYaml; + } + case TypeTags.TUPLE_TAG -> { + ((BArray) currentYaml).add(sm.arrayIndexes.peek(), convertedValue); + return currentYaml; + } + default -> { + return convertedValue; + } + } + } + + private static String getCurrentFieldPath(YamlParser.ComposerState sm) { + Iterator> itr = sm.fieldNameHierarchy.iterator(); + StringBuilder result = new StringBuilder(itr.hasNext() ? itr.next().peek() : ""); + while (itr.hasNext()) { + result.append(".").append(itr.next().peek()); + } + return result.toString(); + } + + private static Object convertToExpectedType(BString value, Type type, Types.YAMLSchema schema) { + switch (type.getTag()) { + case TypeTags.CHAR_STRING_TAG -> { + if (value.length() != 1) { + return DiagnosticLog.error(DiagnosticErrorCode.INCOMPATIBLE_TYPE, type, value); + } + return value; + } + case TypeTags.FINITE_TYPE_TAG -> { + return ((FiniteType) type).getValueSpace().stream() + .filter(finiteValue -> !(convertToSingletonValue(value.getValue(), finiteValue, schema) + instanceof BError)) + .findFirst() + .orElseGet(() -> DiagnosticLog.error(DiagnosticErrorCode.INCOMPATIBLE_TYPE, type, value)); + } + case TypeTags.TYPE_REFERENCED_TYPE_TAG -> { + return convertToExpectedType(value, TypeUtils.getReferredType(type), schema); + } + default -> { + return fromStringWithType(value, type, schema); + } + } + } + + private static Optional> initNewMapValue(YamlParser.ComposerState state, Type expType) { + YamlParser.ParserContext parentContext = state.parserContexts.peek(); + state.parserContexts.push(YamlParser.ParserContext.MAP); + if (expType == null) { + state.fieldNameHierarchy.push(new Stack<>()); + return Optional.empty(); + } + if (state.currentYamlNode != null) { + state.nodesStack.push(state.currentYamlNode); + } + BMap nextMapValue = checkTypeAndCreateMappingValue(state, expType, parentContext); + return Optional.of(nextMapValue); + } + + static BMap checkTypeAndCreateMappingValue(YamlParser.ComposerState state, Type expType, + YamlParser.ParserContext parentContext) { + Type currentType = TypeUtils.getReferredType(expType); + BMap nextMapValue; + switch (currentType.getTag()) { + case TypeTags.RECORD_TYPE_TAG -> { + RecordType recordType = (RecordType) currentType; + nextMapValue = ValueCreator.createRecordValue(expType.getPackage(), expType.getName()); + state.updateFieldHierarchiesAndRestType(getAllFieldsInRecord(recordType), + recordType.getRestFieldType()); + } + case TypeTags.MAP_TAG -> { + nextMapValue = ValueCreator.createMapValue((MapType) currentType); + state.updateFieldHierarchiesAndRestType(new HashMap<>(), ((MapType) currentType).getConstrainedType()); + } + case TypeTags.JSON_TAG -> { + nextMapValue = ValueCreator.createMapValue(JSON_MAP_TYPE); + state.updateFieldHierarchiesAndRestType(new HashMap<>(), currentType); + } + case TypeTags.ANYDATA_TAG -> { + nextMapValue = ValueCreator.createMapValue(ANYDATA_MAP_TYPE); + state.updateFieldHierarchiesAndRestType(new HashMap<>(), currentType); + } + case TypeTags.INTERSECTION_TAG -> { + Optional mutableType = getMutableType((IntersectionType) currentType); + if (mutableType.isEmpty()) { + throw DiagnosticLog.error(DiagnosticErrorCode.INVALID_TYPE, currentType, "map type"); + } + return checkTypeAndCreateMappingValue(state, mutableType.get(), parentContext); + } + case TypeTags.UNION_TAG -> { + nextMapValue = ValueCreator.createMapValue(JSON_MAP_TYPE); + state.parserContexts.push(YamlParser.ParserContext.MAP); + state.unionDepth++; + state.fieldNameHierarchy.push(new Stack<>()); + } + default -> { + if (parentContext == YamlParser.ParserContext.ARRAY) { + throw DiagnosticLog.error(DiagnosticErrorCode.INVALID_TYPE, currentType, "map type"); + } + throw DiagnosticLog.error(DiagnosticErrorCode.INVALID_TYPE_FOR_FIELD, getCurrentFieldPath(state)); + } + } + return nextMapValue; + } + + static Optional getMutableType(IntersectionType intersectionType) { + for (Type constituentType : intersectionType.getConstituentTypes()) { + if (constituentType.getTag() == TypeTags.READONLY_TAG) { + continue; + } + return Optional.of(constituentType); + } + return Optional.empty(); + } + + static Type getMemberType(Type expectedType, int index, boolean allowDataProjection) { + if (expectedType == null) { + return null; + } + + if (expectedType.getTag() == TypeTags.ARRAY_TAG) { + ArrayType arrayType = (ArrayType) expectedType; + if (arrayType.getState() == ArrayType.ArrayState.OPEN + || arrayType.getState() == ArrayType.ArrayState.CLOSED && index < arrayType.getSize()) { + return arrayType.getElementType(); + } + + if (!allowDataProjection) { + throw DiagnosticLog.error(DiagnosticErrorCode.ARRAY_SIZE_MISMATCH); + } + return null; + } else if (expectedType.getTag() == TypeTags.TUPLE_TAG) { + TupleType tupleType = (TupleType) expectedType; + List tupleTypes = tupleType.getTupleTypes(); + if (tupleTypes.size() < index + 1) { + Type restType = tupleType.getRestType(); + if (restType == null && !allowDataProjection) { + throw DiagnosticLog.error(DiagnosticErrorCode.ARRAY_SIZE_MISMATCH); + } + return restType; + } + return tupleTypes.get(index); + } + return expectedType; + } + + public static Object fromStringWithType(BString string, Type expType, Types.YAMLSchema schema) { + String value = string.getValue(); + return switch (expType.getTag()) { + case TypeTags.INT_TAG -> stringToInt(value); + case TypeTags.BYTE_TAG -> stringToByte(value); + case TypeTags.SIGNED8_INT_TAG -> stringToSigned8Int(value); + case TypeTags.SIGNED16_INT_TAG-> stringToSigned16Int(value); + case TypeTags.SIGNED32_INT_TAG -> stringToSigned32Int(value); + case TypeTags.UNSIGNED8_INT_TAG -> stringToUnsigned8Int(value); + case TypeTags.UNSIGNED16_INT_TAG -> stringToUnsigned16Int(value); + case TypeTags.UNSIGNED32_INT_TAG -> stringToUnsigned32Int(value); + case TypeTags.FLOAT_TAG -> stringToFloat(value); + case TypeTags.DECIMAL_TAG -> stringToDecimal(value); + case TypeTags.CHAR_STRING_TAG -> stringToChar(value); + case TypeTags.STRING_TAG -> stringToString(string, schema); + case TypeTags.BOOLEAN_TAG -> stringToBoolean(value); + case TypeTags.NULL_TAG -> stringToNull(value); + case TypeTags.FINITE_TYPE_TAG -> stringToFiniteType(value, (FiniteType) expType, schema); + case TypeTags.UNION_TAG -> stringToUnion(string, (UnionType) expType, schema); + case TypeTags.JSON_TAG, TypeTags.ANYDATA_TAG -> stringToUnion(string, JSON_TYPE_WITH_BASIC_TYPES, schema); + case TypeTags.TYPE_REFERENCED_TYPE_TAG -> fromStringWithType(string, + ((ReferenceType) expType).getReferredType(), schema); + case TypeTags.INTERSECTION_TAG -> fromStringWithType(string, + ((IntersectionType) expType).getEffectiveType(), schema); + default -> returnError(value, expType.toString()); + }; + } + + private static Object stringToString(BString string, Types.YAMLSchema schema) { + String value = string.getValue(); + if (schema == Types.YAMLSchema.JSON_SCHEMA) { + if (value.equals("null") || value.equals("true") || value.equals("false")) { + return returnError(value, PredefinedTypes.TYPE_STRING.toString()); + } + } else if (schema == Types.YAMLSchema.CORE_SCHEMA) { + if (TagResolutionUtils.isCoreSchemaNull(value) || TagResolutionUtils.isCoreSchemaBoolean(value)) { + return returnError(value, PredefinedTypes.TYPE_STRING.toString()); + } + } + return string; + } + + private static Object stringToFiniteType(String value, FiniteType finiteType, Types.YAMLSchema schema) { + return finiteType.getValueSpace().stream() + .filter(finiteValue -> !(convertToSingletonValue(value, finiteValue, schema) instanceof BError)) + .findFirst() + .orElseGet(() -> returnError(value, finiteType.toString())); + } + + private static Object convertToSingletonValue(String str, Object singletonValue, Types.YAMLSchema schema) { + String singletonStr = String.valueOf(singletonValue); + if (str.equals(singletonStr)) { + BString value = StringUtils.fromString(str); + Type expType = TypeUtils.getType(singletonValue); + return fromStringWithType(value, expType, schema); + } else { + return returnError(str, singletonStr); + } + } + + public static BString convertValueToBString(Object value) { + if (value instanceof BString) { + return (BString) value; + } else if (value instanceof Long || value instanceof String || value instanceof Integer + || value instanceof BDecimal || value instanceof Double || value instanceof Boolean) { + return StringUtils.fromString(String.valueOf(value)); + } + throw new RuntimeException("cannot convert to BString"); + } + + private static int stringToByte(String value) throws NumberFormatException { + int intValue = Integer.parseInt(value); + if (!isByteLiteral(intValue)) { + throw DiagnosticLog.error(DiagnosticErrorCode.INCOMPATIBLE_TYPE, PredefinedTypes.TYPE_BYTE, value); + } + return intValue; + } + + private static Long stringToInt(String value) throws NumberFormatException { + return Long.parseLong(value); + } + + private static Double stringToFloat(String value) throws NumberFormatException { + if (hasFloatOrDecimalLiteralSuffix(value)) { + throw new NumberFormatException(); + } + return Double.parseDouble(value); + } + + private static BDecimal stringToDecimal(String value) throws NumberFormatException { + return ValueCreator.createDecimalValue(value); + } + + private static long stringToSigned8Int(String value) throws NumberFormatException { + long intValue = Long.parseLong(value); + if (!isSigned8LiteralValue(intValue)) { + throw DiagnosticLog.error(DiagnosticErrorCode.INCOMPATIBLE_TYPE, PredefinedTypes.TYPE_INT_SIGNED_8, value); + } + return intValue; + } + + private static long stringToSigned16Int(String value) throws NumberFormatException { + long intValue = Long.parseLong(value); + if (!isSigned16LiteralValue(intValue)) { + throw DiagnosticLog.error(DiagnosticErrorCode.INCOMPATIBLE_TYPE, PredefinedTypes.TYPE_INT_SIGNED_16, value); + } + return intValue; + } + + private static long stringToSigned32Int(String value) throws NumberFormatException { + long intValue = Long.parseLong(value); + if (!isSigned32LiteralValue(intValue)) { + throw DiagnosticLog.error(DiagnosticErrorCode.INCOMPATIBLE_TYPE, PredefinedTypes.TYPE_INT_SIGNED_32, value); + } + return intValue; + } + + private static long stringToUnsigned8Int(String value) throws NumberFormatException { + long intValue = Long.parseLong(value); + if (!isUnsigned8LiteralValue(intValue)) { + throw DiagnosticLog.error(DiagnosticErrorCode.INCOMPATIBLE_TYPE, + PredefinedTypes.TYPE_INT_UNSIGNED_8, value); + } + return intValue; + } + + private static long stringToUnsigned16Int(String value) throws NumberFormatException { + long intValue = Long.parseLong(value); + if (!isUnsigned16LiteralValue(intValue)) { + throw DiagnosticLog.error(DiagnosticErrorCode.INCOMPATIBLE_TYPE, + PredefinedTypes.TYPE_INT_UNSIGNED_16, value); + } + return intValue; + } + + private static long stringToUnsigned32Int(String value) throws NumberFormatException { + long intValue = Long.parseLong(value); + if (!isUnsigned32LiteralValue(intValue)) { + throw DiagnosticLog.error(DiagnosticErrorCode.INCOMPATIBLE_TYPE, + PredefinedTypes.TYPE_INT_UNSIGNED_32, value); + } + return intValue; + } + + private static Object stringToBoolean(String value) throws NumberFormatException { + if (value.equals("true")) { + return true; + } + + if (value.equals("false")) { + return false; + } + return returnError(value, "boolean"); + } + + private static Object stringToNull(String value) throws NumberFormatException { + if (value.equals("null")) { + return null; + } + return returnError(value, "()"); + } + + private static BString stringToChar(String value) throws NumberFormatException { + if (!isCharLiteralValue(value)) { + throw DiagnosticLog.error(DiagnosticErrorCode.INCOMPATIBLE_TYPE, + PredefinedTypes.TYPE_STRING_CHAR, value); + } + return StringUtils.fromString(value); + } + + private static BError returnError(String string, String expType) { + return DiagnosticLog.error(DiagnosticErrorCode.CANNOT_CONVERT_TO_EXPECTED_TYPE, + PredefinedTypes.TYPE_STRING.getName(), string, expType); + } + + static void updateNextMapValue(YamlParser.ComposerState state) { + Type expType = state.expectedTypes.peek(); + + Optional> nextMap = initNewMapValue(state, expType); + if (nextMap.isPresent()) { + state.currentYamlNode = nextMap.get(); + } else { + // This will restrict from checking the fieldHierarchy. + state.jsonFieldDepth++; + } + } + + static void updateExpectedType(YamlParser.ComposerState state) { + if (state.unionDepth > 0) { + return; + } + state.expectedTypes.push(getMemberType(state.expectedTypes.peek(), + state.arrayIndexes.peek(), state.allowDataProjection)); + } + + static void updateNextMapValueBasedOnExpType(YamlParser.ComposerState state) { + updateNextMapValue(state); + } + + static void updateNextArrayValueBasedOnExpType(YamlParser.ComposerState state) { + updateNextArrayValue(state); + } + + static void updateNextArrayValue(YamlParser.ComposerState state) { + state.arrayIndexes.push(0); + Optional nextArray = initNewArrayValue(state); + nextArray.ifPresent(array -> state.currentYamlNode = array); + } + + static Optional initNewArrayValue(YamlParser.ComposerState state) { + state.parserContexts.push(YamlParser.ParserContext.ARRAY); + if (state.expectedTypes.peek() == null) { + return Optional.empty(); + } + + Object currentYamlNode = state.currentYamlNode; + Type expType = TypeUtils.getReferredType(state.expectedTypes.peek()); + if (expType.getTag() == TypeTags.INTERSECTION_TAG) { + Optional type = getMutableType((IntersectionType) expType); + if (type.isEmpty()) { + throw DiagnosticLog.error(DiagnosticErrorCode.INVALID_TYPE, expType, "array type"); + } + expType = type.get(); + } + BArray nextArrValue = initArrayValue(state, expType); + if (currentYamlNode == null) { + return Optional.ofNullable(nextArrValue); + } + + state.nodesStack.push(currentYamlNode); + return Optional.ofNullable(nextArrValue); + } + + private static Object stringToUnion(BString string, UnionType expType, Types.YAMLSchema schema) + throws NumberFormatException { + List memberTypes = new ArrayList<>(expType.getMemberTypes()); + memberTypes.sort(Comparator.comparingInt(t -> { + int index = TYPE_PRIORITY_ORDER.indexOf(TypeUtils.getReferredType(t).getTag()); + return index == -1 ? Integer.MAX_VALUE : index; + })); + for (Type memberType : memberTypes) { + try { + Object result = fromStringWithType(string, memberType, schema); + if (result instanceof BError) { + continue; + } + return result; + } catch (Exception e) { + // Skip + } + } + return returnError(string.getValue(), expType.toString()); + } + + public static Object constructReadOnlyValue(Object value) { + return CloneReadOnly.cloneReadOnly(value); + } + + private static boolean hasFloatOrDecimalLiteralSuffix(String value) { + int length = value.length(); + if (length == 0) { + return false; + } + + switch (value.charAt(length - 1)) { + case 'F': + case 'f': + case 'D': + case 'd': + return true; + default: + return false; + } + } + + private static boolean isByteLiteral(long longValue) { + return (longValue >= BBYTE_MIN_VALUE && longValue <= BBYTE_MAX_VALUE); + } + + private static boolean isSigned32LiteralValue(Long longObject) { + return (longObject >= SIGNED32_MIN_VALUE && longObject <= SIGNED32_MAX_VALUE); + } + + private static boolean isSigned16LiteralValue(Long longObject) { + return (longObject.intValue() >= SIGNED16_MIN_VALUE && longObject.intValue() <= SIGNED16_MAX_VALUE); + } + + private static boolean isSigned8LiteralValue(Long longObject) { + return (longObject.intValue() >= SIGNED8_MIN_VALUE && longObject.intValue() <= SIGNED8_MAX_VALUE); + } + + private static boolean isUnsigned32LiteralValue(Long longObject) { + return (longObject >= 0 && longObject <= UNSIGNED32_MAX_VALUE); + } + + private static boolean isUnsigned16LiteralValue(Long longObject) { + return (longObject.intValue() >= 0 && longObject.intValue() <= UNSIGNED16_MAX_VALUE); + } + + private static boolean isUnsigned8LiteralValue(Long longObject) { + return (longObject.intValue() >= 0 && longObject.intValue() <= UNSIGNED8_MAX_VALUE); + } + + private static boolean isCharLiteralValue(String value) { + return value.codePoints().count() == 1; + } +} diff --git a/native/src/main/java/io/ballerina/lib/data/yaml/parser/YamlParser.java b/native/src/main/java/io/ballerina/lib/data/yaml/parser/YamlParser.java new file mode 100644 index 0000000..927fada --- /dev/null +++ b/native/src/main/java/io/ballerina/lib/data/yaml/parser/YamlParser.java @@ -0,0 +1,2458 @@ +/* + * 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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.lib.data.yaml.parser; + +import io.ballerina.lib.data.yaml.common.Types; +import io.ballerina.lib.data.yaml.common.Types.Collection; +import io.ballerina.lib.data.yaml.common.YamlEvent; +import io.ballerina.lib.data.yaml.lexer.IndentUtils; +import io.ballerina.lib.data.yaml.lexer.LexerState; +import io.ballerina.lib.data.yaml.lexer.Token; +import io.ballerina.lib.data.yaml.lexer.YamlLexer; +import io.ballerina.lib.data.yaml.utils.Constants; +import io.ballerina.lib.data.yaml.utils.DiagnosticErrorCode; +import io.ballerina.lib.data.yaml.utils.DiagnosticLog; +import io.ballerina.lib.data.yaml.utils.Error; +import io.ballerina.lib.data.yaml.utils.JsonTraverse; +import io.ballerina.lib.data.yaml.utils.OptionsUtils; +import io.ballerina.lib.data.yaml.utils.TagResolutionUtils; +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.flags.SymbolFlags; +import io.ballerina.runtime.api.types.ArrayType; +import io.ballerina.runtime.api.types.Field; +import io.ballerina.runtime.api.types.IntersectionType; +import io.ballerina.runtime.api.types.MapType; +import io.ballerina.runtime.api.types.RecordType; +import io.ballerina.runtime.api.types.TupleType; +import io.ballerina.runtime.api.types.Type; +import io.ballerina.runtime.api.types.UnionType; +import io.ballerina.runtime.api.utils.StringUtils; +import io.ballerina.runtime.api.utils.TypeUtils; +import io.ballerina.runtime.api.values.BArray; +import io.ballerina.runtime.api.values.BError; +import io.ballerina.runtime.api.values.BMap; +import io.ballerina.runtime.api.values.BString; +import org.ballerinalang.langlib.value.CloneReadOnly; + +import java.io.Reader; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Stack; + +import static io.ballerina.lib.data.yaml.common.Types.Collection.SEQUENCE; +import static io.ballerina.lib.data.yaml.common.Types.DocumentType.ANY_DOCUMENT; +import static io.ballerina.lib.data.yaml.common.Types.DocumentType.BARE_DOCUMENT; +import static io.ballerina.lib.data.yaml.common.Types.DocumentType.DIRECTIVE_DOCUMENT; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.ANCHOR; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.COMMENT; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.DIRECTIVE; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.DIRECTIVE_MARKER; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.DOUBLE_QUOTE_DELIMITER; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.EMPTY_LINE; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.EOL; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.FOLDED; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.MAPPING_END; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.MAPPING_KEY; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.MAPPING_VALUE; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.PLANAR_CHAR; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.SEPARATION_IN_LINE; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.SEPARATOR; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.SEQUENCE_END; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.SEQUENCE_ENTRY; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.SINGLE_QUOTE_DELIMITER; +import static io.ballerina.lib.data.yaml.lexer.Token.TokenType.TAG; +import static io.ballerina.lib.data.yaml.parser.Directive.reservedDirective; +import static io.ballerina.lib.data.yaml.parser.Directive.tagDirective; +import static io.ballerina.lib.data.yaml.parser.Directive.yamlDirective; +import static io.ballerina.lib.data.yaml.parser.ParserUtils.ParserOption.EXPECT_MAP_KEY; +import static io.ballerina.lib.data.yaml.parser.ParserUtils.ParserOption.EXPECT_MAP_VALUE; +import static io.ballerina.lib.data.yaml.parser.ParserUtils.ParserOption.EXPECT_SEQUENCE_ENTRY; +import static io.ballerina.lib.data.yaml.parser.ParserUtils.ParserOption.EXPECT_SEQUENCE_VALUE; +import static io.ballerina.lib.data.yaml.parser.ParserUtils.getAllFieldsInRecord; +import static io.ballerina.lib.data.yaml.utils.Constants.DEFAULT_GLOBAL_TAG_HANDLE; +import static io.ballerina.lib.data.yaml.utils.Constants.DEFAULT_LOCAL_TAG_HANDLE; + +/** + * Core parsing of YAML strings. + * + * @since 0.1.0 + */ +public class YamlParser { + + public static final Map DEFAULT_TAG_HANDLES = Map.of("!", DEFAULT_LOCAL_TAG_HANDLE, + "!!", DEFAULT_GLOBAL_TAG_HANDLE); + + public static class ComposerState { + private final ParserState parserState; + private final Map anchorBuffer = new HashMap<>(); + Object currentYamlNode; + Field currentField; + Deque nodesStack = new ArrayDeque<>(); + Stack> fieldHierarchy = new Stack<>(); + Stack> visitedFieldHierarchy = new Stack<>(); + Stack restType = new Stack<>(); + Stack expectedTypes = new Stack<>(); + Stack> fieldNameHierarchy = new Stack<>(); + int jsonFieldDepth = 0; + Stack arrayIndexes = new Stack<>(); + Stack parserContexts = new Stack<>(); + YamlEvent terminatedDocEvent = null; + int unionDepth = 0; + boolean rootValueInitialized = false; + final Types.YAMLSchema schema; + final boolean allowAnchorRedefinition; + final boolean allowMapEntryRedefinition; + final boolean allowDataProjection; + final boolean nilAsOptionalField; + final boolean absentAsNilableType; + final boolean strictTupleOrder; + boolean expectedTypeIsReadonly = false; + boolean isPossibleStream = false; + DynamicTupleState dynamicTupleState = null; + + + public ComposerState(ParserState parserState, OptionsUtils.ReadConfig readConfig) { + this.parserState = parserState; + this.schema = readConfig.schema(); + this.allowAnchorRedefinition = readConfig.allowAnchorRedefinition(); + this.allowMapEntryRedefinition = readConfig.allowMapEntryRedefinition(); + this.allowDataProjection = readConfig.allowDataProjection(); + this.nilAsOptionalField = readConfig.nilAsOptionalField(); + this.absentAsNilableType = readConfig.absentAsNilableType(); + this.strictTupleOrder = readConfig.strictTupleOrder(); + } + + public int getLine() { + return parserState.getLine(); + } + + public int getColumn() { + return parserState.getColumn(); + } + + private void updateIndexOfArrayElement() { + int arrayIndex = arrayIndexes.pop(); + arrayIndexes.push(arrayIndex + 1); + } + + public void updateFieldHierarchiesAndRestType(Map fields, Type restType) { + this.fieldHierarchy.push(new HashMap<>(fields)); + this.visitedFieldHierarchy.push(new HashMap<>()); + this.restType.push(restType); + this.fieldNameHierarchy.push(new Stack<>()); + } + + private void checkUnionAndFinalizeArrayObject() { + arrayIndexes.pop(); + if (unionDepth > 0) { + finalizeObject(); + return; + } + finalizeArrayObjectAndRemoveExpectedType(); + } + + public void checkUnionAndFinalizeNonArrayObject() { + if (unionDepth > 0) { + fieldNameHierarchy.pop(); + finalizeObject(); + return; + } + finalizeNonArrayObjectAndRemoveExpectedType(); + } + + private void finalizeArrayObjectAndRemoveExpectedType() { + finalizeObject(); + expectedTypes.pop(); + } + + public void finalizeNonArrayObjectAndRemoveExpectedType() { + finalizeNonArrayObject(); + expectedTypes.pop(); + } + + private void finalizeNonArrayObject() { + if (jsonFieldDepth > 0) { + jsonFieldDepth--; + } + + if (!expectedTypes.isEmpty() && expectedTypes.peek() == null) { + parserContexts.pop(); + fieldNameHierarchy.pop(); + return; + } + + Map remainingFields = fieldHierarchy.pop(); + visitedFieldHierarchy.pop(); + fieldNameHierarchy.pop(); + restType.pop(); + for (Field field : remainingFields.values()) { + if (absentAsNilableType && field.getFieldType().isNilable()) { + continue; + } + + if (SymbolFlags.isFlagOn(field.getFlags(), SymbolFlags.REQUIRED)) { + throw DiagnosticLog.error(DiagnosticErrorCode.REQUIRED_FIELD_NOT_PRESENT, field.getFieldName()); + } + } + finalizeObject(); + } + + public Object verifyAndConvertToUnion(Object json) { + if (unionDepth > 0) { + return json; + } + BMap options = ValueCreator.createMapValue(); + BMap allowDataProjectionMap = ValueCreator.createMapValue(); + if (!allowDataProjection) { + options.put(Constants.ALLOW_DATA_PROJECTION, false); + } else { + allowDataProjectionMap.put(Constants.NIL_AS_OPTIONAL_FIELD, nilAsOptionalField); + allowDataProjectionMap.put(Constants.ABSENT_AS_NILABLE_TYPE, absentAsNilableType); + options.put(Constants.ALLOW_DATA_PROJECTION, allowDataProjectionMap); + } + + return JsonTraverse.traverse(json, options, expectedTypes.peek(), schema); + } + + private void finalizeObject() { + parserContexts.pop(); + + if (unionDepth > 0) { + unionDepth--; + currentYamlNode = verifyAndConvertToUnion(currentYamlNode); + } + + // Skip the value and continue to next state. + if (!expectedTypes.isEmpty() && expectedTypes.peek() == null) { + return; + } + + if (nodesStack.isEmpty()) { + return; + } + + Object parentNode = nodesStack.pop(); + Type parentNodeType = TypeUtils.getType(parentNode); + int parentNodeTypeTag = TypeUtils.getReferredType(parentNodeType).getTag(); + if (parentNodeTypeTag == TypeTags.RECORD_TYPE_TAG || parentNodeTypeTag == TypeTags.MAP_TAG) { + Type expType = TypeUtils.getReferredType(expectedTypes.peek()); + if (expType.getTag() == TypeTags.INTERSECTION_TAG) { + currentYamlNode = CloneReadOnly.cloneReadOnly(currentYamlNode); + } + ((BMap) parentNode).put(StringUtils.fromString(fieldNameHierarchy.peek().pop()), + currentYamlNode); + currentYamlNode = parentNode; + return; + } + + switch (TypeUtils.getType(parentNode).getTag()) { + case TypeTags.ARRAY_TAG -> { + // Handle projection in array. + ArrayType arrayType = (ArrayType) parentNodeType; + if (arrayType.getState() == ArrayType.ArrayState.CLOSED && + arrayType.getSize() <= arrayIndexes.peek()) { + break; + } + Type expType = TypeUtils.getReferredType(expectedTypes.peek()); + if (expType.getTag() == TypeTags.INTERSECTION_TAG) { + currentYamlNode = CloneReadOnly.cloneReadOnly(currentYamlNode); + } + ((BArray) parentNode).add(arrayIndexes.peek(), currentYamlNode); + } + case TypeTags.TUPLE_TAG -> { + Type expType = TypeUtils.getReferredType(expectedTypes.peek()); + if (expType.getTag() == TypeTags.INTERSECTION_TAG) { + currentYamlNode = CloneReadOnly.cloneReadOnly(currentYamlNode); + } + ((BArray) parentNode).add(arrayIndexes.peek(), currentYamlNode); + } + default -> { + } + } + + currentYamlNode = parentNode; + } + + public void finalizeAnchorValueObject() { + // Skip the value and continue to next state. + if (!expectedTypes.isEmpty() && expectedTypes.peek() == null) { + return; + } + + if (nodesStack.isEmpty()) { + return; + } + + Object parentNode = nodesStack.pop(); + Type parentNodeType = TypeUtils.getType(parentNode); + int parentNodeTypeTag = TypeUtils.getReferredType(parentNodeType).getTag(); + if (parentNodeTypeTag == TypeTags.RECORD_TYPE_TAG || parentNodeTypeTag == TypeTags.MAP_TAG) { + ((BMap) parentNode).put(StringUtils.fromString(fieldNameHierarchy.peek().pop()), + currentYamlNode); + currentYamlNode = parentNode; + return; + } + + switch (TypeUtils.getType(parentNode).getTag()) { + case TypeTags.ARRAY_TAG -> { + // Handle projection in array. + ArrayType arrayType = (ArrayType) parentNodeType; + if (arrayType.getState() == ArrayType.ArrayState.CLOSED && + arrayType.getSize() <= arrayIndexes.peek()) { + break; + } + ((BArray) parentNode).add(arrayIndexes.peek(), currentYamlNode); + } + case TypeTags.TUPLE_TAG -> { + ((BArray) parentNode).add(arrayIndexes.peek(), currentYamlNode); + } + default -> { + } + } + + currentYamlNode = parentNode; + } + + static boolean hasMemberWithArraySubType(Type type) { + Type referredType = TypeUtils.getReferredType(type); + int referredTypeTag = referredType.getTag(); + if (referredTypeTag == TypeTags.ARRAY_TAG || referredType.getTag() == TypeTags.TUPLE_TAG + || referredTypeTag == TypeTags.ANYDATA_TAG || referredTypeTag == TypeTags.JSON_TAG) { + return true; + } else if (referredTypeTag == TypeTags.UNION_TAG) { + for (Type memberType : ((UnionType) type).getMemberTypes()) { + if (hasMemberWithArraySubType(memberType)) { + return true; + } + } + } else if (referredTypeTag == TypeTags.INTERSECTION_TAG) { + for (Type constituentType : ((IntersectionType) type).getConstituentTypes()) { + if (constituentType.getTag() == TypeTags.READONLY_TAG) { + continue; + } + return hasMemberWithArraySubType(constituentType); + } + } else if (referredTypeTag == TypeTags.TYPE_REFERENCED_TYPE_TAG) { + return hasMemberWithArraySubType(TypeUtils.getReferredType(type)); + } + return false; + } + + public void handleExpectedType(Type type) { + switch (type.getTag()) { + case TypeTags.RECORD_TYPE_TAG -> { + RecordType recordType = (RecordType) type; + expectedTypes.add(recordType); + updateFieldHierarchiesAndRestType(getAllFieldsInRecord(recordType), recordType.getRestFieldType()); + } + case TypeTags.ARRAY_TAG -> { + isPossibleStream = true; + expectedTypes.add(type); + arrayIndexes.push(0); + Type elementType = TypeUtils.getReferredType(((ArrayType) type).getElementType()); + Type unionType = TypeCreator.createUnionType(type, elementType); + expectedTypes.push(unionType); + } + case TypeTags.TUPLE_TAG -> { + isPossibleStream = true; + expectedTypes.add(type); + arrayIndexes.push(0); + TupleType tupleType = (TupleType) type; + List tupleElementTypes = tupleType.getTupleTypes(); + List unionMembers = new ArrayList<>(); + unionMembers.add(type); + unionMembers.addAll(tupleElementTypes); + Type tupleRestType = tupleType.getRestType(); + if (tupleRestType != null) { + unionMembers.add(tupleRestType); + } + Type unionType = TypeCreator.createUnionType(unionMembers); + expectedTypes.push(unionType); + + } + case TypeTags.NULL_TAG, TypeTags.BOOLEAN_TAG, TypeTags.INT_TAG, TypeTags.BYTE_TAG, + TypeTags.SIGNED8_INT_TAG, TypeTags.SIGNED16_INT_TAG, TypeTags.SIGNED32_INT_TAG, + TypeTags.UNSIGNED8_INT_TAG, TypeTags.UNSIGNED16_INT_TAG, TypeTags.UNSIGNED32_INT_TAG, + TypeTags.FLOAT_TAG, TypeTags.DECIMAL_TAG, TypeTags.CHAR_STRING_TAG, TypeTags.STRING_TAG, + TypeTags.FINITE_TYPE_TAG -> + expectedTypes.push(type); + case TypeTags.UNION_TAG -> { + UnionType unionType = (UnionType) type; + for (Type memberType : unionType.getMemberTypes()) { + if (hasMemberWithArraySubType(memberType)) { + isPossibleStream = true; + break; + } + } + expectedTypes.push(type); + } + case TypeTags.JSON_TAG, TypeTags.ANYDATA_TAG -> { + isPossibleStream = true; + expectedTypes.push(type); + updateFieldHierarchiesAndRestType(new HashMap<>(), type); + } + case TypeTags.MAP_TAG -> { + expectedTypes.push(type); + updateFieldHierarchiesAndRestType(new HashMap<>(), ((MapType) type).getConstrainedType()); + } + case TypeTags.INTERSECTION_TAG -> { + Type effectiveType = ((IntersectionType) type).getEffectiveType(); + if (!SymbolFlags.isFlagOn(SymbolFlags.READONLY, effectiveType.getFlags())) { + throw DiagnosticLog.error(DiagnosticErrorCode.UNSUPPORTED_TYPE, type); + } + + for (Type constituentType : ((IntersectionType) type).getConstituentTypes()) { + if (constituentType.getTag() == TypeTags.READONLY_TAG) { + continue; + } + handleExpectedType(TypeUtils.getReferredType(constituentType)); + expectedTypeIsReadonly = true; + break; + } + } + case TypeTags.TYPE_REFERENCED_TYPE_TAG -> handleExpectedType(TypeUtils.getReferredType(type)); + default -> throw DiagnosticLog.error(DiagnosticErrorCode.UNSUPPORTED_TYPE, type); + } + } + } + + public enum ParserContext { + MAP, + ARRAY + } + + /** + * Parses the contents in the given {@link Reader} and returns subtype of anydata value. + * + * @param reader reader which contains the YAML content + * @param options represent the options that can be used to modify the behaviour of conversion + * @param expectedType Shape of the YAML content required + * @return subtype of anydata value + * @throws BError for any parsing error + */ + public static Object compose(Reader reader, BMap options, Type expectedType) throws BError { + OptionsUtils.ReadConfig readConfig = OptionsUtils.resolveReadConfig(options); + ComposerState composerState = new ComposerState(new ParserState(reader), readConfig); + composerState.handleExpectedType(expectedType); + try { + return composerState.isPossibleStream ? composeStream(composerState) : composeDocument(composerState); + } catch (Error.YamlParserException e) { + return DiagnosticLog.error(DiagnosticErrorCode.YAML_PARSER_EXCEPTION, + e.getMessage(), e.getLine(), e.getColumn()); + } + } + + private static Object composeDocument(ComposerState state) throws Error.YamlParserException { + return composeDocument(state, null); + } + + private static Object composeDocument(ComposerState state, YamlEvent eventParam) throws Error.YamlParserException { + YamlEvent event = eventParam == null ? handleEvent(state, ANY_DOCUMENT) : eventParam; + + // Ignore the start document marker for explicit documents + if (event.getKind() == YamlEvent.EventKind.DOCUMENT_MARKER_EVENT && + ((YamlEvent.DocumentMarkerEvent) event).isExplicit()) { + event = handleEvent(state, ANY_DOCUMENT); + } + + Object output = composeNode(state, event, false); + + // Return an error if there is another root event + event = handleEvent(state); + if (ParserUtils.isStreamEndEvent(event)) { + return handleOutput(state, output); + } + if (event.getKind() == YamlEvent.EventKind.DOCUMENT_MARKER_EVENT) { + state.terminatedDocEvent = event; + return handleOutput(state, output); + } + throw new Error.YamlParserException("there can only be one root event to a document", state.getLine(), + state.getColumn()); + } + + private static class DynamicTupleState { + UnionType tupleMembersUnion; + List> indexToTupleMemberMapping; + final int tupleSize; + final TupleType tupleType; + final Type restType; + int restIndex; + + DynamicTupleState(TupleType tupleType) { + this.tupleType = tupleType; + this.tupleSize = tupleType.getTupleTypes().size(); + this.restIndex = tupleSize; + this.restType = tupleType.getRestType(); + this.indexToTupleMemberMapping = constructTupleMembersTableEntry(tupleType); + this.tupleMembersUnion = createTupleMembersUnion(tupleType, restType); + + } + + private boolean isTupleValueCompleted() { + return indexToTupleMemberMapping.size() == 0; + } + + private boolean canAddMoreRestMembers() { + return restType != null; + } + + private void updateTupleMemberIndexTableAndAddToTuple(Object value, BArray tupleValue) { + Type type = io.ballerina.lib.data.yaml.utils.TypeUtils.getType(value); + int typeHashOfBValue = type.hashCode(); + for (int i = 0; i < indexToTupleMemberMapping.size(); i++) { + Map typeMapping = indexToTupleMemberMapping.get(i); + if (typeMapping.containsKey(typeHashOfBValue)) { + indexToTupleMemberMapping.remove(i); + tupleValue.add(typeMapping.get(typeHashOfBValue), value); + return; + } + } + if (restType != null && TypeUtils.getReferredType(restType).hashCode() == typeHashOfBValue) { + tupleValue.add(restIndex++, value); + } + } + + private void updateUnionType() { + List allMembers = new ArrayList<>(); + List tupleMembers = tupleType.getTupleTypes(); + for (Map mapping : indexToTupleMemberMapping) { + mapping.values().stream().findFirst().ifPresent(index -> allMembers.add(tupleMembers.get(index))); + } + if (restType != null) { + allMembers.add(restType); + } + tupleMembersUnion = TypeCreator.createUnionType(allMembers); + } + + private static UnionType createTupleMembersUnion(TupleType tupleType, Type restType) { + List allMembers = new ArrayList<>(tupleType.getTupleTypes()); + if (restType != null) { + allMembers.add(restType); + } + return TypeCreator.createUnionType(allMembers); + } + + private static List> constructTupleMembersTableEntry(TupleType tupleType) { + List memberTypes = tupleType.getTupleTypes(); + List> tupleIndexTable = new LinkedList<>(); + for (int i = 0; i < memberTypes.size(); i++) { + Map typeMapping = new HashMap<>(); + createIndexToTypeMapping(memberTypes.get(i), typeMapping, i); + tupleIndexTable.add(typeMapping); + } + return tupleIndexTable; + } + + private static void createIndexToTypeMapping(Type type, Map typeMapping, int index) { + Type referredType = TypeUtils.getReferredType(type); + if (referredType.getTag() == TypeTags.UNION_TAG) { + UnionType unionType = (UnionType) referredType; + for (Type memberType: unionType.getMemberTypes()) { + createIndexToTypeMapping(memberType, typeMapping, index); + } + } else { + int hashValue = referredType.hashCode(); + typeMapping.put(hashValue, index); + } + } + } + + private static Object composeStream(ComposerState state) throws Error.YamlParserException { + YamlEvent event = handleEvent(state, ANY_DOCUMENT); + + state.currentYamlNode = Values.initRootArrayValue(state); + + int prevUnionDepth = state.unionDepth; + boolean isBeginWithStream = isBeginWithStream(state, event); + boolean isTupleExpected = state.expectedTypes.get(0).getTag() == TypeTags.TUPLE_TAG; + boolean tupleOrArrayExpected = state.expectedTypes.size() == 2; + boolean processFirstElement = false; + + if (!tupleOrArrayExpected && state.unionDepth == 1) { + state.expectedTypes.push(PredefinedTypes.TYPE_JSON); + state.unionDepth = 0; + } + + // Iterate all the documents + while (!ParserUtils.isStreamEndEvent(event)) { + Values.updateExpectedType(state); + composeDocument(state, event); + event = getNextYamlDocEvent(state); + + if (!isTupleExpected) { + state.updateIndexOfArrayElement(); + } + if (!processFirstElement && state.expectedTypes.size() > 1) { + state.expectedTypes.pop(); + BArray bArray = (BArray) state.currentYamlNode; + // check the yaml input contain only a single document + if (ParserUtils.isStreamEndEvent(event) && !isBeginWithStream) { + state.unionDepth--; + Object result = state.verifyAndConvertToUnion(bArray.getValues()[0]); + return handleOutput(state, result); + } + processFirstElement = true; + Type peekType = state.expectedTypes.peek(); + Type elementType; + if (peekType.getTag() == TypeTags.ARRAY_TAG) { + elementType = TypeUtils.getReferredType(((ArrayType) peekType).getElementType()); + } else if (peekType.getTag() == TypeTags.UNION_TAG) { + state.expectedTypes.add(PredefinedTypes.TYPE_JSON); + continue; + } else { + TupleType tupleType = (TupleType) peekType; + if (!state.strictTupleOrder) { + state.dynamicTupleState = new DynamicTupleState(tupleType); + } + List tupleTypes = tupleType.getTupleTypes(); + Type restType = tupleType.getRestType(); + if (tupleTypes.size() > 0) { + elementType = tupleTypes.get(0); + } else { + elementType = restType; + } + } + if (isTupleExpected && !state.strictTupleOrder) { + DynamicTupleState dynamicTupleState = state.dynamicTupleState; + state.expectedTypes.add(dynamicTupleState.tupleMembersUnion); + state.unionDepth = 0; + Object result; + try { + result = state.verifyAndConvertToUnion(bArray.getValues()[0]); + } catch (Exception e) { + state.expectedTypes.pop(); + state.currentYamlNode = Values.initRootArrayValue(state); + state.expectedTypes.add(dynamicTupleState.tupleMembersUnion); + state.unionDepth = 1; + state.nodesStack.add(state.currentYamlNode); + state.currentYamlNode = bArray; + continue; + } + state.expectedTypes.pop(); + state.currentYamlNode = Values.initRootArrayValue(state); + + dynamicTupleState.updateTupleMemberIndexTableAndAddToTuple(result, (BArray) state.currentYamlNode); + dynamicTupleState.updateUnionType(); + + if (dynamicTupleState.isTupleValueCompleted() && !dynamicTupleState.canAddMoreRestMembers()) { + break; + } + state.expectedTypes.add(dynamicTupleState.tupleMembersUnion); + state.unionDepth = 1; + state.nodesStack.add(state.currentYamlNode); + state.currentYamlNode = bArray; + continue; + } + + if (elementType.getTag() == TypeTags.UNION_TAG) { + state.expectedTypes.add(elementType); + } else { + state.expectedTypes.push(elementType); + state.unionDepth = 0; + Object result = state.verifyAndConvertToUnion(bArray.getValues()[0]); + state.expectedTypes.pop(); + state.currentYamlNode = Values.initRootArrayValue(state); + ((BArray) state.currentYamlNode).add(0, result); + } + continue; + } + + int peekTag = state.expectedTypes.peek().getTag(); + if (!processFirstElement && (peekTag == TypeTags.ANYDATA_TAG || peekTag == TypeTags.JSON_TAG)) { + processFirstElement = true; + BArray bArray = (BArray) state.currentYamlNode; + if (ParserUtils.isStreamEndEvent(event) && !isBeginWithStream) { + Object result = state.verifyAndConvertToUnion(bArray.getValues()[0]); + return handleOutput(state, result); + } + } + + if (isTupleExpected && !state.strictTupleOrder) { + BArray bArray = (BArray) state.currentYamlNode; + state.expectedTypes.pop(); + state.expectedTypes.add(state.dynamicTupleState.tupleMembersUnion); + state.unionDepth = 0; + Object result; + try { + result = state.verifyAndConvertToUnion(bArray.getValues()[0]); + } catch (Exception e) { + state.unionDepth = 1; + continue; + } + state.expectedTypes.pop(); + state.currentYamlNode = state.nodesStack.pop(); + + state.dynamicTupleState.updateTupleMemberIndexTableAndAddToTuple(result, + (BArray) state.currentYamlNode); + state.dynamicTupleState.updateUnionType(); + if (state.dynamicTupleState.isTupleValueCompleted() && + !state.dynamicTupleState.canAddMoreRestMembers()) { + break; + } + state.expectedTypes.add(state.dynamicTupleState.tupleMembersUnion); + state.unionDepth = 1; + state.nodesStack.add(state.currentYamlNode); + state.currentYamlNode = bArray; + } + } + + if (!state.strictTupleOrder && state.dynamicTupleState != null + && !state.dynamicTupleState.isTupleValueCompleted()) { + throw DiagnosticLog.error(DiagnosticErrorCode.ARRAY_SIZE_MISMATCH); + } + + state.unionDepth = prevUnionDepth; + if (state.unionDepth == 1) { + state.unionDepth--; + if (state.expectedTypes.size() > 1) { + state.expectedTypes.pop(); + } + if (isTupleExpected && state.dynamicTupleState != null + && state.dynamicTupleState.canAddMoreRestMembers()) { + state.currentYamlNode = state.nodesStack.pop(); + } + return handleOutput(state, state.verifyAndConvertToUnion(state.currentYamlNode)); + } + return handleOutput(state, state.currentYamlNode); + } + + private static YamlEvent getNextYamlDocEvent(ComposerState state) throws Error.YamlParserException { + YamlEvent event; + if (state.terminatedDocEvent != null && + state.terminatedDocEvent.getKind() == YamlEvent.EventKind.DOCUMENT_MARKER_EVENT) { + // Explicit document markers should be passed to the composeDocument + if (((YamlEvent.DocumentMarkerEvent) state.terminatedDocEvent).isExplicit()) { + event = state.terminatedDocEvent; + state.terminatedDocEvent = null; + } else { // All the trailing document end markers should be ignored + state.terminatedDocEvent = null; + event = handleEvent(state, ANY_DOCUMENT); + + while (event.getKind() == YamlEvent.EventKind.DOCUMENT_MARKER_EVENT + && !(((YamlEvent.DocumentMarkerEvent) event).isExplicit())) { + event = handleEvent(state, ANY_DOCUMENT); + } + } + } else { // Obtain the stream end event + event = handleEvent(state, ANY_DOCUMENT); + } + return event; + } + + private static boolean isBeginWithStream(ComposerState state, YamlEvent event) { + return event.getKind() == YamlEvent.EventKind.DOCUMENT_MARKER_EVENT && state.terminatedDocEvent != null + && ((YamlEvent.DocumentMarkerEvent) state.terminatedDocEvent).isExplicit(); + } + + private static Object composeNode(ComposerState state, YamlEvent event, boolean mapOrSequenceScalar) + throws Error.YamlParserException { + + // Check for aliases + YamlEvent.EventKind eventKind = event.getKind(); + + if (eventKind == YamlEvent.EventKind.ALIAS_EVENT) { + YamlEvent.AliasEvent aliasEvent = (YamlEvent.AliasEvent) event; + Object alias = state.anchorBuffer.get(aliasEvent.getAlias()); + if (alias == null) { + throw new Error.YamlParserException("anchor does not exist", state.getLine(), state.getColumn()); + } + return alias; + } + + // Ignore end events + if (eventKind == YamlEvent.EventKind.END_EVENT) { + return null; + } + + // Ignore document markers + if (eventKind == YamlEvent.EventKind.DOCUMENT_MARKER_EVENT) { + state.terminatedDocEvent = event; + return null; + } + + Object output; + // Check for collections + if (eventKind == YamlEvent.EventKind.START_EVENT) { + YamlEvent.StartEvent startEvent = (YamlEvent.StartEvent) event; + + switch (startEvent.getStartType()) { + case SEQUENCE -> { + output = castData(state, composeSequence(state, startEvent.isFlowStyle()), + Types.FailSafeSchema.SEQUENCE, event.getTag()); + } + case MAPPING -> { + output = castData(state, composeMapping(state, startEvent.isFlowStyle(), startEvent.isImplicit()), + Types.FailSafeSchema.MAPPING, event.getTag()); + } + default -> { + throw new Error.YamlParserException("only sequence and mapping are allowed as node start events", + state.getLine(), state.getColumn()); + } + } + checkAnchor(state, event, output); + return state.currentYamlNode; + } + + YamlEvent.ScalarEvent scalarEvent = (YamlEvent.ScalarEvent) event; + + // Check for scalar + output = castData(state, scalarEvent.getValue(), Types.FailSafeSchema.STRING, event.getTag()); + checkAnchor(state, event, output); + if (mapOrSequenceScalar) { + return output; + } + processValue(state, scalarEvent.getValue()); + return state.currentYamlNode; + } + + private static Object handleOutput(ComposerState state, Object output) { + return state.expectedTypeIsReadonly ? Values.constructReadOnlyValue(output) : output; + } + + private static void processValue(ComposerState state, String value) { + Type expType; + if (state.unionDepth > 0) { + expType = PredefinedTypes.TYPE_JSON; + } else { + expType = state.expectedTypes.pop(); + if (expType == null) { + return; + } + } + state.currentYamlNode = Values.convertAndUpdateCurrentValueNode(state, value, expType); + } + + public static Object composeSequence(YamlParser.ComposerState state, boolean flowStyle) + throws Error.YamlParserException { + boolean firstElement = true; + if (!state.rootValueInitialized) { + state.currentYamlNode = Values.initRootArrayValue(state); + } else { + Values.updateNextArrayValueBasedOnExpType(state); + } + + YamlEvent event = handleEvent(state, EXPECT_SEQUENCE_VALUE); + + // Iterate until the end sequence event is detected + boolean terminated = false; + while (!terminated) { + if (event.getKind() == YamlEvent.EventKind.DOCUMENT_MARKER_EVENT) { + state.terminatedDocEvent = event; + if (!flowStyle) { + break; + } + throw new Error.YamlParserException("unexpected event", state.getLine(), state.getColumn()); + } + + if (event.getKind() == YamlEvent.EventKind.END_EVENT) { + YamlEvent.EndEvent endEvent = (YamlEvent.EndEvent) event; + + switch (endEvent.getEndType()) { + case MAPPING -> throw new Error.YamlParserException("unexpected event", + state.getLine(), state.getColumn()); + case STREAM -> { + if (!flowStyle) { + terminated = true; + break; + } + throw new Error.YamlParserException("unexpected event", state.getLine(), state.getColumn()); + } + case SEQUENCE -> { + terminated = true; + } + } + } + if (!terminated) { + if (!firstElement) { + state.updateIndexOfArrayElement(); + } + firstElement = false; + Values.updateExpectedType(state); + Object value = composeNode(state, event, true); + if (value instanceof String scalarValue) { + processValue(state, scalarValue); + } else if (event.getKind() == YamlEvent.EventKind.ALIAS_EVENT) { + state.nodesStack.push(state.currentYamlNode); + state.currentYamlNode = state.verifyAndConvertToUnion(value); + state.finalizeAnchorValueObject(); + state.expectedTypes.pop(); + } else if (value == null || value instanceof Double + || value instanceof Long || value instanceof Boolean) { + state.currentYamlNode = Values.updateCurrentValueNode(state, state.currentYamlNode, value); + } + event = handleEvent(state, EXPECT_SEQUENCE_ENTRY); + } + } + + Object tmpCurrentYaml = state.currentYamlNode; + state.checkUnionAndFinalizeArrayObject(); + return tmpCurrentYaml; + } + + public static Object composeMapping(ComposerState state, boolean flowStyle, boolean implicitMapping) + throws Error.YamlParserException { + if (!state.rootValueInitialized) { + state.currentYamlNode = Values.initRootMapValue(state); + } else { + Values.updateNextMapValueBasedOnExpType(state); + } + Set keys = new HashSet<>(); + YamlEvent event = handleEvent(state, EXPECT_MAP_KEY); + + // Iterate until an end event is detected + boolean terminated = false; + while (!terminated) { + if (event.getKind() == YamlEvent.EventKind.DOCUMENT_MARKER_EVENT) { + state.terminatedDocEvent = event; + if (!flowStyle) { + break; + } + throw new Error.YamlParserException("unexpected event", state.getLine(), state.getColumn()); + } + + if (event.getKind() == YamlEvent.EventKind.END_EVENT) { + YamlEvent.EndEvent endEvent = (YamlEvent.EndEvent) event; + switch (endEvent.getEndType()) { + case MAPPING -> terminated = true; + case SEQUENCE -> throw new Error.YamlParserException("unexpected event", + state.getLine(), state.getColumn()); + default -> { + if (!flowStyle) { + terminated = true; + break; + } + throw new Error.YamlParserException("unexpected event", state.getLine(), state.getColumn()); + } + } + if (terminated) { + break; + } + } + + // Cannot have a nested block mapping if a value is assigned + if (event.getKind() == YamlEvent.EventKind.START_EVENT + && !((YamlEvent.StartEvent) event).isFlowStyle()) { + throw new RuntimeException("Cannot have nested mapping under a key-pair that is already assigned"); + } + + // Compose the key + String key = (String) composeNode(state, event, true); + + if (!state.allowMapEntryRedefinition && !keys.add(key)) { + throw new Error.YamlParserException("cannot have duplicate map entries for '${key.toString()}", + state.getLine(), state.getColumn()); + } + Values.handleFieldName(key, state); + + // Compose the value + event = handleEvent(state, EXPECT_MAP_VALUE); + + // Check for mapping end events + if (event.getKind() == YamlEvent.EventKind.END_EVENT) { + YamlEvent.EndEvent endEvent = (YamlEvent.EndEvent) event; + switch (endEvent.getEndType()) { + case MAPPING -> { + terminated = true; + } + case SEQUENCE -> throw new Error.YamlParserException("unexpected event error", + state.getLine(), state.getColumn()); + default -> { + if (!flowStyle) { + terminated = true; + break; + } + throw new Error.YamlParserException("unexpected event error", + state.getLine(), state.getColumn()); + } + } + if (terminated) { + break; + } + } else { + Object value = composeNode(state, event, true); + if (value instanceof String scalarValue) { + Type expType; + if (state.unionDepth > 0) { + expType = PredefinedTypes.TYPE_JSON; + state.currentYamlNode = Values.convertAndUpdateCurrentValueNode(state, + scalarValue, expType); + } else { + expType = state.expectedTypes.pop(); + if (expType == null && state.currentField == null) { + state.fieldNameHierarchy.peek().pop(); + } else if (expType == null) { + break; + } else if (state.jsonFieldDepth > 0 || state.currentField != null) { + state.currentYamlNode = Values.convertAndUpdateCurrentValueNode(state, + scalarValue, expType); + } else if (state.restType.peek() != null) { + try { + state.currentYamlNode = Values.convertAndUpdateCurrentValueNode(state, + scalarValue, expType); + // this element will be ignored in projection + } catch (BError ignored) { } + } + } + } else if (event.getKind() == YamlEvent.EventKind.ALIAS_EVENT) { + state.nodesStack.push(state.currentYamlNode); + state.currentYamlNode = state.verifyAndConvertToUnion(value); + state.finalizeAnchorValueObject(); + state.expectedTypes.pop(); + } else if (value == null || value instanceof Double + || value instanceof Long || value instanceof Boolean) { + state.currentYamlNode = Values.updateCurrentValueNode(state, state.currentYamlNode, value); + } + } + + // Terminate after single key-value pair if implicit mapping flag is set. + if (implicitMapping) { + break; + } + + event = handleEvent(state, EXPECT_MAP_KEY); + } + + Object tmpCurrentYaml = state.currentYamlNode; + state.checkUnionAndFinalizeNonArrayObject(); + return tmpCurrentYaml; + } + + + /** + * Update the alias dictionary for the given alias. + * + * @param state - Current composer state + * @param event - The event representing the alias name + * @param assignedValue - Anchored value to the alias + */ + public static void checkAnchor(ComposerState state, YamlEvent event, Object assignedValue) + throws Error.YamlParserException { + + if (event.getAnchor() != null) { + if (state.anchorBuffer.containsKey(event.getAnchor()) && !state.allowAnchorRedefinition) { + throw new Error.YamlParserException("duplicate anchor definition", state.getLine(), state.getColumn()); + } + state.anchorBuffer.put(event.getAnchor(), assignedValue); + } + } + + public static Object castData(ComposerState state, Object data, Types.FailSafeSchema kind, String tag) + throws Error.YamlParserException { + // Check for explicit keys + if (tag != null) { + // Check for the tags in the YAML failsafe schema + if (tag.equals(DEFAULT_GLOBAL_TAG_HANDLE + "str")) { + if (kind == Types.FailSafeSchema.STRING) { + return data.toString(); + } + } + + if (tag.equals(DEFAULT_GLOBAL_TAG_HANDLE + "seq")) { + if (kind == Types.FailSafeSchema.SEQUENCE) { + return data; + } + } + + if (tag.equals(DEFAULT_GLOBAL_TAG_HANDLE + "map")) { + if (kind == Types.FailSafeSchema.MAPPING) { + return data; + } + } + + if (tag.equals(DEFAULT_GLOBAL_TAG_HANDLE + "int")) { + if (state.schema == Types.YAMLSchema.JSON_SCHEMA) { + return TagResolutionUtils.constructSimpleInt(data.toString(), state); + } else if (state.schema == Types.YAMLSchema.CORE_SCHEMA) { + return TagResolutionUtils.constructInt(data.toString(), state); + } + } + + if (tag.equals(DEFAULT_GLOBAL_TAG_HANDLE + "float")) { + if (state.schema == Types.YAMLSchema.JSON_SCHEMA) { + return TagResolutionUtils.constructSimpleFloat(data.toString(), state); + } else if (state.schema == Types.YAMLSchema.CORE_SCHEMA) { + return TagResolutionUtils.constructFloat(data.toString(), state); + } + } + + if (tag.equals(DEFAULT_GLOBAL_TAG_HANDLE + "bool")) { + if (state.schema == Types.YAMLSchema.JSON_SCHEMA) { + return TagResolutionUtils.constructSimpleBool(data.toString(), state); + } else if (state.schema == Types.YAMLSchema.CORE_SCHEMA) { + return TagResolutionUtils.constructBool(data.toString(), state); + } + } + + if (tag.equals(DEFAULT_GLOBAL_TAG_HANDLE + "null")) { + if (state.schema == Types.YAMLSchema.JSON_SCHEMA) { + return TagResolutionUtils.constructSimpleNull(data.toString(), state); + } else if (state.schema == Types.YAMLSchema.CORE_SCHEMA) { + return TagResolutionUtils.constructNull(data.toString(), state); + } + } + + throw new Error.YamlParserException("tag schema not supported", state.getLine(), state.getColumn()); + } + return data; + } + + + /** + * Obtain an event for the for a set of tokens. + * + * @param state - Current parser state + * @return - Parsed event + */ + private static YamlEvent handleEvent(ComposerState state) throws Error.YamlParserException { + if (state.terminatedDocEvent != null && + state.terminatedDocEvent.getKind() == YamlEvent.EventKind.DOCUMENT_MARKER_EVENT) { + return state.terminatedDocEvent; + } + return parse(state.parserState, ParserUtils.ParserOption.DEFAULT, BARE_DOCUMENT); + } + + /** + * Obtain an event for the for a set of tokens. + * + * @param state - Current parser state + * @param docType - Document type to be parsed + * @return - Parsed event + */ + private static YamlEvent handleEvent(ComposerState state, Types.DocumentType docType) + throws Error.YamlParserException { + if (state.terminatedDocEvent != null && + state.terminatedDocEvent.getKind() == YamlEvent.EventKind.DOCUMENT_MARKER_EVENT) { + return state.terminatedDocEvent; + } + return parse(state.parserState, ParserUtils.ParserOption.DEFAULT, docType); + } + + /** + * Obtain an event for the for a set of tokens. + * + * @param state - Current parser state + * @param option - Expected values inside a mapping collection + * @return - Parsed event + */ + private static YamlEvent handleEvent(ComposerState state, ParserUtils.ParserOption option) + throws Error.YamlParserException { + if (state.terminatedDocEvent != null && + state.terminatedDocEvent.getKind() == YamlEvent.EventKind.DOCUMENT_MARKER_EVENT) { + return state.terminatedDocEvent; + } + return parse(state.parserState, option, BARE_DOCUMENT); + } + + /** + * Obtain an event for the for a set of tokens. + * + * @param state - Current parser state + * @param option - Expected values inside a mapping collection + * @param docType - Document type to be parsed + * @return - Parsed event + */ + private static YamlEvent parse(ParserState state, ParserUtils.ParserOption option, + Types.DocumentType docType) throws Error.YamlParserException { + // Empty the event buffer before getting new tokens + final List eventBuffer = state.getEventBuffer(); + + if (!eventBuffer.isEmpty()) { + return eventBuffer.remove(0); + } + state.updateLexerState(LexerState.LEXER_START_STATE); + if (!state.isExplicitDoc()) { + getNextToken(state); + } + + // Ignore the whitespace at the head + if (state.getCurrentToken().getType() == SEPARATION_IN_LINE) { + getNextToken(state); + } + + Token.TokenType currentTokenType = state.getCurrentToken().getType(); + // Set the next line if the end of line is detected + if (currentTokenType == EOL || currentTokenType == EMPTY_LINE || currentTokenType == COMMENT) { + + LexerState lexerState = state.getLexerState(); + if (lexerState.isEndOfStream()) { + if (docType == DIRECTIVE_DOCUMENT) { + throw new Error.YamlParserException("invalid document", state.getLine(), state.getColumn()); + } + return new YamlEvent.EndEvent(Collection.STREAM); + } + state.initLexer(); + return parse(state, option, docType); + } + + // Only directive tokens are allowed in directive document + if (currentTokenType != DIRECTIVE && currentTokenType != DIRECTIVE_MARKER) { + if (docType == DIRECTIVE_DOCUMENT) { + throw new Error.YamlParserException("'${state.currentToken.token}' " + + "is not allowed in a directive document", state.getLine(), state.getColumn()); + } + } + + switch (currentTokenType) { + case DIRECTIVE -> { + // Directives are not allowed in bare documents + if (docType == BARE_DOCUMENT) { + throw new Error.YamlParserException("directives are not allowed in a bare document", + state.getLine(), state.getColumn()); + } + + switch (state.getCurrentToken().getValue()) { + case "YAML" -> yamlDirective(state); + case "TAG" -> tagDirective(state); + default -> reservedDirective(state); + } + getNextToken(state, List.of(SEPARATION_IN_LINE, EOL)); + return parse(state, ParserUtils.ParserOption.DEFAULT, DIRECTIVE_DOCUMENT); + } + case DOCUMENT_MARKER, DIRECTIVE_MARKER -> { + boolean explicit = state.getCurrentToken().getType() == DIRECTIVE_MARKER; + state.setExplicitDoc(explicit); + + getNextToken(state, List.of(SEPARATION_IN_LINE, EOL, COMMENT)); + state.getLexerState().resetState(); + + if (!explicit) { + state.setYamlVersion(null); + state.setCustomTagHandles(new HashMap<>()); + } + + if (state.getCurrentToken().getType() == SEPARATION_IN_LINE) { + getNextToken(state, true); + + Token bufferedToken = state.getBufferedToken(); + Token.TokenType bufferedTokenType = bufferedToken.getType(); + + // There cannot be nodes next to the document marker. + if (bufferedTokenType != EOL && bufferedTokenType != COMMENT && !explicit) { + throw new Error.YamlParserException("'${state.tokenBuffer.token}' token cannot " + + "start in the same line as the document marker", state.getLine(), state.getColumn()); + } + + // Block collection nodes cannot be next to the directive marker. + if (explicit && (bufferedTokenType == PLANAR_CHAR && bufferedToken.getIndentation() != null + || bufferedTokenType == SEQUENCE_ENTRY)) { + throw new Error.YamlParserException("'${state.tokenBuffer.token}' token cannot start " + + "in the same line as the directive marker", state.getLine(), state.getColumn()); + } + } + return new YamlEvent.DocumentMarkerEvent(explicit); + } + case DOUBLE_QUOTE_DELIMITER, SINGLE_QUOTE_DELIMITER, PLANAR_CHAR, ALIAS -> { + return appendData(state, option, true); + } + case TAG, TAG_HANDLE, ANCHOR -> { + return nodeComplete(state, option); + } + case MAPPING_VALUE -> { // Empty node as the key + if (state.getLexerState().isFlowCollection()) { + if (option == EXPECT_SEQUENCE_ENTRY || option == EXPECT_SEQUENCE_VALUE) { + state.getEventBuffer().add(new YamlEvent.ScalarEvent()); + return new YamlEvent.StartEvent(Collection.MAPPING, false, true); + } + return new YamlEvent.ScalarEvent(); + } else { + IndentUtils.Indentation indentation = state.getCurrentToken().getIndentation(); + separate(state); + switch (indentation.change()) { + case INDENT_INCREASE -> { // Increase in indent + state.getEventBuffer().add(new YamlEvent.ScalarEvent()); + return new YamlEvent.StartEvent(Collection.MAPPING); + } + case INDENT_NO_CHANGE -> { // Same indent + return new YamlEvent.ScalarEvent(); + } + case INDENT_DECREASE -> { // Decrease in indent + + for (Collection collection: indentation.collection()) { + state.getEventBuffer().add(new YamlEvent.EndEvent(collection)); + } + if (option == EXPECT_MAP_VALUE) { + state.getEventBuffer().add(new YamlEvent.ScalarEvent()); + } + return state.getEventBuffer().remove(0); + } + } + } + } + case SEPARATOR -> { // Empty node as the value in flow mappings + if (option == EXPECT_MAP_VALUE) { // Check for empty values in flow mappings + return new YamlEvent.ScalarEvent(); + } + } + case MAPPING_KEY -> { // Explicit key + state.setExplicitKey(true); + state.setLastExplicitKeyLine(state.getLineIndex()); + return appendData(state, option); + } + case SEQUENCE_ENTRY -> { + if (state.getLexerState().isFlowCollection()) { + throw new Error.YamlParserException("cannot have block sequence under flow collection", + state.getLine(), state.getColumn()); + } + if (state.isExpectBlockSequenceValue()) { + throw new Error.YamlParserException("cannot have nested sequence for a defined value", + state.getLine(), state.getColumn()); + } + + switch (state.getCurrentToken().getIndentation().change()) { + case INDENT_INCREASE -> { // Increase in indent + return new YamlEvent.StartEvent(SEQUENCE); + } + case INDENT_NO_CHANGE -> { // Same indent + YamlEvent event = parse(state, EXPECT_SEQUENCE_VALUE, docType); + if (option == EXPECT_SEQUENCE_VALUE) { + state.getEventBuffer().add(event); + return new YamlEvent.ScalarEvent(); + } + return event; + } + case INDENT_DECREASE -> { // Decrease in indent + for (Collection collection: state.getCurrentToken().getIndentation().collection()) { + state.getEventBuffer().add(new YamlEvent.EndEvent(collection)); + } + return state.getEventBuffer().remove(0); + } + } + } + case MAPPING_START -> { + return new YamlEvent.StartEvent(Collection.MAPPING, true, false); + } + case SEQUENCE_START -> { + return new YamlEvent.StartEvent(SEQUENCE, true, false); + } + case SEQUENCE_END -> { + if (state.getLexerState().isFlowCollection()) { + separate(state); + Token.TokenType bufferedTokenType = state.getBufferedToken().getType(); + if (bufferedTokenType == SEPARATOR) { + getNextToken(state); + } else if (bufferedTokenType != MAPPING_END && bufferedTokenType != SEQUENCE_END) { + throw new Error.YamlParserException("unexpected token error", + state.getLine(), state.getColumn()); + } + } + return new YamlEvent.EndEvent(SEQUENCE); + } + case MAPPING_END -> { + if (option == EXPECT_MAP_VALUE) { + state.getEventBuffer().add(new YamlEvent.EndEvent(Collection.MAPPING)); + return new YamlEvent.ScalarEvent(); + } + if (state.getLexerState().isFlowCollection()) { + separate(state); + Token.TokenType bufferedTokenType = state.getBufferedToken().getType(); + if (bufferedTokenType == SEPARATOR) { + getNextToken(state); + } else if (bufferedTokenType != MAPPING_END && bufferedTokenType != SEQUENCE_END) { + throw new Error.YamlParserException("unexpected token error", + state.getLine(), state.getColumn()); + } + } + return new YamlEvent.EndEvent(Collection.MAPPING); + } + case LITERAL, FOLDED -> { + state.updateLexerState(LexerState.LEXER_LITERAL); + return appendData(state, option, true); + } + } + + throw new Error.YamlParserException("`Invalid token '${state.currentToken.token}' " + + "as the first for generating an event", state.getLine(), state.getColumn()); + } + + /** Verifies the grammar production for separation between nodes. + * + * @param state - Current parser state + */ + private static void separate(ParserState state) throws Error.YamlParserException { + state.updateLexerState(LexerState.LEXER_START_STATE); + getNextToken(state, true); + + Token.TokenType bufferedTokenType = state.getBufferedToken().getType(); + // Skip the check when either end-of-line or separate-in-line is not detected. + if (!(bufferedTokenType == EOL || bufferedTokenType == SEPARATION_IN_LINE || bufferedTokenType == COMMENT)) { + return; + } + + // Consider the separate for the latter contexts + getNextToken(state); + + if (state.getCurrentToken().getType() == SEPARATION_IN_LINE) { + // Check for s-b comment + getNextToken(state, true); + bufferedTokenType = state.getBufferedToken().getType(); + if (bufferedTokenType != EOL && bufferedTokenType != COMMENT) { + return; + } + getNextToken(state); + } + + // For the rest of the contexts, check either separation in line or comment lines + Token.TokenType currentTokenType = state.getCurrentToken().getType(); + while (currentTokenType == EOL || currentTokenType == EMPTY_LINE || currentTokenType == COMMENT) { + try { + state.initLexer(); + } catch (Exception ex) { + return; + } + + getNextToken(state, true); + + switch (state.getBufferedToken().getType()) { + case EOL, EMPTY_LINE, COMMENT -> { // Check for multi-lines + getNextToken(state); + } + case SEPARATION_IN_LINE -> { // Check for l-comment + getNextToken(state); + getNextToken(state, true); + bufferedTokenType = state.getBufferedToken().getType(); + if (bufferedTokenType != EOL && bufferedTokenType != COMMENT) { + return; + } + getNextToken(state); + } + default -> { + return; + } + } + currentTokenType = state.getCurrentToken().getType(); + } + } + + private static YamlEvent nodeComplete(ParserState state, ParserUtils.ParserOption option) + throws Error.YamlParserException { + return nodeComplete(state, option, new TagStructure()); + } + + private static YamlEvent nodeComplete(ParserState state, ParserUtils.ParserOption option, + TagStructure definedProperties) throws Error.YamlParserException { + TagStructure tagStructure = new TagStructure(); + state.setTagPropertiesInLine(true); + + switch (state.getCurrentToken().getType()) { + case TAG_HANDLE -> { + String tagHandle = state.getCurrentToken().getValue(); + + // Obtain the tagPrefix associated with the tag handle + state.getLexerState().updateLexerState(LexerState.LEXER_NODE_PROPERTY); + getNextToken(state, List.of(TAG)); + String tagPrefix = state.getCurrentToken().getValue(); + tagStructure.tag = generateCompleteTagName(state, tagHandle, tagPrefix); + + // Check if there is a separate + separate(state); + + // Obtain the anchor if there exists + tagStructure.anchor = nodeAnchor(state); + } + case TAG -> { + // Obtain the tagPrefix name + tagStructure.tag = state.getCurrentToken().getValue(); + + // There must be a separate after the tagPrefix + separate(state); + + // Obtain the anchor if there exists + tagStructure.anchor = nodeAnchor(state); + } + case ANCHOR -> { + // Obtain the anchor name + tagStructure.anchor = state.getCurrentToken().getValue(); + + // Check if there is a separate + separate(state); + + // Obtain the tag if there exists + TagDetails tagDetails = nodeTag(state); + + // Construct the complete tag + if (tagDetails.tagPrefix != null) { + tagStructure.tag = tagDetails.tagHandle == null ? tagDetails.tagPrefix : + generateCompleteTagName(state, tagDetails.tagHandle, tagDetails.tagPrefix); + } + } + } + return appendData(state, option, false, tagStructure, definedProperties); + } + + private static TagDetails nodeTag(ParserState state) throws Error.YamlParserException { + String tagPrefix = null; + String tagHandle = null; + switch (state.getBufferedToken().getType()) { + case TAG -> { + getNextToken(state); + tagPrefix = state.getCurrentToken().getValue(); + separate(state); + } + case TAG_HANDLE -> { + getNextToken(state); + tagHandle = state.getCurrentToken().getValue(); + + state.getLexerState().updateLexerState(LexerState.LEXER_NODE_PROPERTY); + getNextToken(state, List.of(TAG)); + tagPrefix = state.getCurrentToken().getValue(); + separate(state); + } + } + return new TagDetails(tagPrefix, tagHandle); + } + + record TagDetails(String tagPrefix, String tagHandle) { + } + + + private static String nodeAnchor(ParserState state) throws Error.YamlParserException { + String anchor = null; + if (state.getBufferedToken().getType() == ANCHOR) { + getNextToken(state); + anchor = state.getCurrentToken().getValue(); + separate(state); + } + return anchor; + } + + /** + * Convert the shorthand tag to the complete tag by mapping the tag handle. + * @param state - Current parser state + * @param tagHandle - Tag handle of the event + * @param tagPrefix - Tag prefix of the event + * @return - The complete tag name of the event + */ + private static String generateCompleteTagName(ParserState state, String tagHandle, String tagPrefix) + throws Error.YamlParserException { + String tagHandleName; + // Check if the tag handle is defined in the custom tags. + if (state.getCustomTagHandles().containsKey(tagHandle)) { + tagHandleName = state.getCustomTagHandles().get(tagHandle); + } else { // Else, check if the tag handle is in the default tags. + if (DEFAULT_TAG_HANDLES.containsKey(tagHandle)) { + tagHandleName = DEFAULT_TAG_HANDLES.get(tagHandle); + } else { + throw new Error.YamlParserException("tag handle is not defined", state.getLine(), state.getColumn()); + } + } + return tagHandleName + tagPrefix; + } + + /** + * Merge tag structure with the respective data. + * + * @param state - Current parser state + * @param option - Selected parser option + * @return - The constructed scalar or start event + */ + private static YamlEvent appendData(ParserState state, ParserUtils.ParserOption option) + throws Error.YamlParserException { + return appendData(state, option, false, new TagStructure(), null); + } + + private static YamlEvent appendData(ParserState state, ParserUtils.ParserOption option, boolean peeked) + throws Error.YamlParserException { + return appendData(state, option, peeked, new TagStructure(), null); + } + + /** + * Merge tag structure with the respective data. + * + * @param state - Current parser state + * @param option - Selected parser option + * @param peeked If the expected token is already in the state + * @param tagStructure - Constructed tag structure if exists + * @param definedProperties - Tag properties defined by the previous node + * @return - The constructed scalar or start event + */ + private static YamlEvent appendData(ParserState state, ParserUtils.ParserOption option, boolean peeked, + TagStructure tagStructure, TagStructure definedProperties) + throws Error.YamlParserException { + + state.setExpectBlockSequenceValue(true); + YamlEvent buffer = null; + + // Check for nested explicit keys + if (!peeked) { + getNextToken(state, true); + if (state.getBufferedToken().getType() == MAPPING_KEY) { + state.setExplicitKey(true); + getNextToken(state); + } + } + boolean explicitKey = state.isExplicitKey(); + + if (option == EXPECT_MAP_VALUE && state.getCurrentToken().getType() == MAPPING_KEY) { + buffer = new YamlEvent.ScalarEvent(); + } + + IndentUtils.Indentation indentation = null; + if (state.isExplicitKey()) { + indentation = state.getCurrentToken().getIndentation(); + separate(state); + } + + state.updateLexerState(LexerState.LEXER_START_STATE); + + if (state.getLastExplicitKeyLine() == state.getLineIndex() && !state.getLexerState().isFlowCollection() + && option == EXPECT_MAP_KEY) { + throw new Error.YamlParserException("cannot have a scalar next to a block key-value pair", + state.getLine(), state.getColumn()); + } + + YamlEvent event = content(state, peeked, state.isExplicitKey(), tagStructure); + boolean isAlias = event.getKind() == YamlEvent.EventKind.ALIAS_EVENT; + + state.setExplicitKey(false); + if (!explicitKey) { + indentation = state.getCurrentToken().getIndentation(); + } + + // The tokens described in the indentation.tokens belong to the second node. + TagStructure newNodeTagStructure = new TagStructure(); + TagStructure currentNodeTagStructure = new TagStructure(); + if (indentation != null) { + switch (indentation.tokens().size()) { + case 0 -> { + newNodeTagStructure = tagStructure; + } + case 1 -> { + switch (indentation.tokens().get(0)) { + case ANCHOR -> { + if (isAlias && tagStructure.anchor != null) { + throw new Error.YamlParserException("an alias node cannot have an anchor", + state.getLine(), state.getColumn()); + } + newNodeTagStructure.tag = tagStructure.tag; + currentNodeTagStructure.anchor = tagStructure.anchor; + } + case TAG -> { + if (isAlias && tagStructure.tag != null) { + throw new Error.YamlParserException("an alias node cannot have a tag", + state.getLine(), state.getColumn()); + } + newNodeTagStructure.anchor = tagStructure.anchor; + currentNodeTagStructure.tag = tagStructure.tag; + } + } + } + case 2 -> { + if (isAlias && (tagStructure.anchor != null || tagStructure.tag != null)) { + throw new Error.YamlParserException("an alias node cannot have tag properties", + state.getLine(), state.getColumn()); + } + currentNodeTagStructure = tagStructure; + } + } + } else { + if (isAlias && (tagStructure.anchor != null || tagStructure.tag != null)) { + throw new Error.YamlParserException("an alias node cannot have tag properties", + state.getLine(), state.getColumn()); + } + currentNodeTagStructure = tagStructure; + } + + // Check if the current node is a key + boolean isJsonKey = state.getLexerState().isJsonKey(); + + // Ignore the whitespace and lines if there is any + Token.TokenType currentTokenType = state.getCurrentToken().getType(); + if (currentTokenType != MAPPING_VALUE && currentTokenType != SEPARATOR) { + separate(state); + } + getNextToken(state, true); + + // Check if the next token is a mapping value or + Token.TokenType bufferedTokenType = state.getBufferedToken().getType(); + if (bufferedTokenType == MAPPING_VALUE || bufferedTokenType == SEPARATOR) { + getNextToken(state); + } + + state.getLexerState().setJsonKey(false); + + // If there are no whitespace, and the current token is "," + if (state.getCurrentToken().getType() == SEPARATOR) { + if (!state.getLexerState().isFlowCollection()) { + throw new Error.YamlParserException("',' are only allowed in flow collections", + state.getLine(), state.getColumn()); + } + separate(state); + if (option == EXPECT_MAP_KEY) { + state.getEventBuffer().add(new YamlEvent.ScalarEvent()); + } + } else if (state.getCurrentToken().getType() == MAPPING_VALUE) { + // If there are no whitespace, and the current token is ':' + if (state.getLastKeyLine() == state.getLineIndex() && !state.getLexerState().isFlowCollection()) { + throw new Error.YamlParserException("two block mapping keys cannot be defined in the same line", + state.getLine(), state.getColumn()); + } + + // In a block scalar, if there is a mapping key as in the same line as a mapping value, + // then that mapping value does not correspond to the mapping key. the mapping value forms a + // new mapping pair which represents the explicit key. + if (state.getLastExplicitKeyLine() == state.getLineIndex() && !state.getLexerState().isFlowCollection()) { + throw new Error.YamlParserException("mappings are not allowed as keys for explicit keys", + state.getLine(), state.getColumn()); + } + state.setLastKeyLine(state.getLineIndex()); + + if (state.isExplicitDoc()) { + throw new Error.YamlParserException("'${lexer:PLANAR_CHAR}' token cannot " + + "start in the same line as the directive marker", state.getLine(), state.getColumn()); + } + + separate(state); + if (state.isEmptyKey() && (option == EXPECT_MAP_VALUE || option == EXPECT_SEQUENCE_VALUE)) { + state.setEmptyKey(false); + state.getEventBuffer().add(new YamlEvent.ScalarEvent()); + } else if (option == EXPECT_MAP_VALUE) { + buffer = constructEvent(new YamlEvent.ScalarEvent(), newNodeTagStructure); + } else if (option == EXPECT_SEQUENCE_ENTRY || option == EXPECT_SEQUENCE_VALUE + && state.getLexerState().isFlowCollection()) { + buffer = new YamlEvent.StartEvent(Collection.MAPPING, false, true); + } + } else { + // There is already tag properties defined and the value is not a key + if (definedProperties != null) { + if (definedProperties.anchor != null && tagStructure.anchor != null) { + throw new Error.YamlParserException("only one anchor is allowed for a node", + state.getLine(), state.getColumn()); + } + if (definedProperties.tag != null && tagStructure.tag != null) { + throw new Error.YamlParserException("only one tag is allowed for a node", + state.getLine(), state.getColumn()); + } + } + + if (option == EXPECT_MAP_KEY && !explicitKey) { + throw new Error.YamlParserException("expected a key for the block mapping", + state.getLine(), state.getColumn()); + } + + if (explicitKey) { + IndentUtils.Indentation peekedIndentation = state.getBufferedToken().getIndentation(); + if (peekedIndentation != null + && peekedIndentation.change() == IndentUtils.Indentation.IndentationChange.INDENT_INCREASE + && state.getBufferedToken().getType() != MAPPING_KEY) { + throw new Error.YamlParserException("invalid explicit key", state.getLine(), state.getColumn()); + } + } + } + + if (indentation != null && !state.isIndentationProcessed()) { + int collectionSize = indentation.collection().size(); + switch (indentation.change()) { + case INDENT_INCREASE -> { // Increased + // Block sequence + if (event.getKind() == YamlEvent.EventKind.START_EVENT + && ((YamlEvent.StartEvent) event).getStartType() == SEQUENCE) { + return constructEvent( + new YamlEvent.StartEvent(indentation.collection().remove(collectionSize - 1)), + tagStructure); + } + // Block mapping + buffer = constructEvent( + new YamlEvent.StartEvent(indentation.collection().remove(collectionSize - 1)), + newNodeTagStructure); + } + case INDENT_DECREASE -> { // Decreased + buffer = new YamlEvent.EndEvent(indentation.collection().remove(0)); + for (Collection collection: indentation.collection()) { + state.getEventBuffer().add(new YamlEvent.EndEvent(collection)); + } + } + } + } + state.setIndentationProcessed(false); + + if (isJsonKey && currentNodeTagStructure.tag == null) { + currentNodeTagStructure.tag = DEFAULT_GLOBAL_TAG_HANDLE + "str"; + } + event = constructEvent(event, isAlias ? null : currentNodeTagStructure); + + if (buffer == null) { + return event; + } + if (explicitKey) { + state.getEventBuffer().add(0, event); + } else { + state.getEventBuffer().add(event); + } + + return buffer; + } + + private static YamlEvent constructEvent(YamlEvent yamlEvent, TagStructure newNodeTagStructure) { + YamlEvent event = yamlEvent.clone(); + if (newNodeTagStructure != null) { + event.setAnchor(newNodeTagStructure.anchor); + event.setTag(newNodeTagStructure.tag); + } + return event; + } + + /** Extract the data for the given node. + * + * @param state - Current parser state + * @param peeked - If the expected token is already in the state + * @param explicitKey - Whether the current node is an explicit key + * @param tagStructure - Tag structure of the current node + * @return Parser Event + */ + private static YamlEvent content(ParserState state, boolean peeked, boolean explicitKey, + TagStructure tagStructure) throws Error.YamlParserException { + + if (!peeked) { + separate(state); + getNextToken(state); + } + + // Check for flow and block nodes + switch (state.getCurrentToken().getType()) { + case SINGLE_QUOTE_DELIMITER -> { + state.getLexerState().setJsonKey(true); + String value = singleQuoteScalar(state); + checkEmptyKey(state); + return new YamlEvent.ScalarEvent(value); + } + case DOUBLE_QUOTE_DELIMITER -> { + state.getLexerState().setJsonKey(true); + String value = doubleQuoteScalar(state); + checkEmptyKey(state); + return new YamlEvent.ScalarEvent(value); + } + case PLANAR_CHAR -> { + String value = planarScalar(state); + checkEmptyKey(state); + return new YamlEvent.ScalarEvent(value); + } + case SEQUENCE_START -> { + return new YamlEvent.StartEvent(SEQUENCE); + } + case SEQUENCE_ENTRY -> { + if (state.isTagPropertiesInLine()) { + throw new Error.YamlParserException("'-' cannot be defined after tag properties", + state.getLine(), state.getColumn()); + } + + switch (state.getCurrentToken().getIndentation().change()) { + case INDENT_INCREASE -> { + return new YamlEvent.StartEvent(SEQUENCE); + } + case INDENT_NO_CHANGE -> { + return new YamlEvent.ScalarEvent(); + } + case INDENT_DECREASE -> { + state.setIndentationProcessed(true); + for (Collection collection: state.getCurrentToken().getIndentation().collection()) { + state.getEventBuffer().add(new YamlEvent.EndEvent(collection)); + } + return constructEvent(new YamlEvent.ScalarEvent(), tagStructure); + } + } + } + case MAPPING_START -> { + return new YamlEvent.StartEvent(Collection.MAPPING); + } + case LITERAL, FOLDED -> { + if (state.getLexerState().isFlowCollection()) { + throw new Error.YamlParserException("cannot have a block node inside a flow node", + state.getLine(), state.getColumn()); + } + String value = blockScalar(state, state.getCurrentToken().getType() == FOLDED); + checkEmptyKey(state); + return new YamlEvent.ScalarEvent(value); + } + case ALIAS -> { + return new YamlEvent.AliasEvent(state.getCurrentToken().getValue()); + } + case ANCHOR, TAG, TAG_HANDLE -> { + YamlEvent event = nodeComplete(state, EXPECT_MAP_KEY, tagStructure); + if (explicitKey) { + return event; + } + if (event.getKind() == YamlEvent.EventKind.START_EVENT && + ((YamlEvent.StartEvent) event).getStartType() == Collection.MAPPING) { + return new YamlEvent.StartEvent(Collection.MAPPING); + } + if (event.getKind() == YamlEvent.EventKind.END_EVENT) { + state.getEventBuffer().add(0, event); + return new YamlEvent.ScalarEvent(); + } + } + case MAPPING_END -> { + if (explicitKey) { + state.getEventBuffer().add(new YamlEvent.ScalarEvent()); + } + state.getEventBuffer().add(new YamlEvent.EndEvent(Collection.MAPPING)); + return new YamlEvent.ScalarEvent(); + } + } + + return new YamlEvent.ScalarEvent(); + } + + /** + * Parse the string of a block scalar. + * + * @param state - Current parser state + * @param isFolded - If set, then the parses folded block scalar. Else, parses literal block scalar. + * @return - Parsed block scalar value + */ + private static String blockScalar(ParserState state, boolean isFolded) throws Error.YamlParserException { + String chompingIndicator = ""; + state.getLexerState().updateLexerState(LexerState.LEXER_BLOCK_HEADER); + getNextToken(state); + + // Scan for block-header + switch (state.getCurrentToken().getType()) { + case CHOMPING_INDICATOR -> { // Strip and keep chomping indicators + chompingIndicator = state.getCurrentToken().getValue(); + getNextToken(state, List.of(EOL)); + + if (state.getLexerState().isEndOfStream()) { + state.initLexer(); + } + } + case EOL -> { // Clip chomping indicator + state.initLexer(); + chompingIndicator = "="; + } + } + + state.getLexerState().updateLexerState(LexerState.LEXER_LITERAL); + StringBuilder lexemeBuffer = new StringBuilder(); + StringBuilder newLineBuffer = new StringBuilder(); + boolean isFirstLine = true; + boolean onlyEmptyLine = false; + boolean prevTokenIndented = false; + + getNextToken(state, true); + + boolean terminated = false; + while (!terminated) { + switch (state.getBufferedToken().getType()) { + case PRINTABLE_CHAR -> { + String bufferedTokenValue = state.getBufferedToken().getValue(); + char bufferedTokenValueFirstChar = bufferedTokenValue.charAt(0); + if (!isFirstLine) { + String suffixChar = "\n"; + if (isFolded && prevTokenIndented && + (bufferedTokenValueFirstChar != ' ' && bufferedTokenValueFirstChar != '\t')) { + suffixChar = newLineBuffer.length() == 0 ? " " : ""; + } + lexemeBuffer.append(newLineBuffer).append(suffixChar); + newLineBuffer = new StringBuilder(); + } + + lexemeBuffer.append(bufferedTokenValue); + prevTokenIndented = bufferedTokenValueFirstChar != ' ' && bufferedTokenValueFirstChar != '\t'; + isFirstLine = false; + } + case EOL -> { + // Terminate at the end of the line + if (state.getLexerState().isEndOfStream()) { + terminated = true; + break; + } + state.initLexer(); + } + case EMPTY_LINE -> { + if (!isFirstLine) { + newLineBuffer.append("\n"); + } + if (state.getLexerState().isEndOfStream()) { + terminated = true; + break; + } + state.initLexer(); + onlyEmptyLine = isFirstLine; + isFirstLine = false; + } + case TRAILING_COMMENT -> { + state.getLexerState().setTrailingComment(true); + + // Terminate at the end of the line + if (state.getLexerState().isEndOfStream()) { + getNextToken(state); + terminated = true; + break; + } + state.initLexer(); + getNextToken(state); + getNextToken(state, true); + + // Ignore the tokens inside trailing comments + Token.TokenType bufferedTokenType = state.getBufferedToken().getType(); + while (bufferedTokenType == EOL || bufferedTokenType == EMPTY_LINE) { + // Terminate at the end of the line + if (state.getLexerState().isEndOfStream()) { + break; + } + state.initLexer(); + getNextToken(state); + getNextToken(state, true); + bufferedTokenType = state.getBufferedToken().getType(); + } + + state.getLexerState().setTrailingComment(false); + terminated = true; + } + default -> { + // Break the character when the token does not belong to planar scalar + terminated = true; + } + } + if (!terminated) { + getNextToken(state); + getNextToken(state, true); + } + } + + // Adjust the tail based on the chomping values + switch (chompingIndicator) { + case "+" -> { + lexemeBuffer.append("\n"); + lexemeBuffer.append(newLineBuffer); + } + case "=" -> { + if (!onlyEmptyLine) { + lexemeBuffer.append("\n"); + } + } + } + + return lexemeBuffer.toString(); + } + + /** + * Parse the string of a planar scalar. + * + * @param state - Current parser state. + * @return - Parsed planar scalar value + */ + private static String planarScalar(ParserState state) throws Error.YamlParserException { + return planarScalar(state, true); + } + + /** + * Parse the string of a planar scalar. + * + * @param state - Current parser state. + * @param allowTokensAsPlanar - If set, then the restricted tokens are allowed as a planar scalar + * @return - Parsed planar scalar value + */ + private static String planarScalar(ParserState state, boolean allowTokensAsPlanar) + throws Error.YamlParserException { + // Process the first planar char + StringBuilder lexemeBuffer = new StringBuilder(state.getCurrentToken().getValue()); + boolean isFirstLine = true; + StringBuilder newLineBuffer = new StringBuilder(); + state.getLexerState().setAllowTokensAsPlanar(allowTokensAsPlanar); + + getNextToken(state, true); + + boolean terminate = false; + // Iterate the content until an invalid token is found + while (!terminate) { + switch (state.getBufferedToken().getType()) { + case PLANAR_CHAR -> { + if (state.getBufferedToken().getIndentation() != null) { + terminate = true; + break; + } + getNextToken(state); + if (newLineBuffer.length() > 0) { + lexemeBuffer.append(newLineBuffer); + newLineBuffer = new StringBuilder(); + } else { // Add a whitespace if there are no preceding empty lines + lexemeBuffer.append(" "); + } + lexemeBuffer.append(state.getCurrentToken().getValue()); + } + case EOL -> { + getNextToken(state); + + // Terminate at the end of the line + LexerState lexerState = state.getLexerState(); + if (lexerState.isEndOfStream()) { + terminate = true; + break; + } + state.initLexer(); + + isFirstLine = false; + } + case COMMENT -> { + getNextToken(state); + terminate = true; + } + case EMPTY_LINE -> { + newLineBuffer.append("\n"); + getNextToken(state); + // Terminate at the end of the line + if (state.getLexerState().isEndOfStream()) { + terminate = true; + break; + } + state.initLexer(); + } + case SEPARATION_IN_LINE -> { + getNextToken(state); + // Continue to scan planar char if the white space at the end-of-line + getNextToken(state, true); + if (state.getBufferedToken().getType() == MAPPING_VALUE) { + terminate = true; + } + } + default -> { // Break the character when the token does not belong to planar scalar + terminate = true; + } + } + if (!terminate) { + getNextToken(state, true); + } + } + + if (state.getBufferedToken().getIndentation() == null) { + verifyKey(state, isFirstLine); + } + state.getLexerState().setAllowTokensAsPlanar(false); + return trimTailWhitespace(lexemeBuffer.toString()); + } + + /** + * Parse the string of a double-quoted scalar. + * + * @param state - Current parser state + * @return - Parsed double-quoted scalar value + */ + private static String doubleQuoteScalar(ParserState state) throws Error.YamlParserException { + state.getLexerState().updateLexerState(LexerState.LEXER_DOUBLE_QUOTE); + String lexemeBuffer = ""; + state.getLexerState().setFirstLine(true); + boolean emptyLine = false; + boolean escaped = false; + + getNextToken(state); + + // Iterate the content until the delimiter is found + while (state.getCurrentToken().getType() != DOUBLE_QUOTE_DELIMITER) { + switch (state.getCurrentToken().getType()) { + case DOUBLE_QUOTE_CHAR -> { // Regular double quoted string char + String lexeme = state.getCurrentToken().getValue(); + + // Check for double escaped character + if (lexeme.length() > 0 && lexeme.charAt(lexeme.length() - 1) == '\\') { + escaped = true; + lexemeBuffer += lexeme.substring(0, lexeme.length() - 1); + } else if (!state.getLexerState().isFirstLine()) { + if (escaped) { + escaped = false; + } else { // Trim the white space if not escaped + if (!emptyLine) { // Add a white space if there are not preceding empty lines + lexemeBuffer += " "; + } + } + lexemeBuffer += lexeme; + } else { + lexemeBuffer += lexeme; + } + + if (emptyLine) { + emptyLine = false; + } + } + case EOL -> { // Processing new lines + if (!escaped) { // If not escaped, trim the trailing white spaces + lexemeBuffer = trimTailWhitespace(lexemeBuffer, state.getLexerState().getLastEscapedChar()); + } + + state.getLexerState().setFirstLine(false); + state.initLexer(); + + // Add a whitespace if the delimiter is on a new line + getNextToken(state, true); + if (state.getBufferedToken().getType() == DOUBLE_QUOTE_DELIMITER && !emptyLine) { + lexemeBuffer += " "; + } + } + case EMPTY_LINE -> { + if (escaped && !state.getLexerState().isFirstLine()) { // Whitespace is preserved when escaped + lexemeBuffer += state.getCurrentToken().getValue() + "\n"; + } else if (!state.getLexerState().isFirstLine()) { // Whitespace is ignored when line folding + lexemeBuffer = trimTailWhitespace(lexemeBuffer); + lexemeBuffer += "\n"; + } + emptyLine = true; + state.initLexer(); + + boolean firstLineBuffer = state.getLexerState().isFirstLine(); + state.getLexerState().setFirstLine(false); + + getNextToken(state, true); + if (state.getBufferedToken().getType() == DOUBLE_QUOTE_DELIMITER && firstLineBuffer) { + lexemeBuffer += " "; + } + state.getLexerState().setFirstLine(false); + } + default -> { + throw new Error.YamlParserException("invalid double quote scalar", + state.getLine(), state.getColumn()); + } + } + getNextToken(state); + } + + verifyKey(state, state.getLexerState().isFirstLine()); + state.getLexerState().setFirstLine(true); + return lexemeBuffer; + } + + private static void checkEmptyKey(ParserState state) throws Error.YamlParserException { + separate(state); + getNextToken(state, true); + + Token bufferedToken = state.getBufferedToken(); + + if (bufferedToken.getType() != MAPPING_VALUE || bufferedToken.getIndentation() == null) { + return; + } + + state.setEmptyKey(true); + IndentUtils.Indentation indentation = bufferedToken.getIndentation(); + switch (indentation.change()) { + case INDENT_INCREASE -> { + int collectionSize = indentation.collection().size(); + state.getEventBuffer().add( + new YamlEvent.StartEvent(indentation.collection().remove(collectionSize - 1))); + } + case INDENT_DECREASE -> { + for (Collection collection: indentation.collection()) { + state.getEventBuffer().add(new YamlEvent.EndEvent(collection)); + } + } + } + } + + /** Parse the string of a single-quoted scalar. + * + * @param state - Current parser state + * @return - Parsed single-quoted scalar value + */ + private static String singleQuoteScalar(ParserState state) throws Error.YamlParserException { + state.getLexerState().updateLexerState(LexerState.LEXER_SINGLE_QUOTE); + String lexemeBuffer = ""; + state.getLexerState().setFirstLine(true); + boolean emptyLine = false; + + getNextToken(state); + + // Iterate the content until the delimiter is found + while (state.getCurrentToken().getType() != SINGLE_QUOTE_DELIMITER) { + switch (state.getCurrentToken().getType()) { + case SINGLE_QUOTE_CHAR -> { + String lexeme = state.getCurrentToken().getValue(); + + if (!state.getLexerState().isFirstLine()) { + if (emptyLine) { + emptyLine = false; + } else { // Add a white space if there are not preceding empty lines + lexemeBuffer += " "; + } + } + lexemeBuffer += lexeme; + } + case EOL -> { + // Trim trailing white spaces + lexemeBuffer = trimTailWhitespace(lexemeBuffer); + state.getLexerState().setFirstLine(false); + state.initLexer(); + + + // Add a whitespace if the delimiter is on a new line + getNextToken(state, true); + if (state.getBufferedToken().getType() == SINGLE_QUOTE_DELIMITER && !emptyLine) { + lexemeBuffer += " "; + } + } + case EMPTY_LINE -> { + if (!state.getLexerState().isFirstLine()) { // Whitespace is ignored when line folding + lexemeBuffer = trimTailWhitespace(lexemeBuffer); + lexemeBuffer += "\n"; + } + emptyLine = true; + state.initLexer(); + + + boolean firstLineBuffer = state.getLexerState().isFirstLine(); + state.getLexerState().setFirstLine(false); + + getNextToken(state, true); + if (state.getBufferedToken().getType() == SINGLE_QUOTE_DELIMITER && firstLineBuffer) { + lexemeBuffer += " "; + } + state.getLexerState().setFirstLine(false); + } + default -> { + throw new Error.YamlParserException("invalid single quote character", + state.getLine(), state.getColumn()); + } + } + getNextToken(state); + } + + verifyKey(state, state.getLexerState().isFirstLine()); + state.getLexerState().setFirstLine(true); + return lexemeBuffer; + } + + /** + * Trims the trailing whitespace of a string. + */ + private static String trimTailWhitespace(String value) { + return trimTailWhitespace(value, -1); + } + + /** + * Trims the trailing whitespace of a string. + */ + private static String trimTailWhitespace(String value, int lastEscapedChar) { + int i = value.length() - 1; + + if (i < 0) { + return ""; + } + + char charAtI = value.charAt(i); + while (charAtI == ' ' || charAtI == '\t') { + if (i < 1 || (lastEscapedChar != -1 && i == lastEscapedChar)) { + break; + } + i -= 1; + charAtI = value.charAt(i); + } + + return value.substring(0, i + 1); + } + + /** + * Check if the given key adheres to either a explicit or a implicit key. + * + * @param state - Current parser state + * @param isSingleLine - If the scalar only spanned for one line + */ + private static void verifyKey(ParserState state, boolean isSingleLine) throws Error.YamlParserException { + // Explicit keys can span multiple lines. + if (state.isExplicitKey()) { + return; + } + + // Regular keys can only exist within one line + state.getLexerState().updateLexerState(LexerState.LEXER_START_STATE); + getNextToken(state, true); + if (state.getBufferedToken().getType() == MAPPING_VALUE && !isSingleLine) { + throw new Error.YamlParserException("mapping keys cannot span multiple lines", + state.getLine(), state.getColumn()); + } + } + + /** + * Assert the next lexer token with the predicted token. + * + * @param state - Current parser state + */ + public static void getNextToken(ParserState state) throws Error.YamlParserException { + getNextToken(state, List.of(Token.TokenType.DUMMY)); + } + + /** + * Assert the next lexer token with the predicted token. + * + * @param state - Current parser state + * @param peek Store the token in the buffer + */ + public static void getNextToken(ParserState state, boolean peek) throws Error.YamlParserException { + getNextToken(state, List.of(Token.TokenType.DUMMY), peek); + } + + /** + * Assert the next lexer token with the predicted token. + * + * @param state - Current parser state + * @param expectedTokens Predicted tokens + */ + public static void getNextToken(ParserState state, List expectedTokens) + throws Error.YamlParserException { + getNextToken(state, expectedTokens, false); + } + + /** + * Assert the next lexer token with the predicted token. + * + * @param state - Current parser state + * @param expectedTokens Predicted tokens + * @param peek Store the token in the buffer + */ + public static void getNextToken(ParserState state, List expectedTokens, boolean peek) + throws Error.YamlParserException { + Token token; + + // Obtain a token form the lexer if there is none in the buffer. + if (state.getBufferedToken().getType() == Token.TokenType.DUMMY) { + state.updateLexerState(YamlLexer.scanTokens(state.getLexerState())); + token = state.getLexerState().getToken(); + } else { + token = state.getBufferedToken(); + state.setBufferedToken(ParserState.DUMMY_TOKEN); + } + + // Add the token to the tokenBuffer if the peek flag is set. + if (peek) { + state.setBufferedToken(token); + } else { + state.setCurrentToken(token); + } + + // Bypass error handling. + if (expectedTokens.get(0) == Token.TokenType.DUMMY) { + return; + } + + if (!expectedTokens.contains(token.getType())) { + throw new Error.YamlParserException("expected token differ from the actual token", + state.getLine(), state.getColumn()); + } + } + + public static class TagStructure { + String anchor = null; + String tag = null; + } +} diff --git a/native/src/main/java/io/ballerina/lib/data/yaml/serializer/Serializer.java b/native/src/main/java/io/ballerina/lib/data/yaml/serializer/Serializer.java new file mode 100644 index 0000000..f0dabbd --- /dev/null +++ b/native/src/main/java/io/ballerina/lib/data/yaml/serializer/Serializer.java @@ -0,0 +1,130 @@ +/* + * 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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.lib.data.yaml.serializer; + +import io.ballerina.lib.data.yaml.common.Types; +import io.ballerina.lib.data.yaml.common.YamlEvent; +import io.ballerina.runtime.api.TypeTags; +import io.ballerina.runtime.api.values.BArray; +import io.ballerina.runtime.api.values.BMap; +import io.ballerina.runtime.api.values.BString; +import io.ballerina.runtime.api.values.BValue; + +import java.util.ArrayList; +import java.util.List; + +import static io.ballerina.lib.data.yaml.utils.Constants.DEFAULT_GLOBAL_MAP_TAG_HANDLE; +import static io.ballerina.lib.data.yaml.utils.Constants.DEFAULT_GLOBAL_SEQ_TAG_HANDLE; +import static io.ballerina.lib.data.yaml.utils.Constants.DEFAULT_GLOBAL_STR_TAG_HANDLE; + +/** + * Converts a Ballerina value to stream of YAML events. + * + * @since 0.1.0 + */ +public class Serializer { + + public static class SerializerState { + List events; + final char delimiter; + final boolean forceQuotes; + final boolean flowStyle; + final int blockLevel; + boolean isStream; + + public SerializerState(char delimiter, boolean forceQuotes, int blockLevel, + boolean flowStyle, boolean isStream) { + this.events = new ArrayList<>(); + this.delimiter = delimiter; + this.forceQuotes = forceQuotes; + this.blockLevel = blockLevel; + this.flowStyle = flowStyle; + this.isStream = isStream; + } + + public List getEvents() { + return events; + } + } + + public static void serialize(SerializerState state, Object data) { + serialize(state, data, 0, null); + } + + public static void serialize(SerializerState state, Object value, int depthLevel, String excludeTag) { + if (value instanceof BValue) { + int typeTag = ((BValue) value).getType().getTag(); + if (typeTag == TypeTags.ARRAY_TAG || typeTag == TypeTags.TUPLE_TAG) { + serializeSequence(state, ((BArray) value), depthLevel); + return; + } else if (typeTag == TypeTags.MAP_TAG || typeTag == TypeTags.RECORD_TYPE_TAG) { + serializeMapping(state, ((BMap) value), + depthLevel); + return; + } + } + serializeString(state, value); + } + + private static void serializeString(SerializerState state, Object data) { + String value = data.toString(); + if (value.contains("\n")) { + value = state.delimiter + value.replaceAll("\n", "\\n") + state.delimiter; + } else { + value = state.forceQuotes ? state.delimiter + value + state.delimiter : value; + } + + YamlEvent scalarEvent = new YamlEvent.ScalarEvent(value); + scalarEvent.setTag(DEFAULT_GLOBAL_STR_TAG_HANDLE); + state.events.add(scalarEvent); + } + + private static void serializeSequence(SerializerState state, BArray data, int depthLevel) { + if (state.isStream) { + state.isStream = false; + for (int i = 0; i < data.size(); i++) { + serialize(state, data.get(i), depthLevel + 1, DEFAULT_GLOBAL_SEQ_TAG_HANDLE); + } + } else { + YamlEvent startEvent = new YamlEvent.StartEvent(Types.Collection.SEQUENCE, + state.flowStyle, false); + startEvent.setTag(DEFAULT_GLOBAL_SEQ_TAG_HANDLE); + state.events.add(startEvent); + + for (int i = 0; i < data.size(); i++) { + serialize(state, data.get(i), depthLevel + 1, DEFAULT_GLOBAL_SEQ_TAG_HANDLE); + } + + state.events.add(new YamlEvent.EndEvent(Types.Collection.SEQUENCE)); + } + } + + private static void serializeMapping(SerializerState state, BMap bMap, + int depthLevel) { + state.events.add(new YamlEvent.StartEvent(Types.Collection.MAPPING, state.flowStyle, false)); + + BString[] keys = bMap.getKeys(); + for (BString key: keys) { + serialize(state, key, depthLevel, DEFAULT_GLOBAL_MAP_TAG_HANDLE); + serialize(state, bMap.get(key), depthLevel + 1, DEFAULT_GLOBAL_MAP_TAG_HANDLE); + } + + state.events.add(new YamlEvent.EndEvent(Types.Collection.MAPPING)); + } +} diff --git a/native/src/main/java/io/ballerina/lib/data/yaml/utils/Constants.java b/native/src/main/java/io/ballerina/lib/data/yaml/utils/Constants.java new file mode 100644 index 0000000..737e9aa --- /dev/null +++ b/native/src/main/java/io/ballerina/lib/data/yaml/utils/Constants.java @@ -0,0 +1,52 @@ +/* + * 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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.lib.data.yaml.utils; + +import io.ballerina.runtime.api.utils.StringUtils; +import io.ballerina.runtime.api.values.BString; + +/** + * Constants for yaml data. + * + * @since 0.1.0 + */ +public class Constants { + + public static final String DEFAULT_LOCAL_TAG_HANDLE = "!"; + public static final String DEFAULT_GLOBAL_TAG_HANDLE = "tag:yaml.org,2002:"; + public static final String DEFAULT_GLOBAL_SEQ_TAG_HANDLE = DEFAULT_GLOBAL_TAG_HANDLE + "seq"; + public static final String DEFAULT_GLOBAL_MAP_TAG_HANDLE = DEFAULT_GLOBAL_TAG_HANDLE + "map"; + public static final String DEFAULT_GLOBAL_STR_TAG_HANDLE = DEFAULT_GLOBAL_TAG_HANDLE + "str"; + public static final BString INDENTATION_POLICY = StringUtils.fromString("indentationPolicy"); + public static final BString BLOCK_LEVEL = StringUtils.fromString("blockLevel"); + public static final BString CANONICAL = StringUtils.fromString("canonical"); + public static final BString USE_SINGLE_QUOTES = StringUtils.fromString("useSingleQuotes"); + public static final BString FORCE_QUOTES = StringUtils.fromString("forceQuotes"); + public static final BString SCHEMA = StringUtils.fromString("schema"); + public static final BString IS_STREAM = StringUtils.fromString("isStream"); + public static final BString FLOW_STYLE = StringUtils.fromString("flowStyle"); + public static final BString ALLOW_ANCHOR_REDEFINITION = StringUtils.fromString("allowAnchorRedefinition"); + public static final BString ALLOW_MAP_ENTRY_REDEFINITION = StringUtils.fromString("allowMapEntryRedefinition"); + public static final BString ALLOW_DATA_PROJECTION = StringUtils.fromString("allowDataProjection"); + public static final BString NIL_AS_OPTIONAL_FIELD = StringUtils.fromString("nilAsOptionalField"); + public static final BString ABSENT_AS_NILABLE_TYPE = StringUtils.fromString("absentAsNilableType"); + public static final BString STRICT_TUPLE_ORDER = StringUtils.fromString("strictTupleOrder"); + public static final BString END_OF_YAML_DOCUMENT = StringUtils.fromString("..."); + public static final BString START_OF_YAML_DOCUMENT = StringUtils.fromString("---"); +} diff --git a/native/src/main/java/io/ballerina/lib/data/yaml/utils/DiagnosticErrorCode.java b/native/src/main/java/io/ballerina/lib/data/yaml/utils/DiagnosticErrorCode.java new file mode 100644 index 0000000..5ffb40a --- /dev/null +++ b/native/src/main/java/io/ballerina/lib/data/yaml/utils/DiagnosticErrorCode.java @@ -0,0 +1,52 @@ +/* + * 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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.lib.data.yaml.utils; + +/** + * Represents a diagnostic error code. + * + * @since 0.1.0 + */ +public enum DiagnosticErrorCode { + + UNSUPPORTED_TYPE("YAML_ERROR_001", "unsupported.type"), + YAML_READER_FAILURE("YAML_ERROR_002", "yaml.reader.failure"), + YAML_PARSER_EXCEPTION("YAML_ERROR_003", "yaml.parser.exception"), + DUPLICATE_FIELD("YAML_ERROR_004", "duplicate.field"), + INCOMPATIBLE_TYPE("YAML_ERROR_005", "incompatible.type"), + UNDEFINED_FIELD("YAML_ERROR_006", "undefined.field"), + ARRAY_SIZE_MISMATCH("YAML_ERROR_007", "array.size.mismatch"), + INVALID_TYPE("YAML_ERROR_008", "invalid.type"), + INCOMPATIBLE_VALUE_FOR_FIELD("YAML_ERROR_009", "incompatible.value.for.field"), + REQUIRED_FIELD_NOT_PRESENT("YAML_ERROR_010", "required.field.not.present"), + INVALID_TYPE_FOR_FIELD("YAML_ERROR_011", "invalid.type.for.field"), + CANNOT_CONVERT_TO_EXPECTED_TYPE("YAML_ERROR_012", "cannot.convert.to.expected.type"); + + final String diagnosticId; + final String messageKey; + + DiagnosticErrorCode(String diagnosticId, String messageKey) { + this.diagnosticId = diagnosticId; + this.messageKey = messageKey; + } + + public String messageKey() { + return messageKey; + } +} diff --git a/native/src/main/java/io/ballerina/lib/data/yaml/utils/DiagnosticLog.java b/native/src/main/java/io/ballerina/lib/data/yaml/utils/DiagnosticLog.java new file mode 100644 index 0000000..163e0c6 --- /dev/null +++ b/native/src/main/java/io/ballerina/lib/data/yaml/utils/DiagnosticLog.java @@ -0,0 +1,53 @@ +/* + * 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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.lib.data.yaml.utils; + +import io.ballerina.runtime.api.creators.ErrorCreator; +import io.ballerina.runtime.api.utils.StringUtils; +import io.ballerina.runtime.api.values.BError; + +import java.text.MessageFormat; +import java.util.Locale; +import java.util.ResourceBundle; + +/** + * Diagnostic log for YAML data module. + * + * @since 0.1.0 + */ +public class DiagnosticLog { + private static final String ERROR_PREFIX = "error"; + private static final String ERROR = "Error"; + private static final ResourceBundle MESSAGES = ResourceBundle.getBundle("error", Locale.getDefault()); + + public static BError error(DiagnosticErrorCode code, Object... args) { + String msg = formatMessage(code, args); + return getYamlError(msg); + } + + private static String formatMessage(DiagnosticErrorCode code, Object[] args) { + String msgKey = MESSAGES.getString(ERROR_PREFIX + "." + code.messageKey()); + return MessageFormat.format(msgKey, args); + } + + public static BError getYamlError(String message) { + return ErrorCreator.createError(ModuleUtils.getModule(), ERROR, StringUtils.fromString(message), + null, null); + } +} diff --git a/native/src/main/java/io/ballerina/lib/data/yaml/utils/Error.java b/native/src/main/java/io/ballerina/lib/data/yaml/utils/Error.java new file mode 100644 index 0000000..1651b27 --- /dev/null +++ b/native/src/main/java/io/ballerina/lib/data/yaml/utils/Error.java @@ -0,0 +1,46 @@ +/* + * 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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.lib.data.yaml.utils; + +/** + * Error class use to handle all the parsing level exceptions. + * + * @since 0.1.0 + */ +public class Error { + + public static class YamlParserException extends Exception { + private final int line; + private final int column; + + public YamlParserException(String msg, int line, int column) { + super(msg); + this.line = line; + this.column = column; + } + + public int getLine() { + return line; + } + + public int getColumn() { + return column; + } + } +} diff --git a/native/src/main/java/io/ballerina/lib/data/yaml/utils/JsonTraverse.java b/native/src/main/java/io/ballerina/lib/data/yaml/utils/JsonTraverse.java new file mode 100644 index 0000000..975b964 --- /dev/null +++ b/native/src/main/java/io/ballerina/lib/data/yaml/utils/JsonTraverse.java @@ -0,0 +1,324 @@ +/* + * 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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.lib.data.yaml.utils; + +import io.ballerina.lib.data.yaml.common.Types; +import io.ballerina.lib.data.yaml.parser.ParserUtils; +import io.ballerina.lib.data.yaml.parser.Values; +import io.ballerina.runtime.api.PredefinedTypes; +import io.ballerina.runtime.api.TypeTags; +import io.ballerina.runtime.api.creators.ValueCreator; +import io.ballerina.runtime.api.flags.SymbolFlags; +import io.ballerina.runtime.api.types.ArrayType; +import io.ballerina.runtime.api.types.Field; +import io.ballerina.runtime.api.types.IntersectionType; +import io.ballerina.runtime.api.types.MapType; +import io.ballerina.runtime.api.types.RecordType; +import io.ballerina.runtime.api.types.TupleType; +import io.ballerina.runtime.api.types.Type; +import io.ballerina.runtime.api.types.UnionType; +import io.ballerina.runtime.api.utils.StringUtils; +import io.ballerina.runtime.api.utils.TypeUtils; +import io.ballerina.runtime.api.utils.ValueUtils; +import io.ballerina.runtime.api.values.BArray; +import io.ballerina.runtime.api.values.BError; +import io.ballerina.runtime.api.values.BMap; +import io.ballerina.runtime.api.values.BString; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Stack; + +/** + * Traverse json tree. + * + * @since 0.1.0 + */ +public class JsonTraverse { + + private static final ThreadLocal tlJsonTree = ThreadLocal.withInitial(JsonTree::new); + + public static Object traverse(Object json, BMap options, Type type, Types.YAMLSchema schema) { + JsonTree jsonTree = tlJsonTree.get(); + try { + Object allowDataProjection = options.get(Constants.ALLOW_DATA_PROJECTION); + jsonTree.schema = schema; + if (allowDataProjection instanceof Boolean) { + jsonTree.allowDataProjection = false; + } else if (allowDataProjection instanceof BMap) { + jsonTree.allowDataProjection = true; + jsonTree.absentAsNilableType = + (Boolean) ((BMap) allowDataProjection).get(Constants.ABSENT_AS_NILABLE_TYPE); + jsonTree.nilAsOptionalField = + (Boolean) ((BMap) allowDataProjection).get(Constants.NIL_AS_OPTIONAL_FIELD); + } + return jsonTree.traverseJson(json, type); + } finally { + jsonTree.reset(); + } + } + + private static class JsonTree { + Field currentField; + Stack> fieldHierarchy = new Stack<>(); + Stack restType = new Stack<>(); + Deque fieldNames = new ArrayDeque<>(); + Type rootArray; + boolean allowDataProjection = true; + boolean nilAsOptionalField = false; + boolean absentAsNilableType = false; + Types.YAMLSchema schema = Types.YAMLSchema.CORE_SCHEMA; + + void reset() { + currentField = null; + fieldHierarchy.clear(); + restType.clear(); + fieldNames.clear(); + rootArray = null; + allowDataProjection = false; + nilAsOptionalField = false; + absentAsNilableType = false; + } + + private Object traverseJson(Object json, Type type) { + Type referredType = TypeUtils.getReferredType(type); + switch (referredType.getTag()) { + case TypeTags.RECORD_TYPE_TAG -> { + RecordType recordType = (RecordType) referredType; + fieldHierarchy.push(ParserUtils.getAllFieldsInRecord(recordType)); + restType.push(recordType.getRestFieldType()); + return traverseMapJsonOrArrayJson(json, + ValueCreator.createRecordValue(type.getPackage(), type.getName()), referredType); + } + case TypeTags.ARRAY_TAG -> { + rootArray = referredType; + return traverseMapJsonOrArrayJson(json, ValueCreator.createArrayValue((ArrayType) referredType), + referredType); + } + case TypeTags.TUPLE_TAG -> { + rootArray = referredType; + return traverseMapJsonOrArrayJson(json, ValueCreator.createTupleValue((TupleType) referredType), + referredType); + } + case TypeTags.NULL_TAG, TypeTags.BOOLEAN_TAG, TypeTags.INT_TAG, TypeTags.FLOAT_TAG, + TypeTags.DECIMAL_TAG, TypeTags.STRING_TAG, TypeTags.CHAR_STRING_TAG , TypeTags.BYTE_TAG, + TypeTags.SIGNED8_INT_TAG, TypeTags.SIGNED16_INT_TAG, TypeTags.SIGNED32_INT_TAG, + TypeTags.UNSIGNED8_INT_TAG, TypeTags.UNSIGNED16_INT_TAG, TypeTags.UNSIGNED32_INT_TAG, + TypeTags.FINITE_TYPE_TAG -> { + return Values.fromStringWithType(Values.convertValueToBString(json), referredType, schema); + } + case TypeTags.UNION_TAG -> { + for (Type memberType : ((UnionType) referredType).getMemberTypes()) { + try { + return traverseJson(json, memberType); + } catch (Exception e) { + // Ignore + } + } + throw DiagnosticLog.error(DiagnosticErrorCode.INVALID_TYPE, type, PredefinedTypes.TYPE_ANYDATA); + } + case TypeTags.JSON_TAG, TypeTags.ANYDATA_TAG -> { + return json; + } + case TypeTags.MAP_TAG -> { + MapType mapType = (MapType) referredType; + fieldHierarchy.push(new HashMap<>()); + restType.push(mapType.getConstrainedType()); + return traverseMapJsonOrArrayJson(json, ValueCreator.createMapValue(mapType), referredType); + } + case TypeTags.INTERSECTION_TAG -> { + Type effectiveType = ((IntersectionType) referredType).getEffectiveType(); + if (!SymbolFlags.isFlagOn(SymbolFlags.READONLY, effectiveType.getFlags())) { + throw DiagnosticLog.error(DiagnosticErrorCode.UNSUPPORTED_TYPE, type); + } + for (Type constituentType : ((IntersectionType) referredType).getConstituentTypes()) { + if (constituentType.getTag() == TypeTags.READONLY_TAG) { + continue; + } + return Values.constructReadOnlyValue(traverseJson(json, constituentType)); + } + throw DiagnosticLog.error(DiagnosticErrorCode.UNSUPPORTED_TYPE, type); + } + default -> + throw DiagnosticLog.error(DiagnosticErrorCode.INVALID_TYPE, type, PredefinedTypes.TYPE_ANYDATA); + } + } + + private Object traverseMapJsonOrArrayJson(Object json, Object currentJsonNode, Type type) { + if (json instanceof BMap bMap) { + return traverseMapValue(bMap, currentJsonNode); + } else if (json instanceof BArray bArray) { + return traverseArrayValue(bArray, currentJsonNode); + } else { + // JSON value not compatible with map or array. + if (type.getTag() == TypeTags.RECORD_TYPE_TAG) { + this.fieldHierarchy.pop(); + this.restType.pop(); + } + + if (fieldNames.isEmpty()) { + throw DiagnosticLog.error(DiagnosticErrorCode.INCOMPATIBLE_TYPE, type, json); + } + throw DiagnosticLog.error(DiagnosticErrorCode.INVALID_TYPE_FOR_FIELD, getCurrentFieldPath()); + } + } + + private Object traverseMapValue(BMap map, Object currentJsonNode) { + for (BString key : map.getKeys()) { + currentField = fieldHierarchy.peek().remove(key.toString()); + if (currentField == null) { + // Add to the rest field + if (restType.peek() != null) { + Type restFieldType = TypeUtils.getReferredType(restType.peek()); + addRestField(restFieldType, key, map.get(key), currentJsonNode); + } + if (allowDataProjection) { + continue; + } + throw DiagnosticLog.error(DiagnosticErrorCode.UNDEFINED_FIELD, key); + } + + String fieldName = currentField.getFieldName(); + fieldNames.push(fieldName); + Type currentFieldType = TypeUtils.getReferredType(currentField.getFieldType()); + int currentFieldTypeTag = currentFieldType.getTag(); + Object mapValue = map.get(key); + + if (nilAsOptionalField && !currentFieldType.isNilable() && mapValue == null + && SymbolFlags.isFlagOn(currentField.getFlags(), SymbolFlags.OPTIONAL)) { + continue; + } + + switch (currentFieldTypeTag) { + case TypeTags.NULL_TAG, TypeTags.BOOLEAN_TAG, TypeTags.INT_TAG, TypeTags.FLOAT_TAG, + TypeTags.DECIMAL_TAG, TypeTags.STRING_TAG -> { + BString bStringVal = StringUtils.fromString(mapValue.toString()); +// Object value = convertToBasicType(mapValue, currentFieldType); + Object value = Values.fromStringWithType(bStringVal, currentFieldType, schema); + ((BMap) currentJsonNode).put(StringUtils.fromString(fieldNames.pop()), value); + } + default -> + ((BMap) currentJsonNode).put(StringUtils.fromString(fieldName), + traverseJson(mapValue, currentFieldType)); + } + } + Map currentField = fieldHierarchy.pop(); + checkOptionalFieldsAndLogError(currentField); + restType.pop(); + return currentJsonNode; + } + + private Object traverseArrayValue(BArray array, Object currentJsonNode) { + switch (rootArray.getTag()) { + case TypeTags.ARRAY_TAG -> { + ArrayType arrayType = (ArrayType) rootArray; + int expectedArraySize = arrayType.getSize(); + long sourceArraySize = array.getLength(); + if (!allowDataProjection && expectedArraySize < sourceArraySize) { + throw DiagnosticLog.error(DiagnosticErrorCode.ARRAY_SIZE_MISMATCH); + } + + Type elementType = arrayType.getElementType(); + if (expectedArraySize == -1 || expectedArraySize > sourceArraySize) { + traverseArrayMembers(array.getLength(), array, elementType, currentJsonNode); + } else { + traverseArrayMembers(expectedArraySize, array, elementType, currentJsonNode); + } + } + case TypeTags.TUPLE_TAG -> { + TupleType tupleType = (TupleType) rootArray; + Type restType = tupleType.getRestType(); + int expectedTupleTypeCount = tupleType.getTupleTypes().size(); + for (int i = 0; i < array.getLength(); i++) { + Object jsonMember = array.get(i); + Object nextJsonNode; + if (i < expectedTupleTypeCount) { + nextJsonNode = traverseJson(jsonMember, tupleType.getTupleTypes().get(i)); + } else if (restType != null) { + nextJsonNode = traverseJson(jsonMember, restType); + } else if (!allowDataProjection) { + throw DiagnosticLog.error(DiagnosticErrorCode.ARRAY_SIZE_MISMATCH); + } else { + continue; + } + ((BArray) currentJsonNode).add(i, nextJsonNode); + } + } + } + return currentJsonNode; + } + + private void traverseArrayMembers(long length, BArray array, Type elementType, Object currentJsonNode) { + for (int i = 0; i < length; i++) { + ((BArray) currentJsonNode).add(i, traverseJson(array.get(i), elementType)); + } + } + + private void addRestField(Type restFieldType, BString key, Object jsonMember, Object currentJsonNode) { + Object nextJsonValue; + switch (restFieldType.getTag()) { + case TypeTags.ANYDATA_TAG, TypeTags.JSON_TAG -> + ((BMap) currentJsonNode).put(key, jsonMember); + case TypeTags.BOOLEAN_TAG, TypeTags.INT_TAG, TypeTags.FLOAT_TAG, TypeTags.DECIMAL_TAG, + TypeTags.STRING_TAG -> { + ((BMap) currentJsonNode).put(key, convertToBasicType(jsonMember, restFieldType)); + } + default -> { + nextJsonValue = traverseJson(jsonMember, restFieldType); + ((BMap) currentJsonNode).put(key, nextJsonValue); + } + } + } + + private void checkOptionalFieldsAndLogError(Map currentField) { + currentField.values().forEach(field -> { + if (field.getFieldType().isNilable() && absentAsNilableType) { + return; + } + if (SymbolFlags.isFlagOn(field.getFlags(), SymbolFlags.REQUIRED)) { + throw DiagnosticLog.error(DiagnosticErrorCode.REQUIRED_FIELD_NOT_PRESENT, field.getFieldName()); + } + }); + } + + private Object convertToBasicType(Object json, Type targetType) { + try { + return ValueUtils.convert(json, targetType); + } catch (BError e) { + if (fieldNames.isEmpty()) { + throw DiagnosticLog.error(DiagnosticErrorCode.INCOMPATIBLE_TYPE, targetType, String.valueOf(json)); + } + throw DiagnosticLog.error(DiagnosticErrorCode.INCOMPATIBLE_VALUE_FOR_FIELD, String.valueOf(json), + targetType, getCurrentFieldPath()); + } + } + + private String getCurrentFieldPath() { + Iterator itr = fieldNames.descendingIterator(); + StringBuilder sb = new StringBuilder(itr.hasNext() ? itr.next() : ""); + while (itr.hasNext()) { + sb.append(".").append(itr.next()); + } + return sb.toString(); + } + } +} diff --git a/native/src/main/java/io/ballerina/stdlib/data/yaml/Native.java b/native/src/main/java/io/ballerina/lib/data/yaml/utils/ModuleUtils.java similarity index 59% rename from native/src/main/java/io/ballerina/stdlib/data/yaml/Native.java rename to native/src/main/java/io/ballerina/lib/data/yaml/utils/ModuleUtils.java index 76f0210..586a88e 100644 --- a/native/src/main/java/io/ballerina/stdlib/data/yaml/Native.java +++ b/native/src/main/java/io/ballerina/lib/data/yaml/utils/ModuleUtils.java @@ -16,22 +16,31 @@ * under the License. */ -package io.ballerina.stdlib.data.yaml; +package io.ballerina.lib.data.yaml.utils; import io.ballerina.runtime.api.Environment; -import io.ballerina.runtime.api.values.BMap; -import io.ballerina.runtime.api.values.BString; -import io.ballerina.runtime.api.values.BTypedesc; +import io.ballerina.runtime.api.Module; /** - * This class is used to convert json inform of string, byte[], byte-stream to record or json type. + * This class will hold module related utility functions. * * @since 0.1.0 */ -public class Native { +public class ModuleUtils { - public static Object fromYamlStringWithType(Environment env, Object yamlSource, BMap options, - BTypedesc typed) { - return null; + /** + * Time standard library package ID. + */ + private static Module module; + + private ModuleUtils() { + } + + public static void setModule(Environment env) { + module = env.getCurrentModule(); + } + + public static Module getModule() { + return module; } } diff --git a/native/src/main/java/io/ballerina/lib/data/yaml/utils/OptionsUtils.java b/native/src/main/java/io/ballerina/lib/data/yaml/utils/OptionsUtils.java new file mode 100644 index 0000000..dd873c7 --- /dev/null +++ b/native/src/main/java/io/ballerina/lib/data/yaml/utils/OptionsUtils.java @@ -0,0 +1,75 @@ +/* + * 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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.lib.data.yaml.utils; + +import io.ballerina.lib.data.yaml.common.Types; +import io.ballerina.runtime.api.values.BMap; +import io.ballerina.runtime.api.values.BString; + +/** + * This class will parse the user configs to Java records. + * + * @since 0.1.0 + */ +public class OptionsUtils { + + public record WriteConfig(int indentationPolicy, int blockLevel, boolean canonical, boolean useSingleQuotes, + boolean forceQuotes, Types.YAMLSchema schema, boolean isStream, boolean flowStyle) { + } + + public static WriteConfig resolveWriteOptions(BMap options) { + Object indentationPolicy = options.get(Constants.INDENTATION_POLICY); + Object blockLevel = options.get(Constants.BLOCK_LEVEL); + Object canonical = options.get(Constants.CANONICAL); + Object useSingleQuotes = options.get(Constants.USE_SINGLE_QUOTES); + Object forceQuotes = options.get(Constants.FORCE_QUOTES); + Object schema = options.get(Constants.SCHEMA); + Object isStream = options.get(Constants.IS_STREAM); + Object flowStyle = options.get(Constants.FLOW_STYLE); + + return new WriteConfig(Math.toIntExact(((Long) indentationPolicy)), Math.toIntExact(((Long) blockLevel)), + ((Boolean) canonical), ((Boolean) useSingleQuotes), ((Boolean) forceQuotes), + Types.YAMLSchema.valueOf(((BString) schema).getValue()), ((Boolean) isStream), ((Boolean) flowStyle)); + } + + public record ReadConfig(Types.YAMLSchema schema, boolean allowAnchorRedefinition, + boolean allowMapEntryRedefinition, boolean allowDataProjection, + boolean nilAsOptionalField, boolean absentAsNilableType, boolean strictTupleOrder) { + } + + public static ReadConfig resolveReadConfig(BMap options) { + Object schema = options.get(Constants.SCHEMA); + Object allowAnchorRedefinition = options.get(Constants.ALLOW_ANCHOR_REDEFINITION); + Object allowMapEntryRedefinition = options.get(Constants.ALLOW_MAP_ENTRY_REDEFINITION); + Object allowDataProjection = options.get(Constants.ALLOW_DATA_PROJECTION); + Object isStream = options.get(Constants.IS_STREAM); + if (allowDataProjection instanceof Boolean) { + return new ReadConfig(Types.YAMLSchema.valueOf(((BString) schema).getValue()), + ((Boolean) allowAnchorRedefinition), ((Boolean) allowMapEntryRedefinition), + false, false, false, false); + } + Object nilAsOptionalField = ((BMap) allowDataProjection).get(Constants.NIL_AS_OPTIONAL_FIELD); + Object absentAsNilableType = ((BMap) allowDataProjection).get(Constants.ABSENT_AS_NILABLE_TYPE); + Object strictTupleOrder = ((BMap) allowDataProjection).get(Constants.STRICT_TUPLE_ORDER); + + return new ReadConfig(Types.YAMLSchema.valueOf(((BString) schema).getValue()), + ((Boolean) allowAnchorRedefinition), ((Boolean) allowMapEntryRedefinition), true, + ((Boolean) nilAsOptionalField), ((Boolean) absentAsNilableType), ((Boolean) strictTupleOrder)); + } +} diff --git a/native/src/main/java/io/ballerina/lib/data/yaml/utils/TagResolutionUtils.java b/native/src/main/java/io/ballerina/lib/data/yaml/utils/TagResolutionUtils.java new file mode 100644 index 0000000..176cadd --- /dev/null +++ b/native/src/main/java/io/ballerina/lib/data/yaml/utils/TagResolutionUtils.java @@ -0,0 +1,150 @@ +/* + * 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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.lib.data.yaml.utils; + +import io.ballerina.lib.data.yaml.parser.YamlParser; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Resolve tagged values and create BValues. + * + * @since 0.1.0 + */ +public class TagResolutionUtils { + + public static Object constructSimpleNull(String value, YamlParser.ComposerState state) + throws Error.YamlParserException { + if (value.equals("null")) { + return null; + } + throw new Error.YamlParserException("cannot cast " + value + "to null", state.getLine(), state.getColumn()); + } + + public static Object constructNull(String value, YamlParser.ComposerState state) + throws Error.YamlParserException { + if (isCoreSchemaNull(value)) { + return null; + } + throw new Error.YamlParserException("cannot cast " + value + "to null", state.getLine(), state.getColumn()); + } + + public static Object constructSimpleBool(String value, YamlParser.ComposerState state) + throws Error.YamlParserException { + if (value.equals("true")) { + return true; + } else if (value.equals("false")) { + return false; + } + throw new Error.YamlParserException("cannot cast " + value + "to boolean", state.getLine(), state.getColumn()); + } + + public static Object constructBool(String value, YamlParser.ComposerState state) + throws Error.YamlParserException { + + if (isCoreSchemaTrue(value)) { + return true; + } else if (isCoreSchemaFalse(value)) { + return false; + } + throw new Error.YamlParserException("cannot cast " + value + "to boolean", state.getLine(), state.getColumn()); + } + + public static Object constructSimpleInt(String value, YamlParser.ComposerState state) + throws Error.YamlParserException { + Pattern simpleIntPattern = Pattern.compile("-?(\\d+)"); + Matcher matcher = simpleIntPattern.matcher(value); + if (matcher.find()) { + return Long.valueOf(value); + } + throw new Error.YamlParserException("cannot cast " + value + "to int", state.getLine(), state.getColumn()); + } + + public static Object constructInt(String value, YamlParser.ComposerState state) + throws Error.YamlParserException { + if (value.length() > 1) { + if (value.startsWith("0o")) { + return Long.parseLong(value.substring(2), 8); + } else if (value.startsWith("0x")) { + return Long.parseLong(value.substring(2), 16); + } + } + Pattern simpleIntPattern = Pattern.compile("[+-]?(\\d+)"); + Matcher matcher = simpleIntPattern.matcher(value); + if (matcher.find()) { + return Long.valueOf(value); + } + throw new Error.YamlParserException("cannot cast " + value + "to int", state.getLine(), state.getColumn()); + } + + public static Object constructSimpleFloat(String value, YamlParser.ComposerState state) + throws Error.YamlParserException { + Pattern simpleFloatPattern = Pattern.compile("-?(0|\\d+)(\\.\\d*)?([eE][-+]?\\d+)?"); + Matcher matcher = simpleFloatPattern.matcher(value); + if (matcher.find()) { + return Double.parseDouble(value); + } + throw new Error.YamlParserException("cannot cast " + value + "to float", state.getLine(), state.getColumn()); + } + + public static Object constructFloat(String value, YamlParser.ComposerState state) + throws Error.YamlParserException { + if (value.length() > 1) { + if (value.startsWith(".")) { + String valueSuffix = value.substring(1); + if (valueSuffix.equals("nan") || valueSuffix.equals("NaN") || valueSuffix.equals("NAN")) { + return Double.NaN; + } + if (valueSuffix.equals("inf") || valueSuffix.equals("Inf") || valueSuffix.equals("INF")) { + return Double.POSITIVE_INFINITY; + } + } else if (value.startsWith("+.") || value.startsWith("-.")) { + String valueSuffix = value.substring(1); + boolean isInfinity = valueSuffix.equals("inf") || + valueSuffix.equals("Inf") || valueSuffix.equals("INF"); + if (isInfinity) { + return value.startsWith("+") ? Double.POSITIVE_INFINITY : Double.NEGATIVE_INFINITY; + } + } + } + Pattern floatPattern = Pattern.compile("[-+]?(\\.\\d+|\\d+(\\.\\d*)?)([eE][-+]?\\d+)?"); + Matcher matcher = floatPattern.matcher(value); + if (matcher.find()) { + return Double.parseDouble(value); + } + throw new Error.YamlParserException("cannot cast " + value + "to float", state.getLine(), state.getColumn()); + } + + public static boolean isCoreSchemaNull(String value) { + return value.equals("null") || value.equals("Null") || value.equals("NULL") || value.equals("~"); + } + + public static boolean isCoreSchemaBoolean(String value) { + return isCoreSchemaTrue(value) || isCoreSchemaFalse(value); + } + + private static boolean isCoreSchemaTrue(String value) { + return value.equals("true") || value.equals("True") || value.equals("TRUE"); + } + + private static boolean isCoreSchemaFalse(String value) { + return value.equals("false") || value.equals("False") || value.equals("FALSE"); + } +} diff --git a/native/src/main/java/io/ballerina/lib/data/yaml/utils/TypeUtils.java b/native/src/main/java/io/ballerina/lib/data/yaml/utils/TypeUtils.java new file mode 100644 index 0000000..06ef95f --- /dev/null +++ b/native/src/main/java/io/ballerina/lib/data/yaml/utils/TypeUtils.java @@ -0,0 +1,44 @@ +package io.ballerina.lib.data.yaml.utils; + +import io.ballerina.runtime.api.PredefinedTypes; +import io.ballerina.runtime.api.types.Type; +import io.ballerina.runtime.api.values.BObject; +import io.ballerina.runtime.api.values.BString; +import io.ballerina.runtime.api.values.BValue; + +public class TypeUtils { + + public static Type getType(Object value) { + if (value == null) { + return PredefinedTypes.TYPE_NULL; + } else { + if (value instanceof Number) { + if (value instanceof Long) { + return PredefinedTypes.TYPE_INT; + } + + if (value instanceof Double) { + return PredefinedTypes.TYPE_FLOAT; + } + + if (value instanceof Integer || value instanceof Byte) { + return PredefinedTypes.TYPE_BYTE; + } + } else { + if (value instanceof BString) { + return PredefinedTypes.TYPE_STRING; + } + + if (value instanceof Boolean) { + return PredefinedTypes.TYPE_BOOLEAN; + } + + if (value instanceof BObject) { + return ((BObject) value).getOriginalType(); + } + } + + return ((BValue) value).getType(); + } + } +} diff --git a/native/src/main/java/module-info.java b/native/src/main/java/module-info.java index 08ac336..cbbec37 100644 --- a/native/src/main/java/module-info.java +++ b/native/src/main/java/module-info.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 @@ -16,10 +16,10 @@ * under the License. */ -module io.ballerina.stdlib.data { +module io.ballerina.lib.data { requires io.ballerina.runtime; requires io.ballerina.lang.value; requires junit; requires org.apache.commons.lang3; - exports io.ballerina.stdlib.data.yaml; + exports io.ballerina.lib.data.yaml; } diff --git a/native/src/main/resources/META-INF/native-image/io.ballerina.stdlib/data/yaml-native/resource-config.json b/native/src/main/resources/META-INF/native-image/io.ballerina.stdlib/data/yaml-native/resource-config.json new file mode 100644 index 0000000..befe09e --- /dev/null +++ b/native/src/main/resources/META-INF/native-image/io.ballerina.stdlib/data/yaml-native/resource-config.json @@ -0,0 +1,6 @@ +{ + "bundles":[{ + "name":"error", + "locales":[""] + }] +} diff --git a/native/src/main/resources/error.properties b/native/src/main/resources/error.properties new file mode 100644 index 0000000..42897d0 --- /dev/null +++ b/native/src/main/resources/error.properties @@ -0,0 +1,58 @@ +# +# Copyright (c) 2024, WSO2 LLC. (http://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 +# in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +# ------------------------- +# YAML Data module error messages +# ------------------------- + +error.unsupported.type=\ + unsupported type ''{0}'' + +error.yaml.reader.failure=\ + error reading while YAML: ''{0}'' + +error.yaml.parser.exception=\ + ''{0}'' at line: ''{1}'' column: ''{2}'' + +error.duplicate.field=\ + duplicate field ''{0}'' + +error.incompatible.type=\ + incompatible expected type ''{0}'' for value ''{1}'' + +error.undefined.field=\ + undefined field ''{0}'' + +error.array.size.mismatch=\ + array size is not compatible with the expected size + +error.invalid.type=\ + invalid type ''{0}'' expected ''{1}'' + +error.incompatible.value.for.field=\ + incompatible value ''{0}'' for type ''{1}'' in field ''{2}'' + +error.required.field.not.present=\ + required field ''{0}'' not present in YAML + +error.invalid.type.for.field=\ + invalid type for field ''{0}'' + +error.cannot.convert.to.expected.type=\ + ''{0}'' value ''{1}'' cannot be converted to ''{2}'' + diff --git a/settings.gradle b/settings.gradle index d1cab16..15b490f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -16,6 +16,26 @@ * under the License. */ +pluginManagement { + plugins { + id "com.github.spotbugs" version "${spotbugsVersion}" + id "com.github.johnrengelman.shadow" version "${shadowJarPluginVersion}" + id "de.undercouch.download" version "${downloadPluginVersion}" + id "net.researchgate.release" version "${releasePluginVersion}" + id "io.ballerina.plugin" version "${ballerinaGradlePluginVersion}" + } + repositories { + gradlePluginPortal() + maven { + url = 'https://maven.pkg.github.com/ballerina-platform/*' + credentials { + username System.getenv("packageUser") + password System.getenv("packagePAT") + } + } + } +} + plugins { id "com.gradle.enterprise" version "3.2" } @@ -24,10 +44,14 @@ rootProject.name = 'data.yaml' include(':checkstyle') include(':data.yaml-native') include(':data.yaml-ballerina') +include(':data.yaml-compiler-plugin') +include(':data.yaml-compiler-plugin-tests') project(':checkstyle').projectDir = file("build-config${File.separator}checkstyle") project(':data.yaml-native').projectDir = file('native') project(':data.yaml-ballerina').projectDir = file('ballerina') +project(':data.yaml-compiler-plugin').projectDir = file('compiler-plugin') +project(':data.yaml-compiler-plugin-tests').projectDir = file('compiler-plugin-test') gradleEnterprise { buildScan {