ousia 2.0.0

Postgres ORM with native double-entry ledger, graph relations, and atomic money operations for Rust
Documentation
use std::collections::BTreeMap;

use serde::{Deserialize, Serialize};
use uuid::Uuid;

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct IndexMeta(pub BTreeMap<String, IndexValue>);

impl IndexMeta {
    pub fn meta(&self) -> &BTreeMap<String, IndexValue> {
        &self.0
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum IndexValueInner {
    String(String),
    Int(i64),
    Float(f64),
}

impl IndexValueInner {
    pub fn as_string(&self) -> Option<&str> {
        match self {
            IndexValueInner::String(s) => Some(s),
            _ => None,
        }
    }

    pub fn as_int(&self) -> Option<i64> {
        match self {
            IndexValueInner::Int(i) => Some(*i),
            _ => None,
        }
    }

    pub fn as_float(&self) -> Option<f64> {
        match self {
            IndexValueInner::Float(f) => Some(*f),
            _ => None,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum IndexValue {
    String(String),
    Int(i64),
    Float(f64),
    Bool(bool),
    Uuid(Uuid),
    Timestamp(chrono::DateTime<chrono::Utc>),
    Array(Vec<IndexValueInner>),
}

impl IndexValue {
    pub fn as_string(&self) -> Option<&str> {
        match self {
            IndexValue::String(s) => Some(s),
            _ => None,
        }
    }

    pub fn as_int(&self) -> Option<i64> {
        match self {
            IndexValue::Int(i) => Some(*i),
            _ => None,
        }
    }

    pub fn as_float(&self) -> Option<f64> {
        match self {
            IndexValue::Float(f) => Some(*f),
            _ => None,
        }
    }

    pub fn as_bool(&self) -> Option<bool> {
        match self {
            IndexValue::Bool(b) => Some(*b),
            _ => None,
        }
    }

    pub fn as_timestamp(&self) -> Option<chrono::DateTime<chrono::Utc>> {
        match self {
            IndexValue::Timestamp(t) => Some(*t),
            _ => None,
        }
    }

    pub fn as_array(&self) -> Option<Vec<IndexValueInner>> {
        match self {
            IndexValue::Array(a) => Some(a.clone()),
            _ => None,
        }
    }
}

// Helper trait to convert types to IndexValue
pub trait ToIndexValue {
    fn to_index_value(&self) -> IndexValue;
}

impl ToIndexValue for String {
    fn to_index_value(&self) -> IndexValue {
        IndexValue::String(self.clone())
    }
}

impl ToIndexValue for &str {
    fn to_index_value(&self) -> IndexValue {
        IndexValue::String(self.to_string())
    }
}

impl ToIndexValue for i64 {
    fn to_index_value(&self) -> IndexValue {
        IndexValue::Int(*self)
    }
}

impl ToIndexValue for i32 {
    fn to_index_value(&self) -> IndexValue {
        IndexValue::Int(*self as i64)
    }
}

impl ToIndexValue for f64 {
    fn to_index_value(&self) -> IndexValue {
        IndexValue::Float(*self)
    }
}

impl ToIndexValue for f32 {
    fn to_index_value(&self) -> IndexValue {
        IndexValue::Float(*self as f64)
    }
}

impl ToIndexValue for bool {
    fn to_index_value(&self) -> IndexValue {
        IndexValue::Bool(*self)
    }
}

impl ToIndexValue for chrono::DateTime<chrono::Utc> {
    fn to_index_value(&self) -> IndexValue {
        IndexValue::Timestamp(*self)
    }
}

impl ToIndexValue for IndexValueInner {
    fn to_index_value(&self) -> IndexValue {
        match self {
            IndexValueInner::String(s) => IndexValue::String(s.clone()),
            IndexValueInner::Int(i) => IndexValue::Int(*i),
            IndexValueInner::Float(f) => IndexValue::Float(*f),
        }
    }
}

impl ToIndexValue for Vec<IndexValueInner> {
    fn to_index_value(&self) -> IndexValue {
        IndexValue::Array(self.clone())
    }
}

impl ToIndexValue for Vec<String> {
    fn to_index_value(&self) -> IndexValue {
        IndexValue::Array(
            self.iter()
                .map(|s| IndexValueInner::String(s.clone()))
                .collect(),
        )
    }
}

impl ToIndexValue for Vec<&str> {
    fn to_index_value(&self) -> IndexValue {
        IndexValue::Array(
            self.iter()
                .map(|s| IndexValueInner::String(s.to_string()))
                .collect(),
        )
    }
}

impl ToIndexValue for Vec<i64> {
    fn to_index_value(&self) -> IndexValue {
        IndexValue::Array(self.iter().map(|i| IndexValueInner::Int(*i)).collect())
    }
}

impl ToIndexValue for Vec<f64> {
    fn to_index_value(&self) -> IndexValue {
        IndexValue::Array(self.iter().map(|f| IndexValueInner::Float(*f)).collect())
    }
}

impl ToIndexValue for Uuid {
    fn to_index_value(&self) -> IndexValue {
        IndexValue::Uuid(self.clone())
    }
}

impl<T: ToIndexValue + Default> ToIndexValue for Option<T> {
    fn to_index_value(&self) -> IndexValue {
        match self {
            Some(val) => val.to_index_value(),
            None => T::default().to_index_value(),
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IndexKind {
    Search, // equality + adapter-defined text matching
    Sort,   // ordered comparison
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IndexField {
    pub name: &'static str,
    pub kinds: &'static [IndexKind],
}

pub trait IndexQuery {
    fn indexed_fields() -> &'static [IndexField];
}

#[derive(Debug, Clone)]
pub struct QueryFilter {
    pub field: &'static IndexField,
    pub value: IndexValue,
    pub mode: QueryMode,
}

#[derive(Debug, Clone)]
pub enum QueryMode {
    Search(QuerySearch),
    Sort(QuerySort),
    /// Produces `ORDER BY RANDOM()`. Field and value on the filter are ignored.
    /// Note: requires a full table scan — only use on small result sets or with a prior
    /// LIMIT applied via a subquery in your application.
    SortRandom,
}

impl QueryMode {
    pub fn as_search(&self) -> Option<&QuerySearch> {
        match self {
            QueryMode::Search(search) => Some(search),
            _ => None,
        }
    }

    pub fn as_sort(&self) -> Option<&QuerySort> {
        match self {
            QueryMode::Sort(sort) => Some(sort),
            _ => None,
        }
    }

    pub fn is_random_sort(&self) -> bool {
        matches!(self, QueryMode::SortRandom)
    }

    pub fn search(comp: Comparison, op: Option<Operator>) -> Self {
        QueryMode::Search(QuerySearch {
            comparison: comp,
            operator: op.unwrap_or_default(),
        })
    }

    /// Search using default comparison '=' and 'AND' operator
    pub fn search_default() -> Self {
        QueryMode::Search(QuerySearch {
            comparison: Comparison::Equal,
            operator: Operator::And,
        })
    }

    pub fn sort(asc: bool) -> Self {
        QueryMode::Sort(QuerySort { ascending: asc })
    }

    /// Sort using AND operator
    pub fn sort_default() -> Self {
        QueryMode::Sort(QuerySort { ascending: true })
    }

    /// Random sort — produces `ORDER BY RANDOM()`.
    pub fn sort_random() -> Self {
        QueryMode::SortRandom
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QuerySearch {
    pub comparison: Comparison,
    pub operator: Operator,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QuerySort {
    pub ascending: bool,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Comparison {
    Equal,
    NotEqual,
    BeginsWith,
    NotBeginsWith,
    Contains,
    NotContains,
    ContainsAll,
    NotContainsAll,
    /// Scalar field is NOT IN the supplied array of values.
    NotIn,
    GreaterThan,
    LessThan,
    GreaterThanOrEqual,
    LessThanOrEqual,
}

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum Operator {
    #[default]
    And,
    Or,
}

/// Sentinel `IndexField` used with `QueryFilter::random_sort()`.
/// The field name and kinds are irrelevant — the sort produces `ORDER BY RANDOM()`.
pub static SORT_RANDOM_FIELD: IndexField = IndexField {
    name: "__random__",
    kinds: &[],
};

/// Pagination cursor
#[derive(Debug, Clone, Copy)]
pub struct Cursor {
    pub last_id: Uuid,
}

impl Into<Cursor> for Uuid {
    fn into(self) -> Cursor {
        Cursor { last_id: self }
    }
}

impl QueryFilter {
    /// Returns a filter that produces `ORDER BY RANDOM()`.
    /// Field and value are irrelevant and will not be bound.
    pub fn random_sort() -> Self {
        Self {
            field: &SORT_RANDOM_FIELD,
            value: IndexValue::Bool(false),
            mode: QueryMode::SortRandom,
        }
    }
}