All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.expr.UpdateExpression.kt Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2024, OpenSavvy and contributors.
 *
 * 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 opensavvy.ktmongo.dsl.expr

import opensavvy.ktmongo.bson.BsonContext
import opensavvy.ktmongo.bson.BsonFieldWriter
import opensavvy.ktmongo.dsl.DangerousMongoApi
import opensavvy.ktmongo.dsl.KtMongoDsl
import opensavvy.ktmongo.dsl.LowLevelApi
import opensavvy.ktmongo.dsl.expr.common.AbstractCompoundExpression
import opensavvy.ktmongo.dsl.expr.common.AbstractExpression
import opensavvy.ktmongo.dsl.expr.common.Expression
import opensavvy.ktmongo.dsl.path.Field
import opensavvy.ktmongo.dsl.path.FieldDsl
import opensavvy.ktmongo.dsl.path.Path
import opensavvy.ktmongo.dsl.tree.acceptAll
import kotlin.reflect.KClass

/**
 * Implementation of [UpdateOperators].
 */
@KtMongoDsl
class UpdateExpression(
	context: BsonContext,
) : AbstractCompoundExpression(context),
	UpsertOperators,
	FieldDsl {

	// region Low-level operations

	private class OperatorCombinator(
		val type: KClass,
		val combinator: (List, BsonContext) -> T
	) {
		@Suppress("UNCHECKED_CAST") // This is a private class, it should not be used incorrectly
		operator fun invoke(sources: List, context: BsonContext) =
			combinator(sources as List, context)
	}

	@LowLevelApi
	override fun simplify(children: List): AbstractExpression? {
		if (children.isEmpty())
			return null

		val simplifiedChildren = combinators.fold(children) { newChildren, combinator ->
			@Suppress("UNCHECKED_CAST") // safe because of the filter
			val matching = newChildren
				.filter { it::class == combinator.type }
				as List

			if (matching.size <= 1)
			// At least two elements are required to combine them into a single one!
				return@fold newChildren

			val childrenWithoutMatching = newChildren - matching.toSet()
			childrenWithoutMatching + combinator(matching, context)
		}

		@OptIn(DangerousMongoApi::class)
		if (simplifiedChildren != children)
			return UpdateExpression(context).apply {
				acceptAll(simplifiedChildren)
			}
		return this
	}

	@LowLevelApi
	private sealed class UpdateExpressionNode(context: BsonContext) : AbstractExpression(context)

	// endregion
	// region $set

	@OptIn(LowLevelApi::class, DangerousMongoApi::class)
	@Suppress("INVISIBLE_REFERENCE")
	@KtMongoDsl
	override infix fun <@kotlin.internal.OnlyInputTypes V> Field.set(value: V) {
		accept(SetExpressionNode(listOf(this.path to value), context))
	}

	@LowLevelApi
	private class SetExpressionNode(
		val mappings: List>,
		context: BsonContext,
	) : UpdateExpressionNode(context) {

		override fun simplify() =
			this.takeUnless { mappings.isEmpty() }

		override fun write(writer: BsonFieldWriter) = with(writer) {
			writeDocument("\$set") {
				for ((field, value) in mappings) {
					writeObjectSafe(field.toString(), value, context)
				}
			}
		}
	}

	// endregion
	// region $setOnInsert

	@OptIn(LowLevelApi::class, DangerousMongoApi::class)
	@Suppress("INVISIBLE_REFERENCE")
	@KtMongoDsl
	override infix fun <@kotlin.internal.OnlyInputTypes V> Field.setOnInsert(value: V) {
		accept(SetOnInsertExpressionNode(listOf(this.path to value), context))
	}

	@LowLevelApi
	private class SetOnInsertExpressionNode(
		val mappings: List>,
		context: BsonContext,
	) : UpdateExpressionNode(context) {
		override fun simplify(): AbstractExpression? =
			this.takeUnless { mappings.isEmpty() }

		override fun write(writer: BsonFieldWriter) = with(writer) {
			writeDocument("\$setOnInsert") {
				for ((field, value) in mappings) {
					writeObjectSafe(field.toString(), value, context)
				}
			}
		}
	}

	// endregion
	// region $inc

	@OptIn(LowLevelApi::class, DangerousMongoApi::class)
	@Suppress("INVISIBLE_REFERENCE")
	@KtMongoDsl
	override infix fun <@kotlin.internal.OnlyInputTypes V : Number> Field.inc(amount: V) {
		accept(IncrementExpressionNode(listOf(this.path to amount), context))
	}

	@LowLevelApi
	private class IncrementExpressionNode(
		val mappings: List>,
		context: BsonContext,
	) : UpdateExpressionNode(context) {
		override fun simplify(): AbstractExpression? =
			this.takeUnless { mappings.isEmpty() }

		override fun write(writer: BsonFieldWriter) = with(writer) {
			writeDocument("\$inc") {
				for ((field, value) in mappings) {
					writeObjectSafe(field.toString(), value, context)
				}
			}
		}
	}

	// endregion
	// region $unset

	@OptIn(LowLevelApi::class, DangerousMongoApi::class)
	@Suppress("INVISIBLE_REFERENCE")
	@KtMongoDsl
	override fun <@kotlin.internal.OnlyInputTypes V> Field.unset() {
		accept(UnsetExpressionNode(listOf(this.path), context))
	}

	@LowLevelApi
	private class UnsetExpressionNode(
		val fields: List,
		context: BsonContext,
	) : UpdateExpressionNode(context) {
		override fun simplify(): AbstractExpression? =
			this.takeUnless { fields.isEmpty() }

		override fun write(writer: BsonFieldWriter) = with(writer) {
			writeDocument("\$unset") {
				for (field in fields) {
					writeBoolean(field.toString(), true)
				}
			}
		}
	}

	// endregion
	// region $rename

	@OptIn(LowLevelApi::class, DangerousMongoApi::class)
	@Suppress("INVISIBLE_REFERENCE")
	@KtMongoDsl
	override infix fun <@kotlin.internal.OnlyInputTypes V> Field.renameTo(newName: Field) {
		accept(RenameExpressionNode(listOf(this.path to newName.path), context))
	}

	@LowLevelApi
	private class RenameExpressionNode(
		val fields: List>,
		context: BsonContext,
	) : UpdateExpressionNode(context) {
		override fun simplify(): AbstractExpression? =
			this.takeUnless { fields.isEmpty() }

		override fun write(writer: BsonFieldWriter) = with(writer) {
			writeDocument("\$rename") {
				for ((before, after) in fields) {
					writeString(before.toString(), after.toString())
				}
			}
		}
	}

	// endregion

	companion object {
		@OptIn(LowLevelApi::class)
		private val combinators = listOf(
			OperatorCombinator(SetExpressionNode::class) { sources, context ->
				SetExpressionNode(sources.flatMap { it.mappings }, context)
			},
			OperatorCombinator(SetOnInsertExpressionNode::class) { sources, context ->
				SetOnInsertExpressionNode(sources.flatMap { it.mappings }, context)
			},
			OperatorCombinator(IncrementExpressionNode::class) { sources, context ->
				IncrementExpressionNode(sources.flatMap { it.mappings }, context)
			},
			OperatorCombinator(UnsetExpressionNode::class) { sources, context ->
				UnsetExpressionNode(sources.flatMap { it.fields }, context)
			},
			OperatorCombinator(RenameExpressionNode::class) { sources, context ->
				RenameExpressionNode(sources.flatMap { it.fields }, context)
			},
		)
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy