Skip to main content

mnem_core/id/
link.rs

1//! `Link<T>` - a phantom-typed [`Cid`].
2//!
3//! A bare [`Cid`] points at "some content." A [`Link<T>`] points at "content
4//! that is a `T`." The generic parameter is never materialized; it exists
5//! solely to make `fn parents(&self) -> &[Link<Commit>]` refuse a
6//! `Link<Node>` at compile time, closing a large category of
7//! reference-mixing bugs.
8//!
9//! On the wire a `Link<T>` is identical to a [`Cid`] - same bytes, same
10//! CBOR tag. The phantom type is a pure Rust-level convenience.
11
12use core::fmt;
13use core::hash::{Hash, Hasher};
14use core::marker::PhantomData;
15
16use serde::{Deserialize, Deserializer, Serialize, Serializer};
17
18use crate::id::cid::Cid;
19
20/// A typed content reference.
21///
22/// `Link<T>` is a [`Cid`] annotated at the type level with the kind of
23/// object it addresses. Construct via [`Link::new`]; extract via
24/// [`Link::cid`].
25pub struct Link<T: ?Sized> {
26    cid: Cid,
27    _target: PhantomData<fn() -> T>,
28}
29
30impl<T: ?Sized> Link<T> {
31    /// Construct from a raw CID.
32    ///
33    /// No runtime validation is performed - the caller is responsible for
34    /// ensuring the CID actually addresses a `T`. To validate, read the
35    /// content via the object store and decode into `T`.
36    #[must_use]
37    pub const fn new(cid: Cid) -> Self {
38        Self {
39            cid,
40            _target: PhantomData,
41        }
42    }
43
44    /// Borrow the underlying CID.
45    #[must_use]
46    pub const fn cid(&self) -> &Cid {
47        &self.cid
48    }
49
50    /// Consume and return the underlying CID, dropping the phantom type.
51    #[must_use]
52    pub const fn into_cid(self) -> Cid {
53        self.cid
54    }
55
56    /// Reinterpret this link as pointing at a different type without any
57    /// runtime check. Use only when converting between representations of
58    /// the same logical object.
59    #[must_use]
60    pub const fn transmute<U: ?Sized>(self) -> Link<U> {
61        Link::new(self.cid)
62    }
63}
64
65// Manual trait impls that delegate to `cid`, avoiding derive-bound
66// propagation on the phantom type parameter (same pattern as StableId).
67impl<T: ?Sized> Clone for Link<T> {
68    fn clone(&self) -> Self {
69        Self::new(self.cid.clone())
70    }
71}
72
73impl<T: ?Sized> PartialEq for Link<T> {
74    fn eq(&self, other: &Self) -> bool {
75        self.cid == other.cid
76    }
77}
78
79impl<T: ?Sized> Eq for Link<T> {}
80
81impl<T: ?Sized> PartialOrd for Link<T> {
82    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
83        Some(self.cmp(other))
84    }
85}
86
87impl<T: ?Sized> Ord for Link<T> {
88    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
89        self.cid.cmp(&other.cid)
90    }
91}
92
93impl<T: ?Sized> Hash for Link<T> {
94    fn hash<H: Hasher>(&self, state: &mut H) {
95        self.cid.hash(state);
96    }
97}
98
99impl<T: ?Sized> fmt::Debug for Link<T> {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        write!(f, "Link<{}>({})", core::any::type_name::<T>(), self.cid)
102    }
103}
104
105impl<T: ?Sized> fmt::Display for Link<T> {
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        fmt::Display::fmt(&self.cid, f)
108    }
109}
110
111impl<T: ?Sized> Serialize for Link<T> {
112    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
113        self.cid.serialize(s)
114    }
115}
116
117impl<'de, T: ?Sized> Deserialize<'de> for Link<T> {
118    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
119        Cid::deserialize(d).map(Self::new)
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use crate::id::cid::CODEC_DAG_CBOR;
127    use crate::id::multihash::Multihash;
128
129    // Phantom type tags for testing. Real modules (Node, Edge, Commit, Tree)
130    // become the target types in M4+.
131    struct TestNode;
132    struct TestEdge;
133
134    #[test]
135    fn link_distinguishes_target_types_at_compile_time() {
136        let cid = Cid::new(CODEC_DAG_CBOR, Multihash::sha2_256(b"x"));
137        let node_link: Link<TestNode> = Link::new(cid.clone());
138        let edge_link: Link<TestEdge> = Link::new(cid);
139        // These are different types; comparing them is a compile error
140        // and the commented line below demonstrates it:
141        // let _ = node_link == edge_link; // <- compile error
142        let node_link2 = node_link.clone();
143        assert_eq!(node_link, node_link2);
144        assert_eq!(edge_link.cid(), node_link2.cid());
145    }
146
147    #[test]
148    fn link_round_trip_cid_equality() {
149        let cid = Cid::new(CODEC_DAG_CBOR, Multihash::sha2_256(b"round"));
150        let link: Link<TestNode> = Link::new(cid.clone());
151        assert_eq!(link.cid(), &cid);
152        let taken = link.into_cid();
153        assert_eq!(taken, cid);
154    }
155}