Skip to main content

armour_typ/
refs.rs

1//! Parallel metadata channel for branded ID references.
2//!
3//! Deliberately separate from [`Typ`](crate::Typ): `typ_hash` is persisted in
4//! db.info and must not change. `GetRefs` is runtime-only (called when building
5//! a schema registry / validating), so no const-slice gymnastics.
6
7use serde::Serialize;
8
9/// Unique string identifier of an ID family that survives phantom-type erasure.
10/// Implemented for hasher types by `const_hasher!`/`const_hasher_or!`/`hasher!`
11/// (armour-core). Uniqueness across an application is checked by the schema
12/// registry at build time.
13pub trait IdBrand {
14    const BRAND: &'static str;
15}
16
17/// One segment of a field path inside a stored type.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
19#[serde(tag = "seg", content = "v")]
20pub enum PathSeg {
21    /// Named struct field.
22    Field(&'static str),
23    /// Enum variant.
24    Variant(&'static str),
25    /// Position in an unnamed struct / tuple.
26    Index(u32),
27    /// Element of `Vec`/`Array`/`Option`/map.
28    Elem,
29}
30
31/// A branded-ID field located at `path` inside a stored type.
32#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
33pub struct FieldRef {
34    pub path: Vec<PathSeg>,
35    pub brand: &'static str,
36}
37
38impl FieldRef {
39    /// Ref pointing at the value itself (empty path) — used by ID types.
40    pub fn leaf(brand: &'static str) -> Self {
41        Self {
42            path: Vec::new(),
43            brand,
44        }
45    }
46
47    /// Prepend a path segment (used by derive when recursing into fields).
48    #[must_use]
49    pub fn prefixed(mut self, seg: PathSeg) -> Self {
50        self.path.insert(0, seg);
51        self
52    }
53}
54
55/// Runtime introspection of branded-ID fields.
56///
57/// Both methods have empty defaults so manual `GetType` impls stay valid.
58/// `derive(GetType)` emits an `impl GetRefs` automatically (suppress with
59/// `#[get_type(no_refs)]` on the type); standalone `#[derive(GetRefs)]` exists
60/// for key structs without `GetType`. Do not combine both derives on one type.
61pub trait GetRefs {
62    /// Static structure: which fields carry which brands (graph/ER edges).
63    fn refs() -> Vec<FieldRef>
64    where
65        Self: Sized,
66    {
67        Vec::new()
68    }
69
70    /// Visit branded ID values inside a concrete record (validator).
71    fn visit_ids(&self, f: &mut dyn FnMut(&'static str, u64)) {
72        let _ = f;
73    }
74}
75
76/// Empty (leaf) impls for types that cannot contain branded IDs.
77macro_rules! empty_refs {
78    ($($t:ty),* $(,)?) => {
79        $(impl GetRefs for $t {})*
80    };
81}
82
83empty_refs!(
84    bool,
85    u8,
86    u16,
87    u32,
88    u64,
89    usize,
90    i32,
91    i64,
92    f32,
93    f64,
94    core::num::NonZeroU32,
95    core::num::NonZeroU64,
96    String,
97    time::Duration,
98    time::OffsetDateTime,
99    time::PrimitiveDateTime,
100    time::Date,
101    time::Time,
102    std::net::Ipv6Addr,
103    std::net::IpAddr,
104    std::net::SocketAddrV6,
105    uuid::Uuid,
106);
107
108impl<const CAP: usize> GetRefs for [u8; CAP] {}
109impl<const CAP: usize> GetRefs for arrayvec::ArrayString<CAP> {}
110
111#[cfg(feature = "std")]
112impl GetRefs for bytes::Bytes {}
113#[cfg(feature = "std")]
114impl GetRefs for serde_json::Value {}
115#[cfg(feature = "rust_decimal")]
116impl GetRefs for rust_decimal::Decimal {}
117#[cfg(feature = "compact_str")]
118impl GetRefs for compact_str::CompactString {}
119#[cfg(feature = "smol_str")]
120impl GetRefs for smol_str::SmolStr {}
121#[cfg(feature = "solana")]
122impl GetRefs for solana_pubkey::Pubkey {}
123#[cfg(feature = "solana")]
124impl GetRefs for solana_signature::Signature {}
125
126fn elem_refs<T: GetRefs>() -> Vec<FieldRef> {
127    T::refs()
128        .into_iter()
129        .map(|r| r.prefixed(PathSeg::Elem))
130        .collect()
131}
132
133impl<T: GetRefs> GetRefs for Vec<T> {
134    fn refs() -> Vec<FieldRef> {
135        elem_refs::<T>()
136    }
137    fn visit_ids(&self, f: &mut dyn FnMut(&'static str, u64)) {
138        for item in self {
139            item.visit_ids(f);
140        }
141    }
142}
143
144impl<T: GetRefs, const CAP: usize> GetRefs for arrayvec::ArrayVec<T, CAP> {
145    fn refs() -> Vec<FieldRef> {
146        elem_refs::<T>()
147    }
148    fn visit_ids(&self, f: &mut dyn FnMut(&'static str, u64)) {
149        for item in self {
150            item.visit_ids(f);
151        }
152    }
153}
154
155#[cfg(feature = "smallvec")]
156impl<T: GetRefs, const CAP: usize> GetRefs for smallvec::SmallVec<[T; CAP]> {
157    fn refs() -> Vec<FieldRef> {
158        elem_refs::<T>()
159    }
160    fn visit_ids(&self, f: &mut dyn FnMut(&'static str, u64)) {
161        for item in self {
162            item.visit_ids(f);
163        }
164    }
165}
166
167impl<T: GetRefs> GetRefs for Option<T> {
168    fn refs() -> Vec<FieldRef> {
169        elem_refs::<T>()
170    }
171    fn visit_ids(&self, f: &mut dyn FnMut(&'static str, u64)) {
172        if let Some(v) = self {
173            v.visit_ids(f);
174        }
175    }
176}
177
178impl<T: GetRefs> GetRefs for Box<T> {
179    fn refs() -> Vec<FieldRef> {
180        T::refs()
181    }
182    fn visit_ids(&self, f: &mut dyn FnMut(&'static str, u64)) {
183        (**self).visit_ids(f);
184    }
185}
186
187macro_rules! tuple_refs {
188    ($(($($t:ident . $i:tt),+)),+ $(,)?) => {
189        $(
190            impl<$($t: GetRefs),+> GetRefs for ($($t,)+) {
191                fn refs() -> Vec<FieldRef> {
192                    let mut out = Vec::new();
193                    $(out.extend($t::refs().into_iter().map(|r| r.prefixed(PathSeg::Index($i))));)+
194                    out
195                }
196                fn visit_ids(&self, f: &mut dyn FnMut(&'static str, u64)) {
197                    $(self.$i.visit_ids(f);)+
198                }
199            }
200        )+
201    };
202}
203
204tuple_refs!((T1.0, T2.1), (T1.0, T2.1, T3.2), (T1.0, T2.1, T3.2, T4.3),);
205
206impl<K: GetRefs, V: GetRefs> GetRefs for std::collections::HashMap<K, V> {
207    fn refs() -> Vec<FieldRef> {
208        elem_refs::<(K, V)>()
209    }
210    fn visit_ids(&self, f: &mut dyn FnMut(&'static str, u64)) {
211        for (k, v) in self {
212            k.visit_ids(f);
213            v.visit_ids(f);
214        }
215    }
216}
217
218impl<K: GetRefs, V: GetRefs> GetRefs for std::collections::BTreeMap<K, V> {
219    fn refs() -> Vec<FieldRef> {
220        elem_refs::<(K, V)>()
221    }
222    fn visit_ids(&self, f: &mut dyn FnMut(&'static str, u64)) {
223        for (k, v) in self {
224            k.visit_ids(f);
225            v.visit_ids(f);
226        }
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    struct TestBrand;
235    impl IdBrand for TestBrand {
236        const BRAND: &'static str = "TEST";
237    }
238
239    /// Эмуляция брэндированного ID (настоящие Fuid/Id64 — в armour-core).
240    struct FakeId(u64);
241    impl GetRefs for FakeId {
242        fn refs() -> Vec<FieldRef> {
243            vec![FieldRef::leaf(TestBrand::BRAND)]
244        }
245        fn visit_ids(&self, f: &mut dyn FnMut(&'static str, u64)) {
246            f(TestBrand::BRAND, self.0);
247        }
248    }
249
250    #[test]
251    fn refs_leaf_and_prefix() {
252        let r = FieldRef::leaf("TEST").prefixed(PathSeg::Field("a"));
253        assert_eq!(r.path, vec![PathSeg::Field("a")]);
254        assert_eq!(r.brand, "TEST");
255    }
256
257    #[test]
258    fn refs_default_empty() {
259        assert!(u64::refs().is_empty());
260        assert!(String::refs().is_empty());
261        let mut seen = Vec::new();
262        42u64.visit_ids(&mut |b, v| seen.push((b, v)));
263        assert!(seen.is_empty());
264    }
265
266    #[test]
267    fn refs_containers_forward() {
268        let refs = <Vec<Option<FakeId>>>::refs();
269        assert_eq!(refs.len(), 1);
270        assert_eq!(refs[0].path, vec![PathSeg::Elem, PathSeg::Elem]);
271
272        let v: Vec<Option<FakeId>> = vec![Some(FakeId(7)), None, Some(FakeId(9))];
273        let mut seen = Vec::new();
274        v.visit_ids(&mut |b, id| seen.push((b, id)));
275        assert_eq!(seen, vec![("TEST", 7), ("TEST", 9)]);
276    }
277
278    #[test]
279    fn refs_tuple_paths() {
280        let refs = <(u64, FakeId)>::refs();
281        assert_eq!(refs.len(), 1);
282        assert_eq!(refs[0].path, vec![PathSeg::Index(1)]);
283    }
284}