sassi 0.1.0-beta.2

Typed in-memory pool with composable predicate algebra and cross-runtime trait queries.
Documentation
//! [`Cacheable`] trait + [`Field`] accessor — the identity contract for
//! entries stored in a `Punnu`.
//!
//! A `Cacheable` type names its identity column (`Self::Id`), declares
//! a generated companion struct of [`Field`] accessors (`Self::Fields`),
//! and exposes both a deterministic `id()` extractor and a `fields()`
//! constructor that wires every accessor to its real extractor.
//!
//! The companion struct is normally produced by `#[derive(Cacheable)]`
//! (see `sassi-macros`), but hand impls are supported when the macro
//! doesn't fit. Both `id()` and `fields()` are required trait methods
//! so generic code like `PunnuScope<T: Cacheable>` can call `T::fields()`
//! without knowing the concrete type.
//!
//! Correct use starts with explicit identity boundaries: view/projection
//! types should get their own `Punnu<Projection>` instances, collection
//! aggregates should use wrapper structs rather than sentinel values, and
//! tenant/substrate identity should live in the type, id, wrapper, fetcher, or
//! caller-owned pool selection logic that actually enforces that boundary.
//!
//! When a `Punnu<T>` has an L2 backend, `T::cache_type_name()` contributes to
//! the backend keyspace. Use `#[cacheable(type_name = "...")]` or a manual
//! override for long-lived/shared L2 data so backend keys do not change when
//! Rust modules are renamed. The derive default is for local caches, examples,
//! and tests; it is not a durable schema identifier.

use std::hash::Hash;
use std::marker::PhantomData;

/// Identity contract for entries stored in a `Punnu`.
///
/// Caches the **canonical shape** of `T`. Projection or view types should get
/// their own independent `Punnu<ProjectionT>` instance so the type system keeps
/// them disjoint at compile time. The identity-map invariant ("one `id()` → one
/// cached entry") then holds per `Punnu` because the type signature fixes the
/// shape.
///
/// `cache_type_name()` is part of the L2 backend keyspace. Derived types can set
/// it with `#[cacheable(type_name = "myapp.User")]`; hand impls can override the
/// method directly. Treat explicit names as durable schema identifiers: they
/// should be unique inside a namespace and reused only for wire-compatible
/// payloads keyed by the same ids.
pub trait Cacheable: Send + Sync + 'static {
    /// Stable type label used by L2 backends for cache-key and invalidation
    /// keyspaces.
    ///
    /// The default preserves lightweight hand-impl ergonomics, but it is tied
    /// to the Rust type path. Types that store long-lived or shared L2 data
    /// should override this with an application-owned stable name. The derive
    /// macro supports `#[cacheable(type_name = "...")]` for that path. Do not
    /// rely on the default as a durable schema identifier.
    fn cache_type_name() -> &'static str {
        std::any::type_name::<Self>()
    }

    /// Identity type. Must be cheap to clone (the cache copies it on
    /// every lookup), hashable + equality-comparable for the LRU key,
    /// and orderable so iteration over `Punnu` entries is stable across
    /// hash-map reorderings.
    type Id: Hash + Eq + Clone + Ord + Send + Sync + 'static;

    /// Companion accessor struct, normally generated by
    /// `#[derive(Cacheable)]`. One [`Field<Self, FieldType>`] per
    /// declared field of `Self`, plus a `Default` impl so callers can
    /// write `T::Fields::default()` to obtain placeholder accessors.
    type Fields: Default + Send + Sync + 'static;

    /// Identity extraction.
    ///
    /// **MUST be deterministic** (same input → same output across all
    /// calls within a process) and **stable across mutations** of `Self`
    /// — re-inserting after `id()` would change breaks the identity-map
    /// invariant and is a logic bug, not a sassi bug.
    fn id(&self) -> Self::Id;

    /// Construct the field-accessor companion struct with every
    /// accessor wired to its real extractor. Required as a trait method
    /// so generic code like `PunnuScope<T: Cacheable>` can call
    /// `T::fields()` without knowing the concrete type — `Default`-built
    /// `Self::Fields` returns unwired accessors that panic on invocation.
    ///
    /// Normally generated by `#[derive(Cacheable)]`. Hand impls supply
    /// it manually.
    fn fields() -> Self::Fields;
}

/// Field accessor.
///
/// Carries both the column / serde-key name (used by downstream
/// SQL-emitting consumers like djogi) and the in-memory extractor used
/// by sassi's predicate evaluator. The two halves let the same
/// `Field<T, V>` value participate in a SQL `WHERE` emit on a backend
/// AND a Rust-side `evaluate()` walk on a frontend, without diverging.
///
/// Internal storage is `pub(crate)` to keep the (name, extractor) pair
/// stable post-construction. Use [`Field::name`] and [`Field::extract`]
/// accessors instead of direct field access.
///
/// # Example
///
/// ```
/// use sassi::Field;
///
/// struct User { id: i64, age: u32 }
///
/// // Normally generated by `#[derive(Cacheable)]`; shown here for
/// // documentation:
/// let age_field: Field<User, u32> = Field::new("age", |u| &u.age);
/// let alice = User { id: 1, age: 30 };
/// assert_eq!(*age_field.extract(&alice), 30);
/// assert_eq!(age_field.name(), "age");
/// ```
pub struct Field<T, V> {
    /// Column / serde-key name.
    pub(crate) name: &'static str,
    /// Memory-side extractor. Function pointer (not closure) — must be
    /// total and deterministic, no captured state.
    pub(crate) extract: fn(&T) -> &V,
    _marker: PhantomData<(T, V)>,
}

impl<T, V> Field<T, V> {
    /// Construct a new `Field<T, V>`. Normally invoked via the generated
    /// `T::fields()` constructor; available here for hand impls.
    pub const fn new(name: &'static str, extract: fn(&T) -> &V) -> Self {
        Self {
            name,
            extract,
            _marker: PhantomData,
        }
    }

    /// Column / serde-key name. Stable post-construction.
    #[inline]
    pub fn name(&self) -> &'static str {
        self.name
    }

    /// Apply the extractor to a value. Stable post-construction —
    /// the extractor cannot be reassigned, only invoked.
    #[inline]
    pub fn extract<'a>(&self, value: &'a T) -> &'a V {
        (self.extract)(value)
    }
}

// Manual `Copy` / `Clone` rather than derive: the derived bounds would
// require `T: Copy` / `V: Copy` (because of the `PhantomData<(T, V)>`
// field), which we don't want — `Field<T, V>` should be `Copy` for any
// `T`, `V` since all of its real-data fields (`&'static str`, function
// pointer, `PhantomData`) are `Copy` regardless of `T` / `V`.
impl<T, V> Copy for Field<T, V> {}
impl<T, V> Clone for Field<T, V> {
    fn clone(&self) -> Self {
        *self
    }
}

// `Default` for `Field<T, V>` — produces a no-op accessor. Used by the
// `Default` impl on the generated `Fields` companion struct so callers
// can construct it via `T::Fields::default()`. Real wiring happens via
// the `T::fields()` trait method.
impl<T: 'static, V: 'static> Default for Field<T, V> {
    fn default() -> Self {
        // Default extractor must satisfy `fn(&T) -> &V`; with no other
        // information about `T` / `V`, the only sound choice is to
        // reference an immortal default value. Since `V` is unbounded,
        // we cannot construct one — instead, return a function pointer
        // that explicitly panics if invoked. Callers should not invoke
        // accessors obtained via `Default`; they should call the
        // `Cacheable::fields()` trait method instead.
        fn unwired<T, V>(_: &T) -> &V {
            panic!(
                "sassi: extractor invoked on a `Field` produced by `Default`; \
                 use the `Cacheable::fields()` trait method instead"
            )
        }
        Field::new("__sassi_default_unwired__", unwired::<T, V>)
    }
}

impl<T, V> std::fmt::Debug for Field<T, V> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Field")
            .field("name", &self.name)
            .field("type", &std::any::type_name::<V>())
            .finish()
    }
}