diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index 2da792d..5c648ee 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -1,7 +1,7 @@ [package] org = "ballerina" name = "constraint" -version = "1.4.1" +version = "1.5.0" authors = ["Ballerina"] keywords = ["constraint", "validation"] repository = "https://github.com/ballerina-platform/module-ballerina-constraint" @@ -15,5 +15,5 @@ graalvmCompatible = true [[platform.java17.dependency]] groupId = "io.ballerina.stdlib" artifactId = "constraint-native" -version = "1.4.1" -path = "../native/build/libs/constraint-native-1.4.1-SNAPSHOT.jar" +version = "1.5.0" +path = "../native/build/libs/constraint-native-1.5.0-SNAPSHOT.jar" diff --git a/ballerina/CompilerPlugin.toml b/ballerina/CompilerPlugin.toml index 5d3e693..6f1058d 100644 --- a/ballerina/CompilerPlugin.toml +++ b/ballerina/CompilerPlugin.toml @@ -3,4 +3,4 @@ id = "constraint-compiler-plugin" class = "io.ballerina.stdlib.constraint.compiler.ConstraintCompilerPlugin" [[dependency]] -path = "../compiler-plugin/build/libs/constraint-compiler-plugin-1.4.1-SNAPSHOT.jar" +path = "../compiler-plugin/build/libs/constraint-compiler-plugin-1.5.0-SNAPSHOT.jar" diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index eb323ed..d12c174 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -10,7 +10,7 @@ distribution-version = "2201.8.0" [[package]] org = "ballerina" name = "constraint" -version = "1.4.1" +version = "1.5.0" dependencies = [ {org = "ballerina", name = "jballerina.java"}, {org = "ballerina", name = "test"}, diff --git a/ballerina/constraint.bal b/ballerina/constraint.bal index 672fee6..d53144c 100644 --- a/ballerina/constraint.bal +++ b/ballerina/constraint.bal @@ -67,11 +67,13 @@ public type ConstraintRecord record {| # + maxValue - The inclusive upper bound of the constrained type # + minValueExclusive - The exclusive lower bound of the constrained type # + maxValueExclusive - The exclusive upper bound of the constrained type +# + maxDigits - The maximum number of digits in the constrained type public type IntConstraints record {| int|record{| *ConstraintRecord; int value; |} minValue?; int|record{| *ConstraintRecord; int value; |} maxValue?; int|record{| *ConstraintRecord; int value; |} minValueExclusive?; int|record{| *ConstraintRecord; int value; |} maxValueExclusive?; + int|record{| *ConstraintRecord; int value; |} maxDigits?; |}; # Represents the constraints associated with `float` type. @@ -80,11 +82,15 @@ public type IntConstraints record {| # + maxValue - The inclusive upper bound of the constrained type # + minValueExclusive - The exclusive lower bound of the constrained type # + maxValueExclusive - The exclusive upper bound of the constrained type +# + maxIntegerDigits - The maximum number of digits in the integer part of the constrained type +# + maxFractionDigits - The maximum number of digits in the fraction part of the constrained type public type FloatConstraints record {| float|record{| *ConstraintRecord; float value; |} minValue?; float|record{| *ConstraintRecord; float value; |} maxValue?; float|record{| *ConstraintRecord; float value; |} minValueExclusive?; float|record{| *ConstraintRecord; float value; |} maxValueExclusive?; + int|record{| *ConstraintRecord; int value; |} maxIntegerDigits?; + int|record{| *ConstraintRecord; int value; |} maxFractionDigits?; |}; # Represents the constraints associated with `int`, `float` and `decimal` types. @@ -93,11 +99,15 @@ public type FloatConstraints record {| # + maxValue - The inclusive upper bound of the constrained type # + minValueExclusive - The exclusive lower bound of the constrained type # + maxValueExclusive - The exclusive upper bound of the constrained type +# + maxIntegerDigits - The maximum number of digits in the integer part of the constrained type +# + maxFractionDigits - The maximum number of digits in the fraction part of the constrained type public type NumberConstraints record {| decimal|record{| *ConstraintRecord; decimal value; |} minValue?; decimal|record{| *ConstraintRecord; decimal value; |} maxValue?; decimal|record{| *ConstraintRecord; decimal value; |} minValueExclusive?; decimal|record{| *ConstraintRecord; decimal value; |} maxValueExclusive?; + int|record{| *ConstraintRecord; int value; |} maxIntegerDigits?; + int|record{| *ConstraintRecord; int value; |} maxFractionDigits?; |}; # Represents the constraints associated with `string` type. diff --git a/ballerina/tests/float_constraint_on_type_test.bal b/ballerina/tests/float_constraint_on_type_test.bal index 904ed32..681360b 100644 --- a/ballerina/tests/float_constraint_on_type_test.bal +++ b/ballerina/tests/float_constraint_on_type_test.bal @@ -223,3 +223,256 @@ isolated function testFloatConstraintOnTypeFailure2() { test:assertFail("Expected error not found."); } } + +@Float { + maxIntegerDigits: 5, + maxFractionDigits: 4 +} +type MaxDigitFloatType float; + +@test:Config {} +function testMaxDigitFloatPositive() { + MaxDigitFloatType|error validation = validate(12934.8065); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, 12934.8065); + } + + validation = validate(12.80); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, 12.80); + } + + validation = validate(0.0); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, 0.0); + } + + validation = validate(-12934.8065); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, -12934.8065); + } + + validation = validate(-12.80); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, -12.80); + } + + validation = validate(-0.0); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, -0.0); + } + + validation = validate(1234); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, 1234.0); + } + + validation = validate(1234.00000000); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, 1234.0); + } + + validation = validate(0.0001); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, 0.0001); + } + + validation = validate(-0.0001); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, -0.0001); + } + + validation = validate(0); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, 0.0); + } + + validation = validate(+1234.456); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, 1234.456); + } + + validation = validate(1.2345e3); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, 1234.5); + } + + validation = validate(1.2345e+3); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, 1234.5); + } + + validation = validate(101234.2e-3); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, 101.2342); + } + + validation = validate(.2345); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, 0.2345); + } +} + +@test:Config {} +function testMaxDigitFloatNegative() { + MaxDigitFloatType|error validation = validate(1234567.435); + if validation is error { + test:assertEquals(validation.message(), "Validation failed for '$:maxIntegerDigits' constraint(s)."); + } else { + test:assertFail("Expected error not found."); + } + + validation = validate(1234.12345); + if validation is error { + test:assertEquals(validation.message(), "Validation failed for '$:maxFractionDigits' constraint(s)."); + } else { + test:assertFail("Expected error not found."); + } + + validation = validate(1234567.12345); + if validation is error { + test:assertEquals(validation.message(), "Validation failed for '$:maxFractionDigits','$:maxIntegerDigits' constraint(s)."); + } else { + test:assertFail("Expected error not found."); + } + + validation = validate(0.00001); + if validation is error { + test:assertEquals(validation.message(), "Validation failed for '$:maxFractionDigits' constraint(s)."); + } else { + test:assertFail("Expected error not found."); + } + + validation = validate(-0.00001); + if validation is error { + test:assertEquals(validation.message(), "Validation failed for '$:maxFractionDigits' constraint(s)."); + } else { + test:assertFail("Expected error not found."); + } + + validation = validate(1.23e6); + if validation is error { + test:assertEquals(validation.message(), "Validation failed for '$:maxIntegerDigits' constraint(s)."); + } else { + test:assertFail("Expected error not found."); + } + + validation = validate(1.234e-6); + if validation is error { + test:assertEquals(validation.message(), "Validation failed for '$:maxFractionDigits' constraint(s)."); + } else { + test:assertFail("Expected error not found."); + } + + validation = validate(.123456); + if validation is error { + test:assertEquals(validation.message(), "Validation failed for '$:maxFractionDigits' constraint(s)."); + } else { + test:assertFail("Expected error not found."); + } +} + +type DigitFloat float; + +@Float { + maxIntegerDigits: { + value: 4, + message: "Integer digits should be less than or equal to 4" + } +} +type MaxIntegerDigitFloatRefType DigitFloat; + +@test:Config {} +function testMaxIntegerDigitsRefType() { + MaxIntegerDigitFloatRefType|error validation = validate(-1234.12345); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, -1234.12345); + } + + validation = validate(12.1234000); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, 12.1234); + } + + validation = validate(123456); + if validation is error { + test:assertEquals(validation.message(), "Integer digits should be less than or equal to 4."); + } else { + test:assertFail("Expected error not found."); + } + + validation = validate(1234456.001); + if validation is error { + test:assertEquals(validation.message(), "Integer digits should be less than or equal to 4."); + } else { + test:assertFail("Expected error not found."); + } +} + +@Float { + maxFractionDigits: { + value: 3, + message: "Fraction digits should be less than or equal to 3" + } +} +type MaxFractionDigitFloatRefType DigitFloat; + +@test:Config {} +function testMaxFractionDigitsRefType() { + MaxFractionDigitFloatRefType|error validation = validate(1234.12); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, 1234.12); + } + + validation = validate(0.001); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, 0.001); + } + + validation = validate(1234.1234); + if validation is error { + test:assertEquals(validation.message(), "Fraction digits should be less than or equal to 3."); + } else { + test:assertFail("Expected error not found."); + } +} diff --git a/ballerina/tests/int_constraint_on_type_test.bal b/ballerina/tests/int_constraint_on_type_test.bal index fd4b6f5..6abe25a 100644 --- a/ballerina/tests/int_constraint_on_type_test.bal +++ b/ballerina/tests/int_constraint_on_type_test.bal @@ -223,3 +223,146 @@ isolated function testIntConstraintOnTypeFailure2() { test:assertFail("Expected error not found."); } } + +@Int { + maxDigits: 10 +} +type MaxDigitIntType int; + +@test:Config {} +function testMaxDigitIntPositive() { + MaxDigitIntType|error validation = validate(123425); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, 123425); + } + + validation = validate(12.3e8); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, 1230000000); + } + + validation = validate(+1234256789); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, 1234256789); + } + + validation = validate(+1.99e9); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, 1990000000); + } + + validation = validate(-1234256789); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, -1234256789); + } + + validation = validate(-1.99e9); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, -1990000000); + } + + validation = validate(0); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, 0); + } +} + +@test:Config {} +function testMaxDigitIntNegative() { + MaxDigitIntType|error validation = validate(12342567890); + if validation is error { + test:assertEquals(validation.message(), "Validation failed for '$:maxDigits' constraint(s)."); + } else { + test:assertFail("Expected error not found."); + } + + validation = validate(12.3e10); + if validation is error { + test:assertEquals(validation.message(), "Validation failed for '$:maxDigits' constraint(s)."); + } else { + test:assertFail("Expected error not found."); + } + + validation = validate(+12342567890); + if validation is error { + test:assertEquals(validation.message(), "Validation failed for '$:maxDigits' constraint(s)."); + } else { + test:assertFail("Expected error not found."); + } + + validation = validate(+1.99e11); + if validation is error { + test:assertEquals(validation.message(), "Validation failed for '$:maxDigits' constraint(s)."); + } else { + test:assertFail("Expected error not found."); + } + + validation = validate(-12342567890); + if validation is error { + test:assertEquals(validation.message(), "Validation failed for '$:maxDigits' constraint(s)."); + } else { + test:assertFail("Expected error not found."); + } + + validation = validate(-1.99e11); + if validation is error { + test:assertEquals(validation.message(), "Validation failed for '$:maxDigits' constraint(s)."); + } else { + test:assertFail("Expected error not found."); + } +} + +type DigitInteger int; + +@Int { + maxDigits: { + value: 4, + message: "Max digit count exceeded." + } +} +type MaxDigitIntRefType DigitInteger; + +@test:Config {} +function testMaxDigitIntRefType() { + MaxDigitIntRefType|error validation = validate(1234); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, 1234); + } + + validation = validate(-123); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, -123); + } + + validation = validate(12345); + if validation is error { + test:assertEquals(validation.message(), "Max digit count exceeded."); + } else { + test:assertFail("Expected error not found."); + } + + validation = validate(-12000); + if validation is error { + test:assertEquals(validation.message(), "Max digit count exceeded."); + } else { + test:assertFail("Expected error not found."); + } +} diff --git a/ballerina/tests/number_constraint_on_type_test.bal b/ballerina/tests/number_constraint_on_type_test.bal index fbd752e..87ff587 100644 --- a/ballerina/tests/number_constraint_on_type_test.bal +++ b/ballerina/tests/number_constraint_on_type_test.bal @@ -633,3 +633,179 @@ isolated function testNumberConstraintOnUnionTypeFailure8() { test:assertFail("Expected error not found."); } } + +@Number { + maxIntegerDigits: 5, + maxFractionDigits: 4 +} +type MaxDigitNumberType decimal; + +@test:Config {} +function testMaxDigitNumberPositive() { + MaxDigitNumberType|error validation = validate(1.234d); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, 1.234d); + } + + validation = validate(-1.2345d); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, -1.2345d); + } + + validation = validate(+12345.1234000d); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, 12345.1234d); + } + + validation = validate(1.23e+2d); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, 123.0d); + } + + validation = validate(1.23e-2d); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, 0.0123d); + } + + validation = validate(0d); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, 0.0d); + } + + validation = validate(0.0d); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, 0.0d); + } + + validation = validate(-0.000000d); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, 0.0d); + } +} + +@test:Config {} +function testMaxDigitNumberNegative() { + MaxDigitNumberType|error validation = validate(1.234568d); + if validation is error { + test:assertEquals(validation.message(), "Validation failed for '$:maxFractionDigits' constraint(s)."); + } else { + test:assertFail("Expected error not found."); + } + + validation = validate(123456.1234d); + if validation is error { + test:assertEquals(validation.message(), "Validation failed for '$:maxIntegerDigits' constraint(s)."); + } else { + test:assertFail("Expected error not found."); + } + + validation = validate(1.2342698756788e+6d); + if validation is error { + test:assertEquals(validation.message(), "Validation failed for '$:maxFractionDigits','$:maxIntegerDigits' constraint(s)."); + } else { + test:assertFail("Expected error not found."); + } + + validation = validate(0.00001d); + if validation is error { + test:assertEquals(validation.message(), "Validation failed for '$:maxFractionDigits' constraint(s)."); + } else { + test:assertFail("Expected error not found."); + } + + validation = validate(100000.00000d); + if validation is error { + test:assertEquals(validation.message(), "Validation failed for '$:maxIntegerDigits' constraint(s)."); + } else { + test:assertFail("Expected error not found."); + } +} + +type DigitNumber decimal; + +@Number { + maxIntegerDigits: { + value: 4, + message: "Integer digits should be less than or equal to 4" + } +} +type MaxIntegerDigitNumberRefType DigitNumber; + +@test:Config {} +function testMaxIntegerDigitsNumberRefType() { + MaxIntegerDigitNumberRefType|error validation = validate(-1234.12345d); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, -1234.12345d); + } + + validation = validate(12.1234000d); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, 12.1234d); + } + + validation = validate(123456d); + if validation is error { + test:assertEquals(validation.message(), "Integer digits should be less than or equal to 4."); + } else { + test:assertFail("Expected error not found."); + } + + validation = validate(1234456.001d); + if validation is error { + test:assertEquals(validation.message(), "Integer digits should be less than or equal to 4."); + } else { + test:assertFail("Expected error not found."); + } +} + +@Number { + maxFractionDigits: { + value: 3, + message: "Fraction digits should be less than or equal to 3" + } +} +type MaxFractionDigitNumberRefType DigitNumber; + +@test:Config {} +function testMaxFractionDigitsNumberRefType() { + MaxFractionDigitNumberRefType|error validation = validate(1234.12d); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, 1234.12d); + } + + validation = validate(0.001d); + if validation is error { + test:assertFail("Unexpected error found."); + } else { + test:assertEquals(validation, 0.001d); + } + + validation = validate(1234.1234d); + if validation is error { + test:assertEquals(validation.message(), "Fraction digits should be less than or equal to 3."); + } else { + test:assertFail("Expected error not found."); + } +} diff --git a/changelog.md b/changelog.md index 8cf4eb4..fcdf2c2 100644 --- a/changelog.md +++ b/changelog.md @@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Added +- [Add support to overwrite default error message for constraint violations](https://github.com/ballerina-platform/ballerina-standard-library/issues/3211) +- [Add constraint support for number of digits](https://github.com/ballerina-platform/ballerina-standard-library/issues/5081) + ### Changed - [Make some of the Java classes proper utility classes](https://github.com/ballerina-platform/ballerina-standard-library/issues/4929) diff --git a/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/constraint/compiler/CompilerPluginTest.java b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/constraint/compiler/CompilerPluginTest.java index fcc9b75..cd6aa37 100644 --- a/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/constraint/compiler/CompilerPluginTest.java +++ b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/constraint/compiler/CompilerPluginTest.java @@ -30,6 +30,12 @@ import static io.ballerina.stdlib.constraint.compiler.CompilerPluginTestConstants.ANNOTATION_TAG_INT; import static io.ballerina.stdlib.constraint.compiler.CompilerPluginTestConstants.ANNOTATION_TAG_NUMBER; import static io.ballerina.stdlib.constraint.compiler.CompilerPluginTestConstants.ANNOTATION_TAG_STRING; +import static io.ballerina.stdlib.constraint.compiler.CompilerPluginTestConstants.LENGTH_CONSTRAINT; +import static io.ballerina.stdlib.constraint.compiler.CompilerPluginTestConstants.MAX_DIGITS; +import static io.ballerina.stdlib.constraint.compiler.CompilerPluginTestConstants.MAX_FRACTION_DIGITS; +import static io.ballerina.stdlib.constraint.compiler.CompilerPluginTestConstants.MAX_INTEGER_DIGITS; +import static io.ballerina.stdlib.constraint.compiler.CompilerPluginTestConstants.MAX_LENGTH_CONSTRAINT; +import static io.ballerina.stdlib.constraint.compiler.CompilerPluginTestConstants.MIN_LENGTH_CONSTRAINT; import static io.ballerina.stdlib.constraint.compiler.CompilerPluginTestConstants.TYPE_ANYDATA_ARRAY; import static io.ballerina.stdlib.constraint.compiler.CompilerPluginTestConstants.TYPE_BOOLEAN; import static io.ballerina.stdlib.constraint.compiler.CompilerPluginTestConstants.TYPE_DECIMAL; @@ -285,9 +291,12 @@ public void testStringAnnotationTagConstraintsValidity() { DiagnosticResult diagnosticResult = compilation.diagnosticResult(); int expectedErrorCount = 6; Assert.assertEquals(diagnosticResult.errorCount(), expectedErrorCount); - for (int i = 0; i < expectedErrorCount; i++) { - CompilerPluginTestUtils.assertError104(diagnosticResult, i, ANNOTATION_TAG_STRING, TYPE_STRING); - } + CompilerPluginTestUtils.assertError104(diagnosticResult, 0, ANNOTATION_TAG_STRING, LENGTH_CONSTRAINT); + CompilerPluginTestUtils.assertError104(diagnosticResult, 1, ANNOTATION_TAG_STRING, MIN_LENGTH_CONSTRAINT); + CompilerPluginTestUtils.assertError104(diagnosticResult, 2, ANNOTATION_TAG_STRING, MAX_LENGTH_CONSTRAINT); + CompilerPluginTestUtils.assertError104(diagnosticResult, 3, ANNOTATION_TAG_STRING, LENGTH_CONSTRAINT); + CompilerPluginTestUtils.assertError104(diagnosticResult, 4, ANNOTATION_TAG_STRING, MIN_LENGTH_CONSTRAINT); + CompilerPluginTestUtils.assertError104(diagnosticResult, 5, ANNOTATION_TAG_STRING, MAX_LENGTH_CONSTRAINT); } @Test @@ -297,9 +306,12 @@ public void testArrayAnnotationTagConstraintsValidity() { DiagnosticResult diagnosticResult = compilation.diagnosticResult(); int expectedErrorCount = 6; Assert.assertEquals(diagnosticResult.errorCount(), expectedErrorCount); - for (int i = 0; i < expectedErrorCount; i++) { - CompilerPluginTestUtils.assertError104(diagnosticResult, i, ANNOTATION_TAG_ARRAY, TYPE_ANYDATA_ARRAY); - } + CompilerPluginTestUtils.assertError104(diagnosticResult, 0, ANNOTATION_TAG_ARRAY, LENGTH_CONSTRAINT); + CompilerPluginTestUtils.assertError104(diagnosticResult, 1, ANNOTATION_TAG_ARRAY, MIN_LENGTH_CONSTRAINT); + CompilerPluginTestUtils.assertError104(diagnosticResult, 2, ANNOTATION_TAG_ARRAY, MAX_LENGTH_CONSTRAINT); + CompilerPluginTestUtils.assertError104(diagnosticResult, 3, ANNOTATION_TAG_ARRAY, LENGTH_CONSTRAINT); + CompilerPluginTestUtils.assertError104(diagnosticResult, 4, ANNOTATION_TAG_ARRAY, MIN_LENGTH_CONSTRAINT); + CompilerPluginTestUtils.assertError104(diagnosticResult, 5, ANNOTATION_TAG_ARRAY, MAX_LENGTH_CONSTRAINT); } @Test @@ -315,4 +327,18 @@ public void testDateAnnotationTagConstraints() { CompilerPluginTestUtils.assertError101(diagnosticResult, 3, ANNOTATION_TAG_DATE, "CustomRecord"); CompilerPluginTestUtils.assertError101(diagnosticResult, 4, ANNOTATION_TAG_DATE, TYPE_RECORD); } + + @Test + public void testDigitAnnotationTagConstraintsValidity() { + Package currentPackage = CompilerPluginTestUtils.loadPackage("sample_package_21"); + PackageCompilation compilation = currentPackage.getCompilation(); + DiagnosticResult diagnosticResult = compilation.diagnosticResult(); + int expectedErrorCount = 5; + Assert.assertEquals(diagnosticResult.errorCount(), expectedErrorCount); + CompilerPluginTestUtils.assertError104(diagnosticResult, 0, ANNOTATION_TAG_INT, MAX_DIGITS); + CompilerPluginTestUtils.assertError104(diagnosticResult, 1, ANNOTATION_TAG_FLOAT, MAX_INTEGER_DIGITS); + CompilerPluginTestUtils.assertError104(diagnosticResult, 2, ANNOTATION_TAG_FLOAT, MAX_FRACTION_DIGITS); + CompilerPluginTestUtils.assertError104(diagnosticResult, 3, ANNOTATION_TAG_NUMBER, MAX_INTEGER_DIGITS); + CompilerPluginTestUtils.assertError104(diagnosticResult, 4, ANNOTATION_TAG_NUMBER, MAX_FRACTION_DIGITS); + } } diff --git a/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/constraint/compiler/CompilerPluginTestConstants.java b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/constraint/compiler/CompilerPluginTestConstants.java index 6b56048..801a3fa 100644 --- a/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/constraint/compiler/CompilerPluginTestConstants.java +++ b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/constraint/compiler/CompilerPluginTestConstants.java @@ -43,4 +43,10 @@ private CompilerPluginTestConstants() {} static final String TYPE_MAP_OF_ANYDATA = "map"; static final String TYPE_TABLE_OF_ANYDATA = "table>"; static final String TYPE_RECORD = "record"; + static final String LENGTH_CONSTRAINT = "length"; + static final String MIN_LENGTH_CONSTRAINT = "minLength"; + static final String MAX_LENGTH_CONSTRAINT = "maxLength"; + static final String MAX_DIGITS = "maxDigits"; + static final String MAX_INTEGER_DIGITS = "maxIntegerDigits"; + static final String MAX_FRACTION_DIGITS = "maxFractionDigits"; } diff --git a/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/constraint/compiler/CompilerPluginTestUtils.java b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/constraint/compiler/CompilerPluginTestUtils.java index 0c509a6..8c0ce9b 100644 --- a/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/constraint/compiler/CompilerPluginTestUtils.java +++ b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/constraint/compiler/CompilerPluginTestUtils.java @@ -76,10 +76,10 @@ static void assertError103(DiagnosticResult diagnosticResult, int index, String Assert.assertEquals(diagnostic.diagnosticInfo().code(), CONSTRAINT_103.getCode()); } - static void assertError104(DiagnosticResult diagnosticResult, int index, String annotationTag, String fieldType) { + static void assertError104(DiagnosticResult diagnosticResult, int index, String annotationTag, String fieldName) { Diagnostic diagnostic = (Diagnostic) diagnosticResult.errors().toArray()[index]; Assert.assertEquals(diagnostic.diagnosticInfo().messageFormat(), - String.format(CONSTRAINT_104.getMessage(), annotationTag, fieldType)); + String.format(CONSTRAINT_104.getMessage(), fieldName, annotationTag)); Assert.assertEquals(diagnostic.diagnosticInfo().code(), CONSTRAINT_104.getCode()); } } diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_21/Ballerina.toml b/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_21/Ballerina.toml new file mode 100644 index 0000000..2a548b7 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_21/Ballerina.toml @@ -0,0 +1,4 @@ +[package] +org = "constraint_test" +name = "sample_21" +version = "0.1.0" diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_21/sample.bal b/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_21/sample.bal new file mode 100644 index 0000000..e294ebe --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/sample_package_21/sample.bal @@ -0,0 +1,42 @@ +// Copyright (c) 2023 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. +// +// 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; + +@constraint:Int { + maxDigits: -1 +} +type Integer int; + +@constraint:Float { + maxIntegerDigits: -4, + maxFractionDigits: { + value: 0, + message: "maxFractionDigits should be a positive integer" + } +} +type Float float; + +type Record record { + @constraint:Number { + maxIntegerDigits: { + value: -2, + message: "maxIntegerDigits should be a positive integer" + } + } + int i; + @constraint:Number {maxFractionDigits: 0} + decimal d; +}; diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/constraint/compiler/Constants.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/constraint/compiler/Constants.java index b450ac6..ea90e26 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/constraint/compiler/Constants.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/constraint/compiler/Constants.java @@ -54,5 +54,8 @@ private Constants() {} public static final String CONSTRAINT_LENGTH = "length"; public static final String CONSTRAINT_MIN_LENGTH = "minLength"; public static final String CONSTRAINT_MAX_LENGTH = "maxLength"; + public static final String CONSTRAINT_MAX_DIGITS = "maxDigits"; + public static final String CONSTRAINT_MAX_INTEGER_DIGITS = "maxIntegerDigits"; + public static final String CONSTRAINT_MAX_FRACTION_DIGITS = "maxFractionDigits"; public static final String CONSTRAINT_VALUE = "value"; } diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/constraint/compiler/ConstraintCompatibilityMatrix.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/constraint/compiler/ConstraintCompatibilityMatrix.java index 0f967a0..e889aaa 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/constraint/compiler/ConstraintCompatibilityMatrix.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/constraint/compiler/ConstraintCompatibilityMatrix.java @@ -27,6 +27,7 @@ import io.ballerina.stdlib.constraint.compiler.annotation.tag.AnnotationTagInt; import io.ballerina.stdlib.constraint.compiler.annotation.tag.AnnotationTagNumber; import io.ballerina.stdlib.constraint.compiler.annotation.tag.AnnotationTagString; +import io.ballerina.stdlib.constraint.compiler.annotation.tag.DigitsConstrainedAnnotationTag; import io.ballerina.stdlib.constraint.compiler.annotation.tag.LengthConstrainedAnnotationTag; import java.util.ArrayList; @@ -66,7 +67,11 @@ boolean isAnnotationConstraintsCompatible(String annotationTag, ArrayList value = annotationNode.annotValue(); if (value.isPresent()) { SeparatedNodeList constraints = value.get().fields(); @@ -118,20 +118,18 @@ private static void checkAnnotationConstraintsValidity(SyntaxNodeAnalysisContext if (valueExpr.isPresent()) { if (valueExpr.get() instanceof BasicLiteralNode || valueExpr.get() instanceof UnaryExpressionNode) { - getValueFromSimpleValueExpressionNode(ctx, annotationNode, annotationTag, fieldType, + getValueFromSimpleValueExpressionNode(ctx, annotationTag, node, valueExpr.get()); } else if (valueExpr.get() instanceof MappingConstructorExpressionNode) { - getValueFromMappingConstructor(ctx, annotationNode, annotationTag, fieldType, - node, valueExpr.get()); + getValueFromMappingConstructor(ctx, annotationTag, node, valueExpr.get()); } } } } } - private static void getValueFromMappingConstructor(SyntaxNodeAnalysisContext ctx, AnnotationNode annotationNode, - String annotationTag, String fieldType, SpecificFieldNode node, - ExpressionNode valueExpr) { + private static void getValueFromMappingConstructor(SyntaxNodeAnalysisContext ctx, String annotationTag, + SpecificFieldNode node, ExpressionNode valueExpr) { MappingConstructorExpressionNode expressionNode = (MappingConstructorExpressionNode) valueExpr; SeparatedNodeList fields = expressionNode.fields(); for (MappingFieldNode field : fields) { @@ -139,21 +137,19 @@ private static void getValueFromMappingConstructor(SyntaxNodeAnalysisContext ctx if (fieldNode.fieldName().toString().trim().equals(CONSTRAINT_VALUE)) { Optional fieldExpr = fieldNode.valueExpr(); fieldExpr.ifPresent(expressionNode1 -> getValueFromSimpleValueExpressionNode(ctx, - annotationNode, annotationTag, fieldType, node, expressionNode1)); + annotationTag, node, expressionNode1)); } } } - private static void getValueFromSimpleValueExpressionNode(SyntaxNodeAnalysisContext ctx, - AnnotationNode annotationNode, String annotationTag, - String fieldType, SpecificFieldNode node, - ExpressionNode valueExpr) { + private static void getValueFromSimpleValueExpressionNode(SyntaxNodeAnalysisContext ctx, String annotationTag, + SpecificFieldNode node, ExpressionNode valueExpr) { String constraintValue = valueExpr.toString().trim() .replaceAll(SYMBOL_NEW_LINE, EMPTY) .replaceAll(SYMBOL_DECIMAL, EMPTY); String constraintField = node.fieldName().toString().trim(); if (!matrix.isAnnotationConstraintsValid(annotationTag, constraintField, constraintValue)) { - reportConstraintsInvalidity(ctx, annotationTag, fieldType, annotationNode.location()); + reportConstraintsInvalidity(ctx, annotationTag, constraintField, node.location()); } } @@ -179,9 +175,9 @@ private static void reportConstraintsIncompatibility(SyntaxNodeAnalysisContext c } private static void reportConstraintsInvalidity(SyntaxNodeAnalysisContext ctx, String annotationTag, - String fieldType, NodeLocation location) { + String field, NodeLocation location) { DiagnosticInfo diagnosticInfo = new DiagnosticInfo(CONSTRAINT_104.getCode(), - String.format(CONSTRAINT_104.getMessage(), annotationTag, fieldType), CONSTRAINT_104.getSeverity()); + String.format(CONSTRAINT_104.getMessage(), field, annotationTag), CONSTRAINT_104.getSeverity()); ctx.reportDiagnostic(DiagnosticFactory.createDiagnostic(diagnosticInfo, location)); } } diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/constraint/compiler/ConstraintDiagnosticCodes.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/constraint/compiler/ConstraintDiagnosticCodes.java index 1f54e94..27b81e1 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/constraint/compiler/ConstraintDiagnosticCodes.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/constraint/compiler/ConstraintDiagnosticCodes.java @@ -30,7 +30,7 @@ public enum ConstraintDiagnosticCodes { CONSTRAINT_101("CONSTRAINT_101", "invalid `@constraint:%s` annotation on `%s` type", ERROR), CONSTRAINT_102("CONSTRAINT_102", "no constraints found on `@constraint:%s` annotation on `%s` type", ERROR), CONSTRAINT_103("CONSTRAINT_103", "incompatible constraints on `@constraint:%s` annotation on `%s` type", ERROR), - CONSTRAINT_104("CONSTRAINT_104", "invalid constraint value on `@constraint:%s` annotation on `%s` type", ERROR); + CONSTRAINT_104("CONSTRAINT_104", "invalid constraint value for `%s` on `@constraint:%s` annotation", ERROR); private final String code; private final String message; diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/constraint/compiler/annotation/tag/AnnotationTagFloat.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/constraint/compiler/annotation/tag/AnnotationTagFloat.java index 64f86bb..f5987bc 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/constraint/compiler/annotation/tag/AnnotationTagFloat.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/constraint/compiler/annotation/tag/AnnotationTagFloat.java @@ -24,7 +24,7 @@ /** * The class to represent the `@constraint:Float` annotation tag. */ -public class AnnotationTagFloat implements ValueConstrainedAnnotationTag { +public class AnnotationTagFloat implements ValueConstrainedAnnotationTag, DigitsConstrainedAnnotationTag { @Override public boolean isCompatibleFieldType(SyntaxNodeAnalysisContext ctx, TypeSymbol fieldTypeSymbol) { diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/constraint/compiler/annotation/tag/AnnotationTagInt.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/constraint/compiler/annotation/tag/AnnotationTagInt.java index e140712..d294c1e 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/constraint/compiler/annotation/tag/AnnotationTagInt.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/constraint/compiler/annotation/tag/AnnotationTagInt.java @@ -24,7 +24,7 @@ /** * The class to represent the `@constraint:Int` annotation tag. */ -public class AnnotationTagInt implements ValueConstrainedAnnotationTag { +public class AnnotationTagInt implements ValueConstrainedAnnotationTag, DigitsConstrainedAnnotationTag { @Override public boolean isCompatibleFieldType(SyntaxNodeAnalysisContext ctx, TypeSymbol fieldTypeSymbol) { diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/constraint/compiler/annotation/tag/AnnotationTagNumber.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/constraint/compiler/annotation/tag/AnnotationTagNumber.java index 506f477..e5a3c18 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/constraint/compiler/annotation/tag/AnnotationTagNumber.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/constraint/compiler/annotation/tag/AnnotationTagNumber.java @@ -24,7 +24,7 @@ /** * The class to represent the `@constraint:Number` annotation tag. */ -public class AnnotationTagNumber implements ValueConstrainedAnnotationTag { +public class AnnotationTagNumber implements ValueConstrainedAnnotationTag, DigitsConstrainedAnnotationTag { @Override public boolean isCompatibleFieldType(SyntaxNodeAnalysisContext ctx, TypeSymbol fieldTypeSymbol) { diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/constraint/compiler/annotation/tag/DigitsConstrainedAnnotationTag.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/constraint/compiler/annotation/tag/DigitsConstrainedAnnotationTag.java new file mode 100644 index 0000000..c6f9ac6 --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/constraint/compiler/annotation/tag/DigitsConstrainedAnnotationTag.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023, WSO2 LLC. (http://www.wso2.com) All Rights Reserved. + * + * 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.stdlib.constraint.compiler.annotation.tag; + +import static io.ballerina.stdlib.constraint.compiler.Constants.CONSTRAINT_MAX_DIGITS; +import static io.ballerina.stdlib.constraint.compiler.Constants.CONSTRAINT_MAX_FRACTION_DIGITS; +import static io.ballerina.stdlib.constraint.compiler.Constants.CONSTRAINT_MAX_INTEGER_DIGITS; + +public interface DigitsConstrainedAnnotationTag extends AnnotationTag { + + default boolean isValidConstraintValue(String constraintField, String constraintValue) { + return !CONSTRAINT_MAX_DIGITS.equals(constraintField) && + !CONSTRAINT_MAX_INTEGER_DIGITS.equals(constraintField) && + !CONSTRAINT_MAX_FRACTION_DIGITS.equals(constraintField) || Double.parseDouble(constraintValue) > 0; + } +} diff --git a/docs/spec/spec.md b/docs/spec/spec.md index 13fb7de..7381e53 100644 --- a/docs/spec/spec.md +++ b/docs/spec/spec.md @@ -3,7 +3,7 @@ _Owners_: @TharmiganK @shafreenAnfar @chamil321 _Reviewers_: @shafreenAnfar @chamil321 _Created_: 2022/08/09 -_Updated_: 2023/05/18 +_Updated_: 2023/10/26 _Edition_: Swan Lake ## Introduction @@ -31,6 +31,7 @@ specification is considered a bug. * 2.3. [Constraint annotation on array types](#23-constraint-annotation-on-array-types) * 2.4. [Constraint annotation on `Date` record types](#24-constraint-annotation-on-date-record-types) 3. [`validate` function](#3-validate-function) +4. [Custom error messages](#4-custom-error-messages) ## 1. Overview Validating user input is a common requirement in most applications. This can prevent user entry errors before the app @@ -100,6 +101,7 @@ public type IntConstraints record {| int maxValue?; int minValueExclusive?; int maxValueExclusive?; + int maxDigits?; |}; // Float constraints which applies only when the value is `float`. @@ -109,6 +111,8 @@ public type FloatConstraints record {| float maxValue?; float minValueExclusive?; float maxValueExclusive?; + int maxIntegerDigits?; + int maxFractionDigits?; |}; // Number constraints which applies when the value is `int|float|decimal`. @@ -118,6 +122,8 @@ public type NumberConstraints record {| decimal maxValue?; decimal minValueExclusive?; decimal maxValueExclusive?; + int maxIntegerDigits?; + int maxFractionDigits?; |}; ``` @@ -129,6 +135,9 @@ All the supported constraints on number types are illustrated in the following t | maxValue | v <= c | | minValueExclusive | v > c | | maxValueExclusive | v < c | +|maxDigits | Number of digits in v <= c | +|maxIntegerDigits | Number of integer digits in v <= c | +|maxFractionDigits | Number of fraction digits in v <= c | When defining constraints on number types, either `minValue` or `minValueExclusive` can be present. Similarly, either `maxValue` or `maxValueExclusive` can be present. @@ -337,3 +346,59 @@ public function func1() returns error? { } ``` +## 4. Custom error messages + +The Constraint library provides default error messages for constraint violations. The default error message include the JSON +path of the violated constraint with the constraint name. When there are multiple constraint failures the failed constraints +are separated by a comma. The following is an example of a default error message. + +```ballerina + Employee employee = { + name: "a", // minimum length is 4 + age: 10, // minimum value is 18 + interns: ["intern1", "intern2", "intern3", "intern4"], // maximum length is 3 + dob: { + year: 2220, + month: 10, + day: 2 + } // should be a past date + } + + Employee|error validation = constraint:validate(employee); + + Error message : Validation failed for '$.name:minLengeth','$.age:minValue','$.interns:maxLength','$.dob:pastDate' constraint(s). +``` + +The Constraint library allows the developer to provide custom error messages for each constraint. This can be done by defining +the constraint as a record with `value` and `message` fields. The `value` field should be the constraint value and the `message` +field should be the custom error message. The following is an example of defining a custom error message: + +```ballerina +@constraint:String { + minLength : { + value : 5, + message : "UserName should have atleast 5 characters" + }, + maxLength : { + value : 12, + message : "UserName can have atmost 12 characters" + }, + pattern : { + value : re `^[a-zA-Z0-9]+$`, + message : "Only alpha numeric characters are allowed in UserName" + } +} +type UserName string; +``` + +In the case of `Date` constraints, the `message` field can be used as an annotation field to provide a custom error message for invalid dates. +```ballerina +@constraint:Date { + option : { + value : constraint:PAST, + message : "Date of Birth should be in the past" // Only returned when the past date constraint is violated + }, + message : "Invalid date found for Date of Birth" +} +type DateOfBirth time:Date; +``` diff --git a/gradle.properties b/gradle.properties index 1cba03f..41dac2d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ org.gradle.caching=true group=io.ballerina.stdlib -version=1.4.1-SNAPSHOT +version=1.5.0-SNAPSHOT puppycrawlCheckstyleVersion=10.12.0 slf4jVersion=1.7.30 testngVersion=7.6.1 diff --git a/native/src/main/java/io/ballerina/stdlib/constraint/Constants.java b/native/src/main/java/io/ballerina/stdlib/constraint/Constants.java index dbfad47..cb87c22 100644 --- a/native/src/main/java/io/ballerina/stdlib/constraint/Constants.java +++ b/native/src/main/java/io/ballerina/stdlib/constraint/Constants.java @@ -30,6 +30,7 @@ private Constants() {} public static final String PREFIX_RECORD_FIELD = "$field$"; public static final String ANNOTATION_RECORD_REGEX = "^ballerina/constraint:[0-9]+:.+"; + public static final String SCIENTIFIC_NOTATION_REGEX = "^(-?[0-9]+)\\.([0-9]+)[Ee]([+-]?[0-9]+)$"; public static final String SYMBOL_DOLLAR_SIGN = "$"; public static final String SYMBOL_DOT = "."; @@ -38,6 +39,8 @@ private Constants() {} public static final String SYMBOL_OPEN_SQUARE_BRACKET = "["; public static final String SYMBOL_CLOSE_SQUARE_BRACKET = "]"; public static final String SYMBOL_SEPARATOR = ":"; + public static final String DOT_SEPARATOR = "\\."; + public static final String ZERO_STRING = "0"; public static final String ANNOTATION_TAG_INT = "Int"; public static final String ANNOTATION_TAG_FLOAT = "Float"; @@ -50,6 +53,9 @@ private Constants() {} public static final String CONSTRAINT_MAX_VALUE = "maxValue"; public static final String CONSTRAINT_MIN_VALUE_EXCLUSIVE = "minValueExclusive"; public static final String CONSTRAINT_MAX_VALUE_EXCLUSIVE = "maxValueExclusive"; + public static final String CONSTRAINT_MAX_DIGITS = "maxDigits"; + public static final String CONSTRAINT_MAX_INTEGER_DIGITS = "maxIntegerDigits"; + public static final String CONSTRAINT_MAX_FRACTION_DIGITS = "maxFractionDigits"; public static final String CONSTRAINT_LENGTH = "length"; public static final String CONSTRAINT_MIN_LENGTH = "minLength"; public static final String CONSTRAINT_MAX_LENGTH = "maxLength"; diff --git a/native/src/main/java/io/ballerina/stdlib/constraint/validators/FloatConstraintValidator.java b/native/src/main/java/io/ballerina/stdlib/constraint/validators/FloatConstraintValidator.java index e5d7793..a54f863 100644 --- a/native/src/main/java/io/ballerina/stdlib/constraint/validators/FloatConstraintValidator.java +++ b/native/src/main/java/io/ballerina/stdlib/constraint/validators/FloatConstraintValidator.java @@ -18,29 +18,17 @@ package io.ballerina.stdlib.constraint.validators; -import io.ballerina.runtime.api.values.BMap; -import io.ballerina.runtime.api.values.BString; import io.ballerina.stdlib.constraint.ConstraintErrorInfo; -import io.ballerina.stdlib.constraint.validators.interfaces.ValueValidator; import java.util.List; -import java.util.Map; /** * Extern functions for validating float constraints `@constraint:Float` of Ballerina. */ -public class FloatConstraintValidator implements ValueValidator { - - private final List failedConstraintsInfo; +public class FloatConstraintValidator extends NumberConstraintValidator { public FloatConstraintValidator(List failedConstraintsInfo) { - this.failedConstraintsInfo = failedConstraintsInfo; - } - - public void validate(BMap constraints, Object fieldValue, String path, boolean isMemberValue) { - for (Map.Entry constraint : constraints.entrySet()) { - validate(constraint, fieldValue, isMemberValue, failedConstraintsInfo, path); - } + super(failedConstraintsInfo); } @Override diff --git a/native/src/main/java/io/ballerina/stdlib/constraint/validators/IntConstraintValidator.java b/native/src/main/java/io/ballerina/stdlib/constraint/validators/IntConstraintValidator.java index 12cb226..cc8d3a5 100644 --- a/native/src/main/java/io/ballerina/stdlib/constraint/validators/IntConstraintValidator.java +++ b/native/src/main/java/io/ballerina/stdlib/constraint/validators/IntConstraintValidator.java @@ -18,29 +18,17 @@ package io.ballerina.stdlib.constraint.validators; -import io.ballerina.runtime.api.values.BMap; -import io.ballerina.runtime.api.values.BString; import io.ballerina.stdlib.constraint.ConstraintErrorInfo; -import io.ballerina.stdlib.constraint.validators.interfaces.ValueValidator; import java.util.List; -import java.util.Map; /** * Extern functions for validating int constraints `@constraint:Int` of Ballerina. */ -public class IntConstraintValidator implements ValueValidator { - - private final List failedConstraintsInfo; +public class IntConstraintValidator extends NumberConstraintValidator { public IntConstraintValidator(List failedConstraintsInfo) { - this.failedConstraintsInfo = failedConstraintsInfo; - } - - public void validate(BMap constraints, Number fieldValue, String path, boolean isMemberValue) { - for (Map.Entry constraint : constraints.entrySet()) { - validate(constraint, fieldValue, isMemberValue, failedConstraintsInfo, path); - } + super(failedConstraintsInfo); } @Override @@ -63,4 +51,22 @@ public boolean validateMinValueExclusive(Object fieldValue, Object constraintVal public boolean validateMaxValueExclusive(Object fieldValue, Object constraintValue) { return ((Number) fieldValue).longValue() < (Long) constraintValue; } + + @Override + public boolean validateMaxDigits(Object fieldValue, Object constraintValue) { + long fieldNumericValue = ((Number) fieldValue).longValue(); + String numericString = Long.toString(fieldNumericValue); + int length = fieldNumericValue < 0 ? numericString.length() - 1 : numericString.length(); + return length <= (Long) constraintValue; + } + + @Override + public boolean validateMaxIntegerDigits(Object fieldValue, Object constraintValue) { + return true; + } + + @Override + public boolean validateMaxFractionDigits(Object fieldValue, Object constraintValue) { + return true; + } } diff --git a/native/src/main/java/io/ballerina/stdlib/constraint/validators/NumberConstraintValidator.java b/native/src/main/java/io/ballerina/stdlib/constraint/validators/NumberConstraintValidator.java index 3821ad6..8477556 100644 --- a/native/src/main/java/io/ballerina/stdlib/constraint/validators/NumberConstraintValidator.java +++ b/native/src/main/java/io/ballerina/stdlib/constraint/validators/NumberConstraintValidator.java @@ -22,17 +22,20 @@ import io.ballerina.runtime.api.values.BMap; import io.ballerina.runtime.api.values.BString; import io.ballerina.stdlib.constraint.ConstraintErrorInfo; +import io.ballerina.stdlib.constraint.validators.interfaces.DigitsValidator; import io.ballerina.stdlib.constraint.validators.interfaces.ValueValidator; import java.util.List; import java.util.Map; +import java.util.Objects; /** * Extern functions for validating number constraints `@constraint:Number` of Ballerina. */ -public class NumberConstraintValidator implements ValueValidator { +public class NumberConstraintValidator implements ValueValidator, DigitsValidator { private final List failedConstraintsInfo; + private DigitParts digitParts; public NumberConstraintValidator(List failedConstraintsInfo) { this.failedConstraintsInfo = failedConstraintsInfo; @@ -40,10 +43,18 @@ public NumberConstraintValidator(List failedConstraintsInfo public void validate(BMap constraints, Number fieldValue, String path, boolean isMemberValue) { for (Map.Entry constraint : constraints.entrySet()) { + DigitsValidator.super.checkDigitsConstraintValue(constraint, path); validate(constraint, fieldValue, isMemberValue, failedConstraintsInfo, path); } } + @Override + public void validate(Map.Entry constraint, Object fieldValue, boolean isMemberValue, + List failedConstraintsInfo, String path) { + DigitsValidator.super.validate(constraint, fieldValue, isMemberValue, failedConstraintsInfo, path); + ValueValidator.super.validate(constraint, fieldValue, isMemberValue, failedConstraintsInfo, path); + } + @Override public boolean validateMinValue(Object fieldValue, Object constraintValue) { return ((Number) fieldValue).doubleValue() >= ((BDecimal) constraintValue).value().doubleValue(); @@ -63,4 +74,27 @@ public boolean validateMinValueExclusive(Object fieldValue, Object constraintVal public boolean validateMaxValueExclusive(Object fieldValue, Object constraintValue) { return ((Number) fieldValue).doubleValue() < ((BDecimal) constraintValue).value().doubleValue(); } + + @Override + public boolean validateMaxDigits(Object fieldValue, Object constraintValue) { + return true; + } + + @Override + public boolean validateMaxIntegerDigits(Object fieldValue, Object constraintValue) { + double fieldNumericValue = ((Number) fieldValue).doubleValue(); + if (Objects.isNull(digitParts)) { + digitParts = getDigitPartsFromDouble(fieldNumericValue); + } + return digitParts.integerDigits() <= (Long) constraintValue; + } + + @Override + public boolean validateMaxFractionDigits(Object fieldValue, Object constraintValue) { + double fieldNumericValue = ((Number) fieldValue).doubleValue(); + if (Objects.isNull(digitParts)) { + digitParts = getDigitPartsFromDouble(fieldNumericValue); + } + return digitParts.fractionDigits() <= (Long) constraintValue; + } } diff --git a/native/src/main/java/io/ballerina/stdlib/constraint/validators/interfaces/DigitsValidator.java b/native/src/main/java/io/ballerina/stdlib/constraint/validators/interfaces/DigitsValidator.java new file mode 100644 index 0000000..e707b61 --- /dev/null +++ b/native/src/main/java/io/ballerina/stdlib/constraint/validators/interfaces/DigitsValidator.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2023, WSO2 LLC. (http://www.wso2.com) All Rights Reserved. + * + * 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.stdlib.constraint.validators.interfaces; + +import io.ballerina.runtime.api.values.BMap; +import io.ballerina.runtime.api.values.BString; +import io.ballerina.stdlib.constraint.ConstraintErrorInfo; +import io.ballerina.stdlib.constraint.InternalValidationException; + +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static io.ballerina.stdlib.constraint.Constants.CONSTRAINT_MAX_DIGITS; +import static io.ballerina.stdlib.constraint.Constants.CONSTRAINT_MAX_FRACTION_DIGITS; +import static io.ballerina.stdlib.constraint.Constants.CONSTRAINT_MAX_INTEGER_DIGITS; +import static io.ballerina.stdlib.constraint.Constants.DOT_SEPARATOR; +import static io.ballerina.stdlib.constraint.Constants.MESSAGE; +import static io.ballerina.stdlib.constraint.Constants.SCIENTIFIC_NOTATION_REGEX; +import static io.ballerina.stdlib.constraint.Constants.SYMBOL_SEPARATOR; +import static io.ballerina.stdlib.constraint.Constants.VALUE; +import static io.ballerina.stdlib.constraint.Constants.ZERO_STRING; + +public interface DigitsValidator { + + default void validate(Map.Entry constraint, Object fieldValue, boolean isMemberValue, + List failedConstraints, String path) { + Object constraintValue = constraint.getValue(); + String message = null; + if (constraintValue instanceof BMap) { + message = ((BMap) constraintValue).getStringValue(MESSAGE).getValue(); + constraintValue = ((BMap) constraintValue).get(VALUE); + } + switch (constraint.getKey().getValue()) { + case CONSTRAINT_MAX_DIGITS: + if (!validateMaxDigits(fieldValue, constraintValue)) { + failedConstraints.add(new ConstraintErrorInfo(path, message, CONSTRAINT_MAX_DIGITS, isMemberValue)); + } + break; + case CONSTRAINT_MAX_INTEGER_DIGITS: + if (!validateMaxIntegerDigits(fieldValue, constraintValue)) { + failedConstraints.add(new ConstraintErrorInfo(path, message, CONSTRAINT_MAX_INTEGER_DIGITS, + isMemberValue)); + } + break; + case CONSTRAINT_MAX_FRACTION_DIGITS: + if (!validateMaxFractionDigits(fieldValue, constraintValue)) { + failedConstraints.add(new ConstraintErrorInfo(path, message, CONSTRAINT_MAX_FRACTION_DIGITS, + isMemberValue)); + } + break; + default: + break; + } + } + + default void checkDigitsConstraintValue(Map.Entry constraint, String path) { + Object constraintValue = constraint.getValue() instanceof BMap ? ((BMap) constraint.getValue()).get(VALUE) : + constraint.getValue(); + switch (constraint.getKey().getValue()) { + case CONSTRAINT_MAX_DIGITS: + case CONSTRAINT_MAX_INTEGER_DIGITS: + case CONSTRAINT_MAX_FRACTION_DIGITS: + long constraintLongValue = (long) constraintValue; + String constraintField = constraint.getKey().getValue(); + if (constraintLongValue <= 0) { + throw new InternalValidationException("invalid value found for " + path + SYMBOL_SEPARATOR + + constraintField + " constraint. Digits constraints should be positive"); + } + break; + default: + break; + } + } + + record DigitParts(int integerDigits, int fractionDigits) { + } + + default DigitParts getDigitPartsFromDouble(double value) { + String valueString = Double.toString(value); + if (valueString.matches(SCIENTIFIC_NOTATION_REGEX)) { + return getDigitPartsFromScientificNotation(value, valueString); + } else { + return getDigitPartsFromString(value, valueString); + } + } + + private static DigitParts getDigitPartsFromString(double value, String valueString) { + String[] parts = valueString.split(DOT_SEPARATOR); + int intDigits = value < 0 ? parts[0].length() - 1 : parts[0].length(); + if (parts.length == 1) { + return new DigitParts(intDigits, 0); + } else { + return new DigitParts(intDigits, parts[1].length()); + } + } + + private static DigitParts getDigitPartsFromScientificNotation(double value, String valueString) { + Pattern pattern = Pattern.compile(SCIENTIFIC_NOTATION_REGEX); + Matcher matcher = pattern.matcher(valueString); + int intDigits = 0; + int fractionDigits = 0; + if (matcher.matches()) { + String groupA = matcher.group(1); + String groupB = matcher.group(2); + String groupC = matcher.group(3); + + int countA = value < 0 ? groupA.length() - 1 : groupA.length(); + int countB = groupB.equals(ZERO_STRING) ? 0 : groupB.length(); + int intC = Integer.parseInt(groupC); + + if (intC > 0) { + intDigits = countA + Math.min(countB, intC); + fractionDigits = countB - Math.min(countB, intC); + } else { + intDigits = Math.max(1, countA + intC); + fractionDigits = countB - intC; + } + } + return new DigitParts(intDigits, fractionDigits); + } + + boolean validateMaxDigits(Object fieldValue, Object constraintValue); + + boolean validateMaxIntegerDigits(Object fieldValue, Object constraintValue); + + boolean validateMaxFractionDigits(Object fieldValue, Object constraintValue); +}