armour-typ 0.1.3

Shared schema/type descriptors (Typ, ScalarTyp, GetType) for the armour ecosystem
Documentation
//! Parallel metadata channel for branded ID references.
//!
//! Deliberately separate from [`Typ`](crate::Typ): `typ_hash` is persisted in
//! db.info and must not change. `GetRefs` is runtime-only (called when building
//! a schema registry / validating), so no const-slice gymnastics.

use serde::Serialize;

/// Unique string identifier of an ID family that survives phantom-type erasure.
/// Implemented for hasher types by `const_hasher!`/`const_hasher_or!`/`hasher!`
/// (armour-core). Uniqueness across an application is checked by the schema
/// registry at build time.
pub trait IdBrand {
    const BRAND: &'static str;
}

/// The unbranded marker. `Fuid<()>` / `Id64<()>` are IDs with no foreign-key
/// brand: the empty `BRAND` is treated as "no reference" by the `GetRefs` impls
/// (no graph edge, skipped during validation). Implementing it here also makes
/// the blanket `impl<H: IdBrand> GetRefs for Fuid<H>` total over all hashers,
/// so a raw `Fuid<()>` field compiles under `derive(GetType)` without a separate
/// `Fuid<()>` impl — which coherence would reject, since this trait is upstream.
impl IdBrand for () {
    const BRAND: &'static str = "";
}

/// One segment of a field path inside a stored type.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
#[serde(tag = "seg", content = "v")]
pub enum PathSeg {
    /// Named struct field.
    Field(&'static str),
    /// Enum variant.
    Variant(&'static str),
    /// Position in an unnamed struct / tuple.
    Index(u32),
    /// Element of `Vec`/`Array`/`Option`/map.
    Elem,
}

/// A branded-ID field located at `path` inside a stored type.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct FieldRef {
    pub path: Vec<PathSeg>,
    pub brand: &'static str,
}

impl FieldRef {
    /// Ref pointing at the value itself (empty path) — used by ID types.
    pub fn leaf(brand: &'static str) -> Self {
        Self {
            path: Vec::new(),
            brand,
        }
    }

    /// Prepend a path segment (used by derive when recursing into fields).
    #[must_use]
    pub fn prefixed(mut self, seg: PathSeg) -> Self {
        self.path.insert(0, seg);
        self
    }
}

/// Runtime introspection of branded-ID fields.
///
/// Both methods have empty defaults, so a manual `GetType` impl needs only a
/// trivial `impl GetRefs for T {}` to participate.
///
/// `GetRefs` is **opt-in and decoupled from `GetType`**: `derive(GetType)` never
/// emits `GetRefs` and imposes no `GetRefs` bound on fields, so types holding an
/// unbranded `Fuid<()>`, a non-`IdBrand` hasher, or a field whose type has a
/// manual `GetType` impl keep compiling — regardless of what any other crate in
/// the build graph enables. Derive it explicitly with `#[derive(GetRefs)]` —
/// typically `#[derive(GetType, GetRefs)]` on stored types, or `GetRefs` alone on
/// composite collection keys that don't implement `GetType`. Exclude a field from
/// a derived `GetRefs` with `#[get_type(no_refs)]`.
pub trait GetRefs {
    /// Static structure: which fields carry which brands (graph/ER edges).
    fn refs() -> Vec<FieldRef>
    where
        Self: Sized,
    {
        Vec::new()
    }

    /// Visit branded ID values inside a concrete record (validator).
    fn visit_ids(&self, f: &mut dyn FnMut(&'static str, u64)) {
        let _ = f;
    }
}

/// Empty (leaf) impls for types that cannot contain branded IDs.
macro_rules! empty_refs {
    ($($t:ty),* $(,)?) => {
        $(impl GetRefs for $t {})*
    };
}

empty_refs!(
    bool,
    u8,
    u16,
    u32,
    u64,
    usize,
    i32,
    i64,
    f32,
    f64,
    core::num::NonZeroU32,
    core::num::NonZeroU64,
    String,
    time::Duration,
    time::OffsetDateTime,
    time::PrimitiveDateTime,
    time::Date,
    time::Time,
    std::net::Ipv6Addr,
    std::net::IpAddr,
    std::net::SocketAddrV6,
    uuid::Uuid,
);

impl<const CAP: usize> GetRefs for [u8; CAP] {}
impl<const CAP: usize> GetRefs for arrayvec::ArrayString<CAP> {}

#[cfg(feature = "std")]
impl GetRefs for bytes::Bytes {}
#[cfg(feature = "std")]
impl GetRefs for serde_json::Value {}
#[cfg(feature = "rust_decimal")]
impl GetRefs for rust_decimal::Decimal {}
#[cfg(feature = "compact_str")]
impl GetRefs for compact_str::CompactString {}
#[cfg(feature = "smol_str")]
impl GetRefs for smol_str::SmolStr {}
#[cfg(feature = "solana")]
impl GetRefs for solana_pubkey::Pubkey {}
#[cfg(feature = "solana")]
impl GetRefs for solana_signature::Signature {}

fn elem_refs<T: GetRefs>() -> Vec<FieldRef> {
    T::refs()
        .into_iter()
        .map(|r| r.prefixed(PathSeg::Elem))
        .collect()
}

impl<T: GetRefs> GetRefs for Vec<T> {
    fn refs() -> Vec<FieldRef> {
        elem_refs::<T>()
    }
    fn visit_ids(&self, f: &mut dyn FnMut(&'static str, u64)) {
        for item in self {
            item.visit_ids(f);
        }
    }
}

impl<T: GetRefs, const CAP: usize> GetRefs for arrayvec::ArrayVec<T, CAP> {
    fn refs() -> Vec<FieldRef> {
        elem_refs::<T>()
    }
    fn visit_ids(&self, f: &mut dyn FnMut(&'static str, u64)) {
        for item in self {
            item.visit_ids(f);
        }
    }
}

#[cfg(feature = "smallvec")]
impl<T: GetRefs, const CAP: usize> GetRefs for smallvec::SmallVec<[T; CAP]> {
    fn refs() -> Vec<FieldRef> {
        elem_refs::<T>()
    }
    fn visit_ids(&self, f: &mut dyn FnMut(&'static str, u64)) {
        for item in self {
            item.visit_ids(f);
        }
    }
}

impl<T: GetRefs> GetRefs for Option<T> {
    fn refs() -> Vec<FieldRef> {
        elem_refs::<T>()
    }
    fn visit_ids(&self, f: &mut dyn FnMut(&'static str, u64)) {
        if let Some(v) = self {
            v.visit_ids(f);
        }
    }
}

impl<T: GetRefs> GetRefs for Box<T> {
    fn refs() -> Vec<FieldRef> {
        T::refs()
    }
    fn visit_ids(&self, f: &mut dyn FnMut(&'static str, u64)) {
        (**self).visit_ids(f);
    }
}

macro_rules! tuple_refs {
    ($(($($t:ident . $i:tt),+)),+ $(,)?) => {
        $(
            impl<$($t: GetRefs),+> GetRefs for ($($t,)+) {
                fn refs() -> Vec<FieldRef> {
                    let mut out = Vec::new();
                    $(out.extend($t::refs().into_iter().map(|r| r.prefixed(PathSeg::Index($i))));)+
                    out
                }
                fn visit_ids(&self, f: &mut dyn FnMut(&'static str, u64)) {
                    $(self.$i.visit_ids(f);)+
                }
            }
        )+
    };
}

tuple_refs!((T1.0, T2.1), (T1.0, T2.1, T3.2), (T1.0, T2.1, T3.2, T4.3),);

impl<K: GetRefs, V: GetRefs> GetRefs for std::collections::HashMap<K, V> {
    fn refs() -> Vec<FieldRef> {
        elem_refs::<(K, V)>()
    }
    fn visit_ids(&self, f: &mut dyn FnMut(&'static str, u64)) {
        for (k, v) in self {
            k.visit_ids(f);
            v.visit_ids(f);
        }
    }
}

impl<K: GetRefs, V: GetRefs> GetRefs for std::collections::BTreeMap<K, V> {
    fn refs() -> Vec<FieldRef> {
        elem_refs::<(K, V)>()
    }
    fn visit_ids(&self, f: &mut dyn FnMut(&'static str, u64)) {
        for (k, v) in self {
            k.visit_ids(f);
            v.visit_ids(f);
        }
    }
}

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

    struct TestBrand;
    impl IdBrand for TestBrand {
        const BRAND: &'static str = "TEST";
    }

    /// Эмуляция брэндированного ID (настоящие Fuid/Id64 — в armour-core).
    struct FakeId(u64);
    impl GetRefs for FakeId {
        fn refs() -> Vec<FieldRef> {
            vec![FieldRef::leaf(TestBrand::BRAND)]
        }
        fn visit_ids(&self, f: &mut dyn FnMut(&'static str, u64)) {
            f(TestBrand::BRAND, self.0);
        }
    }

    #[test]
    fn refs_leaf_and_prefix() {
        let r = FieldRef::leaf("TEST").prefixed(PathSeg::Field("a"));
        assert_eq!(r.path, vec![PathSeg::Field("a")]);
        assert_eq!(r.brand, "TEST");
    }

    #[test]
    fn refs_default_empty() {
        assert!(u64::refs().is_empty());
        assert!(String::refs().is_empty());
        let mut seen = Vec::new();
        42u64.visit_ids(&mut |b, v| seen.push((b, v)));
        assert!(seen.is_empty());
    }

    #[test]
    fn refs_containers_forward() {
        let refs = <Vec<Option<FakeId>>>::refs();
        assert_eq!(refs.len(), 1);
        assert_eq!(refs[0].path, vec![PathSeg::Elem, PathSeg::Elem]);

        let v: Vec<Option<FakeId>> = vec![Some(FakeId(7)), None, Some(FakeId(9))];
        let mut seen = Vec::new();
        v.visit_ids(&mut |b, id| seen.push((b, id)));
        assert_eq!(seen, vec![("TEST", 7), ("TEST", 9)]);
    }

    #[test]
    fn refs_tuple_paths() {
        let refs = <(u64, FakeId)>::refs();
        assert_eq!(refs.len(), 1);
        assert_eq!(refs[0].path, vec![PathSeg::Index(1)]);
    }
}