Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add support for type adapters #1592

Draft
wants to merge 32 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
7fa81d6
Adapter singleton and instance adapter discoverability for RealmInstant
clementetb Dec 1, 2023
eb98479
Add support for other types
clementetb Dec 4, 2023
8c3f941
Use map for instanced adapters
clementetb Dec 4, 2023
f64bdf3
Clean up configuration + tests
clementetb Dec 4, 2023
b5b6a20
Add some compile tests
clementetb Dec 5, 2023
57e8bf3
Enable Realm list tests
clementetb Dec 5, 2023
43dd5dc
Enable collections
clementetb Dec 5, 2023
c53aa14
Disable derived numerical types
clementetb Dec 5, 2023
681a73e
More tests
clementetb Dec 8, 2023
b607f36
Add support for objects
clementetb Dec 8, 2023
37bfd02
Add collections support
clementetb Dec 11, 2023
9c8ba57
Add type check for unsupported types
clementetb Dec 11, 2023
da05877
Add check on type adapter valid realm types
clementetb Dec 11, 2023
9b97c13
More testing
clementetb Dec 11, 2023
e9fd266
Test with other annotations
clementetb Dec 11, 2023
45b1ebe
Add compiler tests for using the annotation on type parameters
clementetb Dec 12, 2023
4f6cf36
Add type parameter adapter for lists
clementetb Dec 19, 2023
ad08eb9
Add set and dictionary support
clementetb Dec 19, 2023
ab53da9
Add more tests
clementetb Dec 19, 2023
148c0ac
Linting
clementetb Dec 20, 2023
058afc7
Fix nullable collections not being processed
clementetb Dec 20, 2023
38fe50f
Fix expected IR
clementetb Dec 20, 2023
a7111b1
Remove uses type adapter compile computed property
clementetb Dec 20, 2023
6408db1
Add more tests around adapter supported types
clementetb Dec 20, 2023
385e369
Linting
clementetb Dec 20, 2023
77cdc2e
Clean up compiler tests
clementetb Dec 20, 2023
3b18d8d
Add roundtrip tests
clementetb Dec 20, 2023
b4686c0
Add roundtrip for adapted type parameters
clementetb Dec 20, 2023
f3b0407
Disable RealmSet tests on TypedAdapter
clementetb Dec 20, 2023
4ba7dab
Add changelog entry
clementetb Dec 21, 2023
3b76bee
Add documentation
clementetb Dec 21, 2023
faab0ab
Update compiler plugin tests
clementetb Dec 21, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* None.

### Enhancements
* None.
* Add support for persisting custom types using Realm type adapters. (Issue [#587](https://github.com/realm/realm-kotlin/issues/587))

### Fixed
* None.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import io.realm.kotlin.log.LogLevel
import io.realm.kotlin.log.RealmLog
import io.realm.kotlin.log.RealmLogger
import io.realm.kotlin.types.BaseRealmObject
import io.realm.kotlin.types.RealmTypeAdapter
import kotlinx.coroutines.CoroutineDispatcher
import kotlin.reflect.KClass

Expand All @@ -49,6 +50,16 @@ public fun interface CompactOnLaunchCallback {
public fun shouldCompact(totalBytes: Long, usedBytes: Long): Boolean
}

/**
* Builder for setting up any runtime type adapters.
*/
public class TypeAdapterBuilder {
internal val typeAdapters: MutableList<RealmTypeAdapter<*, *>> = mutableListOf()
public fun add(adapter: RealmTypeAdapter<*, *>) {
typeAdapters.add(adapter)
}
}

/**
* This interface is used to write data to a Realm file when the file is first created.
* It will be used in a way similar to using [Realm.writeBlocking].
Expand Down Expand Up @@ -196,6 +207,11 @@ public interface Configuration {
*/
public val initialRealmFileConfiguration: InitialRealmFileConfiguration?

/**
* List of types adapters that would be available in runtime.
*/
public val typeAdapters: List<RealmTypeAdapter<*, *>>

/**
* Base class for configuration builders that holds properties available to both
* [RealmConfiguration] and [SyncConfiguration].
Expand Down Expand Up @@ -238,6 +254,7 @@ public interface Configuration {
protected var initialDataCallback: InitialDataCallback? = null
protected var inMemory: Boolean = false
protected var initialRealmFileConfiguration: InitialRealmFileConfiguration? = null
protected var typeAdapters: List<RealmTypeAdapter<*, *>> = listOf()

/**
* Sets the filename of the realm file.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,14 @@ public interface RealmConfiguration : Configuration {
public fun directory(directoryPath: String): Builder =
apply { this.directory = directoryPath }

/**
* Sets the type adapters that would be available in runtime.
*/
public fun typeAdapters(block: TypeAdapterBuilder.() -> Unit): Builder =
apply {
this.typeAdapters = TypeAdapterBuilder().apply(block).typeAdapters
}

/**
* Setting this will change the behavior of how migration exceptions are handled. Instead of
* throwing an exception the on-disc Realm will be cleared and recreated with the new Realm
Expand Down Expand Up @@ -192,7 +200,8 @@ public interface RealmConfiguration : Configuration {
initialDataCallback,
inMemory,
initialRealmFileConfiguration,
realmLogger
realmLogger,
typeAdapters,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import io.realm.kotlin.internal.util.CoroutineDispatcherFactory
import io.realm.kotlin.migration.AutomaticSchemaMigration
import io.realm.kotlin.migration.RealmMigration
import io.realm.kotlin.types.BaseRealmObject
import io.realm.kotlin.types.RealmTypeAdapter
import kotlin.reflect.KClass

// TODO Public due to being accessed from `library-sync`
Expand All @@ -67,7 +68,8 @@ public open class ConfigurationImpl(
override val isFlexibleSyncConfiguration: Boolean,
inMemory: Boolean,
initialRealmFileConfiguration: InitialRealmFileConfiguration?,
logger: ContextLogger
logger: ContextLogger,
override val typeAdapters: List<RealmTypeAdapter<*, *>>,
) : InternalConfiguration {

override val path: String
Expand Down Expand Up @@ -102,6 +104,7 @@ public open class ConfigurationImpl(
override val initialDataCallback: InitialDataCallback?
override val inMemory: Boolean
override val initialRealmFileConfiguration: InitialRealmFileConfiguration?
override val typeAdapterMap: Map<KClass<*>, RealmTypeAdapter<*, *>>

override fun createNativeConfiguration(): RealmConfigurationPointer {
val nativeConfig: RealmConfigurationPointer = RealmInterop.realm_config_new()
Expand Down Expand Up @@ -148,6 +151,7 @@ public open class ConfigurationImpl(
this.initialDataCallback = initialDataCallback
this.inMemory = inMemory
this.initialRealmFileConfiguration = initialRealmFileConfiguration
this.typeAdapterMap = typeAdapters.associateBy { it::class }

// We need to freeze `compactOnLaunchCallback` reference on initial thread for Kotlin Native
val compactCallback = compactOnLaunchCallback?.let { callback ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import io.realm.kotlin.types.ObjectId
import io.realm.kotlin.types.RealmAny
import io.realm.kotlin.types.RealmInstant
import io.realm.kotlin.types.RealmObject
import io.realm.kotlin.types.RealmTypeAdapter
import io.realm.kotlin.types.RealmUUID
import io.realm.kotlin.types.geo.GeoBox
import io.realm.kotlin.types.geo.GeoCircle
Expand Down Expand Up @@ -259,6 +260,29 @@ internal object IntConverter : CoreIntConverter, CompositeConverter<Int, Long>()
public inline fun intToLong(value: Int?): Long? = value?.toLong()
public inline fun longToInt(value: Long?): Int? = value?.toInt()

public fun getTypeAdapter(
obj: RealmObjectReference<out BaseRealmObject>,
adapterClass: KClass<*>,
): RealmTypeAdapter<Any?, Any?> =
obj.owner.owner
.configuration
.typeAdapterMap.let { adapters ->
require(adapters.contains(adapterClass)) { "User provided adaptes don't contains adapter ${adapterClass.simpleName}" }
adapters[adapterClass] as RealmTypeAdapter<Any?, Any?>
}

public inline fun toRealm(
obj: RealmObjectReference<out BaseRealmObject>,
adapterClass: KClass<*>,
userValue: Any?,
): Any? = getTypeAdapter(obj, adapterClass).toRealm(userValue)

public inline fun fromRealm(
obj: RealmObjectReference<out BaseRealmObject>,
adapterClass: KClass<*>,
realmValue: Any,
): Any? = getTypeAdapter(obj, adapterClass).toPublic(realmValue)

internal object RealmInstantConverter : PassThroughPublicConverter<RealmInstant>() {
override inline fun fromRealmValue(realmValue: RealmValue): RealmInstant? =
if (realmValue.isNull()) null else realmValueToRealmInstant(realmValue)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import io.realm.kotlin.internal.interop.RealmConfigurationPointer
import io.realm.kotlin.internal.interop.SchemaMode
import io.realm.kotlin.internal.util.CoroutineDispatcherFactory
import io.realm.kotlin.types.BaseRealmObject
import io.realm.kotlin.types.RealmTypeAdapter
import kotlin.reflect.KClass

/**
Expand All @@ -36,6 +37,7 @@ public interface InternalConfiguration : Configuration {
public val writeDispatcherFactory: CoroutineDispatcherFactory
public val schemaMode: SchemaMode
public val logger: ContextLogger
public val typeAdapterMap: Map<KClass<*>, RealmTypeAdapter<*, *>>

// Temporary work-around for https://github.com/realm/realm-kotlin/issues/724
public val isFlexibleSyncConfiguration: Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import io.realm.kotlin.internal.interop.SchemaMode
import io.realm.kotlin.internal.util.CoroutineDispatcherFactory
import io.realm.kotlin.migration.RealmMigration
import io.realm.kotlin.types.BaseRealmObject
import io.realm.kotlin.types.RealmTypeAdapter
import kotlin.reflect.KClass

public const val REALM_FILE_EXTENSION: String = ".realm"
Expand All @@ -47,7 +48,8 @@ internal class RealmConfigurationImpl(
initialDataCallback: InitialDataCallback?,
inMemory: Boolean,
override val initialRealmFileConfiguration: InitialRealmFileConfiguration?,
logger: ContextLogger
logger: ContextLogger,
typeAdapters: List<RealmTypeAdapter<*, *>>
) : ConfigurationImpl(
directory,
name,
Expand All @@ -69,6 +71,7 @@ internal class RealmConfigurationImpl(
false,
inMemory,
initialRealmFileConfiguration,
logger
logger,
typeAdapters,
),
RealmConfiguration
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import io.realm.kotlin.notifications.internal.UpdatedListImpl
import io.realm.kotlin.query.RealmQuery
import io.realm.kotlin.types.BaseRealmObject
import io.realm.kotlin.types.RealmList
import io.realm.kotlin.types.RealmTypeAdapter
import kotlinx.coroutines.channels.ProducerScope
import kotlinx.coroutines.flow.Flow
import kotlin.reflect.KClass
Expand Down Expand Up @@ -249,6 +250,37 @@ internal interface ListOperator<E> : CollectionOperator<E, RealmListPointer> {
fun copy(realmReference: RealmReference, nativePointer: RealmListPointer): ListOperator<E>
}

internal class TypeAdaptedListOperator<E, S>(
private val listOperator: ListOperator<S>,
private val typeAdapter: RealmTypeAdapter<S, E>
) : ListOperator<E> {
override val nativePointer: RealmListPointer by listOperator::nativePointer
override val mediator: Mediator by listOperator::mediator
override val realmReference: RealmReference by listOperator::realmReference
override val valueConverter: RealmValueConverter<E> by lazy { throw IllegalStateException("TypeAdaptedListOperator does not have a valueConverter") }

override fun get(index: Int): E = typeAdapter.toPublic(listOperator.get(index))

override fun copy(
realmReference: RealmReference,
nativePointer: RealmListPointer,
): ListOperator<E> = TypeAdaptedListOperator(listOperator.copy(realmReference, nativePointer), typeAdapter)

override fun set(
index: Int,
element: E,
updatePolicy: UpdatePolicy,
cache: UnmanagedToManagedObjectCache,
): E = typeAdapter.toPublic(listOperator.set(index, typeAdapter.toRealm(element), updatePolicy, cache))

override fun insert(
index: Int,
element: E,
updatePolicy: UpdatePolicy,
cache: UnmanagedToManagedObjectCache,
) = listOperator.insert(index, typeAdapter.toRealm(element), updatePolicy, cache)
}

internal class PrimitiveListOperator<E>(
override val mediator: Mediator,
override val realmReference: RealmReference,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import io.realm.kotlin.types.BaseRealmObject
import io.realm.kotlin.types.RealmAny
import io.realm.kotlin.types.RealmDictionary
import io.realm.kotlin.types.RealmMap
import io.realm.kotlin.types.RealmTypeAdapter
import kotlinx.coroutines.channels.ProducerScope
import kotlinx.coroutines.flow.Flow
import kotlin.reflect.KClass
Expand Down Expand Up @@ -286,6 +287,55 @@ internal interface MapOperator<K, V> : CollectionOperator<V, RealmMapPointer> {
fun copy(realmReference: RealmReference, nativePointer: RealmMapPointer): MapOperator<K, V>
}

internal class TypeAdaptedMapOperator<K, E, S>(
val mapOperator: MapOperator<K, S>,
val typeAdapter: RealmTypeAdapter<S, E>
) : MapOperator<K, E> {
override var modCount: Int by mapOperator::modCount
override val keyConverter: RealmValueConverter<K> by mapOperator::keyConverter
override val nativePointer: RealmMapPointer by mapOperator::nativePointer

override fun getEntryInternal(position: Int): Pair<K, E> = mapOperator
.getEntryInternal(position)
.run {
Pair(first, typeAdapter.toPublic(second))
}

override fun copy(
realmReference: RealmReference,
nativePointer: RealmMapPointer,
): MapOperator<K, E> = TypeAdaptedMapOperator(mapOperator.copy(realmReference, nativePointer), typeAdapter)

override fun areValuesEqual(expected: E?, actual: E?): Boolean = mapOperator.areValuesEqual(
expected?.let { typeAdapter.toRealm(it) }, actual?.let { typeAdapter.toRealm(it) }
)

override fun containsValueInternal(value: E): Boolean = mapOperator.containsValueInternal(typeAdapter.toRealm(value))

override fun getInternal(key: K): E? = mapOperator.getInternal(key)
?.let { typeAdapter.toPublic(it) }

override fun eraseInternal(key: K): Pair<E?, Boolean> = mapOperator.eraseInternal(key).run {
Pair(first?.let { typeAdapter.toPublic(it) }, second)
}

override fun insertInternal(
key: K,
value: E?,
updatePolicy: UpdatePolicy,
cache: UnmanagedToManagedObjectCache,
): Pair<E?, Boolean> = mapOperator.insertInternal(
key,
value?.let { typeAdapter.toRealm(it) }, updatePolicy, cache
).run {
Pair(first?.let { typeAdapter.toPublic(it) }, second)
}

override val mediator: Mediator by mapOperator::mediator
override val realmReference: RealmReference by mapOperator::realmReference
override val valueConverter: RealmValueConverter<E> by lazy { throw IllegalStateException("TypeAdaptedMapOperator does not have a valueConverter") }
}

internal open class PrimitiveMapOperator<K, V> constructor(
override val mediator: Mediator,
override val realmReference: RealmReference,
Expand Down
Loading