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/// The unbranded marker. `Fuid<()>` / `Id64<()>` are IDs with no foreign-key
18/// brand: the empty `BRAND` is treated as "no reference" by the `GetRefs` impls
19/// (no graph edge, skipped during validation). Implementing it here also makes
20/// the blanket `impl<H: IdBrand> GetRefs for Fuid<H>` total over all hashers,
21/// so a raw `Fuid<()>` field compiles under `derive(GetType)` without a separate
22/// `Fuid<()>` impl — which coherence would reject, since this trait is upstream.
23impl IdBrand for () {
24    const BRAND: &'static str = "";
25}
26
27/// One segment of a field path inside a stored type.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
29#[serde(tag = "seg", content = "v")]
30pub enum PathSeg {
31    /// Named struct field.
32    Field(&'static str),
33    /// Enum variant.
34    Variant(&'static str),
35    /// Position in an unnamed struct / tuple.
36    Index(u32),
37    /// Element of `Vec`/`Array`/`Option`/map.
38    Elem,
39}
40
41/// A branded-ID field located at `path` inside a stored type.
42#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
43pub struct FieldRef {
44    pub path: Vec<PathSeg>,
45    pub brand: &'static str,
46}
47
48impl FieldRef {
49    /// Ref pointing at the value itself (empty path) — used by ID types.
50    pub fn leaf(brand: &'static str) -> Self {
51        Self {
52            path: Vec::new(),
53            brand,
54        }
55    }
56
57    /// Prepend a path segment (used by derive when recursing into fields).
58    #[must_use]
59    pub fn prefixed(mut self, seg: PathSeg) -> Self {
60        self.path.insert(0, seg);
61        self
62    }
63}
64
65/// Runtime introspection of branded-ID fields.
66///
67/// Both methods have empty defaults, so a manual `GetType` impl needs only a
68/// trivial `impl GetRefs for T {}` to participate.
69///
70/// `GetRefs` is **opt-in and decoupled from `GetType`**: `derive(GetType)` never
71/// emits `GetRefs` and imposes no `GetRefs` bound on fields, so types holding an
72/// unbranded `Fuid<()>`, a non-`IdBrand` hasher, or a field whose type has a
73/// manual `GetType` impl keep compiling — regardless of what any other crate in
74/// the build graph enables. Derive it explicitly with `#[derive(GetRefs)]` —
75/// typically `#[derive(GetType, GetRefs)]` on stored types, or `GetRefs` alone on
76/// composite collection keys that don't implement `GetType`. Exclude a field from
77/// a derived `GetRefs` with `#[get_type(no_refs)]`.
78pub trait GetRefs {
79    /// Static structure: which fields carry which brands (graph/ER edges).
80    fn refs() -> Vec<FieldRef>
81    where
82        Self: Sized,
83    {
84        Vec::new()
85    }
86
87    /// Visit branded ID values inside a concrete record (validator).
88    fn visit_ids(&self, f: &mut dyn FnMut(&'static str, u64)) {
89        let _ = f;
90    }
91}
92
93/// Empty (leaf) impls for types that cannot contain branded IDs.
94macro_rules! empty_refs {
95    ($($t:ty),* $(,)?) => {
96        $(impl GetRefs for $t {})*
97    };
98}
99
100empty_refs!(
101    bool,
102    u8,
103    u16,
104    u32,
105    u64,
106    usize,
107    i32,
108    i64,
109    f32,
110    f64,
111    core::num::NonZeroU32,
112    core::num::NonZeroU64,
113    String,
114    time::Duration,
115    time::OffsetDateTime,
116    time::PrimitiveDateTime,
117    time::Date,
118    time::Time,
119    std::net::Ipv6Addr,
120    std::net::IpAddr,
121    std::net::SocketAddrV6,
122    uuid::Uuid,
123);
124
125impl<const CAP: usize> GetRefs for [u8; CAP] {}
126impl<const CAP: usize> GetRefs for arrayvec::ArrayString<CAP> {}
127
128#[cfg(feature = "std")]
129impl GetRefs for bytes::Bytes {}
130#[cfg(feature = "std")]
131impl GetRefs for serde_json::Value {}
132#[cfg(feature = "rust_decimal")]
133impl GetRefs for rust_decimal::Decimal {}
134#[cfg(feature = "compact_str")]
135impl GetRefs for compact_str::CompactString {}
136#[cfg(feature = "smol_str")]
137impl GetRefs for smol_str::SmolStr {}
138#[cfg(feature = "solana")]
139impl GetRefs for solana_pubkey::Pubkey {}
140#[cfg(feature = "solana")]
141impl GetRefs for solana_signature::Signature {}
142
143fn elem_refs<T: GetRefs>() -> Vec<FieldRef> {
144    T::refs()
145        .into_iter()
146        .map(|r| r.prefixed(PathSeg::Elem))
147        .collect()
148}
149
150impl<T: GetRefs> GetRefs for Vec<T> {
151    fn refs() -> Vec<FieldRef> {
152        elem_refs::<T>()
153    }
154    fn visit_ids(&self, f: &mut dyn FnMut(&'static str, u64)) {
155        for item in self {
156            item.visit_ids(f);
157        }
158    }
159}
160
161impl<T: GetRefs, const CAP: usize> GetRefs for arrayvec::ArrayVec<T, CAP> {
162    fn refs() -> Vec<FieldRef> {
163        elem_refs::<T>()
164    }
165    fn visit_ids(&self, f: &mut dyn FnMut(&'static str, u64)) {
166        for item in self {
167            item.visit_ids(f);
168        }
169    }
170}
171
172#[cfg(feature = "smallvec")]
173impl<T: GetRefs, const CAP: usize> GetRefs for smallvec::SmallVec<[T; CAP]> {
174    fn refs() -> Vec<FieldRef> {
175        elem_refs::<T>()
176    }
177    fn visit_ids(&self, f: &mut dyn FnMut(&'static str, u64)) {
178        for item in self {
179            item.visit_ids(f);
180        }
181    }
182}
183
184impl<T: GetRefs> GetRefs for Option<T> {
185    fn refs() -> Vec<FieldRef> {
186        elem_refs::<T>()
187    }
188    fn visit_ids(&self, f: &mut dyn FnMut(&'static str, u64)) {
189        if let Some(v) = self {
190            v.visit_ids(f);
191        }
192    }
193}
194
195impl<T: GetRefs> GetRefs for Box<T> {
196    fn refs() -> Vec<FieldRef> {
197        T::refs()
198    }
199    fn visit_ids(&self, f: &mut dyn FnMut(&'static str, u64)) {
200        (**self).visit_ids(f);
201    }
202}
203
204macro_rules! tuple_refs {
205    ($(($($t:ident . $i:tt),+)),+ $(,)?) => {
206        $(
207            impl<$($t: GetRefs),+> GetRefs for ($($t,)+) {
208                fn refs() -> Vec<FieldRef> {
209                    let mut out = Vec::new();
210                    $(out.extend($t::refs().into_iter().map(|r| r.prefixed(PathSeg::Index($i))));)+
211                    out
212                }
213                fn visit_ids(&self, f: &mut dyn FnMut(&'static str, u64)) {
214                    $(self.$i.visit_ids(f);)+
215                }
216            }
217        )+
218    };
219}
220
221tuple_refs!((T1.0, T2.1), (T1.0, T2.1, T3.2), (T1.0, T2.1, T3.2, T4.3),);
222
223impl<K: GetRefs, V: GetRefs> GetRefs for std::collections::HashMap<K, V> {
224    fn refs() -> Vec<FieldRef> {
225        elem_refs::<(K, V)>()
226    }
227    fn visit_ids(&self, f: &mut dyn FnMut(&'static str, u64)) {
228        for (k, v) in self {
229            k.visit_ids(f);
230            v.visit_ids(f);
231        }
232    }
233}
234
235impl<K: GetRefs, V: GetRefs> GetRefs for std::collections::BTreeMap<K, V> {
236    fn refs() -> Vec<FieldRef> {
237        elem_refs::<(K, V)>()
238    }
239    fn visit_ids(&self, f: &mut dyn FnMut(&'static str, u64)) {
240        for (k, v) in self {
241            k.visit_ids(f);
242            v.visit_ids(f);
243        }
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250
251    struct TestBrand;
252    impl IdBrand for TestBrand {
253        const BRAND: &'static str = "TEST";
254    }
255
256    /// Эмуляция брэндированного ID (настоящие Fuid/Id64 — в armour-core).
257    struct FakeId(u64);
258    impl GetRefs for FakeId {
259        fn refs() -> Vec<FieldRef> {
260            vec![FieldRef::leaf(TestBrand::BRAND)]
261        }
262        fn visit_ids(&self, f: &mut dyn FnMut(&'static str, u64)) {
263            f(TestBrand::BRAND, self.0);
264        }
265    }
266
267    #[test]
268    fn refs_leaf_and_prefix() {
269        let r = FieldRef::leaf("TEST").prefixed(PathSeg::Field("a"));
270        assert_eq!(r.path, vec![PathSeg::Field("a")]);
271        assert_eq!(r.brand, "TEST");
272    }
273
274    #[test]
275    fn refs_default_empty() {
276        assert!(u64::refs().is_empty());
277        assert!(String::refs().is_empty());
278        let mut seen = Vec::new();
279        42u64.visit_ids(&mut |b, v| seen.push((b, v)));
280        assert!(seen.is_empty());
281    }
282
283    #[test]
284    fn refs_containers_forward() {
285        let refs = <Vec<Option<FakeId>>>::refs();
286        assert_eq!(refs.len(), 1);
287        assert_eq!(refs[0].path, vec![PathSeg::Elem, PathSeg::Elem]);
288
289        let v: Vec<Option<FakeId>> = vec![Some(FakeId(7)), None, Some(FakeId(9))];
290        let mut seen = Vec::new();
291        v.visit_ids(&mut |b, id| seen.push((b, id)));
292        assert_eq!(seen, vec![("TEST", 7), ("TEST", 9)]);
293    }
294
295    #[test]
296    fn refs_tuple_paths() {
297        let refs = <(u64, FakeId)>::refs();
298        assert_eq!(refs.len(), 1);
299        assert_eq!(refs[0].path, vec![PathSeg::Index(1)]);
300    }
301}