diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml
index b424cf2..05d726b 100644
--- a/ballerina/Ballerina.toml
+++ b/ballerina/Ballerina.toml
@@ -14,3 +14,9 @@ groupId = "io.ballerina.lib"
artifactId = "data-native"
version = "1.0.0"
path = "../native/build/libs/data.xmldata-native-1.0.0-SNAPSHOT.jar"
+
+[[platform.java17.dependency]]
+groupId = "io.ballerina.stdlib"
+artifactId = "constraint-native"
+version = "1.5.0"
+path = "./lib/constraint-native-1.5.0.jar"
diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml
index c3c6683..1209aa9 100644
--- a/ballerina/Dependencies.toml
+++ b/ballerina/Dependencies.toml
@@ -7,14 +7,28 @@
dependencies-toml-version = "2"
distribution-version = "2201.10.0-20240724-114000-40e856f7"
+[[package]]
+org = "ballerina"
+name = "constraint"
+version = "1.5.0"
+scope = "testOnly"
+dependencies = [
+ {org = "ballerina", name = "jballerina.java"}
+]
+modules = [
+ {org = "ballerina", packageName = "constraint", moduleName = "constraint"}
+]
+
[[package]]
org = "ballerina"
name = "data.xmldata"
version = "1.0.0"
dependencies = [
+ {org = "ballerina", name = "constraint"},
{org = "ballerina", name = "io"},
{org = "ballerina", name = "jballerina.java"},
- {org = "ballerina", name = "test"}
+ {org = "ballerina", name = "test"},
+ {org = "ballerina", name = "time"}
]
modules = [
{org = "ballerina", packageName = "data.xmldata", moduleName = "data.xmldata"}
@@ -99,3 +113,15 @@ modules = [
{org = "ballerina", packageName = "test", moduleName = "test"}
]
+[[package]]
+org = "ballerina"
+name = "time"
+version = "2.4.0"
+scope = "testOnly"
+dependencies = [
+ {org = "ballerina", name = "jballerina.java"}
+]
+modules = [
+ {org = "ballerina", packageName = "time", moduleName = "time"}
+]
+
diff --git a/ballerina/build.gradle b/ballerina/build.gradle
index c63d338..4fc312a 100644
--- a/ballerina/build.gradle
+++ b/ballerina/build.gradle
@@ -66,10 +66,25 @@ ballerina {
testCoverageParam = "--code-coverage --coverage-format=xml --includes=io.ballerina.lib.data.xmldata*:ballerina.*"
}
+configurations {
+ externalJars
+}
+
+dependencies {
+ externalJars(group: 'io.ballerina.stdlib', name: 'constraint-native', version: "${stdlibConstraintVersion}") {
+ transitive = false
+ }
+}
+
task updateTomlFiles {
doLast {
+ def stdlibDependentConstraintNativeVersion = stdlibConstraintVersion
+ def stdlibDependentConstraintVersion = stripBallerinaExtensionVersion("${stdlibDependentConstraintNativeVersion}")
+
def newConfig = ballerinaTomlFilePlaceHolder.text.replace("@project.version@", project.version)
newConfig = newConfig.replace("@toml.version@", tomlVersion)
+ newConfig = newConfig.replace("@stdlib.constraintnative.version@", stdlibDependentConstraintNativeVersion)
+ newConfig = newConfig.replace("@constraint.version@", stdlibDependentConstraintVersion)
ballerinaTomlFile.text = newConfig
def newCompilerPluginToml = compilerPluginTomlFilePlaceHolder.text.replace("@project.version@", project.version)
diff --git a/ballerina/tests/constraint_annotation_test.bal b/ballerina/tests/constraint_annotation_test.bal
new file mode 100644
index 0000000..ee01362
--- /dev/null
+++ b/ballerina/tests/constraint_annotation_test.bal
@@ -0,0 +1,570 @@
+// 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/constraint;
+import ballerina/test;
+import ballerina/time;
+
+public type ValidationPerson record {|
+ @constraint:String {
+ maxLength: 5,
+ minLength: 2
+ }
+ string name;
+ @constraint:Int {
+ minValueExclusive: 5
+ }
+ int age;
+ @constraint:Float {
+ minValue: 150,
+ maxValue: 185.20,
+ maxFractionDigits: 2
+ }
+ float height;
+ @constraint:Date {
+ option: {
+ value: "PAST",
+ message: "Date of birth should be past value"
+ },
+ message: "Invalid date found for date of birth"
+ }
+ time:Date dob;
+ Family family;
+|};
+
+public type Family record {|
+ @constraint:Array {
+ maxLength: 2,
+ minLength: 1
+ }
+ string[] members;
+ @constraint:Int {
+ maxDigits: 4
+ }
+ int id;
+|};
+
+@test:Config {
+ groups: ["constraint-validation"]
+}
+function testValidConstraintAnnotationForParseString() returns error? {
+ string xmlStr = string `
+
+ John
+ 6
+ 180.20
+
+ 1990
+ 12
+ 31
+
+
+ 2221
+ John
+ Doe
+
+
+ `;
+
+ ValidationPerson person = check parseString(xmlStr);
+ test:assertEquals(person.name, "John");
+ test:assertEquals(person.age, 6);
+ test:assertEquals(person.height, 180.20);
+ test:assertEquals(person.dob.year, 1990);
+ test:assertEquals(person.dob.month, 12);
+ test:assertEquals(person.dob.day, 31);
+ test:assertEquals(person.family.id, 2221);
+ test:assertEquals(person.family.members.length(), 2);
+ test:assertEquals(person.family.members[0], "John");
+ test:assertEquals(person.family.members[1], "Doe");
+}
+
+@constraint:Array {
+ length: 2
+}
+public type Weight decimal[];
+
+public type ValidationItem record {|
+ Weight weight;
+|};
+
+@test:Config {
+ groups: ["constraint-validation"],
+ dataProvider: invalidConstraintAnnotation
+}
+function testInvalidConstraintAnnotationForParseString(string sourceData, typedesc expType, string expectedError) {
+ anydata|Error err = parseString(sourceData, {}, expType);
+ test:assertEquals(err is Error, true);
+ test:assertEquals((err).message(), expectedError);
+}
+
+function invalidConstraintAnnotation() returns [string, typedesc, string][] {
+ return [
+ [
+ string `
+
+ John Doe
+ 6
+ 180.20
+
+ 1990
+ 12
+ 31
+
+
+ 2221
+ John
+ Doe
+
+
+ `,
+ ValidationPerson,
+ "Validation failed for '$.name:maxLength' constraint(s)."
+ ],
+ [
+ string `
+
+ John
+ 4
+ 180.20
+
+ 1990
+ 12
+ 31
+
+
+ 2221
+ John
+ Doe
+
+
+ `,
+ ValidationPerson,
+ "Validation failed for '$.age:minValueExclusive' constraint(s)."
+ ],
+ [
+ string `
+
+ John
+ 6
+ 185.21
+
+ 1990
+ 12
+ 31
+
+
+ 2221
+ John
+ Doe
+
+
+ `,
+ ValidationPerson,
+ "Validation failed for '$.height:maxValue' constraint(s)."
+ ],
+ [
+ string `
+
+ John
+ 6
+ 167.252
+
+ 1990
+ 12
+ 31
+
+
+ 2221
+ John
+ Doe
+
+
+ `,
+ ValidationPerson,
+ "Validation failed for '$.height:maxFractionDigits' constraint(s)."
+ ],
+ [
+ string `
+
+ John
+ 6
+ 167.25
+
+ 5000
+ 12
+ 31
+
+
+ 2221
+ John
+ Doe
+
+
+ `,
+ ValidationPerson,
+ "Date of birth should be past value."
+ ],
+ [
+ string `
+
+ John
+ 6
+ 167.25
+
+ 1999
+ 12
+ 31
+
+
+ 22213
+ John
+ Doe
+ Ross
+
+
+ `,
+ ValidationPerson,
+ "Validation failed for '$.family.id:maxDigits','$.family.members:maxLength' constraint(s)."
+ ],
+ [
+ string `
+ -
+ 1.2
+ 2.3
+ 3.4
+
+ `,
+ ValidationItem,
+ "Validation failed for '$.weight:length' constraint(s)."
+ ]
+ ];
+}
+
+@test:Config {
+ groups: ["constraint-validation"],
+ dataProvider: disableConstraintValidation
+}
+function testDisableConstraintValidationForParseString(string sourceData, typedesc expType, anydata result) returns error? {
+ anydata err = check parseString(sourceData, {enableConstraintValidation: false}, expType);
+ test:assertEquals(err, result);
+}
+
+function disableConstraintValidation() returns [string, typedesc, anydata][] {
+ return [
+ [
+ string `
+
+ John Doe
+ 4
+ 185.215
+
+ 5000
+ 12
+ 31
+
+
+ 22213
+ John
+ Doe
+ Ross
+
+
+ `,
+ ValidationPerson,
+ {
+ "name": "John Doe",
+ "age": 4,
+ "height": 185.215,
+ "dob": {
+ "year": 5000,
+ "month": 12,
+ "day": 31
+ },
+ "family": {
+ "id": 22213,
+ "members": [
+ "John",
+ "Doe",
+ "Ross"
+ ]
+ }
+ }
+ ],
+ [
+ string `
+ -
+ 1.2
+ 2.3
+ 3.4
+
+ `,
+ ValidationItem,
+ {"weight": [1.2, 2.3, 3.4]}
+ ]
+ ];
+}
+
+@test:Config {
+ groups: ["constraint-validation"]
+}
+function testValidConstraintAnnotationForParseAsType() returns error? {
+ xml xmlVal = xml `
+
+ John
+ 6
+ 180.20
+
+ 1990
+ 12
+ 31
+
+
+ 2221
+ John
+ Doe
+
+
+ `;
+
+ ValidationPerson person = check parseAsType(xmlVal);
+ test:assertEquals(person.name, "John");
+ test:assertEquals(person.age, 6);
+ test:assertEquals(person.height, 180.20);
+ test:assertEquals(person.dob.year, 1990);
+ test:assertEquals(person.dob.month, 12);
+ test:assertEquals(person.dob.day, 31);
+ test:assertEquals(person.family.members.length(), 2);
+ test:assertEquals(person.family.members[0], "John");
+ test:assertEquals(person.family.members[1], "Doe");
+}
+
+@test:Config {
+ groups: ["constraint-validation"],
+ dataProvider: invalidConstraintAnnotationForParseAsType
+}
+function testInvalidConstraintAnnotationForParseAsType(xml sourceData, typedesc expType, string expectedError) {
+ anydata|Error err = parseAsType(sourceData, {}, expType);
+ test:assertEquals(err is Error, true);
+ test:assertEquals((err).message(), expectedError);
+}
+
+function invalidConstraintAnnotationForParseAsType() returns [xml, typedesc, string][] {
+ return [
+ [
+ xml `
+
+ John Doe
+ 6
+ 180.20
+
+ 1990
+ 12
+ 31
+
+
+ 2221
+ John
+ Doe
+
+
+ `,
+ ValidationPerson,
+ "Validation failed for '$.name:maxLength' constraint(s)."
+ ],
+ [
+ xml `
+
+ John
+ 4
+ 180.20
+
+ 1990
+ 12
+ 31
+
+
+ 2221
+ John
+ Doe
+
+
+ `,
+ ValidationPerson,
+ "Validation failed for '$.age:minValueExclusive' constraint(s)."
+ ],
+ [
+ xml `
+
+ John
+ 6
+ 185.21
+
+ 1990
+ 12
+ 31
+
+
+ 2221
+ John
+ Doe
+
+
+ `,
+ ValidationPerson,
+ "Validation failed for '$.height:maxValue' constraint(s)."
+ ],
+ [
+ xml `
+
+ John
+ 6
+ 167.252
+
+ 1990
+ 12
+ 31
+
+
+ 2221
+ John
+ Doe
+
+
+ `,
+ ValidationPerson,
+ "Validation failed for '$.height:maxFractionDigits' constraint(s)."
+ ],
+ [
+ xml `
+
+ John
+ 6
+ 167.25
+
+ 5000
+ 12
+ 31
+
+
+ 2221
+ John
+ Doe
+
+
+ `,
+ ValidationPerson,
+ "Date of birth should be past value."
+ ],
+ [
+ xml `
+
+ John
+ 6
+ 167.25
+
+ 1999
+ 12
+ 31
+
+
+ 22213
+ John
+ Doe
+ Ross
+
+
+ `,
+ ValidationPerson,
+ "Validation failed for '$.family.id:maxDigits','$.family.members:maxLength' constraint(s)."
+ ],
+ [
+ xml `
+ -
+ 1.2
+ 2.3
+ 3.4
+
+ `,
+ ValidationItem,
+ "Validation failed for '$.weight:length' constraint(s)."
+ ]
+ ];
+}
+
+@test:Config {
+ groups: ["constraint-validation"],
+ dataProvider: disableConstraintValidationForParseAsType
+}
+function testDisableConstraintValidationForParseAsType(xml sourceData, typedesc expType, anydata result) returns error? {
+ anydata err = check parseAsType(sourceData, {enableConstraintValidation: false}, expType);
+ test:assertEquals(err, result);
+}
+
+function disableConstraintValidationForParseAsType() returns [xml, typedesc, anydata][] {
+ return [
+ [
+ xml `
+
+ John Doe
+ 4
+ 185.215
+
+ 5000
+ 12
+ 31
+
+
+ 22213
+ John
+ Doe
+ Ross
+
+
+ `,
+ ValidationPerson,
+ {
+ "name": "John Doe",
+ "age": 4,
+ "height": 185.215,
+ "dob": {
+ "year": 5000,
+ "month": 12,
+ "day": 31
+ },
+ "family": {
+ "id": 22213,
+ "members": [
+ "John",
+ "Doe",
+ "Ross"
+ ]
+ }
+ }
+ ],
+ [
+ xml `
+ -
+ 1.2
+ 2.3
+ 3.4
+
+ `,
+ ValidationItem,
+ {"weight": [1.2, 2.3, 3.4]}
+ ]
+ ];
+}
diff --git a/ballerina/xml_api.bal b/ballerina/xml_api.bal
index d2f7f98..697be47 100644
--- a/ballerina/xml_api.bal
+++ b/ballerina/xml_api.bal
@@ -69,6 +69,8 @@ public type SourceOptions record {|
boolean allowDataProjection = true;
# choose between semantic and syntactic equality
boolean useSemanticEquality = true;
+ # enable or disable constraint validation
+ boolean enableConstraintValidation = true;
|};
# Represents the error type of the ballerina/data.xmldata module. This error type represents any error that can occur
diff --git a/build-config/resources/Ballerina.toml b/build-config/resources/Ballerina.toml
index 5f14962..e881ae9 100644
--- a/build-config/resources/Ballerina.toml
+++ b/build-config/resources/Ballerina.toml
@@ -14,3 +14,9 @@ groupId = "io.ballerina.lib"
artifactId = "data-native"
version = "@toml.version@"
path = "../native/build/libs/data.xmldata-native-@project.version@.jar"
+
+[[platform.java17.dependency]]
+groupId = "io.ballerina.stdlib"
+artifactId = "constraint-native"
+version = "@constraint.version@"
+path = "./lib/constraint-native-@stdlib.constraintnative.version@.jar"
diff --git a/build.gradle b/build.gradle
index a3939ce..a272ee8 100644
--- a/build.gradle
+++ b/build.gradle
@@ -68,6 +68,8 @@ subprojects {
dependencies {
/* Standard libraries */
ballerinaStdLibs "io.ballerina.stdlib:io-ballerina:${stdlibIoVersion}"
+ ballerinaStdLibs "io.ballerina.stdlib:constraint-ballerina:${stdlibConstraintVersion}"
+ ballerinaStdLibs "io.ballerina.stdlib:time-ballerina:${stdlibTimeVersion}"
}
}
diff --git a/gradle.properties b/gradle.properties
index 89a09c9..8f52d97 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -12,4 +12,7 @@ githubJohnrengelmanShadowVersion=8.1.1
underCouchDownloadVersion=4.0.4
researchgateReleaseVersion=2.8.0
ballerinaGradlePluginVersion=2.0.1
+
stdlibIoVersion=1.6.0
+stdlibTimeVersion=2.4.0
+stdlibConstraintVersion=1.5.0
diff --git a/native/build.gradle b/native/build.gradle
index 250c984..ceb83f2 100644
--- a/native/build.gradle
+++ b/native/build.gradle
@@ -33,6 +33,8 @@ dependencies {
implementation group: 'org.ballerinalang', name: 'ballerina-lang', version: "${ballerinaLangVersion}"
implementation group: 'org.ballerinalang', name: 'ballerina-runtime', version: "${ballerinaLangVersion}"
implementation group: 'org.ballerinalang', name: 'value', version: "${ballerinaLangVersion}"
+
+ implementation group: 'io.ballerina.stdlib', name: 'constraint-native', version: "${stdlibConstraintVersion}"
}
checkstyle {
diff --git a/native/src/main/java/io/ballerina/lib/data/xmldata/io/DataReaderTask.java b/native/src/main/java/io/ballerina/lib/data/xmldata/io/DataReaderTask.java
index 4ec6d0f..126d8ad 100644
--- a/native/src/main/java/io/ballerina/lib/data/xmldata/io/DataReaderTask.java
+++ b/native/src/main/java/io/ballerina/lib/data/xmldata/io/DataReaderTask.java
@@ -87,7 +87,7 @@ public void run() {
DataReaderTask.ResultConsumer