
commonMain.maryk.core.models.RootDataModel.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of core-jvm Show documentation
Show all versions of core-jvm Show documentation
Maryk is a Kotlin Multiplatform library which helps you to store, query and send data in a structured way over multiple platforms. The data store stores any value with a version, so it is possible to request only the changed data or live listen for updates.
The newest version!
package maryk.core.models
import maryk.core.definitions.MarykPrimitive
import maryk.core.exceptions.ContextNotFoundException
import maryk.core.exceptions.DefNotFoundException
import maryk.core.exceptions.RequestException
import maryk.core.models.definitions.RootDataModelDefinition
import maryk.core.models.migration.MigrationStatus
import maryk.core.models.serializers.ObjectDataModelSerializer
import maryk.core.properties.IsPropertyContext
import maryk.core.properties.PropertiesCollectionDefinition
import maryk.core.properties.PropertiesCollectionDefinitionWrapper
import maryk.core.properties.definitions.EmbeddedObjectDefinition
import maryk.core.properties.definitions.IsPropertyDefinition
import maryk.core.properties.definitions.index.IsIndexable
import maryk.core.properties.definitions.index.UUIDKey
import maryk.core.properties.definitions.wrapper.AnyDefinitionWrapper
import maryk.core.properties.definitions.wrapper.EmbeddedObjectDefinitionWrapper
import maryk.core.properties.definitions.wrapper.IsDefinitionWrapper
import maryk.core.properties.references.AnyOutPropertyReference
import maryk.core.properties.references.IsPropertyReference
import maryk.core.properties.types.Version
import maryk.core.query.ContainsDefinitionsContext
import maryk.core.query.DefinitionsConversionContext
import maryk.core.values.MutableValueItems
import maryk.core.values.ObjectValues
import maryk.core.values.ValueItem
import maryk.core.values.ValueItems
import maryk.json.IsJsonLikeReader
import maryk.json.IsJsonLikeWriter
import maryk.json.JsonToken
import maryk.json.PresetJsonTokenReader
import maryk.lib.exceptions.ParseException
import maryk.lib.synchronizedIteration
import maryk.yaml.IsYamlReader
import maryk.yaml.YamlWriter
/**
* Base class for all DataModels which are at the root and can be stored into a DataStore.
*/
open class RootDataModel internal constructor(
meta: (String?) -> RootDataModelDefinition,
) : TypedValuesDataModel(), IsRootDataModel, MarykPrimitive {
constructor(
keyDefinition: () -> IsIndexable = { UUIDKey },
version: Version = Version(1),
indices: (() -> List)? = null,
reservedIndices: List? = null,
reservedNames: List? = null,
name: String? = null,
) : this({ passedName ->
RootDataModelDefinition(
name = name ?: passedName ?: throw DefNotFoundException("RootDataModel should have a name. Please define in a class instead of anonymous object or pass name as property."),
keyDefinition = keyDefinition.invoke(),
version = version,
indices = indices?.invoke(),
reservedIndices = reservedIndices,
reservedNames = reservedNames,
)
})
@Suppress("UNCHECKED_CAST", "LeakingThis")
private val typedThis: DM = this as DM
final override val Meta: RootDataModelDefinition by lazy { meta(this::class.simpleName) }
operator fun , *>> invoke(
parent: AnyOutPropertyReference? = null,
referenceGetter: DM.() -> (AnyOutPropertyReference?) -> R
) = referenceGetter(typedThis)(parent)
override fun equals(other: Any?) =
super.equals(other) && other is RootDataModel<*> && this.Meta == other.Meta
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + Meta.hashCode()
return result
}
override fun isMigrationNeeded(
storedDataModel: IsStorableDataModel<*>,
migrationReasons: MutableList
): MigrationStatus {
val indicesToIndex = mutableListOf()
if (storedDataModel is IsRootDataModel) {
// Only process indices if they are present on new model.
// If they are present on stored but not on new, accept it.
Meta.indices?.let { indices ->
val orderedIndices = indices.sortedBy { it.referenceStorageByteArray }
if (storedDataModel.Meta.indices == null) {
// Only index the values which have stored properties on the stored model
val toIndex = orderedIndices.filter { it.isCompatibleWithModel(storedDataModel) }
indicesToIndex.addAll(toIndex)
} else {
val storedOrderedIndices = storedDataModel.Meta.indices!!.sortedBy { it.referenceStorageByteArray }
synchronizedIteration(
orderedIndices.iterator(),
storedOrderedIndices.iterator(),
{ newValue, storedValue ->
newValue.referenceStorageByteArray compareTo storedValue.referenceStorageByteArray
},
processOnlyOnIterator1 = { newIndex ->
// Only index the values which have stored properties on the stored model
if (newIndex.isCompatibleWithModel(storedDataModel)) {
indicesToIndex.add(newIndex)
}
}
)
}
}
if (storedDataModel.Meta.version.major != this.Meta.version.major) {
migrationReasons += "Major version was increased: ${storedDataModel.Meta.version} -> ${this.Meta.version}"
}
if (storedDataModel.Meta.keyDefinition !== this.Meta.keyDefinition) {
migrationReasons += "Key definition was not the same"
}
} else {
migrationReasons += "Stored model is not a root data model"
}
val parentResult = super.isMigrationNeeded(storedDataModel, migrationReasons)
return if (indicesToIndex.isEmpty()) {
parentResult
} else when (parentResult) {
is MigrationStatus.NeedsMigration -> MigrationStatus.NeedsMigration(
storedDataModel,
migrationReasons,
indicesToIndex
)
else -> MigrationStatus.NewIndicesOnExistingProperties(indicesToIndex)
}
}
object Model: DefinitionModel>() {
val properties = PropertiesCollectionDefinitionWrapper>(
1u,
"properties",
PropertiesCollectionDefinition(
capturer = { context, propDefs ->
context?.apply {
this.propertyDefinitions = propDefs
} ?: throw ContextNotFoundException()
}
),
getter = {
@Suppress("UNCHECKED_CAST")
it as IsTypedDataModel>
}
).also(this::addSingle)
val meta = EmbeddedObjectDefinitionWrapper(
2u,
"meta",
EmbeddedObjectDefinition(required = true, final = true, dataModel = { RootDataModelDefinition.Model }),
getter = RootDataModel<*>::Meta,
).also(this::addSingle)
override fun invoke(values: ObjectValues, IsObjectDataModel>>): RootDataModel<*> =
RootDataModel(meta = { values(meta.index) }).also {
values>(properties.index).forEach(it::addSingle)
}
override val Serializer = object: ObjectDataModelSerializer, IsObjectDataModel>, ContainsDefinitionsContext, ContainsDefinitionsContext>(this) {
override fun writeObjectAsJson(
obj: RootDataModel<*>,
writer: IsJsonLikeWriter,
context: ContainsDefinitionsContext?,
skip: List>>?
) {
writer.writeStartObject()
for (def in meta.dataModel) {
// Skip name if defined higher
if (def == meta.dataModel.name && context != null && context.currentDefinitionName == obj.Meta.name) {
context.currentDefinitionName = "" // Reset after use
continue
}
val value = def.getPropertyAndSerialize(obj.Meta, context) ?: continue
writer.writeFieldName(def.name)
def.writeJsonValue(value, writer, context)
}
if (writer is YamlWriter) {
// Write optimized format when writing yaml
for (property in obj) {
properties.valueDefinition.writeJsonValue(property, writer, context)
}
} else {
@Suppress("UNCHECKED_CAST")
this.writeJsonValue(
properties as IsDefinitionWrapper,
writer,
obj,
context
)
}
writer.writeEndObject()
}
override fun walkJsonToRead(
reader: IsJsonLikeReader,
values: MutableValueItems,
context: ContainsDefinitionsContext?
) {
val deserializedProperties = properties.newMutableCollection(context as? DefinitionsConversionContext)
val metaValues = mutableListOf()
var keyDefinitionToReadLater: List? = null
var indicesToReadLater: List? = null
// Inject name if it was defined as a map key in a higher level
context?.currentDefinitionName?.let { name ->
if (name.isNotBlank()) {
if (values.contains(RootDataModelDefinition.Model.name.index)) {
throw RequestException("Name $name was already defined by map")
}
// Reset it so no deeper value can reuse it
context.currentDefinitionName = ""
metaValues += ValueItem(RootDataModelDefinition.Model.name.index, name)
}
}
walker@ do {
val token = reader.currentToken
when (token) {
is JsonToken.StartComplexFieldName -> {
deserializedProperties += properties.valueDefinition.readJson(reader, context)
}
is JsonToken.FieldName -> {
val value = token.value ?: throw ParseException("Empty field name not allowed in JSON")
when (val definition = RootDataModelDefinition.Model[value]) {
null -> {
if (value == properties.name) {
reader.nextToken() // continue for field name
deserializedProperties += properties.readJson(reader, context as DefinitionsConversionContext)
} else {
reader.skipUntilNextField()
continue@walker
}
}
RootDataModelDefinition.Model.indices -> {
indicesToReadLater = mutableListOf().apply {
reader.skipUntilNextField(::add)
}
continue@walker
}
RootDataModelDefinition.Model.key -> {
keyDefinitionToReadLater = mutableListOf().apply {
reader.skipUntilNextField(::add)
}
continue@walker
}
else -> {
reader.nextToken()
metaValues += ValueItem(definition.index, definition.definition.readJson(reader, context))
}
}
}
else -> break@walker
}
reader.nextToken()
} while (token !is JsonToken.Stopped)
fun readDelayed(
tokensToReadLater: List?,
propertyDefinitionWrapper: IsDefinitionWrapper<*, *, DefinitionsConversionContext, *>,
) {
tokensToReadLater?.let { jsonTokens ->
val lateReader = if (reader is IsYamlReader) {
jsonTokens.map { reader.pushToken(it) }
reader.pushToken(reader.currentToken)
reader.nextToken()
reader
} else {
PresetJsonTokenReader(jsonTokens)
}
metaValues += ValueItem(
propertyDefinitionWrapper.index,
propertyDefinitionWrapper.readJson(lateReader, context as DefinitionsConversionContext?)
)
if (reader is IsYamlReader) {
reader.nextToken()
}
}
}
(context as? DefinitionsConversionContext)?.let {
it.propertyDefinitions = deserializedProperties as IsDataModel?
}
readDelayed(keyDefinitionToReadLater, RootDataModelDefinition.Model.key)
readDelayed(indicesToReadLater, RootDataModelDefinition.Model.indices)
metaValues.sortBy { it.index }
if (model.isNotEmpty()) {
values[properties.index] = deserializedProperties
}
if (metaValues.isNotEmpty()) {
values[meta.index] = ObjectValues(
RootDataModelDefinition.Model,
ValueItems(*metaValues.toTypedArray()),
).toDataObject()
}
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy