diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/ext/RealmDictionaryExt.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/ext/RealmDictionaryExt.kt index b3d48fa629..3110f3d63b 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/ext/RealmDictionaryExt.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/ext/RealmDictionaryExt.kt @@ -32,6 +32,8 @@ import io.realm.kotlin.types.RealmDictionaryMutableEntry import io.realm.kotlin.types.RealmList import io.realm.kotlin.types.RealmObject import io.realm.kotlin.types.RealmSet +import io.realm.kotlin.types.TypedRealmObject +import kotlin.reflect.KClass /** * Instantiates an **unmanaged** [RealmDictionary] from a variable number of [Pair]s of [String] @@ -112,3 +114,10 @@ public fun RealmDictionary.query( } else { throw IllegalArgumentException("Unmanaged dictionary values cannot be queried.") } + +/** + * TODO Docs + */ +public inline fun RealmDictionary.projectInto(target: KClass): Map { + TODO() +} diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/ext/RealmListExt.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/ext/RealmListExt.kt index 81e4004a9e..86a0e182c0 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/ext/RealmListExt.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/ext/RealmListExt.kt @@ -29,6 +29,7 @@ import io.realm.kotlin.types.RealmDictionary import io.realm.kotlin.types.RealmList import io.realm.kotlin.types.RealmSet import io.realm.kotlin.types.TypedRealmObject +import kotlin.reflect.KClass /** * Instantiates an **unmanaged** [RealmList]. @@ -72,3 +73,10 @@ public fun RealmList.query( } else { throw IllegalArgumentException("Unmanaged list cannot be queried") } + +/** + * TODO Docs + */ +public inline fun RealmList.projectInto(target: KClass): List { + TODO() +} diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/ext/RealmResultsExt.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/ext/RealmResultsExt.kt index 07e52429b6..59c137d36b 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/ext/RealmResultsExt.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/ext/RealmResultsExt.kt @@ -2,9 +2,14 @@ package io.realm.kotlin.ext import io.realm.kotlin.TypedRealm import io.realm.kotlin.internal.getRealm +import io.realm.kotlin.internal.realmObjectReference +import io.realm.kotlin.internal.realmProjectionCompanionOrNull import io.realm.kotlin.query.RealmResults import io.realm.kotlin.types.RealmList +import io.realm.kotlin.types.RealmProjectionFactory +import io.realm.kotlin.types.RealmSet import io.realm.kotlin.types.TypedRealmObject +import kotlin.reflect.KClass /** * Makes an unmanaged in-memory copy of the elements in a [RealmResults]. This is a deep copy @@ -20,3 +25,22 @@ public inline fun RealmResults.copyFromRealm(d // the Realm is closed, so all error handling is done inside the `getRealm` method. return this.getRealm().copyFromRealm(this, depth) } + +/** + * TODO Docs + */ +public fun RealmResults.projectInto(target: KClass): List { + // TODO Should this also automatically release the pointer for the results object after finishing the + // projection? I would be leaning towards yes, as I suspect this is primary use case. But if + // enough use cases show up for keeping the backing object around, we can add a + // `releaseRealmObjectAfterUse` boolean with a default value of `true` to this this method. + val projectionFactory: RealmProjectionFactory? = target.realmProjectionCompanionOrNull() + return projectionFactory?.let { factory -> + this.map { obj: O -> + projectionFactory.createProjection(obj).also { + obj.realmObjectReference?.objectPointer?.release() + } + } + } ?: throw IllegalStateException("TODO") +} + diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/ext/RealmSetExt.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/ext/RealmSetExt.kt index 6f34237352..ba5451fe59 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/ext/RealmSetExt.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/ext/RealmSetExt.kt @@ -1,3 +1,4 @@ + /* * Copyright 2022 Realm Inc. * @@ -29,6 +30,8 @@ import io.realm.kotlin.types.RealmDictionary import io.realm.kotlin.types.RealmList import io.realm.kotlin.types.RealmObject import io.realm.kotlin.types.RealmSet +import io.realm.kotlin.types.TypedRealmObject +import kotlin.reflect.KClass /** * Instantiates an **unmanaged** [RealmSet]. @@ -70,3 +73,10 @@ public fun RealmSet.query( } else { throw IllegalArgumentException("Unmanaged set cannot be queried") } + +/** + * TODO Docs + */ +public inline fun RealmSet.projectInto(target: KClass): Set { + TODO() +} diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/ext/TypedRealmObjectExt.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/ext/TypedRealmObjectExt.kt index c7a4b17a5b..1652323fcb 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/ext/TypedRealmObjectExt.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/ext/TypedRealmObjectExt.kt @@ -17,10 +17,13 @@ package io.realm.kotlin.ext import io.realm.kotlin.TypedRealm import io.realm.kotlin.internal.getRealm +import io.realm.kotlin.internal.realmObjectCompanionOrNull +import io.realm.kotlin.internal.realmProjectionCompanionOrNull import io.realm.kotlin.types.RealmDictionary import io.realm.kotlin.types.RealmList import io.realm.kotlin.types.RealmSet import io.realm.kotlin.types.TypedRealmObject +import kotlin.reflect.KClass /** * Makes an unmanaged in-memory copy of an already persisted [io.realm.kotlin.types.RealmObject]. @@ -37,3 +40,15 @@ public inline fun T.copyFromRealm(depth: UInt = U ?.copyFromRealm(this, depth) ?: throw IllegalArgumentException("This object is unmanaged. Only managed objects can be copied.") } + +/** + * TODO Docs + */ +public inline fun O.projectInto(target: KClass): T { + // TODO Should this also automatically release the pointer for the object after finishing the + // projection? I would be leaning towards yes, as I suspect this is primary use case. But if + // enough use cases show up for keeping the backing object around, we can add a + // `releaseRealmObjectAfterUse` boolean with a default value of `true` to this this method. + return T::class.realmProjectionCompanionOrNull()?.createProjection(this) + ?: throw IllegalStateException("TODO") +} diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectUtil.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectUtil.kt index 06c796f828..f848d2e870 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectUtil.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectUtil.kt @@ -25,7 +25,10 @@ import io.realm.kotlin.internal.interop.RealmInterop import io.realm.kotlin.internal.interop.RealmObjectPointer import io.realm.kotlin.internal.platform.realmObjectCompanionOrNull import io.realm.kotlin.internal.platform.realmObjectCompanionOrThrow +import io.realm.kotlin.internal.platform.realmProjectionCompanionOrNull import io.realm.kotlin.types.BaseRealmObject +import io.realm.kotlin.types.RealmProjectionFactory +import io.realm.kotlin.types.TypedRealmObject import kotlin.reflect.KClass internal fun RealmObjectInternal.manage( @@ -117,6 +120,11 @@ internal inline fun KClass.realmObjectCompanion return realmObjectCompanionOrThrow(this) } +public inline fun KClass.realmProjectionCompanionOrNull(): RealmProjectionFactory? { + return realmProjectionCompanionOrNull(this) +} + + /** * Convenience property to get easy access to the RealmObjectReference of a BaseRealmObject. * diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/platform/RealmObject.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/platform/RealmObject.kt index 969b2e7ba0..2e0ff9c21b 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/platform/RealmObject.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/platform/RealmObject.kt @@ -18,6 +18,8 @@ package io.realm.kotlin.internal.platform import io.realm.kotlin.internal.RealmObjectCompanion import io.realm.kotlin.types.BaseRealmObject +import io.realm.kotlin.types.RealmProjectionFactory +import io.realm.kotlin.types.TypedRealmObject import kotlin.reflect.KClass /** @@ -31,3 +33,8 @@ internal expect fun realmObjectCompanionOrNull(clazz: KClass): Real * Returns the [RealmObjectCompanion] associated with a given [BaseRealmObject]'s [KClass]. */ internal expect fun realmObjectCompanionOrThrow(clazz: KClass): RealmObjectCompanion + +/** + * TODO + */ +public expect fun realmProjectionCompanionOrNull(clazz: KClass): RealmProjectionFactory? diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/query/ObjectQuery.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/query/ObjectQuery.kt index 348f8c0f0b..91d0dfa889 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/query/ObjectQuery.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/query/ObjectQuery.kt @@ -30,6 +30,7 @@ import io.realm.kotlin.internal.interop.RealmQueryPointer import io.realm.kotlin.internal.interop.RealmResultsPointer import io.realm.kotlin.internal.interop.inputScope import io.realm.kotlin.internal.schema.ClassMetadata +import io.realm.kotlin.notifications.ProjectionsChange import io.realm.kotlin.notifications.ResultsChange import io.realm.kotlin.query.RealmQuery import io.realm.kotlin.query.RealmResults @@ -85,6 +86,10 @@ internal class ObjectQuery constructor( override fun find(): RealmResults = RealmResultsImpl(realmReference, resultsPointer, classKey, clazz, mediator) + override fun find(projection: KClass): List { + TODO("Not yet implemented") + } + override fun query(filter: String, vararg arguments: Any?): RealmQuery = inputScope { val appendedQuery = RealmInterop.realm_query_append_query( @@ -175,6 +180,10 @@ internal class ObjectQuery constructor( .registerObserver(this) } + override fun asFlow(projection: KClass): ProjectionsChange { + TODO("Not yet implemented") + } + override fun delete() { // TODO C-API doesn't implement realm_query_delete_all so just fetch the result and delete // that diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/query/SingleQuery.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/query/SingleQuery.kt index 197068935d..f634074b73 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/query/SingleQuery.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/query/SingleQuery.kt @@ -14,6 +14,7 @@ import io.realm.kotlin.internal.interop.RealmQueryPointer import io.realm.kotlin.internal.realmObjectReference import io.realm.kotlin.internal.runIfManaged import io.realm.kotlin.internal.toRealmObject +import io.realm.kotlin.notifications.ProjectionChange import io.realm.kotlin.notifications.ResultsChange import io.realm.kotlin.notifications.SingleQueryChange import io.realm.kotlin.notifications.internal.DeletedObjectImpl @@ -45,6 +46,10 @@ internal class SingleQuery constructor( ) } + override fun find(projection: KClass): List { + TODO("Not yet implemented") + } + /** * Because Core does not support subscribing to the head element of a query this feature * must be shimmed. @@ -92,6 +97,10 @@ internal class SingleQuery constructor( } } + override fun asFlow(projection: KClass): ProjectionChange { + TODO("Not yet implemented") + } + /** * Thaw the frozen query result, turning it back into a live, thread-confined RealmResults. * The results object is then used to fetch the object with index 0, which can be `null`. diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/notifications/ProjectedObjectChange.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/notifications/ProjectedObjectChange.kt new file mode 100644 index 0000000000..5b40f73346 --- /dev/null +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/notifications/ProjectedObjectChange.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2021 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.kotlin.notifications + +public sealed interface SingleProjectionQueryChange { + /** + * Returns the newest state of object being observed. `null` is returned if there is no object to + * observe. + */ + public val obj: O? +} + +/** + * TODO Docs + */ +public interface PendingProjection : SingleProjectionQueryChange + +/** + * TODO Docs + * TODO Annoying to have both `ProjectionsChanges` and `ProjectionChange`...other name for one of them? + */ +public sealed interface ProjectionChange : SingleProjectionQueryChange { + /** + * Returns the newest state of object being observed. `null` is returned if the object + * has been deleted. + */ + override val obj: O? +} + +/** + * TODO Docs + */ +public interface InitialProjection : SingleProjectionQueryChange { + override val obj: O +} + +/** + * TODO Docs + * TODO Can we actually track all changed fields? + */ +public interface UpdatedProjection : SingleProjectionQueryChange { + override val obj: O + + /** + * Returns the names of properties that has changed. + */ + public val changedFields: Array + + /** + * Checks if a given field has been changed. + * + * @param fieldName to be checked if its value has been changed. + * @return `true` if the field has been changed. It returns `false` the field cannot be found + * or the field hasn't been changed. + */ + public fun isFieldChanged(fieldName: String): Boolean { + return changedFields.firstOrNull { it == fieldName } != null + } +} + +/** + * TODO Docs + */ +public interface DeletedProjection : ProjectionChange diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/notifications/ProjectionsChange.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/notifications/ProjectionsChange.kt new file mode 100644 index 0000000000..2296b2d460 --- /dev/null +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/notifications/ProjectionsChange.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2022 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.realm.kotlin.notifications + +/** + * TODO Docs (copy from ResultsChange) + */ +public sealed interface ProjectionsChange { + public val list: List +} + +/** + * TODO Docs (copy from ResultsChange) + */ +public interface InitialProjections : ProjectionsChange + +/** + * TODO Docs (copy from ResultsChange) + */ +public interface UpdatedProjections : ProjectionsChange, ListChangeSet diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/query/RealmElementQuery.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/query/RealmElementQuery.kt index b8aaf74de6..76a516598b 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/query/RealmElementQuery.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/query/RealmElementQuery.kt @@ -20,12 +20,15 @@ import io.realm.kotlin.Deleteable import io.realm.kotlin.MutableRealm import io.realm.kotlin.RealmConfiguration import io.realm.kotlin.notifications.InitialResults +import io.realm.kotlin.notifications.ListChange +import io.realm.kotlin.notifications.ProjectionsChange import io.realm.kotlin.notifications.ResultsChange import io.realm.kotlin.notifications.UpdatedResults import io.realm.kotlin.types.BaseRealmObject import kotlinx.coroutines.CancellationException import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow +import kotlin.reflect.KClass /** * Query returning [RealmResults]. @@ -63,4 +66,15 @@ public interface RealmElementQuery : Deleteable { * @return a flow representing changes to the [RealmResults] resulting from running this query. */ public fun asFlow(): Flow> + + /** + * TODO + */ + public fun find(projection: KClass): List + + /** + * TODO + */ + public fun asFlow(projection: KClass): ProjectionsChange + } diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/query/RealmSingleQuery.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/query/RealmSingleQuery.kt index 7ce8a28867..f357e281ed 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/query/RealmSingleQuery.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/query/RealmSingleQuery.kt @@ -17,10 +17,12 @@ package io.realm.kotlin.query import io.realm.kotlin.Deleteable +import io.realm.kotlin.notifications.ProjectionChange import io.realm.kotlin.notifications.SingleQueryChange import io.realm.kotlin.types.BaseRealmObject import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow +import kotlin.reflect.KClass /** * Query returning a single [RealmObject] or [EmbeddedRealmObject]. @@ -76,4 +78,15 @@ public interface RealmSingleQuery : Deleteable { * running this query. */ public fun asFlow(): Flow> + + /** + * TODO Docs + */ + public fun find(projection: KClass): List + + /** + * TODO Docs + */ + public fun asFlow(projection: KClass): ProjectionChange + } diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/Projections.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/Projections.kt new file mode 100644 index 0000000000..59da912e70 --- /dev/null +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/Projections.kt @@ -0,0 +1,16 @@ +package io.realm.kotlin.types + +/** + * TODO + */ +public interface RealmProjection { + public fun projectInto(): T +} + +/** + * TODO + */ +public interface RealmProjectionFactory { + public fun createProjection(origin: O): T +} + diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/annotations/Projection.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/annotations/Projection.kt new file mode 100644 index 0000000000..404819020c --- /dev/null +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/types/annotations/Projection.kt @@ -0,0 +1,14 @@ +package io.realm.kotlin.types.annotations + +import io.realm.kotlin.types.TypedRealmObject +import kotlin.reflect.KClass + +@Retention(AnnotationRetention.SOURCE) +@Target(AnnotationTarget.CLASS) +@MustBeDocumented +public annotation class Projection(val origin: KClass) + +@Retention(AnnotationRetention.SOURCE) +@Target(AnnotationTarget.PROPERTY) +@MustBeDocumented +public annotation class ProjectedField(val origin: String) diff --git a/packages/library-base/src/jvm/kotlin/io/realm/kotlin/internal/platform/RealmObject.kt b/packages/library-base/src/jvm/kotlin/io/realm/kotlin/internal/platform/RealmObject.kt index 0bf754fa9d..26286d628a 100644 --- a/packages/library-base/src/jvm/kotlin/io/realm/kotlin/internal/platform/RealmObject.kt +++ b/packages/library-base/src/jvm/kotlin/io/realm/kotlin/internal/platform/RealmObject.kt @@ -18,6 +18,8 @@ package io.realm.kotlin.internal.platform import io.realm.kotlin.internal.RealmObjectCompanion import io.realm.kotlin.types.BaseRealmObject +import io.realm.kotlin.types.RealmProjectionFactory +import io.realm.kotlin.types.TypedRealmObject import kotlin.reflect.KClass import kotlin.reflect.full.companionObjectInstance @@ -31,3 +33,8 @@ internal actual fun realmObjectCompanionOrNull(clazz: KClass): Real internal actual fun realmObjectCompanionOrThrow(clazz: KClass): RealmObjectCompanion = realmObjectCompanionOrNull(clazz) ?: error("Couldn't find companion object of class '${clazz.simpleName}'.\nA common cause for this is when the `io.realm.kotlin` is not applied to the Gradle module that contains the '${clazz.simpleName}' class.") + +public actual fun realmProjectionCompanionOrNull(clazz: KClass): RealmProjectionFactory? = + if (clazz.companionObjectInstance is RealmProjectionFactory<*, *>) { + clazz.companionObjectInstance as RealmProjectionFactory + } else null diff --git a/packages/library-base/src/nativeDarwin/kotlin/io/realm/kotlin/internal/platform/RealmObject.kt b/packages/library-base/src/nativeDarwin/kotlin/io/realm/kotlin/internal/platform/RealmObject.kt index 455cb38dac..d59c3b9dd4 100644 --- a/packages/library-base/src/nativeDarwin/kotlin/io/realm/kotlin/internal/platform/RealmObject.kt +++ b/packages/library-base/src/nativeDarwin/kotlin/io/realm/kotlin/internal/platform/RealmObject.kt @@ -18,6 +18,8 @@ package io.realm.kotlin.internal.platform import io.realm.kotlin.internal.RealmObjectCompanion import io.realm.kotlin.types.BaseRealmObject +import io.realm.kotlin.types.RealmProjectionFactory +import io.realm.kotlin.types.TypedRealmObject import kotlin.reflect.ExperimentalAssociatedObjects import kotlin.reflect.KClass import kotlin.reflect.findAssociatedObject @@ -32,3 +34,10 @@ internal actual fun realmObjectCompanionOrNull(clazz: KClass): Real internal actual fun realmObjectCompanionOrThrow(clazz: KClass): RealmObjectCompanion = realmObjectCompanionOrNull(clazz) ?: error("Couldn't find companion object of class '${clazz.simpleName}'.\nA common cause for this is when the `io.realm.kotlin` is not applied to the Gradle module that contains the '${clazz.simpleName}' class.") + +public actual fun realmProjectionCompanionOrNull(clazz: KClass): RealmProjectionFactory? = + @OptIn(ExperimentalAssociatedObjects::class) + when (val associatedObject = clazz.findAssociatedObject()) { + is RealmProjectionFactory<*, *> -> associatedObject as RealmProjectionFactory + else -> null + } diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/ProjectionTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/ProjectionTests.kt new file mode 100644 index 0000000000..354ebb96d2 --- /dev/null +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/ProjectionTests.kt @@ -0,0 +1,131 @@ +package io.realm.kotlin.test.common + +import io.realm.kotlin.Realm +import io.realm.kotlin.RealmConfiguration +import io.realm.kotlin.ext.projectInto +import io.realm.kotlin.test.platform.PlatformUtils +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import io.realm.kotlin.ext.query +import io.realm.kotlin.query.RealmResults +import io.realm.kotlin.types.RealmProjectionFactory +import io.realm.kotlin.types.annotations.ProjectedField +import io.realm.kotlin.types.annotations.Projection +import kotlin.test.assertEquals + +// Use a compiler plugin to verify that projection is valid and create a helper method +// for doing it +@Projection(QuerySample::class) +data class ClassMappingProjection( + val stringField: String = "" +) { + companion object: RealmProjectionFactory { + override fun createProjection(origin: QuerySample): ClassMappingProjection { + return ClassMappingProjection(origin.stringField) + } + } +} + +// Advanced projection that is both checked at compile time and allow for renaming and child +// mapping +@Projection(QuerySample::class) +data class PropertyMappingProjection( + @ProjectedField("stringField") + val name: String = "", + @ProjectedField("nullableRealmObject.stringField") // TODO Should we support a path of KMutableProperties + val linkedField: String = "", + val nullableRealmObject: QuerySample?, // TODO Should we all forcing this to be non-null? This will require handling null somewhere + @ProjectedField("nullableRealmObject") // TODO Support multiple references to same origin fielsd + val nestedProjectionObject: NestedChild, + val intListField: List, + val intSetField: Set, + val intDictionaryField: Map +) { + // TODO Autogenerate companion object implementing this interface + // TODO Should we make this interface public, so people can create their own logic, but still + // benefit from our APIs? + companion object: RealmProjectionFactory { + // TODO Autogenerate this method + override fun createProjection(origin: QuerySample): PropertyMappingProjection { + // TODO Autogenerate this mapping. + // TODO A lot of the conversion logic would probably benefit from being moved to inline helper functions + return PropertyMappingProjection( + origin.stringField, + origin.nullableRealmObject?.stringField ?: "", + origin.nullableRealmObject, + origin.projectInto(NestedChild::class), + origin.intListField.toList(), + origin.intSetField.toSet(), + origin.intDictionaryField.toMap() + ) + } + } +} + +@Projection(QuerySample::class) +data class NestedChild(val name: String) { + companion object: RealmProjectionFactory { + override fun createProjection(origin: QuerySample): NestedChild { + return NestedChild(origin.stringField) + } + } +} + +// Mapping class used at runtime. Only support very simple mapping and requires a constructor +// with field names matching the ones from the class being mapped. +// TODO Probably better left for a phase 2? +data class SimpleProjectionMapping( + val stringField: String = "" +) + +class ProjectionTests { + + private lateinit var tmpDir: String + private lateinit var realm: Realm + + @BeforeTest + fun setup() { + tmpDir = PlatformUtils.createTempDir() + val configuration = RealmConfiguration.Builder(schema = setOf(QuerySample::class)) + .directory(tmpDir) + .build() + realm = Realm.open(configuration) + realm.writeBlocking { + repeat(5) { + copyToRealm(QuerySample().apply { + stringField = "string-$it" + intField = it + nullableRealmObject = QuerySample().apply { + stringField = "substring-$it" + intField = it + 10 + } + }) + } + } + } + + @AfterTest + fun tearDown() { + if (this::realm.isInitialized && !realm.isClosed()) { + realm.close() + } + PlatformUtils.deleteTempDir(tmpDir) + } + + @Test + fun projectSingleObject() { + val managedObj = realm.query().find().first() + val projectedObj = managedObj.projectInto(PropertyMappingProjection::class) + assertEquals(managedObj.stringField, projectedObj.name) + assertEquals(projectedObj.name, "string-0") + } + + @Test + fun projectResults() { + val results: RealmResults = realm.query().find() + val projectedResults: List = results.projectInto(PropertyMappingProjection::class) + assertEquals(results.size, projectedResults.size) + } + +} \ No newline at end of file