Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow nillable and optional fields to be read as record field #1313

Merged
merged 7 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 180 additions & 0 deletions ballerina/tests/csv_io.bal
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,22 @@ type Employee6 record {
string age;
};

type Employee7 record {|
string name;
string designation;
string company;
string? age;
string? residence;
|};

type Employee8 record {|
string name;
string designation;
string company;
string age?;
string residence;
|};

type RefInt int;

type RefStr string;
Expand Down Expand Up @@ -2000,3 +2016,167 @@ function testFileReadCsvRecordWithsingleHeaderLine() returns Error? {
}
test:assertEquals(i, 3);
}

@test:Config {}
function testFileReadCsvRecordWithEmptyFieldValues() returns Error? {
string resourceFilePath1 = TEST_RESOURCE_PATH + "csvResourceFileWithEmptyValues.csv";
Employee7[] readContent = check fileReadCsv(resourceFilePath1);
Employee7[] content1 = [{
name: "Anne Hamiltom",
designation: "Software Engineer",
company: "Microsoft",
age: (),
residence: "New York"
},
{
name: "John Thomson",
designation: "Software Architect",
company: "WSO2",
age: "38 years",
residence: "Colombo"
},
{
name: "Mary Thompson",
designation: "Banker",
company: "Sampath Bank",
age: "30 years",
residence: ()
}
];
test:assertEquals(readContent.length(), 3);
test:assertEquals(readContent, content1);
}

@test:Config {}
function testFileReadCsvRecordStreamWithEmptyFieldValues() returns Error? {
string resourceFilePath1 = TEST_RESOURCE_PATH + "csvResourceFileWithEmptyValues.csv";
stream<Employee7, Error?> readContent = check fileReadCsvAsStream(resourceFilePath1);
Employee7[] content1 = [{
name: "Anne Hamiltom",
designation: "Software Engineer",
company: "Microsoft",
age: (),
residence: "New York"
},
{
name: "John Thomson",
designation: "Software Architect",
company: "WSO2",
age: "38 years",
residence: "Colombo"
},
{
name: "Mary Thompson",
designation: "Banker",
company: "Sampath Bank",
age: "30 years",
residence: ()
}
];
int i = 0;
check readContent.forEach(function(Employee7 recordVal) {
test:assertEquals(recordVal, content1[i]);
i = i + 1;
});
test:assertEquals(i, 3);
}

@test:Config {}
function testFileReadCsvRecordWithNillableField() returns Error? {
string resourceFilePath1 = TEST_RESOURCE_PATH + "csvResourceFile1.csv";
stream<Employee7, Error?> readContent = check fileReadCsvAsStream(resourceFilePath1);
Employee7[] content1 = [{
name: "Anne Hamiltom",
designation: "Software Engineer",
company: "Microsoft",
age: "26 years",
residence: "New York"
},
{
name: "John Thomson",
designation: "Software Architect",
company: "WSO2",
age: "38 years",
residence: "Colombo"
},
{
name: "Mary Thompson",
designation: "Banker",
company: "Sampath Bank",
age: "30 years",
residence: "Colombo"
}
];
int i = 0;
check readContent.forEach(function(Employee7 recordVal) {
test:assertEquals(recordVal, content1[i]);
i = i + 1;
});
test:assertEquals(i, 3);
}

@test:Config {}
function testFileReadCsvRecordWithOptionalFields() returns Error? {
string resourceFilePath1 = TEST_RESOURCE_PATH + "csvResourceFileWithMissingFields.csv";
stream<Employee8, Error?> readContent = check fileReadCsvAsStream(resourceFilePath1);
Employee8[] content1 = [{
name: "Anne Hamiltom",
designation: "Software Engineer",
company: "Microsoft",
residence: "New York"
},
{
name: "John Thomson",
designation: "Software Architect",
company: "WSO2",
residence: "Colombo"
},
{
name: "Mary Thompson",
designation: "Banker",
company: "Sampath Bank",
residence: "Colombo"
}
];
int i = 0;
check readContent.forEach(function(Employee8 recordVal) {
test:assertEquals(recordVal, content1[i]);
i = i + 1;
});
test:assertEquals(i, 3);
}

@test:Config {}
function testFileReadCsvRecordWithOptionalFieldsHasValues() returns Error? {
string resourceFilePath1 = TEST_RESOURCE_PATH + "csvResourceFile1.csv";
stream<Employee8, Error?> readContent = check fileReadCsvAsStream(resourceFilePath1);
Employee8[] content1 = [{
name: "Anne Hamiltom",
designation: "Software Engineer",
company: "Microsoft",
age: "26 years",
residence: "New York"
},
{
name: "John Thomson",
designation: "Software Architect",
company: "WSO2",
age: "38 years",
residence: "Colombo"
},
{
name: "Mary Thompson",
designation: "Banker",
company: "Sampath Bank",
age: "30 years",
residence: "Colombo"
}
];
int i = 0;
check readContent.forEach(function(Employee8 recordVal) {
test:assertEquals(recordVal, content1[i]);
i = i + 1;
});
test:assertEquals(i, 3);
}

16 changes: 15 additions & 1 deletion ballerina/tests/negative.bal
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ isolated function testFileCsvReadWithDefectiveRecords() returns Error? {
string filePath = TEMP_DIR + "workers2.csv";
Employee6[]|Error csvContent = fileReadCsv(filePath);
test:assertTrue(csvContent is Error);
test:assertEquals((<Error>csvContent).message(), "The CSV file content header count(5) doesn't match with ballerina record field count(4). ");
test:assertEquals((<Error>csvContent).message(), "The csv file contains an additional column - residence.");
}

@test:Config {}
Expand Down Expand Up @@ -239,3 +239,17 @@ function readCsvFileWithUnsupportedMappingType() {
map<anydata>[]|Error out = fileReadCsv(filePath);
test:assertEquals((<Error>out).message(), "Only 'string[]' and 'record{}' types are supported, but found 'map' ");
}

@test:Config {}
function readCsvWithMissingColumnAndNillableField() {
string filePath = TEST_RESOURCE_PATH + "csvResourceFileWithMissingFields.csv";
Employee7[]|Error out = fileReadCsv(filePath);
test:assertEquals((<Error>out).message(), "The csv file does not contain the column - age.");
}

@test:Config {}
function readCsvWithMissingValueAndOptionalField() {
string filePath = TEST_RESOURCE_PATH + "csvResourceFileWithEmptyValues.csv";
Employee8[]|Error out = fileReadCsv(filePath);
test:assertEquals((<Error>out).message(), "Field 'age' does not support nil value.");
}
daneshk marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
name, designation, company, age, residence
Anne Hamiltom, Software Engineer, Microsoft,, New York
John Thomson, Software Architect, WSO2, 38 years, Colombo
Mary Thompson, Banker, Sampath Bank, 30 years,
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
name, designation, company, residence
Anne Hamiltom, Software Engineer, Microsoft, New York
John Thomson, Software Architect, WSO2, Colombo
Mary Thompson, Banker, Sampath Bank, Colombo
2 changes: 2 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ This file contains all the notable changes done to the Ballerina I/O package thr
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Fixed
- [The CSV file read as a record failed when a nillable field was empty](https://github.com/ballerina-platform/ballerina-library/issues/7433)

## [1.6.1] - 2024-08-06
### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

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.Field;
import io.ballerina.runtime.api.types.StructureType;
import io.ballerina.runtime.api.types.Type;
import io.ballerina.runtime.api.types.TypeTags;
Expand Down Expand Up @@ -158,9 +160,6 @@ record = textRecordChannel.read();
return returnStruct;
}
Map<String, Object> struct = (Map<String, Object>) returnStruct;
if (record.length != structType.getFields().size()) {
return IOUtils.createError("Record type and CSV file does not match.");
}
outList.add(ValueCreator.createRecordValue(describingType.getPackage(),
describingType.getName(), struct));

Expand Down Expand Up @@ -224,10 +223,6 @@ record = textRecordChannel.getFields(line);
return returnStruct;
}
final Map<String, Object> struct = (Map<String, Object>) returnStruct;
if (record.length != structType.getFields().size()) {
bufferedReader.close();
return IOUtils.createError("Record type and CSV file does not match.");
}
return ValueCreator.createRecordValue(describingType.getPackage(), describingType.getName(),
struct);
}
Expand Down Expand Up @@ -321,16 +316,20 @@ public static Object closeStream(BObject iterator) {
}

private static void validateHeaders(ArrayList<String> headers, StructureType structType) {
if (headers.size() != structType.getFields().size()) {
throw IOUtils.createError(String.format("The CSV file content header count" +
"(%s) doesn't match with ballerina record field count(%s). ",
headers.size(), structType.getFields().size()));
}
for (String key : structType.getFields().keySet()) {
if (!headers.contains(key.trim())) {
throw IOUtils.createError(String.format("The Record does not contain the " +
"field - %s. ", key.trim()));

structType.getFields().forEach((key, value) -> {
Field field = (Field) value;
if (!headers.contains(field.getFieldName().trim()) &&
!SymbolFlags.isFlagOn(field.getFlags(), SymbolFlags.OPTIONAL)) {
throw IOUtils.createError(String.format("The csv file does not contain the " +
"column - %s.", field.getFieldName().trim()));
}
}
});

headers.forEach(header -> {
if (structType.getFields().get(header) == null) {
throw IOUtils.createError(String.format("The csv file contains an additional column - %s.", header));
}
});
}
}
Loading