Skip to content

Commit

Permalink
Merge pull request #39 from MohamedSabthar/enum-validation
Browse files Browse the repository at this point in the history
Add support for default value and enum validation
  • Loading branch information
MohamedSabthar authored Aug 21, 2024
2 parents c952198 + 99012f8 commit 76d4837
Show file tree
Hide file tree
Showing 45 changed files with 7,842 additions and 5,755 deletions.
10 changes: 5 additions & 5 deletions ballerina/Ballerina.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
org = "ballerinax"
name = "copybook"
version = "0.2.0"
version = "0.2.1"
authors = ["Ballerina"]
keywords = ["copybook", "serdes", "cobol", "mainframe"]
repository = "https://github.com/ballerina-platform/module-ballerinax-copybook"
Expand All @@ -15,14 +15,14 @@ graalvmCompatible = true
[[platform.java17.dependency]]
groupId = "io.ballerina.lib"
artifactId = "copybook-native"
version = "0.2.0"
path="../native/build/libs/copybook-native-0.2.0.jar"
version = "0.2.1-SNAPSHOT"
path="../native/build/libs/copybook-native-0.2.1-SNAPSHOT.jar"

[[platform.java17.dependency]]
groupId = "io.ballerina.lib"
artifactId = "copybook-commons"
version = "0.2.0"
path = "../commons/build/libs/copybook-commons-0.2.0.jar"
version = "0.2.1"
path = "../commons/build/libs/copybook-commons-0.2.1-SNAPSHOT.jar"

[[platform.java17.dependency]]
groupId = "org.antlr"
Expand Down
6 changes: 3 additions & 3 deletions ballerina/Dependencies.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

[ballerina]
dependencies-toml-version = "2"
distribution-version = "2201.8.0-20230830-220400-8a7556d8"
distribution-version = "2201.8.0"

[[package]]
org = "ballerina"
Expand Down Expand Up @@ -35,7 +35,7 @@ modules = [
[[package]]
org = "ballerina"
name = "io"
version = "1.6.0"
version = "1.6.1"
dependencies = [
{org = "ballerina", name = "jballerina.java"},
{org = "ballerina", name = "lang.value"}
Expand Down Expand Up @@ -113,7 +113,7 @@ dependencies = [
[[package]]
org = "ballerinax"
name = "copybook"
version = "0.2.0"
version = "0.2.1"
dependencies = [
{org = "ballerina", name = "constraint"},
{org = "ballerina", name = "file"},
Expand Down
2 changes: 1 addition & 1 deletion ballerina/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ ballerina {
packageOrganization = packageOrg
module = packageName
testCoverageParam = "--code-coverage --coverage-format=xml"
buildOnDockerImage = "nightly"
isConnector = true
}

configurations {
Expand Down
4 changes: 2 additions & 2 deletions ballerina/copybook_reader.bal
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ class CopybookReader {
} else {
GroupValue groupValue = {};
foreach var child in groupItem.getChildren() {
child.accept(self, groupValue);
self.addValue(groupItem.getName(), groupValue, data);
child.accept(self, groupValue);
}
self.addValue(groupItem.getName(), groupValue, data);
}

// Reset the iterator to previous text iterator
Expand Down
8 changes: 8 additions & 0 deletions ballerina/data_item.bal
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ isolated distinct class DataItem {
'class: "io.ballerina.lib.copybook.runtime.converter.Utils"
} external;

isolated function getPossibleEnumValues() returns string[]? = @java:Method {
'class: "io.ballerina.lib.copybook.runtime.converter.Utils"
} external;

isolated function getDefaultValue() returns string? = @java:Method {
'class: "io.ballerina.lib.copybook.runtime.converter.Utils"
} external;

isolated function accept(Visitor visitor, anydata data = ()) {
visitor.visitDataItem(self, data);
}
Expand Down
65 changes: 65 additions & 0 deletions ballerina/default_value_creator.bal
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright (c) 2024 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.

class DefaultValueCreator {
*Visitor;
private string[] defaultValueFragments = [];

isolated function visitSchema(Schema schema, anydata data = ()) {
}

isolated function visitGroupItem(GroupItem groupItem, anydata data = ()) {
if groupItem.getRedefinedItemName() is string {
return;
}
string[] defaultValues = self.defaultValueFragments;
self.defaultValueFragments = [];
foreach Node node in groupItem.getChildren() {
node.accept(self);
}
string groupItemDefaultValue = string:'join("", ...self.defaultValueFragments);
defaultValues.push(self.generateRepeatedString(groupItemDefaultValue, groupItem.getElementCount()));
self.defaultValueFragments = defaultValues;
}

isolated function visitDataItem(DataItem dataItem, anydata data = ()) {
if dataItem.getRedefinedItemName() is string {
return;
}
string dataItemDefaultValue = dataItem.getDefaultValue() ?: "";
if (dataItem.isNumeric() && !dataItem.isDecimal()) || (dataItem.isDecimal() && dataItemDefaultValue != "") {
dataItemDefaultValue = dataItemDefaultValue.padZero(dataItem.getReadLength());
} else {
dataItemDefaultValue = dataItemDefaultValue.padEnd(dataItem.getReadLength());
}
self.defaultValueFragments.push(self.generateRepeatedString(dataItemDefaultValue, dataItem.getElementCount()));
}

private isolated function generateRepeatedString(string value, int count) returns string {
if count < 0 {
return value;
}
string[] values = [];
foreach int i in 1 ... count {
values.push(value);
}
return string:'join("", ...values);
}

isolated function getDefaultValue() returns string {
return string:'join("", ...self.defaultValueFragments);
}
}
79 changes: 62 additions & 17 deletions ballerina/json_to_copybook_convertor.bal
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ class JsonToCopybookConverter {
}
if data.hasKey(typedef.getName()) {
typedef.accept(self, data.get(typedef.getName()));
} else {
self.value.push(self.getDefaultValue(typedef));
}
_ = self.path.pop();
}
Expand Down Expand Up @@ -82,7 +84,7 @@ class JsonToCopybookConverter {
if !value.hasKey(child.getName()) {
redefiningItemNameWithValue = self.findRedefiningItemNameWithValue(value, redefiningItems);
if redefiningItemNameWithValue is () {
self.value.push("".padEnd(computeSize(child)));
self.value.push(self.getDefaultValue(child));
return;
}
self.visitAllowedRedefiningItems[redefiningItemNameWithValue] = ();
Expand All @@ -100,6 +102,12 @@ class JsonToCopybookConverter {
}
}

private isolated function getDefaultValue(Node node) returns string {
DefaultValueCreator defaultValueCreator = new;
node.accept(defaultValueCreator);
return defaultValueCreator.getDefaultValue();
}

private isolated function findRedefiningItemNameWithValue(map<json> parentValue, string[] redefiningItemdNames)
returns string? {
foreach string itemName in redefiningItemdNames {
Expand Down Expand Up @@ -128,22 +136,23 @@ class JsonToCopybookConverter {
return;
}
self.value.push(primitiveValue);
} on fail error e {
if e is Error {
self.errors.push(e);
}
} on fail Error e {
self.errors.push(e);
}
_ = self.path.pop();
}

private isolated function handlePrimitive(PrimitiveType value, DataItem dataItem) returns string|error {
private isolated function handlePrimitive(PrimitiveType value, DataItem dataItem) returns string|Error {
string primitiveValue;
if value is string {
return self.handleStringValue(value, dataItem);
}
if value is int {
return self.handleIntValue(value, dataItem);
primitiveValue = check self.handleStringValue(value, dataItem);
} else if value is int {
primitiveValue = check self.handleIntValue(value, dataItem);
} else {
primitiveValue = check self.handleDecimalValue(<decimal>value, dataItem);
}
return self.handleDecimalValue(<decimal>value, dataItem);
check self.validateEnumValue(primitiveValue, dataItem);
return primitiveValue;
}

private isolated function handleStringValue(string value, DataItem dataItem) returns string|Error {
Expand All @@ -157,7 +166,7 @@ class JsonToCopybookConverter {
return value.padEnd(maxLength);
}

private isolated function handleIntValue(int value, DataItem dataItem) returns string|error {
private isolated function handleIntValue(int value, DataItem dataItem) returns string|Error {
if !dataItem.isNumeric() {
return error Error(string `Numeric value ${value} found at ${self.getPath()}.`
+ " Expecting a non-numeric value");
Expand Down Expand Up @@ -187,12 +196,17 @@ class JsonToCopybookConverter {
}

private isolated function handlePrimitiveArray(PrimitiveArrayType array, DataItem dataItem)
returns string|error {
returns string|Error {
string[] elements = [];
PrimitiveType[] primitiveArray = array; // This is allowed by covariance
foreach int i in 0 ..< primitiveArray.length() {
self.path.push(string `[${i}]`);
elements.push(check self.handlePrimitive(primitiveArray[i], dataItem));
string|Error primitiveValue = self.handlePrimitive(primitiveArray[i], dataItem);
if primitiveValue is Error {
self.errors.push(primitiveValue);
continue;
}
elements.push(primitiveValue);
_ = self.path.pop();
}
int maxElementCount = dataItem.getElementCount();
Expand All @@ -202,7 +216,7 @@ class JsonToCopybookConverter {
return "".'join(...elements).padEnd(computeSize(dataItem));
}

private isolated function handleDecimalValue(decimal input, DataItem dataItem) returns string|error {
private isolated function handleDecimalValue(decimal input, DataItem dataItem) returns string|Error {
// TODO: skipped decimal with V, implment seperately for decimal containing V
// TODO: handle special case Z for fraction
if !dataItem.isDecimal() && !dataItem.isNumeric() {
Expand Down Expand Up @@ -242,7 +256,7 @@ class JsonToCopybookConverter {
}

private isolated function checkDecimalLength(string wholeNumber, string fraction, decimal input,
DataItem dataItem) returns error? {
DataItem dataItem) returns Error? {
// A deducted of 1 made from readLength for decimal seperator "."
int expectedWholeNumberLength = dataItem.getReadLength() - dataItem.getFloatingPointLength() - 1;
// If PIC has + or -, then remove the space allocated for the sign
Expand All @@ -254,14 +268,45 @@ class JsonToCopybookConverter {
+ string `${expectedWholeNumberLength} at ${self.getPath()}`);
} else if fraction.length() > dataItem.getFloatingPointLength() {
string exeedingFractionDigits = fraction.substring(dataItem.getFloatingPointLength());
if check int:fromString(exeedingFractionDigits) > 0 {
int|error exceedingValue = int:fromString(exeedingFractionDigits);
if exceedingValue is error {
return error Error(string `Unable to coerce the provided decimal fraction '${exeedingFractionDigits}': `
+ exceedingValue.message());
}
if exceedingValue > 0 {
return error Error(string `Value '${input}' exceeds the maximum number of fraction digits `
+ string `${dataItem.getFloatingPointLength()} at ${self.getPath()}`);
}
}
return;
}

private isolated function validateEnumValue(string value, DataItem dataItem) returns Error? {
string[]? possibleEnumValues = dataItem.getPossibleEnumValues();
if possibleEnumValues is () {
return;
}
anydata providedValue = value;
anydata[] possibleValues = possibleEnumValues;
do {
if dataItem.isDecimal() {
providedValue = check decimal:fromString(value);
possibleValues = check toDecimalArray(possibleEnumValues);
} else if dataItem.isNumeric() {
providedValue = check int:fromString(value);
possibleValues = check toIntArray(possibleEnumValues);
}
} on fail error e {
return error Error(string `Failed to validate enum value '${value}': ${e.message()}`, e.cause());
}
if possibleValues.indexOf(providedValue) is int {
return;
}
string[] formattedEnumValues = possibleEnumValues.'map(posibleValue => string `'${posibleValue.toString()}'`);
return error Error(string `Value '${value}' is invalid for field '${dataItem.getName()}'. `
+ string `Allowed values are: ${string:'join(", ", ...formattedEnumValues)}.`);
}

private isolated function getPath() returns string {
return string `'${".".'join(...self.path)}'`;
}
Expand Down
2 changes: 1 addition & 1 deletion ballerina/tests/resources/copybook-ascii/copybook-1.txt

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions ballerina/tests/resources/copybook-ascii/copybook-11.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Y
1 change: 1 addition & 0 deletions ballerina/tests/resources/copybook-ascii/copybook-12.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1
1 change: 1 addition & 0 deletions ballerina/tests/resources/copybook-ascii/copybook-13.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
200404202500
1 change: 1 addition & 0 deletions ballerina/tests/resources/copybook-ascii/copybook-15.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0000 000 000000000
1 change: 1 addition & 0 deletions ballerina/tests/resources/copybook-ascii/copybook-16.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
25SABTHAR 179.900
4 changes: 2 additions & 2 deletions ballerina/tests/resources/copybook-ascii/copybook-4.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
-02004
99004
02-004
-02-004
01-004
4 changes: 2 additions & 2 deletions ballerina/tests/resources/copybook-ascii/copybook-6.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
-02004
01004
02-004
-02-004
99-004
Loading

0 comments on commit 76d4837

Please sign in to comment.