diff --git a/compiler-plugin/src/main/kotlin/com/arkivanov/parcelize/darwin/Coder.kt b/compiler-plugin/src/main/kotlin/com/arkivanov/parcelize/darwin/Coder.kt index cbbada0..43c144f 100644 --- a/compiler-plugin/src/main/kotlin/com/arkivanov/parcelize/darwin/Coder.kt +++ b/compiler-plugin/src/main/kotlin/com/arkivanov/parcelize/darwin/Coder.kt @@ -1,5 +1,6 @@ package com.arkivanov.parcelize.darwin +import org.jetbrains.kotlin.ir.backend.js.utils.typeArguments import org.jetbrains.kotlin.ir.builders.IrBuilderWithScope import org.jetbrains.kotlin.ir.builders.createTmpVariable import org.jetbrains.kotlin.ir.builders.irBlock @@ -16,6 +17,8 @@ import org.jetbrains.kotlin.ir.builders.irNotEquals import org.jetbrains.kotlin.ir.builders.irNull import org.jetbrains.kotlin.ir.builders.irString import org.jetbrains.kotlin.ir.builders.irTrue +import org.jetbrains.kotlin.ir.declarations.IrClass +import org.jetbrains.kotlin.ir.declarations.IrField import org.jetbrains.kotlin.ir.expressions.IrExpression import org.jetbrains.kotlin.ir.symbols.IrConstructorSymbol import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol @@ -27,6 +30,10 @@ import org.jetbrains.kotlin.ir.types.makeNullable import org.jetbrains.kotlin.ir.util.companionObject import org.jetbrains.kotlin.ir.util.functions import org.jetbrains.kotlin.ir.util.getPropertyGetter +import org.jetbrains.kotlin.ir.util.isAnnotation +import org.jetbrains.kotlin.ir.util.isObject +import org.jetbrains.kotlin.ir.util.parentAsClass +import org.jetbrains.kotlin.ir.util.render interface Coder { @@ -46,8 +53,21 @@ class CoderFactory( private val symbols: Symbols, ) { - fun get(type: IrType): Coder = - get(type = type.toSupportedType(symbols)) + fun get(field: IrField): Coder = + get( + type = IrTypeToSupportedTypeMapper( + symbols = symbols, + typeParcelers = field.parentAsClass.extractTypeParcelers(), + ).map(type = field.type), + ) + + private fun IrClass.extractTypeParcelers(): Map = + annotations + .filter { it.isAnnotation(typeParcelerName) } + .associateBy( + keySelector = { requireNotNull(it.typeArguments[0]) }, + valueTransform = { requireNotNull(it.typeArguments[1]) }, + ) fun get(type: SupportedType): Coder = when (type) { @@ -121,6 +141,13 @@ class CoderFactory( decodeFunction = symbols.decodeDouble, ) + is SupportedType.Custom -> + CustomCoder( + symbols = symbols, + type = type.type, + parcelerType = type.parcelerType, + ) + is SupportedType.String -> StringCoder(symbols = symbols) is SupportedType.Enum -> @@ -246,6 +273,85 @@ private class PrimitiveCoder( } } +private class CustomCoder( + private val symbols: Symbols, + private val type: IrType, + private val parcelerType: IrType, +) : Coder { + init { + require(parcelerType.requireClass().isObject) { "Parceler must be an object: ${parcelerType.render()}" } + } + + override fun IrBuilderWithScope.encode(coder: IrExpression, value: IrExpression, key: IrExpression): IrExpression = + irBlock { + val archiver = + createTmpVariable( + irCallCompat( + callee = symbols.nsKeyedArchiverConstructor, + arguments = listOf(irTrue()), + ) + ) + + +irCallCompat( + callee = parcelerType.requireClass().requireFunction( + name = "write", + valueParameterTypes = listOf(symbols.nsCoderType), + extensionReceiverParameterType = type, + ), + extensionReceiver = value, + dispatchReceiver = irGetObject(parcelerType.classOrNull!!), + arguments = listOf(irGet(archiver)), + ) + + +irCallCompat( + callee = symbols.encodeObject, + extensionReceiver = coder, + arguments = listOf( + irCallCompat(callee = symbols.encodedData, dispatchReceiver = irGet(archiver)), + key, + ) + ) + } + + override fun IrBuilderWithScope.decode(coder: IrExpression, key: IrExpression): IrExpression = + irBlock { + val data = + createTmpVariable( + irCallCompat( + callee = symbols.decodeObject, + extensionReceiver = coder, + arguments = listOf( + irGetObject(symbols.nsDataClass.owner.companionObject()!!.symbol), + key, + ), + ) + ) + + val unarchiver = + createTmpVariable( + irCallCompat( + callee = symbols.nsKeyedUnarchiverConstructor, + arguments = listOf(irGet(data)), + ) + ) + + +irCallCompat( + callee = symbols.setRequireSecureCoding, + dispatchReceiver = irGet(unarchiver), + arguments = listOf(irTrue()), + ) + + +irCallCompat( + callee = parcelerType.requireClass().requireFunction( + name = "create", + valueParameterTypes = listOf(symbols.nsCoderType), + ), + dispatchReceiver = irGetObject(parcelerType.classOrNull!!), + arguments = listOf(irGet(unarchiver)), + ) + } +} + private class StringCoder( private val symbols: Symbols, ) : Coder { @@ -474,7 +580,8 @@ private class CollectionCoder( condition = irNotEquals(arg1 = irGet(index), arg2 = irGet(size)), body = irBlock { +irCallCompat( - callee = collectionConstructor.owner.returnType.requireClass().requireFunction(name = "add"), + callee = collectionConstructor.owner.returnType.requireClass() + .requireFunction(name = "add"), dispatchReceiver = irGet(collection), arguments = listOf( with(itemCoder) { diff --git a/compiler-plugin/src/main/kotlin/com/arkivanov/parcelize/darwin/ParcelizeClassLoweringPass.kt b/compiler-plugin/src/main/kotlin/com/arkivanov/parcelize/darwin/ParcelizeClassLoweringPass.kt index e847b47..d91e5c7 100644 --- a/compiler-plugin/src/main/kotlin/com/arkivanov/parcelize/darwin/ParcelizeClassLoweringPass.kt +++ b/compiler-plugin/src/main/kotlin/com/arkivanov/parcelize/darwin/ParcelizeClassLoweringPass.kt @@ -290,7 +290,7 @@ class ParcelizeClassLoweringPass( } private fun IrBlockBuilder.addEncodeFieldStatement(field: IrField, data: IrExpression, coder: IrExpression) { - +with(coderFactory.get(field.type)) { + +with(coderFactory.get(field)) { encode( coder = coder, value = irGetField(data, field), @@ -396,7 +396,7 @@ class ParcelizeClassLoweringPass( ) { dataConstructorCall.putValueArgument( index = index, - valueArgument = with(coderFactory.get(field.type)) { + valueArgument = with(coderFactory.get(field)) { decode( coder = coder, key = irString(field.name.identifier), diff --git a/compiler-plugin/src/main/kotlin/com/arkivanov/parcelize/darwin/SupportedType.kt b/compiler-plugin/src/main/kotlin/com/arkivanov/parcelize/darwin/SupportedType.kt index 5b45496..52f9381 100644 --- a/compiler-plugin/src/main/kotlin/com/arkivanov/parcelize/darwin/SupportedType.kt +++ b/compiler-plugin/src/main/kotlin/com/arkivanov/parcelize/darwin/SupportedType.kt @@ -1,9 +1,12 @@ package com.arkivanov.parcelize.darwin import org.jetbrains.kotlin.backend.jvm.ir.erasedUpperBound +import org.jetbrains.kotlin.ir.backend.js.utils.typeArguments import org.jetbrains.kotlin.ir.types.IrType import org.jetbrains.kotlin.ir.types.typeOrNull import org.jetbrains.kotlin.ir.util.defaultType +import org.jetbrains.kotlin.ir.util.getAnnotation +import org.jetbrains.kotlin.ir.util.hasAnnotation import org.jetbrains.kotlin.ir.util.isEnumClass import org.jetbrains.kotlin.ir.util.render @@ -17,6 +20,7 @@ sealed interface SupportedType { data class PrimitiveFloat(val isNullable: Boolean) : SupportedType data class PrimitiveDouble(val isNullable: Boolean) : SupportedType data class PrimitiveBoolean(val isNullable: Boolean) : SupportedType + data class Custom(val type: IrType, val parcelerType: IrType) : SupportedType object String : SupportedType data class Enum(val type: IrType) : SupportedType object Parcelable : SupportedType @@ -28,58 +32,76 @@ sealed interface SupportedType { data class MutableMap(val keyType: SupportedType, val valueType: SupportedType) : SupportedType } -fun IrType.toSupportedType(symbols: Symbols): SupportedType = - when { - this == symbols.intType -> SupportedType.PrimitiveInt(isNullable = false) - this == symbols.intNType -> SupportedType.PrimitiveInt(isNullable = true) - this == symbols.longType -> SupportedType.PrimitiveLong(isNullable = false) - this == symbols.longNType -> SupportedType.PrimitiveLong(isNullable = true) - this == symbols.shortType -> SupportedType.PrimitiveShort(isNullable = false) - this == symbols.shortNType -> SupportedType.PrimitiveShort(isNullable = true) - this == symbols.byteType -> SupportedType.PrimitiveByte(isNullable = false) - this == symbols.byteNType -> SupportedType.PrimitiveByte(isNullable = true) - this == symbols.charType -> SupportedType.PrimitiveChar(isNullable = false) - this == symbols.charNType -> SupportedType.PrimitiveChar(isNullable = true) - this == symbols.floatType -> SupportedType.PrimitiveFloat(isNullable = false) - this == symbols.floatNType -> SupportedType.PrimitiveFloat(isNullable = true) - this == symbols.doubleType -> SupportedType.PrimitiveDouble(isNullable = false) - this == symbols.doubleNType -> SupportedType.PrimitiveDouble(isNullable = true) - this == symbols.booleanType -> SupportedType.PrimitiveBoolean(isNullable = false) - this == symbols.booleanNType -> SupportedType.PrimitiveBoolean(isNullable = true) - (this == symbols.stringType) || (this == symbols.stringNType) -> SupportedType.String - erasedUpperBound.isEnumClass -> SupportedType.Enum(type = this) - isParcelable() -> SupportedType.Parcelable - - erasedUpperBoundType == symbols.listType -> - SupportedType.List(itemType = getTypeArgument(0).toSupportedType(symbols)) - - erasedUpperBoundType == symbols.mutableListType -> - SupportedType.MutableList(itemType = getTypeArgument(0).toSupportedType(symbols)) - - erasedUpperBoundType == symbols.setType -> - SupportedType.Set(itemType = getTypeArgument(0).toSupportedType(symbols)) - - erasedUpperBoundType == symbols.mutableSetType -> - SupportedType.MutableSet(itemType = getTypeArgument(0).toSupportedType(symbols)) - - erasedUpperBoundType == symbols.mapType -> - SupportedType.Map( - keyType = getTypeArgument(0).toSupportedType(symbols), - valueType = getTypeArgument(1).toSupportedType(symbols), - ) - - erasedUpperBoundType == symbols.mutableMapType -> - SupportedType.MutableMap( - keyType = getTypeArgument(0).toSupportedType(symbols), - valueType = getTypeArgument(1).toSupportedType(symbols), - ) - - else -> error("Unsupported type: ${render()}") - } - -private fun IrType.getTypeArgument(index: Int): IrType = - asIrSimpleType().arguments[index].typeOrNull!! - -private val IrType.erasedUpperBoundType: IrType - get() = erasedUpperBound.defaultType +class IrTypeToSupportedTypeMapper( + private val symbols: Symbols, + private val typeParcelers: Map, +) { + fun map(type: IrType): SupportedType = + when { + type == symbols.intType -> SupportedType.PrimitiveInt(isNullable = false) + type == symbols.intNType -> SupportedType.PrimitiveInt(isNullable = true) + type == symbols.longType -> SupportedType.PrimitiveLong(isNullable = false) + type == symbols.longNType -> SupportedType.PrimitiveLong(isNullable = true) + type == symbols.shortType -> SupportedType.PrimitiveShort(isNullable = false) + type == symbols.shortNType -> SupportedType.PrimitiveShort(isNullable = true) + type == symbols.byteType -> SupportedType.PrimitiveByte(isNullable = false) + type == symbols.byteNType -> SupportedType.PrimitiveByte(isNullable = true) + type == symbols.charType -> SupportedType.PrimitiveChar(isNullable = false) + type == symbols.charNType -> SupportedType.PrimitiveChar(isNullable = true) + type == symbols.floatType -> SupportedType.PrimitiveFloat(isNullable = false) + type == symbols.floatNType -> SupportedType.PrimitiveFloat(isNullable = true) + type == symbols.doubleType -> SupportedType.PrimitiveDouble(isNullable = false) + type == symbols.doubleNType -> SupportedType.PrimitiveDouble(isNullable = true) + type == symbols.booleanType -> SupportedType.PrimitiveBoolean(isNullable = false) + type == symbols.booleanNType -> SupportedType.PrimitiveBoolean(isNullable = true) + + type.hasAnnotation(writeWithName) -> + SupportedType.Custom( + type = type, + parcelerType = requireNotNull(type.getAnnotation(writeWithName)?.typeArguments?.first()), + ) + + type in typeParcelers -> + SupportedType.Custom( + type = type, + parcelerType = typeParcelers.getValue(type), + ) + + (type == symbols.stringType) || (type == symbols.stringNType) -> SupportedType.String + type.erasedUpperBound.isEnumClass -> SupportedType.Enum(type = type) + type.isParcelable() -> SupportedType.Parcelable + + type.erasedUpperBoundType == symbols.listType -> + SupportedType.List(itemType = map(type.getTypeArgument(0))) + + type.erasedUpperBoundType == symbols.mutableListType -> + SupportedType.MutableList(itemType = map(type.getTypeArgument(0))) + + type.erasedUpperBoundType == symbols.setType -> + SupportedType.Set(itemType = map(type.getTypeArgument(0))) + + type.erasedUpperBoundType == symbols.mutableSetType -> + SupportedType.MutableSet(itemType = map(type.getTypeArgument(0))) + + type.erasedUpperBoundType == symbols.mapType -> + SupportedType.Map( + keyType = map(type.getTypeArgument(0)), + valueType = map(type.getTypeArgument(1)), + ) + + type.erasedUpperBoundType == symbols.mutableMapType -> + SupportedType.MutableMap( + keyType = map(type.getTypeArgument(0)), + valueType = map(type.getTypeArgument(1)), + ) + + else -> error("Unsupported type: ${type.render()}") + } + + private fun IrType.getTypeArgument(index: Int): IrType = + asIrSimpleType().arguments[index].typeOrNull!! + + private val IrType.erasedUpperBoundType: IrType + get() = erasedUpperBound.defaultType +} diff --git a/compiler-plugin/src/main/kotlin/com/arkivanov/parcelize/darwin/Symbols.kt b/compiler-plugin/src/main/kotlin/com/arkivanov/parcelize/darwin/Symbols.kt index 4454c35..7760f17 100644 --- a/compiler-plugin/src/main/kotlin/com/arkivanov/parcelize/darwin/Symbols.kt +++ b/compiler-plugin/src/main/kotlin/com/arkivanov/parcelize/darwin/Symbols.kt @@ -14,6 +14,7 @@ interface Symbols { val nsObjectType: IrType val nsStringClass: IrClassSymbol + val nsDataClass: IrClassSymbol val nsLockType: IrType val nsSecureCodingType: IrType val nsSecureCodingMetaType: IrType @@ -59,6 +60,8 @@ interface Symbols { val hashSetConstructor: IrConstructorSymbol val hashMapConstructor: IrConstructorSymbol val nsMutableArrayConstructor: IrConstructorSymbol + val nsKeyedArchiverConstructor: IrConstructorSymbol + val nsKeyedUnarchiverConstructor: IrConstructorSymbol val illegalStateExceptionConstructor: IrConstructorSymbol val shortToInt: IrSimpleFunctionSymbol val intToShort: IrSimpleFunctionSymbol @@ -69,6 +72,8 @@ interface Symbols { val getCoding: IrSimpleFunctionSymbol val println: IrSimpleFunctionSymbol + val encodedData: IrSimpleFunctionSymbol + val setRequireSecureCoding: IrSimpleFunctionSymbol val encodeInt: IrSimpleFunctionSymbol val decodeInt: IrSimpleFunctionSymbol val encodeLong: IrSimpleFunctionSymbol @@ -84,13 +89,14 @@ interface Symbols { } class DefaultSymbols( - pluginContext: IrPluginContext, + private val pluginContext: IrPluginContext, ) : Symbols { private val parcelableClass: IrClassSymbol = pluginContext.referenceClass(parcelableClassId).require() override val nsObjectType: IrType = pluginContext.referenceClass(nsObjectClassId).require().defaultType override val nsStringClass: IrClassSymbol = pluginContext.referenceClass(nsStringClassId).require() + override val nsDataClass: IrClassSymbol = pluginContext.referenceClass(nsDataClassId).require() override val nsLockType: IrType = pluginContext.referenceClass(nsLockClassId).require().defaultType override val nsSecureCodingType: IrType = pluginContext.referenceClass(nsSecureCodingClassId).require().defaultType override val nsSecureCodingMetaType: IrType = pluginContext.referenceClass(nsSecureCodingMetaClassId).require().defaultType @@ -160,6 +166,20 @@ class DefaultSymbols( .first { it.valueParameters.isEmpty() } .symbol + override val nsKeyedArchiverConstructor: IrConstructorSymbol = + pluginContext.referenceClass(nsKeyedArchiverClassId).require() + .owner + .constructors + .first { it.valueParameters.size == 1 } + .symbol + + override val nsKeyedUnarchiverConstructor: IrConstructorSymbol = + pluginContext.referenceClass(nsKeyedUnarchiverClassId).require() + .owner + .constructors + .first { it.valueParameters.size == 1 } + .symbol + override val illegalStateExceptionConstructor: IrConstructorSymbol = pluginContext.referenceClass(ClassId("kotlin.IllegalStateException")).require() .owner @@ -181,6 +201,18 @@ class DefaultSymbols( valueParameterTypes = listOf(anyNType), ) + override val encodedData: IrSimpleFunctionSymbol = + pluginContext.referenceFunction( + callableId = CallableId(classId = nsKeyedArchiverClassId, callableName = "encodedData"), + valueParameterTypes = listOf(), + ) + + override val setRequireSecureCoding: IrSimpleFunctionSymbol = + pluginContext.referenceFunction( + callableId = CallableId(classId = nsKeyedUnarchiverClassId, callableName = "setRequiresSecureCoding"), + valueParameterTypes = listOf(booleanType), + ) + override val encodeInt: IrSimpleFunctionSymbol = pluginContext.referenceFunction( callableId = CallableId(packageName = packageFoundation, callableName = "encodeInt32"), diff --git a/compiler-plugin/src/main/kotlin/com/arkivanov/parcelize/darwin/Utils.kt b/compiler-plugin/src/main/kotlin/com/arkivanov/parcelize/darwin/Utils.kt index bfa7e91..610e217 100644 --- a/compiler-plugin/src/main/kotlin/com/arkivanov/parcelize/darwin/Utils.kt +++ b/compiler-plugin/src/main/kotlin/com/arkivanov/parcelize/darwin/Utils.kt @@ -59,14 +59,19 @@ val packageCollections: FqName = FqName("kotlin.collections") val parcelizeName = FqName("$packageRuntime.Parcelize") val parcelableClassId = ClassId(packageName = packageRuntime, className = "Parcelable") val parcelableName = parcelableClassId.asSingleFqName() +val typeParcelerName = FqName("$packageRuntime.TypeParceler") +val writeWithName = FqName("$packageRuntime.WriteWith") val nsSecureCodingClassId = ClassId(packageName = packageFoundation, className = "NSSecureCodingProtocol") val nsSecureCodingMetaClassId = ClassId(packageName = packageFoundation, className = "NSSecureCodingProtocolMeta") val nsCoderClassId = ClassId(packageName = packageFoundation, className = "NSCoder") val nsObjectClassId = ClassId(packageName = packageDarwin, className = "NSObject") val nsStringClassId = ClassId(packageName = packageFoundation, className = "NSString") +val nsDataClassId = ClassId(packageName = packageFoundation, className = "NSData") val nsLockClassId = ClassId(packageName = packageFoundation, className = "NSLock") val nsArrayClassId = ClassId(packageName = packageFoundation, className = "NSArray") val nsMutableArrayClassId = ClassId(packageName = packageFoundation, className = "NSMutableArray") +val nsKeyedArchiverClassId = ClassId(packageName = packageFoundation, className = "NSKeyedArchiver") +val nsKeyedUnarchiverClassId = ClassId(packageName = packageFoundation, className = "NSKeyedUnarchiver") val objCClassClassId = ClassId(packageName = packageCinterop, className = "ObjCClass") val exportObjCClassClassId = ClassId(packageName = packageCinterop, className = "ExportObjCClass") val arrayListClassId = ClassId(packageName = packageCollections, className = "ArrayList") @@ -97,6 +102,12 @@ fun CallableId(packageName: FqName, callableName: String): CallableId = callableName = Name.identifier(callableName), ) +fun CallableId(classId: ClassId, callableName: String): CallableId = + CallableId( + classId = classId, + callableName = Name.identifier(callableName), + ) + fun ClassId(packageName: FqName, className: String): ClassId = ClassId(packageName, Name.identifier(className)) @@ -138,10 +149,13 @@ fun IrClass.getFullCapitalizedName(): String { fun IrClass.requireFunction( name: String, valueParameterTypes: List? = null, + extensionReceiverParameterType: IrType? = null ): IrSimpleFunctionSymbol = functions.first { f -> (f.name.asString() == name) && - ((valueParameterTypes == null) || (f.valueParameters.map { it.type.classFqName } == valueParameterTypes.map { it.classFqName })) + ((valueParameterTypes == null) || + (f.valueParameters.map { it.type.classFqName } == valueParameterTypes.map { it.classFqName })) && + (f.extensionReceiverParameter?.type?.classFqName == extensionReceiverParameterType?.classFqName) }.symbol fun IrType.requireClass(): IrClass = @@ -178,10 +192,16 @@ fun IrBuilderWithScope.irCallCompat( fun IrBuilderWithScope.irCallCompat( callee: IrConstructorSymbol, + arguments: List = emptyList(), // FIXME: reuse? block: IrConstructorCall.() -> Unit = {}, ): IrExpression = irBlock(startOffset = SYNTHETIC_OFFSET, endOffset = SYNTHETIC_OFFSET) { - +irCall(callee, callee.owner.returnType).apply(block) + +irCall(callee, callee.owner.returnType).apply { + arguments.forEachIndexed { index, argument -> + putValueArgument(index, argument) + } + block() + } } fun IrBuilderWithScope.irCallCompat( diff --git a/runtime/src/darwinMain/kotlin/com/arkivanov/parcelize/darwin/Coding.kt b/runtime/src/darwinMain/kotlin/com/arkivanov/parcelize/darwin/Coding.kt index 831e56c..97b31a3 100644 --- a/runtime/src/darwinMain/kotlin/com/arkivanov/parcelize/darwin/Coding.kt +++ b/runtime/src/darwinMain/kotlin/com/arkivanov/parcelize/darwin/Coding.kt @@ -3,24 +3,63 @@ package com.arkivanov.parcelize.darwin import platform.Foundation.NSArray import platform.Foundation.NSCoder import platform.Foundation.NSLock +import platform.Foundation.decodeObjectForKey import platform.Foundation.decodeObjectOfClass import platform.Foundation.encodeObject import platform.Foundation.firstObject +/** + * Encodes the provided [Parcelable] [value] with the provided [key]. The [value] can be null. + */ fun NSCoder.encodeParcelableOrNull(value: Parcelable?, key: String) { val coding = value?.coding() encodeObject(coding, key) } +/** + * Decodes a previously encoded [Parcelable] with the provided [key]. The returned [Parcelable] can be null. + */ @Throws(IllegalStateException::class) @Suppress("UNCHECKED_CAST") fun NSCoder.decodeParcelableOrNull(key: String): T? = (decodeObjectOfClass(aClass = NSLock, forKey = key) as NSArray?)?.firstObject as T? +/** + * Encodes the provided [Parcelable] [value] with the provided [key]. + */ fun NSCoder.encodeParcelable(value: Parcelable, key: String) { encodeParcelableOrNull(value, key) } +/** + * Decodes a previously encoded [Parcelable] with the provided [key]. + */ @Throws(IllegalStateException::class) fun NSCoder.decodeParcelable(key: String): T = requireNotNull(decodeParcelableOrNull(key = key)) + +/** + * Encodes the provided [String] [value] with the provided [key]. The [value] can be null. + */ +fun NSCoder.encodeStringOrNull(value: String?, key: String) { + encodeObject(value, key) +} + +/** + * Decodes a previously encoded [String] with the provided [key]. The returned [String] can be null. + */ +fun NSCoder.decodeStringOrNull(key: String): String? = + decodeObjectForKey(key = key) as String? + +/** + * Encodes the provided [String] [value] with the provided [key]. + */ +fun NSCoder.encodeString(value: String, key: String) { + encodeObject(value, key) +} + +/** + * Decodes a previously encoded [String] with the provided [key]. + */ +fun NSCoder.decodeString(key: String): String = + decodeObjectForKey(key = key) as String diff --git a/runtime/src/darwinMain/kotlin/com/arkivanov/parcelize/darwin/Parcelable.kt b/runtime/src/darwinMain/kotlin/com/arkivanov/parcelize/darwin/Parcelable.kt index 58b556e..48c4894 100644 --- a/runtime/src/darwinMain/kotlin/com/arkivanov/parcelize/darwin/Parcelable.kt +++ b/runtime/src/darwinMain/kotlin/com/arkivanov/parcelize/darwin/Parcelable.kt @@ -2,7 +2,13 @@ package com.arkivanov.parcelize.darwin import platform.Foundation.NSSecureCodingProtocol +/** + * Interface for serializable classes. The serialization is performed via [NSSecureCodingProtocol]. + */ interface Parcelable { + /** + * Returns an instance of [NSSecureCodingProtocol] responsible for serialization and deserialization. + */ fun coding(): NSSecureCodingProtocol = NotImplementedCoding() } diff --git a/runtime/src/darwinMain/kotlin/com/arkivanov/parcelize/darwin/Parceler.kt b/runtime/src/darwinMain/kotlin/com/arkivanov/parcelize/darwin/Parceler.kt new file mode 100644 index 0000000..1982a43 --- /dev/null +++ b/runtime/src/darwinMain/kotlin/com/arkivanov/parcelize/darwin/Parceler.kt @@ -0,0 +1,19 @@ +package com.arkivanov.parcelize.darwin + +import platform.Foundation.NSCoder + +/** + * Interface for custom [Parcelize] serializers. + */ +interface Parceler { + + /** + * Creates a new instance of [T] and reads all its data from the provided [coder]. + */ + fun create(coder: NSCoder): T + + /** + * Writes the [T] instance state to the provided [coder]. + */ + fun T.write(coder: NSCoder) +} diff --git a/runtime/src/darwinMain/kotlin/com/arkivanov/parcelize/darwin/Parcelize.kt b/runtime/src/darwinMain/kotlin/com/arkivanov/parcelize/darwin/Parcelize.kt index 819db0b..2b3a76e 100644 --- a/runtime/src/darwinMain/kotlin/com/arkivanov/parcelize/darwin/Parcelize.kt +++ b/runtime/src/darwinMain/kotlin/com/arkivanov/parcelize/darwin/Parcelize.kt @@ -1,5 +1,8 @@ package com.arkivanov.parcelize.darwin +/** + * Instructs the `parcelize-darwin` compiler plugin to generate [Parcelable] implementation for the annotated class. + */ @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.BINARY) annotation class Parcelize diff --git a/runtime/src/darwinMain/kotlin/com/arkivanov/parcelize/darwin/TypeParceler.kt b/runtime/src/darwinMain/kotlin/com/arkivanov/parcelize/darwin/TypeParceler.kt new file mode 100644 index 0000000..825e31d --- /dev/null +++ b/runtime/src/darwinMain/kotlin/com/arkivanov/parcelize/darwin/TypeParceler.kt @@ -0,0 +1,8 @@ +package com.arkivanov.parcelize.darwin + +/** + * Specifies what [Parceler] should be used for a particular type [T]. + */ +@Retention(AnnotationRetention.SOURCE) +@Target(AnnotationTarget.CLASS) +annotation class TypeParceler> diff --git a/runtime/src/darwinMain/kotlin/com/arkivanov/parcelize/darwin/WriteWith.kt b/runtime/src/darwinMain/kotlin/com/arkivanov/parcelize/darwin/WriteWith.kt new file mode 100644 index 0000000..342eabd --- /dev/null +++ b/runtime/src/darwinMain/kotlin/com/arkivanov/parcelize/darwin/WriteWith.kt @@ -0,0 +1,8 @@ +package com.arkivanov.parcelize.darwin + +/** + * Specifies what [Parceler] should be used for the annotated type. + */ +@Retention(AnnotationRetention.SOURCE) +@Target(AnnotationTarget.TYPE) +annotation class WriteWith

> diff --git a/tests/src/darwinTest/kotlin/com/arkivanov/parcelize/darwin/tests/ParcelizeTest.kt b/tests/src/darwinTest/kotlin/com/arkivanov/parcelize/darwin/tests/ParcelizeTest.kt index 5dc52db..69e59f1 100644 --- a/tests/src/darwinTest/kotlin/com/arkivanov/parcelize/darwin/tests/ParcelizeTest.kt +++ b/tests/src/darwinTest/kotlin/com/arkivanov/parcelize/darwin/tests/ParcelizeTest.kt @@ -1,16 +1,17 @@ package com.arkivanov.parcelize.darwin.tests import com.arkivanov.parcelize.darwin.Parcelable +import com.arkivanov.parcelize.darwin.Parceler import com.arkivanov.parcelize.darwin.Parcelize +import com.arkivanov.parcelize.darwin.TypeParceler +import com.arkivanov.parcelize.darwin.WriteWith import com.arkivanov.parcelize.darwin.decodeParcelable import com.arkivanov.parcelize.darwin.encodeParcelable -import kotlinx.cinterop.Arena -import kotlinx.cinterop.readBytes -import kotlinx.cinterop.toCValues -import platform.Foundation.NSData +import platform.Foundation.NSCoder import platform.Foundation.NSKeyedArchiver import platform.Foundation.NSKeyedUnarchiver -import platform.Foundation.dataWithBytes +import platform.Foundation.decodeInt32ForKey +import platform.Foundation.encodeInt32 import kotlin.test.Test import kotlin.test.assertEquals @@ -59,6 +60,8 @@ class ParcelizeTest { enum1 = SomeEnum.A, enum2 = SomeEnum.B, enum3 = null, + notParcelable1 = NotParcelable1(value = 5), + notParcelable2 = NotParcelable2(value = 10), intList1 = listOf(1, 2), intList2 = listOf(3, 4), @@ -509,6 +512,7 @@ class ParcelizeTest { A, B } + @TypeParceler @Parcelize private data class Some( val i1: Int, @@ -550,6 +554,8 @@ class ParcelizeTest { val enum1: SomeEnum, val enum2: SomeEnum?, val enum3: SomeEnum?, + val notParcelable1: @WriteWith NotParcelable1, + val notParcelable2: NotParcelable2, val intList1: List, val intList2: List?, @@ -978,4 +984,30 @@ class ParcelizeTest { private interface SomeInterface : Parcelable private abstract class SomeClass : Parcelable + + private data class NotParcelable1( + val value: Int, + ) + + private object NotParcelable1Parceler : Parceler { + override fun create(coder: NSCoder): NotParcelable1 = + NotParcelable1(value = coder.decodeInt32ForKey(key = "value")) + + override fun NotParcelable1.write(coder: NSCoder) { + coder.encodeInt32(value = value, forKey = "value") + } + } + + private data class NotParcelable2( + val value: Int, + ) + + private object NotParcelable2Parceler : Parceler { + override fun create(coder: NSCoder): NotParcelable2 = + NotParcelable2(value = coder.decodeInt32ForKey(key = "value")) + + override fun NotParcelable2.write(coder: NSCoder) { + coder.encodeInt32(value = value, forKey = "value") + } + } }