diff --git a/.github/scripts/tests.sh b/.github/scripts/tests.sh index 2e1fa96..3295a82 100644 --- a/.github/scripts/tests.sh +++ b/.github/scripts/tests.sh @@ -25,4 +25,8 @@ do ./gradlew :kudos-compiler:test -PJACKSON_VERSION=$jackson_version -PVARIANT=jackson -PKOTLIN_COMPILER=K2 done +# Android JsonReader +echo "[Kudos] Testing with Android JsonReader" +./gradlew :kudos-compiler:test -PVARIANT=jsonReader -PKOTLIN_COMPILER=K2 + cd - diff --git a/gradle.properties b/gradle.properties index 3854aff..9345f6b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -31,4 +31,4 @@ RELEASE_SIGNING_ENABLED=true GSON_VERSION=2.10 JACKSON_VERSION=2.15.0 VARIANT=jsonReader -KOTLIN_COMPILER=K1 +KOTLIN_COMPILER=K2 diff --git a/kudos-android-json-reader/src/main/java/com/kanyun/kudos/json/reader/KudosAndroidJsonReader.kt b/kudos-android-json-reader/src/main/java/com/kanyun/kudos/json/reader/KudosAndroidJsonReader.kt index 39c8ffe..a860f74 100644 --- a/kudos-android-json-reader/src/main/java/com/kanyun/kudos/json/reader/KudosAndroidJsonReader.kt +++ b/kudos-android-json-reader/src/main/java/com/kanyun/kudos/json/reader/KudosAndroidJsonReader.kt @@ -18,6 +18,8 @@ package com.kanyun.kudos.json.reader import android.util.JsonReader import com.kanyun.kudos.json.reader.adapter.KudosJsonAdapter +import com.kanyun.kudos.json.reader.adapter.parseKudosObject +import java.lang.reflect.Type object KudosAndroidJsonReader { inline fun fromJson(json: String): T { @@ -33,4 +35,9 @@ object KudosAndroidJsonReader { throw IllegalArgumentException("class ${clazz.name} must implement KudosJsonAdapter") } } + + fun fromJson(json: String, type: Type): T { + val jsonReader = JsonReader(json.reader()) + return parseKudosObject(jsonReader, type) as T + } } diff --git a/kudos-android-json-reader/src/main/java/com/kanyun/kudos/json/reader/adapter/KudosJsonAdapter.kt b/kudos-android-json-reader/src/main/java/com/kanyun/kudos/json/reader/adapter/KudosJsonAdapter.kt index ba038c7..2a0abba 100644 --- a/kudos-android-json-reader/src/main/java/com/kanyun/kudos/json/reader/adapter/KudosJsonAdapter.kt +++ b/kudos-android-json-reader/src/main/java/com/kanyun/kudos/json/reader/adapter/KudosJsonAdapter.kt @@ -17,13 +17,17 @@ package com.kanyun.kudos.json.reader.adapter import android.util.JsonReader +import android.util.JsonToken +import com.kanyun.kudos.collections.KudosCollection +import com.kanyun.kudos.collections.KudosList +import com.kanyun.kudos.collections.KudosSet import java.lang.reflect.Type interface KudosJsonAdapter { fun fromJson(jsonReader: JsonReader): T } -fun parseKudosObject(jsonReader: JsonReader, type: Type): Any { +fun parseKudosObject(jsonReader: JsonReader, type: Type): Any? { return if (type is ParameterizedTypeImpl) { parseKudosObjectInternal(jsonReader, type.rawType, type.actualTypeArguments) } else { @@ -31,8 +35,8 @@ fun parseKudosObject(jsonReader: JsonReader, type: Type): Any { } } -private fun parseKudosList(jsonReader: JsonReader, typeArguments: Array): List { - val list = mutableListOf() +private fun parseKudosList(jsonReader: JsonReader, typeArguments: Array): List { + val list = mutableListOf() jsonReader.beginArray() while (jsonReader.hasNext()) { list.add(parseKudosObject(jsonReader, typeArguments[0])) @@ -41,6 +45,19 @@ private fun parseKudosList(jsonReader: JsonReader, typeArguments: Array): return list } +private fun parseKudosCollection(jsonReader: JsonReader, type: Type, typeArguments: Array): KudosCollection { + val list = KudosList() + jsonReader.beginArray() + while (jsonReader.hasNext()) { + if (jsonReader.peek() == JsonToken.NULL) { + throw NullPointerException("Element cannot be null for ${type.typeName}.") + } + list.add(parseKudosObject(jsonReader, typeArguments[0])!!) + } + jsonReader.endArray() + return list +} + private fun parseKudosArray(jsonReader: JsonReader, typeArguments: Array): Any { val list = parseKudosList(jsonReader, typeArguments) val array = java.lang.reflect.Array.newInstance(typeArguments[0] as Class<*>, list.size) @@ -50,8 +67,8 @@ private fun parseKudosArray(jsonReader: JsonReader, typeArguments: Array): return array } -private fun parseKudosMap(jsonReader: JsonReader, typeArguments: Array): Map { - val resultMap = mutableMapOf() +private fun parseKudosMap(jsonReader: JsonReader, typeArguments: Array): Map { + val resultMap = mutableMapOf() jsonReader.beginObject() while (jsonReader.hasNext()) { val key = jsonReader.nextName() @@ -66,7 +83,11 @@ private fun parseKudosObjectInternal( jsonReader: JsonReader, type: Type, typeArguments: Array, -): Any { +): Any? { + if (jsonReader.peek() == JsonToken.NULL) { + jsonReader.skipValue() + return null + } val value = when (type) { String::class.javaObjectType -> jsonReader.nextString() Int::class.javaObjectType -> jsonReader.nextInt() @@ -77,6 +98,9 @@ private fun parseKudosObjectInternal( List::class.javaObjectType -> parseKudosList(jsonReader, typeArguments) Set::class.javaObjectType -> parseKudosList(jsonReader, typeArguments).toSet() Map::class.javaObjectType -> parseKudosMap(jsonReader, typeArguments) + KudosList::class.javaObjectType -> parseKudosCollection(jsonReader, type, typeArguments) + KudosSet::class.javaObjectType -> parseKudosCollection(jsonReader, type, typeArguments).toCollection(KudosSet()) + KudosCollection::class.javaObjectType -> parseKudosCollection(jsonReader, type, typeArguments) else -> { parseKudosObjectSpecial(jsonReader, type, typeArguments) } diff --git a/kudos-compiler/build.gradle.kts b/kudos-compiler/build.gradle.kts index dd483fb..163665f 100644 --- a/kudos-compiler/build.gradle.kts +++ b/kudos-compiler/build.gradle.kts @@ -41,7 +41,7 @@ dependencies { testImplementation(project(":android-json-reader")) testImplementation("org.jetbrains.kotlin:kotlin-noarg:1.8.20") testImplementation("org.jetbrains.kotlin:kotlin-compiler-embeddable") - testImplementation("com.bennyhuo.kotlin:kotlin-compile-testing-extensions:1.8.20-1.2.2") + testImplementation("com.bennyhuo.kotlin:kotlin-compile-testing-extensions:1.8.20-1.2.3-SNAPSHOT") } val compileKotlin: KotlinCompile by tasks diff --git a/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosFromJsonFunctionBuilder.kt b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosFromJsonFunctionBuilder.kt index e37a16b..840ae36 100644 --- a/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosFromJsonFunctionBuilder.kt +++ b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosFromJsonFunctionBuilder.kt @@ -16,6 +16,10 @@ package com.kanyun.kudos.compiler +import com.kanyun.kudos.compiler.KudosNames.JSON_READER_PEEK_CALLABLE_ID +import com.kanyun.kudos.compiler.KudosNames.JSON_READER_SKIP_VALUE_CALLABLE_ID +import com.kanyun.kudos.compiler.KudosNames.JSON_TOKEN_CLASS_ID +import com.kanyun.kudos.compiler.KudosNames.JSON_TOKEN_NULL_IDENTIFIER import com.kanyun.kudos.compiler.KudosNames.KUDOS_JSON_ADAPTER_CLASS_ID import com.kanyun.kudos.compiler.utils.irThis import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext @@ -25,9 +29,14 @@ import org.jetbrains.kotlin.ir.builders.Scope import org.jetbrains.kotlin.ir.builders.irBlock import org.jetbrains.kotlin.ir.builders.irBranch import org.jetbrains.kotlin.ir.builders.irCall +import org.jetbrains.kotlin.ir.builders.irContinue import org.jetbrains.kotlin.ir.builders.irElseBranch import org.jetbrains.kotlin.ir.builders.irEquals import org.jetbrains.kotlin.ir.builders.irGet +import org.jetbrains.kotlin.ir.builders.irGetField +import org.jetbrains.kotlin.ir.builders.irIfThen +import org.jetbrains.kotlin.ir.builders.irNotEquals +import org.jetbrains.kotlin.ir.builders.irNull import org.jetbrains.kotlin.ir.builders.irReturn import org.jetbrains.kotlin.ir.builders.irSetField import org.jetbrains.kotlin.ir.builders.irString @@ -36,10 +45,13 @@ import org.jetbrains.kotlin.ir.builders.irVararg import org.jetbrains.kotlin.ir.builders.irWhen import org.jetbrains.kotlin.ir.builders.irWhile import org.jetbrains.kotlin.ir.declarations.IrClass +import org.jetbrains.kotlin.ir.declarations.IrEnumEntry import org.jetbrains.kotlin.ir.declarations.IrField import org.jetbrains.kotlin.ir.declarations.IrFunction +import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction import org.jetbrains.kotlin.ir.expressions.IrBranch import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.expressions.impl.IrGetEnumValueImpl import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol import org.jetbrains.kotlin.ir.types.IrSimpleType import org.jetbrains.kotlin.ir.types.IrType @@ -58,6 +70,8 @@ internal class KudosFromJsonFunctionBuilder( private val irClass: IrClass, private val irFunction: IrFunction, private val pluginContext: IrPluginContext, + private val kudosStatusField: IrField?, + private val validatorFunction: IrSimpleFunction?, startOffset: Int = SYNTHETIC_OFFSET, endOffset: Int = SYNTHETIC_OFFSET, ) : IrBlockBodyBuilder(pluginContext, Scope(irFunction.symbol), startOffset, endOffset) { @@ -100,22 +114,56 @@ internal class KudosFromJsonFunctionBuilder( dispatchReceiver = irGet(jsonReader) }, ) + val jsonReaderPeekExpression = irCall(pluginContext.referenceFunctions(JSON_READER_PEEK_CALLABLE_ID).first()).apply { + dispatchReceiver = irGet(jsonReader) + } + val jsonTokenClass = pluginContext.referenceClass(JSON_TOKEN_CLASS_ID)!! + val jsonTokenNullEntry = jsonTokenClass.owner.declarations.filterIsInstance().first { + it.name == JSON_TOKEN_NULL_IDENTIFIER + } + val jsonTokenNullExpression = IrGetEnumValueImpl( + startOffset, + endOffset, + jsonTokenClass.defaultType, + jsonTokenNullEntry.symbol, + ) + +irIfThen( + context.irBuiltIns.unitType, + irEquals(jsonReaderPeekExpression, jsonTokenNullExpression), + irBlock { + +irCall( + pluginContext.referenceFunctions(JSON_READER_SKIP_VALUE_CALLABLE_ID).first(), + ).apply { + dispatchReceiver = irGet(jsonReader) + } + +irContinue(this@apply) + }, + ) val branches = ArrayList() fields.forEach { field -> branches.add( irBranch( irEquals(irGet(name), irString(field.name.asString())), - irSetField(irFunction.irThis(), field, getNextValue(field)), + irBlock { + +irSetField(irFunction.irThis(), field, getNextValue(field)) + if (kudosStatusField != null) { + +irCall( + pluginContext.referenceFunctions( + CallableId(FqName("java.util"), FqName("Map"), Name.identifier("put")), + ).first(), + ).apply { + putValueArgument(0, irString(field.name.asString())) + putValueArgument(1, irNotEquals(irGetField(irFunction.irThis(), field), irNull())) + dispatchReceiver = irGetField(irFunction.irThis(), kudosStatusField) + } + } + }, ), ) } branches.add( irElseBranch( - irCall( - pluginContext.referenceFunctions( - CallableId(FqName("android.util"), FqName("JsonReader"), Name.identifier("skipValue")), - ).first(), - ).apply { + irCall(pluginContext.referenceFunctions(JSON_READER_SKIP_VALUE_CALLABLE_ID).first()).apply { dispatchReceiver = irGet(jsonReader) }, ), @@ -130,6 +178,12 @@ internal class KudosFromJsonFunctionBuilder( ).apply { dispatchReceiver = irGet(jsonReader) } + if (validatorFunction != null && kudosStatusField != null) { + +irCall(validatorFunction.symbol).apply { + putValueArgument(0, irGetField(irFunction.irThis(), kudosStatusField)) + dispatchReceiver = irFunction.irThis() + } + } +irReturn( irFunction.irThis(), ) diff --git a/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosIrClassTransformer.kt b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosIrClassTransformer.kt index f2b3b04..3d49107 100644 --- a/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosIrClassTransformer.kt +++ b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosIrClassTransformer.kt @@ -18,27 +18,32 @@ package com.kanyun.kudos.compiler import com.kanyun.kudos.compiler.KudosNames.ADAPTER_FACTORY_NAME import com.kanyun.kudos.compiler.KudosNames.JSON_ADAPTER_NAME +import com.kanyun.kudos.compiler.KudosNames.KUDOS_FIELD_STATUS_MAP_IDENTIFIER import com.kanyun.kudos.compiler.KudosNames.KUDOS_VALIDATOR_NAME import com.kanyun.kudos.compiler.options.Options import com.kanyun.kudos.compiler.utils.addOverride import com.kanyun.kudos.compiler.utils.hasKudosAnnotation import com.kanyun.kudos.compiler.utils.irThis import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext +import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder import org.jetbrains.kotlin.descriptors.ClassKind import org.jetbrains.kotlin.descriptors.Modality import org.jetbrains.kotlin.ir.IrStatement import org.jetbrains.kotlin.ir.builders.IrBlockBodyBuilder import org.jetbrains.kotlin.ir.builders.IrBuilderWithScope import org.jetbrains.kotlin.ir.builders.Scope +import org.jetbrains.kotlin.ir.builders.declarations.addField import org.jetbrains.kotlin.ir.builders.declarations.addValueParameter import org.jetbrains.kotlin.ir.builders.declarations.buildConstructor import org.jetbrains.kotlin.ir.builders.irCall +import org.jetbrains.kotlin.ir.builders.irExprBody import org.jetbrains.kotlin.ir.builders.irGet import org.jetbrains.kotlin.ir.builders.irGetField import org.jetbrains.kotlin.ir.builders.irSetField import org.jetbrains.kotlin.ir.declarations.IrClass import org.jetbrains.kotlin.ir.declarations.IrConstructor import org.jetbrains.kotlin.ir.declarations.IrField +import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction import org.jetbrains.kotlin.ir.expressions.IrGetValue import org.jetbrains.kotlin.ir.expressions.IrStatementOrigin import org.jetbrains.kotlin.ir.expressions.impl.IrClassReferenceImpl @@ -90,8 +95,8 @@ class KudosIrClassTransformer( generateJsonAdapter() } generateNoArgConstructor() - generateValidator() - generateFromJson() + val validatorFunction = generateValidator() + generateFromJson(validatorFunction) } private fun generateJsonAdapter() { @@ -201,7 +206,7 @@ class KudosIrClassTransformer( } } - private fun generateValidator() { + private fun generateValidator(): IrSimpleFunction? { val nonDefaults = ArrayList() val collections = ArrayList() val arrays = ArrayList() @@ -231,7 +236,7 @@ class KudosIrClassTransformer( } } - if (nonDefaults.isEmpty() && collections.isEmpty() && arrays.isEmpty()) return + if (nonDefaults.isEmpty() && collections.isEmpty() && arrays.isEmpty()) return null val statusType = context.irBuiltIns.mapClass.typeWith( context.irBuiltIns.stringType, @@ -245,7 +250,7 @@ class KudosIrClassTransformer( } if (validateFunction?.isFakeOverride == false) { - return + return validateFunction } else if (validateFunction?.isFakeOverride == true) { irClass.declarations.remove(validateFunction) } @@ -347,6 +352,7 @@ class KudosIrClassTransformer( } } } + return validateFunction } private fun needsNoargConstructor(declaration: IrClass): Boolean = @@ -361,13 +367,39 @@ class KudosIrClassTransformer( } && (valueParameters.isEmpty() || isPrimary || hasAnnotation(JvmNames.JVM_OVERLOADS_FQ_NAME)) } - private fun generateFromJson() { - irClass.functions.singleOrNull { - it.name.identifier == "fromJson" - }?.takeIf { - it.body == null - }?.let { function -> - KudosFromJsonFunctionBuilder(irClass, function, context).generateBody() + private fun generateFromJson(validatorFunction: IrSimpleFunction?) { + if (irClass.hasKudosAnnotation()) { + val fieldType = context.irBuiltIns.mapClass.typeWith( + context.irBuiltIns.stringClass.defaultType, + context.irBuiltIns.booleanClass.defaultType, + ) + val initExpression = context.referenceFunctions( + CallableId(FqName("kotlin.collections"), Name.identifier("hashMapOf")), + ).first() + val kudosStatusField = if (validatorFunction != null) { + irClass.addField( + KUDOS_FIELD_STATUS_MAP_IDENTIFIER, + fieldType, + ).apply { + initializer = DeclarationIrBuilder( + context, + symbol, + symbol.owner.startOffset, + symbol.owner.endOffset, + ).run { + irExprBody(irCall(initExpression)) + } + } + } else { + null + } + irClass.functions.singleOrNull { + it.name.identifier == "fromJson" + }?.takeIf { + it.body == null + }?.let { function -> + KudosFromJsonFunctionBuilder(irClass, function, context, kudosStatusField, validatorFunction).generateBody() + } } } } diff --git a/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosNames.kt b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosNames.kt index 9301ba2..f3dd1d4 100644 --- a/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosNames.kt +++ b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/KudosNames.kt @@ -16,6 +16,7 @@ package com.kanyun.kudos.compiler +import org.jetbrains.kotlin.name.CallableId import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.Name @@ -41,10 +42,18 @@ object KudosNames { val KUDOS_VALIDATOR_CLASS_ID = ClassId(FqName("com.kanyun.kudos.validator"), Name.identifier("KudosValidator")) val KUDOS_JSON_ADAPTER_CLASS_ID = ClassId(FqName("com.kanyun.kudos.json.reader.adapter"), Name.identifier("KudosJsonAdapter")) val JSON_READER_CLASS_ID = ClassId.fromString("android/util/JsonReader") + val JSON_TOKEN_CLASS_ID = ClassId(FqName("android.util"), Name.identifier("JsonToken")) + + // CallableId + val JSON_READER_SKIP_VALUE_CALLABLE_ID = CallableId(FqName("android.util"), FqName("JsonReader"), Name.identifier("skipValue")) + val JSON_READER_PEEK_CALLABLE_ID = CallableId(FqName("android.util"), FqName("JsonReader"), Name.identifier("peek")) + val JSON_TOKEN_NULL_CALLABLE_ID = CallableId(FqName("android.util"), FqName("JsonToken"), Name.identifier("NULL")) // Name.identifier val KUDOS_FROM_JSON_IDENTIFIER = Name.identifier("fromJson") val JSON_READER_IDENTIFIER = Name.identifier("jsonReader") + val KUDOS_FIELD_STATUS_MAP_IDENTIFIER = Name.identifier("kudosFieldStatusMap") + val JSON_TOKEN_NULL_IDENTIFIER = Name.identifier("NULL") val CONTAINER_FQ_NAMES = setOf( "kotlin.Array", diff --git a/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/k1/symbol/FromJsonFunctionDescriptorImpl.kt b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/k1/symbol/FromJsonFunctionDescriptorImpl.kt index 8a1fae3..08ce71b 100644 --- a/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/k1/symbol/FromJsonFunctionDescriptorImpl.kt +++ b/kudos-compiler/src/main/java/com/kanyun/kudos/compiler/k1/symbol/FromJsonFunctionDescriptorImpl.kt @@ -44,7 +44,7 @@ class FromJsonFunctionDescriptorImpl( emptyList(), valueParameters, classDescriptor.defaultType, - Modality.FINAL, + Modality.OPEN, DescriptorVisibilities.PUBLIC, ) } diff --git a/kudos-compiler/src/test/java/com/kanyun/kudos/compiler/KudosTests.kt b/kudos-compiler/src/test/java/com/kanyun/kudos/compiler/KudosTests.kt index 8af1077..93bb0c6 100644 --- a/kudos-compiler/src/test/java/com/kanyun/kudos/compiler/KudosTests.kt +++ b/kudos-compiler/src/test/java/com/kanyun/kudos/compiler/KudosTests.kt @@ -36,17 +36,20 @@ class KudosTests : TestBase() { @Test fun `common_initBlock`() = testBase() + @Test + fun `common_notNull`() = testBase() + @Test fun `common_propertyTypeCheck`() = testBase() @Test - fun `gson_jsonAdapterCheck`() = testBase() + fun `common_validator`() = testBase() @Test - fun `gson_notNull`() = testBase() + fun `gson_jsonAdapterCheck`() = testBase() @Test - fun `gson_validator`() = testBase() + fun `gson_notNull`() = testBase() @Test fun `jsonReader_deserialize`() = testBase() @@ -63,6 +66,9 @@ class KudosTests : TestBase() { @Test fun `jsonReader_deserializeSetType`() = testBase() + @Test + fun `jsonReader_notNull`() = testBase() + @Test fun `jsonReader_simple`() = testBase() } diff --git a/kudos-compiler/src/test/java/com/kanyun/kudos/compiler/base/TestBase.kt b/kudos-compiler/src/test/java/com/kanyun/kudos/compiler/base/TestBase.kt index b996869..4ba48aa 100644 --- a/kudos-compiler/src/test/java/com/kanyun/kudos/compiler/base/TestBase.kt +++ b/kudos-compiler/src/test/java/com/kanyun/kudos/compiler/base/TestBase.kt @@ -17,7 +17,7 @@ package com.kanyun.kudos.compiler.base import com.bennyhuo.kotlin.compiletesting.extensions.module.COMPILER_OUTPUT_LEVEL_WARN -import com.bennyhuo.kotlin.compiletesting.extensions.module.IR_OUTPUT_TYPE_KOTLIN_LIKE +import com.bennyhuo.kotlin.compiletesting.extensions.module.IR_OUTPUT_TYPE_KOTLIN_LIKE_JC import com.bennyhuo.kotlin.compiletesting.extensions.module.KotlinModule import com.bennyhuo.kotlin.compiletesting.extensions.module.checkResult import com.bennyhuo.kotlin.compiletesting.extensions.source.TextBasedModuleInfoLoader @@ -68,8 +68,9 @@ open class TestBase { import com.google.gson.annotations.JsonAdapter import com.kanyun.kudos.gson.kudosGson import com.kanyun.kudos.gson.adapter.KudosReflectiveTypeAdapterFactory + import java.lang.reflect.Type - inline fun deserialize(string: String): T? { + inline fun deserialize(string: String, type: Type = T::class.java): T? { val gson = kudosGson() return try { val t: T = gson.fromJson(string, object: TypeToken() {}.type) @@ -89,8 +90,9 @@ open class TestBase { import com.kanyun.kudos.jackson.kudosObjectMapper import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.databind.DeserializationFeature + import java.lang.reflect.Type - inline fun deserialize(string: String): T? { + inline fun deserialize(string: String, type: Type = T::class.java): T? { val mapper = kudosObjectMapper() mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); mapper.disable(DeserializationFeature.WRAP_EXCEPTIONS); @@ -110,10 +112,11 @@ open class TestBase { return """ // FILE: JsonReader.kt import com.kanyun.kudos.json.reader.KudosAndroidJsonReader + import java.lang.reflect.Type - inline fun deserialize(string: String): T? { + inline fun deserialize(string: String, type: Type = T::class.java): T? { return try { - val t: T = KudosAndroidJsonReader.fromJson(string) + val t: T = KudosAndroidJsonReader.fromJson(string, type) println(t) t } catch (e: Exception) { @@ -146,7 +149,7 @@ open class TestBase { executeEntries = true, checkCompilerOutput = true, compilerOutputLevel = COMPILER_OUTPUT_LEVEL_WARN, - irOutputType = IR_OUTPUT_TYPE_KOTLIN_LIKE, + irOutputType = IR_OUTPUT_TYPE_KOTLIN_LIKE_JC, ) } diff --git a/kudos-compiler/testData/common/notNull.kt b/kudos-compiler/testData/common/notNull.kt new file mode 100644 index 0000000..3d92aca --- /dev/null +++ b/kudos-compiler/testData/common/notNull.kt @@ -0,0 +1,78 @@ +import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl + +// SOURCE +{{deserialize}} +// FILE: Main.kt [MainKt#main] +import com.kanyun.kudos.annotations.Kudos +import com.kanyun.kudos.collections.KudosCollection +import com.kanyun.kudos.collections.KudosList +import com.kanyun.kudos.collections.KudosSet +import com.kanyun.kudos.json.reader.adapter.ParameterizedTypeImpl + +@Kudos +data class User(val id: Long, val name: String, val age: Int = 10) { + val city: String = "beijing" +} + +@Kudos +data class Collections( + val list: List, + val list2: List, + val list3: List?, +) +@Kudos +class Arrays( + val array: Array, + val array2: Array, + val array3: Array?, +) { + override fun toString(): String { + return "Arrays(array=${array.contentToString()}, array2=${array2.contentToString()}, array3=${array3?.contentToString()})" + } +} + +@Kudos +class Project(val id: Int,val projectDesc: Desc) + +@Kudos +class Desc(val des: String) + +fun main() { + deserialize("""{}""") + deserialize("""{"id": 10}""") + deserialize("""{"name": "Bob"}""") + deserialize>("""[{"id": 10}, {"id": 11}]""", ParameterizedTypeImpl(List::class.java, arrayOf(User::class.java))) + deserialize>("""[{"id": 10, "name": "Bob"}]""", ParameterizedTypeImpl(KudosCollection::class.java, arrayOf(User::class.java))) + deserialize>("""[null]""", ParameterizedTypeImpl(KudosCollection::class.java, arrayOf(User::class.java))) + deserialize>("""[null]""", ParameterizedTypeImpl(KudosList::class.java, arrayOf(User::class.java))) + deserialize>("""[null]""", ParameterizedTypeImpl(KudosSet::class.java, arrayOf(User::class.java))) + + // Maybe supported with Java 8 annotated type. But ... not the moment. + deserialize>("""[null]""", ParameterizedTypeImpl(List::class.java, arrayOf(User::class.java))) + deserialize("""{"name": "Bob"}""") + deserialize("""{"id": 10, "name": "Bob"}""") + + deserialize("""{"list": [null], "list2": []}""") + deserialize("""{"list": ["kudos"], "list2": [null]}""") + + deserialize("""{"array": [null], "array2": []}""") + deserialize("""{"array": ["kudos"], "array2": [null]}""") +} + +// EXPECT +// FILE: MainKt.main.stdout +java.lang.NullPointerException: Missing non-null field 'id'. +java.lang.NullPointerException: Missing non-null field 'name'. +java.lang.NullPointerException: Missing non-null field 'id'. +java.lang.NullPointerException: Missing non-null field 'name'. +[User(id=10, name=Bob, age=10)] +java.lang.NullPointerException: Element cannot be null for com.kanyun.kudos.collections.KudosCollection. +java.lang.NullPointerException: Element cannot be null for com.kanyun.kudos.collections.KudosList. +java.lang.NullPointerException: Element cannot be null for com.kanyun.kudos.collections.KudosSet. +[null] +java.lang.NullPointerException: Missing non-null field 'id'. +User(id=10, name=Bob, age=10) +java.lang.NullPointerException: Element must not be null in List 'list'. +Collections(list=[kudos], list2=[null], list3=null) +java.lang.NullPointerException: Element must not be null in array 'array'. +Arrays(array=[kudos], array2=[null], array3=null) \ No newline at end of file diff --git a/kudos-compiler/testData/gson/validator.kt b/kudos-compiler/testData/common/validator.kt similarity index 94% rename from kudos-compiler/testData/gson/validator.kt rename to kudos-compiler/testData/common/validator.kt index 3ae6779..558b364 100644 --- a/kudos-compiler/testData/gson/validator.kt +++ b/kudos-compiler/testData/common/validator.kt @@ -62,7 +62,7 @@ import com.kanyun.kudos.test.User fun main() { (User() as KudosValidator).validate(emptyMap()) - (Developer("kanyun") as KudosValidator).validate(emptyMap()) + (Developer("kanyun") as KudosValidator).validate(hashMapOf("company" to true)) } // EXPECT diff --git a/kudos-compiler/testData/gson/notNull.kt b/kudos-compiler/testData/gson/notNull.kt index 0961a68..a66fc37 100644 --- a/kudos-compiler/testData/gson/notNull.kt +++ b/kudos-compiler/testData/gson/notNull.kt @@ -1,3 +1,5 @@ +import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl + // SOURCE {{deserialize}} // FILE: Main.kt [MainKt#main] @@ -5,6 +7,7 @@ import com.kanyun.kudos.annotations.Kudos import com.kanyun.kudos.collections.KudosCollection import com.kanyun.kudos.collections.KudosList import com.kanyun.kudos.collections.KudosSet +import com.kanyun.kudos.json.reader.adapter.ParameterizedTypeImpl @Kudos data class User(val id: Long, val name: String, val age: Int = 10) { @@ -28,42 +31,16 @@ class Arrays( } } -fun main() { - deserialize("""{}""") - deserialize("""{"id": 10}""") - deserialize("""{"name": "Bod"}""") - deserialize>("""[{"id": 10}, {"id": 11}]""") - deserialize>("""[{"id": 10, "name": "Bob"}]""") - deserialize>("""[null]""") - deserialize>("""[null]""") - deserialize>("""[null]""") - - // Maybe supported with Java 8 annotated type. But ... not the moment. - deserialize>("""[null]""") - deserialize("""{"name": "Bob"}""") - deserialize("""{"id": 10, "name": "Bob"}""") +@Kudos +class Project(val id: Int,val projectDesc: Desc) - deserialize("""{"list": [null], "list2": []}""") - deserialize("""{"list": ["kudos"], "list2": [null]}""") +@Kudos +class Desc(val des: String) - deserialize("""{"array": [null], "array2": []}""") - deserialize("""{"array": ["kudos"], "array2": [null]}""") +fun main() { + deserialize("""{"id": 10, "projectDesc": null}""") } // EXPECT // FILE: MainKt.main.stdout -java.lang.NullPointerException: Missing non-null field 'id'. -java.lang.NullPointerException: Missing non-null field 'name'. -java.lang.NullPointerException: Missing non-null field 'id'. -java.lang.NullPointerException: Missing non-null field 'name'. -[User(id=10, name=Bob, age=10)] -java.lang.NullPointerException: Element cannot be null for com.kanyun.kudos.collections.KudosCollection. -java.lang.NullPointerException: Element cannot be null for com.kanyun.kudos.collections.KudosList. -java.lang.NullPointerException: Element cannot be null for com.kanyun.kudos.collections.KudosSet. -[null] -java.lang.NullPointerException: Missing non-null field 'id'. -User(id=10, name=Bob, age=10) -java.lang.NullPointerException: Element must not be null in List 'list'. -Collections(list=[kudos], list2=[null], list3=null) -java.lang.NullPointerException: Element must not be null in array 'array'. -Arrays(array=[kudos], array2=[null], array3=null) +java.lang.NullPointerException: Missing non-null field 'projectDesc'. \ No newline at end of file diff --git a/kudos-compiler/testData/jsonReader/notNull.kt b/kudos-compiler/testData/jsonReader/notNull.kt new file mode 100644 index 0000000..6f276d7 --- /dev/null +++ b/kudos-compiler/testData/jsonReader/notNull.kt @@ -0,0 +1,24 @@ +import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl + +// SOURCE +{{deserialize}} +// FILE: Main.kt [MainKt#main] +import com.kanyun.kudos.annotations.Kudos +import com.kanyun.kudos.collections.KudosCollection +import com.kanyun.kudos.collections.KudosList +import com.kanyun.kudos.collections.KudosSet +import com.kanyun.kudos.json.reader.adapter.ParameterizedTypeImpl + +@Kudos +class Project(val id: Int,val projectDesc: Desc) + +@Kudos +class Desc(val des: String) + +fun main() { + deserialize("""{"id": 10, "projectDesc": null}""") +} + +// EXPECT +// FILE: MainKt.main.stdout +java.lang.NullPointerException: Missing non-null field 'projectDesc'. \ No newline at end of file diff --git a/kudos-compiler/testData/jsonReader/simple.kt b/kudos-compiler/testData/jsonReader/simple.kt index 6e4c670..01192d3 100644 --- a/kudos-compiler/testData/jsonReader/simple.kt +++ b/kudos-compiler/testData/jsonReader/simple.kt @@ -15,102 +15,85 @@ class Project(val projectName: String, val projectId: Int, val tags: List { - constructor(descDetail: String) /* primary */ { - super/*Any*/() - /* () */ - - } - - val descDetail: String - field = descDetail - get - - /* fake */ override operator fun equals(other: Any?): Boolean - /* fake */ override fun hashCode(): Int - /* fake */ override fun toString(): String +class Desc(val descDetail: String) : KudosValidator, KudosJsonAdapter { override fun fromJson(jsonReader: JsonReader): Desc { jsonReader.beginObject() - while (jsonReader.hasNext()) { // BLOCK - val tmp0: @FlexibleNullability String? = jsonReader.nextName() + while (jsonReader.hasNext()) { + val tmp0 = jsonReader.nextName() + if (jsonReader.peek() == JsonToken.NULL) { + jsonReader.skipValue() + continue + } when { - EQEQ(arg0 = tmp0, arg1 = "descDetail") -> .#descDetail = jsonReader.nextString() - else -> jsonReader.skipValue() + tmp0 == "descDetail" -> { + .descDetail = jsonReader.nextString() + .kudosFieldStatusMap.put("descDetail", .descDetail != null) + } + else -> { + jsonReader.skipValue() + } } } jsonReader.endObject() + validate(.kudosFieldStatusMap) return } - - constructor() { - super/*Any*/() - /* () */ - + constructor{ + ctor() + init() } - override fun validate(status: Map) { - validateField(name = "descDetail", fieldStatus = status) + validateField("descDetail", status) } - + private var kudosFieldStatusMap: Map = hashMapOf() } - @Kudos -class Project : KudosValidator, KudosJsonAdapter { - constructor(projectName: String, projectId: Int, tags: List, desc: Desc) /* primary */ { - super/*Any*/() - /* () */ - - } - - val projectName: String - field = projectName - get - - val projectId: Int - field = projectId - get - - val tags: List - field = tags - get - - val desc: Desc - field = desc - get - - /* fake */ override operator fun equals(other: Any?): Boolean - /* fake */ override fun hashCode(): Int - /* fake */ override fun toString(): String +class Project(val projectName: String, val projectId: Int, val tags: List, val desc: Desc) : KudosValidator, KudosJsonAdapter { override fun fromJson(jsonReader: JsonReader): Project { jsonReader.beginObject() - while (jsonReader.hasNext()) { // BLOCK - val tmp0: @FlexibleNullability String? = jsonReader.nextName() + while (jsonReader.hasNext()) { + val tmp0 = jsonReader.nextName() + if (jsonReader.peek() == JsonToken.NULL) { + jsonReader.skipValue() + continue + } when { - EQEQ(arg0 = tmp0, arg1 = "projectName") -> .#projectName = jsonReader.nextString() - EQEQ(arg0 = tmp0, arg1 = "projectId") -> .#projectId = jsonReader.nextInt() - EQEQ(arg0 = tmp0, arg1 = "tags") -> .#tags = parseKudosObject(jsonReader = jsonReader, type = ParameterizedTypeImpl(type = KClass::class.(), typeArguments = arrayOf(elements = [KClass::class.()]))) - EQEQ(arg0 = tmp0, arg1 = "desc") -> .#desc = parseKudosObject(jsonReader = jsonReader, type = KClass::class.()) - else -> jsonReader.skipValue() + tmp0 == "projectName" -> { + .projectName = jsonReader.nextString() + .kudosFieldStatusMap.put("projectName", .projectName != null) + } + tmp0 == "projectId" -> { + .projectId = jsonReader.nextInt() + .kudosFieldStatusMap.put("projectId", .projectId != null) + } + tmp0 == "tags" -> { + .tags = parseKudosObject(jsonReader, ParameterizedTypeImpl(List::class.javaObjectType, arrayOf(String::class.javaObjectType))) + .kudosFieldStatusMap.put("tags", .tags != null) + } + tmp0 == "desc" -> { + .desc = parseKudosObject(jsonReader, Desc::class.javaObjectType) + .kudosFieldStatusMap.put("desc", .desc != null) + } + else -> { + jsonReader.skipValue() + } } } jsonReader.endObject() + validate(.kudosFieldStatusMap) return } - - constructor() { - super/*Any*/() - /* () */ - + constructor{ + ctor() + init() } - override fun validate(status: Map) { - validateField(name = "projectName", fieldStatus = status) - validateField(name = "projectId", fieldStatus = status) - validateField(name = "tags", fieldStatus = status) - validateField(name = "desc", fieldStatus = status) - validateCollection(name = "tags", collection = .#tags, typeName = "List") + validateField("projectName", status) + validateField("projectId", status) + validateField("tags", status) + validateField("desc", status) + validateCollection("tags", .tags, "List") } - + private var kudosFieldStatusMap: Map = hashMapOf() } \ No newline at end of file diff --git a/kudos-gson/src/main/java/com/kanyun/kudos/gson/adapter/BoundField.java b/kudos-gson/src/main/java/com/kanyun/kudos/gson/adapter/BoundField.java index f594f0b..9dd9a2c 100644 --- a/kudos-gson/src/main/java/com/kanyun/kudos/gson/adapter/BoundField.java +++ b/kudos-gson/src/main/java/com/kanyun/kudos/gson/adapter/BoundField.java @@ -40,8 +40,10 @@ public void write(JsonWriter writer, Object value) throws IOException, IllegalAc public void read(JsonReader reader, Object value) throws IOException, IllegalAccessException { Object fieldValue = typeAdapter.read(reader); - if (fieldValue != null || !field.getType().isPrimitive()) { + if (fieldValue != null) { isInitialized = true; + } + if (fieldValue != null || !field.getType().isPrimitive()) { field.set(value, fieldValue); } } diff --git a/kudos-runtime/src/main/java/com/kanyun/kudos/validator/KudosValidator.kt b/kudos-runtime/src/main/java/com/kanyun/kudos/validator/KudosValidator.kt index 1c1b4af..1846a62 100644 --- a/kudos-runtime/src/main/java/com/kanyun/kudos/validator/KudosValidator.kt +++ b/kudos-runtime/src/main/java/com/kanyun/kudos/validator/KudosValidator.kt @@ -24,7 +24,7 @@ interface KudosValidator { } fun validateField(name: String, fieldStatus: Map) { - if (fieldStatus[name] == false) { + if (fieldStatus[name] != true) { throw NullPointerException("Missing non-null field '$name'.") } }