diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index 3604b13..ef2b4f3 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -5,7 +5,7 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.9.0-20240326-110600-aca0cc0c" +distribution-version = "2201.8.6" [[package]] org = "ballerina" @@ -42,26 +42,6 @@ modules = [ {org = "ballerina", packageName = "jballerina.java", moduleName = "jballerina.java"} ] -[[package]] -org = "ballerina" -name = "lang.__internal" -version = "0.0.0" -scope = "testOnly" -dependencies = [ - {org = "ballerina", name = "jballerina.java"}, - {org = "ballerina", name = "lang.object"} -] - -[[package]] -org = "ballerina" -name = "lang.array" -version = "0.0.0" -scope = "testOnly" -dependencies = [ - {org = "ballerina", name = "jballerina.java"}, - {org = "ballerina", name = "lang.__internal"} -] - [[package]] org = "ballerina" name = "lang.error" @@ -71,12 +51,6 @@ dependencies = [ {org = "ballerina", name = "jballerina.java"} ] -[[package]] -org = "ballerina" -name = "lang.object" -version = "0.0.0" -scope = "testOnly" - [[package]] org = "ballerina" name = "lang.value" @@ -96,7 +70,6 @@ version = "0.0.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"}, - {org = "ballerina", name = "lang.array"}, {org = "ballerina", name = "lang.error"} ] modules = [ diff --git a/ballerina/tests/from_json_string_test.bal b/ballerina/tests/from_json_string_test.bal index c2ed701..7d1e9dd 100644 --- a/ballerina/tests/from_json_string_test.bal +++ b/ballerina/tests/from_json_string_test.bal @@ -13,13 +13,12 @@ // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. - import ballerina/test; @test:Config { dataProvider: basicTypeDataProviderForParseString } -isolated function testJsonStringToBasicTypes(string sourceData, typedesc expType, +isolated function testJsonStringToBasicTypes(string sourceData, typedesc expType, anydata expectedData) returns Error? { anydata val1 = check parseString(sourceData, {}, expType); test:assertEquals(val1, expectedData); @@ -38,7 +37,7 @@ function basicTypeDataProviderForParseString() returns [string, typedescerr5).message(), "incompatible expected type '()' for value 'Null'"); + + ()|Error err6 = parseString("()"); + test:assertTrue(err6 is Error); + test:assertEquals((err6).message(), "incompatible expected type '()' for value '()'"); } diff --git a/ballerina/tests/from_json_with_projection_options_test.bal b/ballerina/tests/from_json_with_projection_options_test.bal index 33a9ffa..cba413a 100644 --- a/ballerina/tests/from_json_with_projection_options_test.bal +++ b/ballerina/tests/from_json_with_projection_options_test.bal @@ -309,7 +309,7 @@ isolated function testAbsentAsNilableTypeAndAbsentAsNilableTypeForParseString() @test:Config { groups: ["options"] } -isolated function testAbsentAsNilableTypeAndAbsentAsNilableTypeForParseAsString() returns error? { +isolated function testAbsentAsNilableTypeAndAbsentAsNilableTypeForParseString2() returns error? { string jsonData = check io:fileReadString(PATH + "product_list_response.json"); json productJson = check value:fromJsonString(jsonData); ResponseEcom response = check parseAsType(productJson, options3); @@ -410,3 +410,93 @@ isolated function testDisableOptionsOfProjectionTypeForParseAsType2() returns er test:assertTrue(err is Error); test:assertEquals((err).message(), "required field 'email' not present in JSON"); } + +@test:Config +isolated function testAbsentAsNilableTypeAndAbsentAsNilableTypeForParseString3() returns error? { + record {| + string name; + |}|Error val1 = parseString(string `{"name": null}`, options3); + test:assertTrue(val1 is Error); + test:assertEquals((val1).message(), "incompatible value 'null' for type 'string' in field 'name'"); + + record {| + string? name; + |} val2 = check parseString(string `{"name": null}`, options3); + test:assertEquals(val2.name, ()); + + record {| + string name?; + |} val3 = check parseString(string `{"name": null}`, options3); + test:assertEquals(val3, {}); + + record {| + string? name?; + |} val4 = check parseString(string `{"name": null}`, options3); + test:assertEquals(val4?.name, ()); + + record {| + string name; + |}|Error val5 = parseString(string `{}`, options3); + test:assertTrue(val5 is Error); + test:assertEquals((val5).message(), "required field 'name' not present in JSON"); + + record {| + string? name; + |} val6 = check parseString(string `{}`, options3); + test:assertEquals(val6.name, ()); + + record {| + string name?; + |} val7 = check parseString(string `{}`, options3); + test:assertEquals(val7, {}); + + record {| + string? name?; + |} val8 = check parseString(string `{}`, options3); + test:assertEquals(val8?.name, ()); +} + +@test:Config +isolated function testAbsentAsNilableTypeAndAbsentAsNilableTypeForParseAsType2() returns error? { + record {| + string name; + |}|Error val1 = parseAsType({name: null}, options3); + test:assertTrue(val1 is Error); + test:assertEquals((val1).message(), "incompatible value 'null' for type 'string' in field 'name'"); + + record {| + string? name; + |} val2 = check parseAsType({name: null}, options3); + test:assertEquals(val2.name, ()); + + record {| + string name?; + |} val3 = check parseAsType({name: null}, options3); + test:assertEquals(val3, {}); + + record {| + string? name?; + |} val4 = check parseAsType({name: null}, options3); + test:assertEquals(val4?.name, ()); + + record {| + string name; + |}|Error val5 = parseAsType({}, options3); + test:assertTrue(val5 is Error); + test:assertEquals((val5).message(), "required field 'name' not present in JSON"); + + record {| + string? name; + |} val6 = check parseAsType({}, options3); + test:assertEquals(val6.name, ()); + + record {| + string name?; + |} val7 = check parseAsType({}, options3); + test:assertEquals(val7, {}); + + record {| + string? name?; + |} val8 = check parseAsType({}, options3); + test:assertEquals(val8?.name, ()); +} diff --git a/compiler-plugin/src/main/java/io/ballerina/lib/data/jsondata/compiler/JsondataTypeValidator.java b/compiler-plugin/src/main/java/io/ballerina/lib/data/jsondata/compiler/JsondataTypeValidator.java index b350fd6..89bf584 100644 --- a/compiler-plugin/src/main/java/io/ballerina/lib/data/jsondata/compiler/JsondataTypeValidator.java +++ b/compiler-plugin/src/main/java/io/ballerina/lib/data/jsondata/compiler/JsondataTypeValidator.java @@ -176,15 +176,25 @@ private void validateRecordFieldType(TypeSymbol typeSymbol, Optional l private void validateUnionType(UnionTypeSymbol unionTypeSymbol, Optional location, SyntaxNodeAnalysisContext ctx) { int nonPrimitiveMemberCount = 0; + boolean isNilOrErrorPresent = false; List memberTypeSymbols = unionTypeSymbol.memberTypeDescriptors(); for (TypeSymbol memberTypeSymbol : memberTypeSymbols) { - if (isSupportedUnionMemberType(memberTypeSymbol)) { + TypeSymbol referredSymbol = getReferredSymbol(memberTypeSymbol); + if (referredSymbol.typeKind() == TypeDescKind.NIL || referredSymbol.typeKind() == TypeDescKind.ERROR) { + isNilOrErrorPresent = true; + continue; + } + if (isSupportedUnionMemberType(referredSymbol)) { continue; } nonPrimitiveMemberCount++; } - if (nonPrimitiveMemberCount > 1) { + if (isNilOrErrorPresent && memberTypeSymbols.size() == 2) { + return; + } + + if (nonPrimitiveMemberCount >= 1) { reportDiagnosticInfo(ctx, location, JsondataDiagnosticCodes.UNSUPPORTED_UNION_TYPE); } } @@ -205,6 +215,11 @@ private boolean isSupportedUnionMemberType(TypeSymbol typeSymbol) { } } + private TypeSymbol getReferredSymbol(TypeSymbol typeSymbol) { + return typeSymbol.typeKind() == TypeDescKind.TYPE_REFERENCE ? + ((TypeReferenceTypeSymbol) typeSymbol).typeDescriptor() : typeSymbol; + } + private void reportDiagnosticInfo(SyntaxNodeAnalysisContext ctx, Optional location, JsondataDiagnosticCodes diagnosticsCodes) { Location pos = location.orElseGet(() -> currentLocation); diff --git a/native/src/main/java/io/ballerina/lib/data/jsondata/FromString.java b/native/src/main/java/io/ballerina/lib/data/jsondata/FromString.java index 73424d9..c4befa2 100644 --- a/native/src/main/java/io/ballerina/lib/data/jsondata/FromString.java +++ b/native/src/main/java/io/ballerina/lib/data/jsondata/FromString.java @@ -125,6 +125,7 @@ public static Object fromStringWithType(BString string, Type expType) { case TypeTags.UNION_TAG: return stringToUnion(string, (UnionType) expType); case TypeTags.JSON_TAG: + case TypeTags.ANYDATA_TAG: return stringToUnion(string, JSON_TYPE_WITH_BASIC_TYPES); case TypeTags.TYPE_REFERENCED_TYPE_TAG: return fromStringWithType(string, ((ReferenceType) expType).getReferredType()); diff --git a/native/src/main/java/io/ballerina/lib/data/jsondata/json/JsonCreator.java b/native/src/main/java/io/ballerina/lib/data/jsondata/json/JsonCreator.java index 28ebe69..740a448 100644 --- a/native/src/main/java/io/ballerina/lib/data/jsondata/json/JsonCreator.java +++ b/native/src/main/java/io/ballerina/lib/data/jsondata/json/JsonCreator.java @@ -70,25 +70,59 @@ public class JsonCreator { TypeCreator.createUnionType(BASIC_TYPE_MEMBER_TYPES); static BMap initRootMapValue(Type expectedType) { - return switch (expectedType.getTag()) { - case TypeTags.RECORD_TYPE_TAG -> - ValueCreator.createRecordValue(expectedType.getPackage(), expectedType.getName()); - case TypeTags.MAP_TAG -> ValueCreator.createMapValue((MapType) expectedType); - case TypeTags.JSON_TAG -> ValueCreator.createMapValue(Constants.JSON_MAP_TYPE); - case TypeTags.ANYDATA_TAG -> ValueCreator.createMapValue(Constants.ANYDATA_MAP_TYPE); - case TypeTags.UNION_TAG -> throw DiagnosticLog.error(DiagnosticErrorCode.UNSUPPORTED_TYPE, expectedType); + switch (expectedType.getTag()) { + case TypeTags.RECORD_TYPE_TAG -> { + return ValueCreator.createRecordValue(expectedType.getPackage(), expectedType.getName()); + } + case TypeTags.MAP_TAG -> { + return ValueCreator.createMapValue((MapType) expectedType); + } + case TypeTags.JSON_TAG -> { + return ValueCreator.createMapValue(Constants.JSON_MAP_TYPE); + } + case TypeTags.ANYDATA_TAG -> { + return ValueCreator.createMapValue(Constants.ANYDATA_MAP_TYPE); + } + case TypeTags.UNION_TAG -> { + // Only handle T? cases. + // TODO: Support all union cases. + List filteredMembers = (((UnionType) expectedType).getMemberTypes()).stream().filter( + memType -> memType.getTag() != TypeTags.NULL_TAG).toList(); + if (filteredMembers.size() == 1) { + return initRootMapValue(filteredMembers.get(0)); + } + throw DiagnosticLog.error(DiagnosticErrorCode.UNSUPPORTED_TYPE, expectedType); + } default -> throw DiagnosticLog.error(DiagnosticErrorCode.INVALID_TYPE, expectedType, "map type"); - }; + } } static BArray initArrayValue(Type expectedType) { - return switch (expectedType.getTag()) { - case TypeTags.TUPLE_TAG -> ValueCreator.createTupleValue((TupleType) expectedType); - case TypeTags.ARRAY_TAG -> ValueCreator.createArrayValue((ArrayType) expectedType); - case TypeTags.JSON_TAG -> ValueCreator.createArrayValue(PredefinedTypes.TYPE_JSON_ARRAY); - case TypeTags.ANYDATA_TAG -> ValueCreator.createArrayValue(PredefinedTypes.TYPE_ANYDATA_ARRAY); + switch (expectedType.getTag()) { + case TypeTags.TUPLE_TAG -> { + return ValueCreator.createTupleValue((TupleType) expectedType); + } + case TypeTags.ARRAY_TAG -> { + return ValueCreator.createArrayValue((ArrayType) expectedType); + } + case TypeTags.JSON_TAG -> { + return ValueCreator.createArrayValue(PredefinedTypes.TYPE_JSON_ARRAY); + } + case TypeTags.ANYDATA_TAG -> { + return ValueCreator.createArrayValue(PredefinedTypes.TYPE_ANYDATA_ARRAY); + } + case TypeTags.UNION_TAG -> { + // Only handle T? cases. + // TODO: Support all union cases. + List filteredMembers = (((UnionType) expectedType).getMemberTypes()).stream().filter( + memType -> memType.getTag() != TypeTags.NULL_TAG).toList(); + if (filteredMembers.size() == 1) { + return initArrayValue(filteredMembers.get(0)); + } + throw DiagnosticLog.error(DiagnosticErrorCode.UNSUPPORTED_TYPE, expectedType); + } default -> throw DiagnosticLog.error(DiagnosticErrorCode.INVALID_TYPE, expectedType, "list type"); - }; + } } static Optional> initNewMapValue(JsonParser.StateMachine sm) { @@ -136,7 +170,16 @@ static BMap checkTypeAndCreateMappingValue(JsonParser.StateMach } return checkTypeAndCreateMappingValue(sm, mutableType.get(), parentContext); } - case TypeTags.UNION_TAG -> throw DiagnosticLog.error(DiagnosticErrorCode.UNSUPPORTED_TYPE, currentType); + case TypeTags.UNION_TAG -> { + // Only handle T? cases. + // TODO: Support all union cases. + List filteredMembers = (((UnionType) currentType).getMemberTypes()).stream().filter( + memType -> memType.getTag() != TypeTags.NULL_TAG).toList(); + if (filteredMembers.size() == 1) { + return checkTypeAndCreateMappingValue(sm, filteredMembers.get(0), parentContext); + } + throw DiagnosticLog.error(DiagnosticErrorCode.UNSUPPORTED_TYPE, currentType); + } default -> { if (parentContext == JsonParser.StateMachine.ParserContext.ARRAY) { throw DiagnosticLog.error(DiagnosticErrorCode.INVALID_TYPE, currentType, "map type");