armour-typ 0.1.0

Shared schema/type descriptors (Typ, ScalarTyp, GetType) for the armour ecosystem
Documentation
use std::hash::{Hash, Hasher};

use serde::{Serialize, Serializer, ser::SerializeMap};
use xxhash_rust::xxh3::Xxh3;

use crate::scalar::ScalarTyp;

pub type NamedField = (&'static str, Typ);
pub type Arr<T> = &'static [T];
pub type Str = &'static str;
pub type Map<T> = Arr<(u8, T)>;

#[derive(PartialEq, Clone, Serialize, Debug, Copy, Hash)]
#[serde(tag = "type", content = "data")]
pub enum Fields {
    Named(Arr<NamedField>),
    Unnamed(Arr<Typ>),
}

/**
named structs, unnamed structs and tuples
если нет названий полей в структуре, то это неименованная структура или кортеж.
*/
#[derive(PartialEq, Clone, Serialize, Debug, Copy, Hash)]
pub struct StructType {
    pub name: &'static str,
    pub fields: Fields,
}

fn variants<S, T>(variants: Map<T>, serializer: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
    T: Serialize,
{
    let mut map = serializer.serialize_map(Some(variants.len()))?;
    for (key, value) in variants {
        map.serialize_entry(key, value)?;
    }
    map.end()
}

#[derive(PartialEq, Clone, Serialize, Debug, Copy, Hash)]
pub struct EnumType {
    pub name: Str,
    #[serde(serialize_with = "variants")]
    pub variants: Map<NamedField>,
}

#[derive(PartialEq, Eq, Clone, Serialize, Debug, Copy, Hash)]
pub struct SimpleEnumType {
    pub name: Str,
    #[serde(serialize_with = "variants")]
    pub variants: Map<Str>,
}

#[derive(PartialEq, Clone, Debug, Copy, Hash)]
pub enum Typ {
    Scalar(ScalarTyp),
    Array(u32, &'static Typ),
    Vec(&'static Typ),
    Optional(&'static Typ),
    SimpleEnum(SimpleEnumType),
    Struct(StructType),
    Enum(EnumType),
    Custom(&'static str, &'static [Typ]),
}

/// Composite-only mirror for serde; leaves serialize via [`ScalarTyp`] (flat wire).
#[derive(Serialize)]
#[serde(tag = "type", content = "data")]
enum TypComposite {
    Array(u32, &'static Typ),
    Vec(&'static Typ),
    Optional(&'static Typ),
    SimpleEnum(SimpleEnumType),
    Struct(StructType),
    Enum(EnumType),
    Custom(&'static str, &'static [Typ]),
}

impl Serialize for Typ {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        match self {
            Typ::Scalar(s) => s.serialize(serializer),
            Typ::Array(n, inner) => TypComposite::Array(*n, inner).serialize(serializer),
            Typ::Vec(inner) => TypComposite::Vec(inner).serialize(serializer),
            Typ::Optional(inner) => TypComposite::Optional(inner).serialize(serializer),
            Typ::SimpleEnum(e) => TypComposite::SimpleEnum(*e).serialize(serializer),
            Typ::Struct(s) => TypComposite::Struct(*s).serialize(serializer),
            Typ::Enum(e) => TypComposite::Enum(*e).serialize(serializer),
            Typ::Custom(name, args) => TypComposite::Custom(name, args).serialize(serializer),
        }
    }
}

impl Typ {
    /// Schema hash persisted in armdb (`db.info`/`CollectionInfo`) and checked on
    /// open. Byte output MUST stay stable; drift is caught by the golden test below.
    pub fn h(&self) -> u64 {
        let mut hasher = Xxh3::new();
        self.hash(&mut hasher);
        hasher.finish()
    }
}

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

    /// One entry per `Typ` variant family. The third field is the expected
    /// `typ_hash`, captured once from `print_golden` (Step 5). NEVER hand-edit
    /// these values to silence a failure — a mismatch means the persisted
    /// schema-hash format drifted and must be investigated.
    fn golden_cases() -> Vec<(&'static str, Typ, u64)> {
        use ScalarTyp::*;
        vec![
            ("Bool", Typ::Scalar(Bool), 0xd0a66a65c7528968),
            ("U8", Typ::Scalar(U8), 0x0cfbaea74a8cc0e5),
            ("U16", Typ::Scalar(U16), 0x59ea21c711c350c2),
            ("U32", Typ::Scalar(U32), 0xa9c537f75b27a974),
            ("U64", Typ::Scalar(U64), 0xeb8a271e1e70f1b5),
            ("I32", Typ::Scalar(I32), 0xace144d934729048),
            ("I64", Typ::Scalar(I64), 0x280869ef8929d9c9),
            ("F32", Typ::Scalar(F32), 0xa19e70708438a272),
            ("F64", Typ::Scalar(F64), 0x843be9b447f64cd1),
            ("Str", Typ::Scalar(Str), 0xf372327de2e0d2a4),
            ("Datetime", Typ::Scalar(Datetime), 0x54672872443b2b9b),
            ("Timestamp", Typ::Scalar(Timestamp), 0xf41318623c6f44b3),
            ("Decimal", Typ::Scalar(Decimal), 0x04d54e82812b9535),
            ("Id32", Typ::Scalar(Id32), 0xa1254b871fc40f7c),
            ("Id64", Typ::Scalar(Id64), 0x08b62024788c9438),
            ("Fuid", Typ::Scalar(Fuid), 0x6d2bac21c16d425f),
            ("LowId", Typ::Scalar(LowId), 0xb4e78e7417625641),
            ("Bytes", Typ::Scalar(Bytes), 0xaeb2d89f37dc9398),
            ("Void", Typ::Scalar(Void), 0xebae81600548f719),
            ("RustJson", Typ::Scalar(RustJson), 0x4843696f209475a3),
            ("JsonBytes", Typ::Scalar(JsonBytes), 0x32c39c1af3fd6a18),
            ("ArrayBytes", Typ::Scalar(ArrayBytes(4)), 0xdd59821af9eecd6b),
            (
                "Array",
                Typ::Array(3, &Typ::Scalar(U64)),
                0xd03c6458e1eab738,
            ),
            ("Vec", Typ::Vec(&Typ::Scalar(U64)), 0x0b478fcad27d8bf8),
            (
                "Optional",
                Typ::Optional(&Typ::Scalar(U64)),
                0xb5c98357ef64570e,
            ),
            (
                "SimpleEnum",
                Typ::SimpleEnum(SimpleEnumType {
                    name: "SE",
                    variants: &[(0, "A"), (1, "B")],
                }),
                0xb604588033cf6cbf,
            ),
            (
                "StructNamed",
                Typ::Struct(StructType {
                    name: "S",
                    fields: Fields::Named(&[("a", Typ::Scalar(U64)), ("b", Typ::Scalar(Str))]),
                }),
                0x34afafe7990e4fe5,
            ),
            (
                "StructUnnamed",
                Typ::Struct(StructType {
                    name: "",
                    fields: Fields::Unnamed(&[Typ::Scalar(U64), Typ::Scalar(Bool)]),
                }),
                0x50bb6b127532a167,
            ),
            (
                "Enum",
                Typ::Enum(EnumType {
                    name: "E",
                    variants: &[(0, ("A", Typ::Scalar(U64))), (1, ("B", Typ::Scalar(Bool)))],
                }),
                0xf06c341aa43fa98a,
            ),
            (
                "Custom",
                Typ::Custom("C", &[Typ::Scalar(U64), Typ::Scalar(Str)]),
                0xb9d53c9e69fcd4c5,
            ),
        ]
    }

    /// Run explicitly to (re)generate golden values:
    /// `cargo test -p armour-typ golden::print_golden -- --ignored --nocapture`
    #[test]
    #[ignore]
    fn print_golden() {
        for (name, t, _) in golden_cases() {
            println!("(\"{name}\", …, 0x{:016x}),", t.h());
        }
    }

    #[test]
    fn golden_typ_hash() {
        for (name, t, expected) in golden_cases() {
            assert_eq!(t.h(), expected, "typ_hash drift for variant `{name}`");
        }
    }

    /// Exhaustiveness: new `ScalarTyp` variant fails to compile without a golden case.
    #[allow(dead_code)]
    fn _scalar_guard(s: &ScalarTyp) {
        use ScalarTyp::*;
        match s {
            Bool | U8 | U16 | U32 | U64 | I32 | I64 | F32 | F64 | Str | Datetime | Timestamp
            | Decimal | Id32 | Id64 | Fuid | LowId | Bytes | Void | RustJson | JsonBytes => {}
            ArrayBytes(_) => {}
        }
    }

    /// Exhaustiveness: new composite `Typ` variant fails to compile without a golden case.
    #[allow(dead_code)]
    fn _typ_guard(t: &Typ) {
        match t {
            Typ::Scalar(_) => {}
            Typ::Array(_, _) => {}
            Typ::Vec(_) => {}
            Typ::Optional(_) => {}
            Typ::SimpleEnum(_) => {}
            Typ::Struct(_) => {}
            Typ::Enum(_) => {}
            Typ::Custom(_, _) => {}
        }
    }
}