Skip to content

Commit

Permalink
Feature/kson (#9)
Browse files Browse the repository at this point in the history
* ADD: jsonReader support null validator
  • Loading branch information
RicardoJiang authored Nov 14, 2023
1 parent fa262fd commit b6b9a80
Show file tree
Hide file tree
Showing 18 changed files with 348 additions and 145 deletions.
4 changes: 4 additions & 0 deletions .github/scripts/tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 -
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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 <reified T> fromJson(json: String): T {
Expand All @@ -33,4 +35,9 @@ object KudosAndroidJsonReader {
throw IllegalArgumentException("class ${clazz.name} must implement KudosJsonAdapter")
}
}

fun <T> fromJson(json: String, type: Type): T {
val jsonReader = JsonReader(json.reader())
return parseKudosObject(jsonReader, type) as T
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,26 @@
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<T> {
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 {
parseKudosObjectInternal(jsonReader, type, arrayOf())
}
}

private fun parseKudosList(jsonReader: JsonReader, typeArguments: Array<Type>): List<Any> {
val list = mutableListOf<Any>()
private fun parseKudosList(jsonReader: JsonReader, typeArguments: Array<Type>): List<Any?> {
val list = mutableListOf<Any?>()
jsonReader.beginArray()
while (jsonReader.hasNext()) {
list.add(parseKudosObject(jsonReader, typeArguments[0]))
Expand All @@ -41,6 +45,19 @@ private fun parseKudosList(jsonReader: JsonReader, typeArguments: Array<Type>):
return list
}

private fun parseKudosCollection(jsonReader: JsonReader, type: Type, typeArguments: Array<Type>): KudosCollection<Any> {
val list = KudosList<Any>()
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<Type>): Any {
val list = parseKudosList(jsonReader, typeArguments)
val array = java.lang.reflect.Array.newInstance(typeArguments[0] as Class<*>, list.size)
Expand All @@ -50,8 +67,8 @@ private fun parseKudosArray(jsonReader: JsonReader, typeArguments: Array<Type>):
return array
}

private fun parseKudosMap(jsonReader: JsonReader, typeArguments: Array<Type>): Map<String, Any> {
val resultMap = mutableMapOf<String, Any>()
private fun parseKudosMap(jsonReader: JsonReader, typeArguments: Array<Type>): Map<String, Any?> {
val resultMap = mutableMapOf<String, Any?>()
jsonReader.beginObject()
while (jsonReader.hasNext()) {
val key = jsonReader.nextName()
Expand All @@ -66,7 +83,11 @@ private fun parseKudosObjectInternal(
jsonReader: JsonReader,
type: Type,
typeArguments: Array<Type>,
): 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()
Expand All @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion kudos-compiler/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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<IrEnumEntry>().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<IrBranch>()
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)
},
),
Expand All @@ -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(),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -90,8 +95,8 @@ class KudosIrClassTransformer(
generateJsonAdapter()
}
generateNoArgConstructor()
generateValidator()
generateFromJson()
val validatorFunction = generateValidator()
generateFromJson(validatorFunction)
}

private fun generateJsonAdapter() {
Expand Down Expand Up @@ -201,7 +206,7 @@ class KudosIrClassTransformer(
}
}

private fun generateValidator() {
private fun generateValidator(): IrSimpleFunction? {
val nonDefaults = ArrayList<String>()
val collections = ArrayList<IrField>()
val arrays = ArrayList<IrField>()
Expand Down Expand Up @@ -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,
Expand All @@ -245,7 +250,7 @@ class KudosIrClassTransformer(
}

if (validateFunction?.isFakeOverride == false) {
return
return validateFunction
} else if (validateFunction?.isFakeOverride == true) {
irClass.declarations.remove(validateFunction)
}
Expand Down Expand Up @@ -347,6 +352,7 @@ class KudosIrClassTransformer(
}
}
}
return validateFunction
}

private fun needsNoargConstructor(declaration: IrClass): Boolean =
Expand All @@ -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()
}
}
}
}
Loading

0 comments on commit b6b9a80

Please sign in to comment.