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

commonMain.expr.common.Expression.kt Maven / Gradle / Ivy

/*
 * Copyright (c) 2024, OpenSavvy, 4SH 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.common

import opensavvy.ktmongo.bson.Bson
import opensavvy.ktmongo.bson.BsonContext
import opensavvy.ktmongo.bson.BsonFieldWriter
import opensavvy.ktmongo.bson.buildBsonDocument
import opensavvy.ktmongo.dsl.LowLevelApi
import opensavvy.ktmongo.dsl.expr.PredicateOperators
import opensavvy.ktmongo.dsl.tree.Node
import opensavvy.ktmongo.dsl.tree.NodeImpl

/**
 * A node in the BSON AST.
 *
 * Each implementation of this interface is a logical BSON node in our own intermediary representation.
 * Each node knows how to [writeTo] itself into a BSON document.
 *
 * ### Security
 *
 * Implementing this interface allows injecting arbitrary BSON into a request. Be very careful not to make injections possible.
 *
 * ### Implementation notes
 *
 * Prefer implementing [AbstractExpression] instead of implementing this interface directly.
 *
 * ### Debugging notes
 *
 * Use [toString][Any.toString] to view the JSON representation of this expression.
 */
interface Expression : Node {

	/**
	 * The context used to generate this expression.
	 */
	@LowLevelApi
	val context: BsonContext

	/**
	 * Makes this expression immutable.
	 *
	 * After this method has been called, the expression can never be modified again.
	 * This ensures that expressions cannot change after they have been used within other expressions.
	 */
	@LowLevelApi
	override fun freeze()

	/**
	 * Returns a simplified (but equivalent) expression to the current expression.
	 *
	 * Returns `null` when the current expression was simplified into a no-op (= it does nothing).
	 */
	@LowLevelApi
	fun simplify(): Expression?

	/**
	 * Writes the result of [simplifying][simplify] this expression into [writer].
	 */
	@LowLevelApi
	fun writeTo(writer: BsonFieldWriter)

	/**
	 * JSON representation of this expression.
	 */
	override fun toString(): String

	companion object
}

/**
 * Creates a new [BSON document][buildBsonDocument] containing the data from this expression.
 */
@LowLevelApi
fun Expression.toBsonDocument(): Bson =
	buildBsonDocument {
		writeTo(this)
	}

/**
 * Utility implementation for [Expression], which handles the [context], [toString] representation and [freezing][freeze].
 *
 * ### Implementing a new operator
 *
 * Instances of this class are BSON operators, like `$eq`, `$xor`, `$setOnInsert` and `$lookup`.
 *
 * **Custom operators bypass the entirety of the safety features provided by this library.
 * Because they are able to write arbitrary BSON, no checks whatsoever are possible.
 * If you are not careful, this may make injection attacks or data leaking possible.**
 *
 * Before writing your own operator, familiarize yourself with the documentation of [Expression], [AbstractExpression],
 * [CompoundExpression] and [AbstractCompoundExpression], as well as [BsonFieldWriter].
 *
 * Fundamentally, an operator is anything that is able to [write] itself into a BSON document.
 * Operators should not be mutable, except through their [accept][CompoundExpression.accept] method (if they have one).
 *
 * An operator generally looks like the following:
 * ```kotlin
 * @LowLevelApi
 * private class TypePredicateExpressionNode(
 *     val type: BsonType,
 *     context: BsonContext,
 * ) : AbstractExpression(context) {
 *
 *     override fun write(writer: BsonFieldWriter) {
 *         writer.writeInt32("\$type", type.code)
 *     }
 * }
 * ```
 * The [BsonContext] is required at construction because it is needed to implement [toString], which the user could call at any time,
 * including while the operator is being constructed (e.g. when using a debugger). It is extremely important that the
 * `toString` representation they see is consistent with the final BSON sent over the wire.
 *
 * Once you have created your operator, use the [accept][CompoundExpression.accept] method to register it into a DSL:
 * ```kotlin
 * collection.find {
 *     User::name {
 *         accept(TypePredicateExpressionNode(BsonType.Undefined))
 *     }
 * }
 * ```
 *
 * Of course, the operator described above is already made available: [PredicateOperators.hasType].
 *
 * **Note that if your operator accepts a variable number of sub-expressions (e.g. `$and`), you must ensure that it works for any
 * number of expressions, including 1 and 0.** See [simplify].
 *
 * To create an operator that can accept multiple children operators (for example `$and`), implement [AbstractCompoundExpression].
 *
 * Since operators are complex to write, risky to get wrong, and hard to test, we highly recommend to upstream any
 * operator you create so they can benefit from future fixes. Again, **an improperly-written operator may allow data
 * corruption or leaking**.
 */
abstract class AbstractExpression private constructor(
	@property:LowLevelApi override val context: BsonContext,
	private val node: NodeImpl,
) : Node by node, Expression {

	constructor(context: BsonContext) : this(context, NodeImpl())

	/**
	 * `true` if [freeze] has been called. Can never become `false` again.
	 *
	 * If this value is `true`, this node should reject any attempt to mutate it.
	 * It is the responsibility of the implementor to satisfy this invariant.
	 */
	protected val frozen: Boolean
		get() = node.frozen

	/**
	 * Called when this operator should be written to a [writer].
	 *
	 * Note that this function is only called on instances that have passed through [simplify],
	 * so it is guaranteed that this expression is fully simplified already.
	 */
	@LowLevelApi
	protected abstract fun write(writer: BsonFieldWriter)

	@LowLevelApi
	override fun simplify(): AbstractExpression? = this

	@LowLevelApi
	final override fun writeTo(writer: BsonFieldWriter) {
		this.simplify()?.write(writer)
	}

	/**
	 * JSON representation of this expression.
	 *
	 * By default, simplifications are enabled.
	 * Set [simplified] to `false` to disable simplifications.
	 */
	@OptIn(LowLevelApi::class)
	fun toString(simplified: Boolean): String {
		val document = buildBsonDocument {
			if (simplified)
				writeTo(this)
			else
				write(this)
		}

		return document.toString()
	}

	final override fun toString(): String =
		toString(simplified = true)

	companion object
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy