criterium 3.1.3

Lightweigt dynamic database queries for rusqlite.
Documentation
// SPDX-FileCopyrightText: 2025 Slatian
//
// SPDX-License-Identifier: LGPL-3.0-only

//! Matching without a database.
//!
//! This module helps with implementing custom matching logic on top of your own rust datatypes using the [DirectMatch] trait.

use std::{fmt::Display, ops::Not};

/// For implementing directly matching against some datatype `V`
///
/// You want to implent this on your own criteria.
pub trait DirectMatch<V: ?Sized> {
	/// The type that `criterium_match` should return as its output.
	///
	/// Setting this to be a `bool` is a good starting point.
	/// Also see: [DirectMatchResult]
	type Output: DirectMatchResult;

	/// Returns wheter the implementing criterium matches the given data.
	///
	/// This must not have side effects!
	fn criterium_match(&self, data: &V) -> Self::Output;
}

/// Functions a type that wants to be useable as the result of a `DirectMatch` comparison must implement.
///
/// By default these are implemented for:
/// * `bool`
/// * `Option<T: DirectMatchResult + Clone>`
/// * `Result<T: DirectMatchResult + Clone, E: Clone>`
/// * [MaybeDirectMatchResult]
///
/// For `and` and `or` operations the following rules apply:
/// * Using the provided `and` and `or` methods isn't mandatory, any implementation that follows these rules is okay.
/// * `and` implementations may short circuit on the first `false`
/// * `or` implementations may short circuit on the first `true`
/// * An error must always short cicuit
/// * if one or more `unknown` values are encountered and no short circuit condition occurs one of those `unknown` values must be returned at the end. An `unknown` must not result in a short circut.
/// * For non short circuit conditions `and` and `or` implementations may pick **any** allowed value.
pub trait DirectMatchResult: Clone {
	/// Returns the kind of result that this is
	fn kind(&self) -> DirectMatchResultKind;

	/// Inverts the contained value. (Same as `!self`)
	///
	/// * Turns `true` into `false`
	/// * Turns `false` into `true`
	/// * `Unknown` stays `Unknown`
	fn invert(self) -> Self;

	/// Constructs a new self that represents the given value.
	///
	/// This is mainly used for the MatchAlways and MatchNever variants of the CriteriumChain and fallback mechanisms.
	fn new(value: bool) -> Self;

	/// Turns this into an option containing a bool.
	fn as_opt(&self) -> Option<bool> {
		match self.kind() {
			DirectMatchResultKind::False => Some(false),
			DirectMatchResultKind::True => Some(true),
			DirectMatchResultKind::Unknown => None,
			DirectMatchResultKind::Error => None,
		}
	}

	/// Returns the contained value or the fallback if unknown.
	fn unwrap_or(&self, fallback: bool) -> bool {
		self.as_opt().unwrap_or(fallback)
	}

	/// Convenience funcion: Applies a boolean `and` to self and other.
	///
	/// If `self` is `true` or an error `self` is returned, otherwise `other` is returned.
	fn and(self, other: Self) -> Self {
		if matches!(
			self.kind(),
			DirectMatchResultKind::True | DirectMatchResultKind::Error
		) {
			other
		} else {
			self
		}
	}

	/// Convenience function: Applies a boolean `or` to self and other.
	///
	/// If `self` is `false` or an error `self` is returned, otherwise `other` is returned.
	fn or(self, other: Self) -> Self {
		if matches!(
			self.kind(),
			DirectMatchResultKind::True | DirectMatchResultKind::Error
		) {
			self
		} else {
			other
		}
	}
}

/// Classification of what a [DirectMatchResult] represents.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DirectMatchResultKind {
	/// The matching itself was sucessful, but didn't match.
	False,

	/// The matching was sucessful, it matched.
	True,

	/// The match couldn't be executed based on the available data.
	///
	/// This is useful for:
	/// * Simplified datastructures in caches that don't have full knowlege.
	/// * Implementing database criteria on the structs themselves
	/// * Datastructures like bloom filters, that answer `no` and `unknown`
	Unknown,

	/// An error happened while trying to run the match.
	Error,
}

impl DirectMatchResultKind {
	/// Wheter this is the `True` variant
	#[inline]
	pub fn is_true(&self) -> bool {
		matches!(self, Self::True)
	}

	/// Wheter this is the `False` variant
	#[inline]
	pub fn is_false(&self) -> bool {
		matches!(self, Self::False)
	}

	/// Wheter this is the `Unknown` variant
	#[inline]
	pub fn is_unknown(&self) -> bool {
		matches!(self, Self::Unknown)
	}

	/// Wheter this is the `Error` variant
	#[inline]
	pub fn is_error(&self) -> bool {
		matches!(self, Self::Error)
	}
}

impl DirectMatchResult for bool {
	fn kind(&self) -> DirectMatchResultKind {
		match self {
			true => DirectMatchResultKind::True,
			false => DirectMatchResultKind::False,
		}
	}

	fn as_opt(&self) -> Option<bool> {
		Some(*self)
	}

	fn unwrap_or(&self, _fallback: bool) -> bool {
		*self
	}

	fn and(self, other: Self) -> Self {
		self && other
	}

	fn or(self, other: Self) -> Self {
		self || other
	}

	fn new(value: bool) -> Self {
		value
	}

	fn invert(self) -> Self {
		!self
	}
}

impl<T, E> DirectMatchResult for Result<T, E>
where
	T: DirectMatchResult + Clone,
	E: Clone,
{
	fn as_opt(&self) -> Option<bool> {
		match self {
			Ok(v) => v.as_opt(),
			Err(_) => None,
		}
	}

	fn kind(&self) -> DirectMatchResultKind {
		match self {
			Ok(v) => v.kind(),
			Err(_) => DirectMatchResultKind::Error,
		}
	}

	fn new(value: bool) -> Self {
		Ok(T::new(value))
	}

	fn invert(self) -> Self {
		self.map(|v| v.invert())
	}
}

/// A datatype for representing a boolean or an unknown result with matching operators.
///
/// Unknown results are useful i.e. for having a cache in front of a database or API with a simplified data model, if the cache can answer the query then great, otherwise no problem either.
///
/// This could also be used to represent the output from something like a bloom filter.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MaybeDirectMatchResult {
	/// This contins a known value
	Bool(bool),
	/// Represents an unknown value, i.e. a comparison for which the data wasn't available.
	///
	/// Unlike an SQL NULL this won't try to infect everything, see the logic functions.
	Unknown,
}

impl DirectMatchResult for MaybeDirectMatchResult {
	fn as_opt(&self) -> Option<bool> {
		match self {
			Self::Bool(b) => Some(*b),
			Self::Unknown => None,
		}
	}

	fn unwrap_or(&self, fallback: bool) -> bool {
		match self {
			Self::Bool(b) => *b,
			Self::Unknown => fallback,
		}
	}

	fn kind(&self) -> DirectMatchResultKind {
		match self {
			Self::Bool(true) => DirectMatchResultKind::True,
			Self::Bool(false) => DirectMatchResultKind::False,
			Self::Unknown => DirectMatchResultKind::Unknown,
		}
	}

	fn new(value: bool) -> Self {
		Self::Bool(value)
	}

	fn invert(self) -> Self {
		!self
	}
}

impl<T> DirectMatchResult for Option<T>
where
	T: DirectMatchResult,
{
	fn as_opt(&self) -> Option<bool> {
		match self {
			Self::Some(v) => v.as_opt(),
			Self::None => None,
		}
	}

	fn unwrap_or(&self, fallback: bool) -> bool {
		match self {
			Self::Some(v) => v.unwrap_or(fallback),
			Self::None => fallback,
		}
	}

	fn kind(&self) -> DirectMatchResultKind {
		match self {
			Self::Some(v) => v.kind(),
			Self::None => DirectMatchResultKind::Unknown,
		}
	}

	fn new(value: bool) -> Self {
		Some(T::new(value))
	}

	fn invert(self) -> Self {
		self.map(|v| v.invert())
	}
}

impl Not for MaybeDirectMatchResult {
	type Output = MaybeDirectMatchResult;

	fn not(self) -> Self::Output {
		match self {
			Self::Bool(b) => Self::Bool(!b),
			Self::Unknown => Self::Unknown,
		}
	}
}

impl From<Option<bool>> for MaybeDirectMatchResult {
	fn from(value: Option<bool>) -> Self {
		match value {
			Some(b) => Self::Bool(b),
			None => Self::Unknown,
		}
	}
}

impl From<MaybeDirectMatchResult> for Option<bool> {
	fn from(value: MaybeDirectMatchResult) -> Self {
		value.as_opt()
	}
}

impl Display for MaybeDirectMatchResult {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		match self {
			Self::Bool(b) => b.fmt(f),
			Self::Unknown => f.write_str("unknown"),
		}
	}
}