armour-typ 0.3.0

Shared schema/type descriptors (Typ, ScalarTyp, GetType) for the armour ecosystem
Documentation
//! Declarative, serializable schema-graph model. No executable code here —
//! the registry/validator live in `armdb::schema`; this model is what a
//! future RPC `GetSchema` extension can ship to a GUI.

use serde::Serialize;

use crate::refs::PathSeg;
use crate::typ::Typ;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum CollectionKind {
    /// Primary collection owning an entity (FK target for its brand).
    Entity,
    /// Derived collection synchronized from a source (hook/task).
    SecondaryIndex,
    /// Same entity duplicated for another access path.
    Denormalized,
    /// Recomputable counters/aggregates.
    Counters,
    /// Not related to other collections.
    Standalone,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum OnMissing {
    /// Missing target — error finding.
    Error,
    /// Missing target — warning finding.
    Warn,
    /// Missing target is legal (e.g. reply to a deleted message).
    AllowMissing,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub enum RelationKind {
    Fk {
        /// Path of the ID field inside key/value ("k"/"v" prefix in `field_in`).
        field_path: Vec<PathSeg>,
        brand: String,
        on_missing: OnMissing,
        /// false — graph-only edge; validator reports it as UnvalidatedEdge
        /// unless an explicit fk declaration covers it.
        validated: bool,
    },
    /// `to` is a secondary index of `from`.
    Index,
    /// `to` is a denormalization of `from`.
    Denorm,
    /// Field `field` in `from` is recomputed from `to`.
    Counter { field: String },
}

/// Value-model of a collection (how the value is serialized on disk).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum ValueModel {
    /// Fixed-size, zerocopy-transmute (ZeroTree/ZeroMap).
    Zero,
    /// Codec value inline in the index (TypedTree/TypedMap).
    Typed,
    /// Codec value on disk + LRU cache, pointer in the index (VarTypedTree/VarTypedMap).
    VarTyped,
}

/// Collection shape (access pattern).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum CollectionShape {
    /// Ordered scan / range / prefix (SkipList).
    Tree,
    /// Point-lookup, no iteration (HashMap).
    Map,
}

/// Durability backend.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum StorageBackend {
    /// Append-only log + compaction.
    Bitcask,
    /// In-place pwrite into fixed slots.
    Fixed,
}

/// Physical storage class of a collection — fixed at `db.open_*`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct StorageClass {
    pub model: ValueModel,
    pub shape: CollectionShape,
    pub backend: StorageBackend,
}

#[derive(Debug, Clone, Serialize)]
pub struct CollectionNode {
    pub name: String,
    pub kind: CollectionKind,
    /// Value type descriptor (already serializable, reused as-is).
    pub ty: Typ,
    /// Key type descriptor (`S::K`). `None` for external nodes (no handle).
    pub key_ty: Option<Typ>,
    /// Brand of SelfId if the key is a branded ID.
    pub self_brand: Option<String>,
    /// Physical storage class. `None` for external nodes (no handle).
    pub storage: Option<StorageClass>,
}

#[derive(Debug, Clone, Serialize)]
pub struct RelationEdge {
    pub from: String,
    pub to: String,
    pub kind: RelationKind,
}

#[derive(Debug, Clone, Serialize, Default)]
pub struct SchemaGraph {
    pub collections: Vec<CollectionNode>,
    pub relations: Vec<RelationEdge>,
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::scalar::ScalarTyp;

    #[test]
    fn graph_serializes_to_json() {
        let g = SchemaGraph {
            collections: vec![CollectionNode {
                name: "users".into(),
                kind: CollectionKind::Entity,
                ty: Typ::Scalar(ScalarTyp::U64),
                key_ty: None,
                self_brand: Some("USR".into()),
                storage: None,
            }],
            relations: vec![RelationEdge {
                from: "messages".into(),
                to: "users".into(),
                kind: RelationKind::Fk {
                    field_path: vec![PathSeg::Field("sender_id")],
                    brand: "USR".into(),
                    on_missing: OnMissing::Error,
                    validated: true,
                },
            }],
        };
        let json = serde_json::to_string(&g).expect("serialize");
        assert!(json.contains("\"users\""));
        assert!(json.contains("\"sender_id\""));
    }
}