noosphere_core/data/
link.rs

1use anyhow::{anyhow, Result};
2use libipld_cbor::DagCborCodec;
3use libipld_core::{
4    codec::{Codec, Decode, Encode},
5    raw::RawCodec,
6};
7use noosphere_common::ConditionalSend;
8use std::fmt::Debug;
9use std::{
10    fmt::{Display, Formatter},
11    io::{Read, Seek, Write},
12    str::FromStr,
13};
14use std::{hash::Hash, marker::PhantomData, ops::Deref};
15
16use cid::Cid;
17use noosphere_storage::BlockStore;
18use serde::{de::DeserializeOwned, Deserialize, Serialize};
19
20use noosphere_collections::hamt::Hash as HamtHash;
21
22/// A [Link] is a [Cid] with a type attached. The type represents the data that
23/// the [Cid] refers to. This is a helpful construct to use to ensure that data
24/// structures whose fields or elements may be [Cid]s can still retain strong
25/// typing. A [Link] transparently represents its inner [Cid], so a data
26/// structure that uses [Link]s can safely be interpretted in terms of [Cid]s,
27/// and vice-versa.
28#[derive(Ord, PartialOrd, Serialize, Deserialize, Clone)]
29// NOTE: Required because libipld special-cases unit structs and errors
30// SEE: https://github.com/ipld/libipld/blob/65e0b38520f62cfb2b67ebe658846d86dac2f73e/core/src/serde/ser.rs#L192
31#[serde(from = "Cid", into = "Cid")]
32#[repr(transparent)]
33pub struct Link<T>
34where
35    T: Clone,
36{
37    /// The wrapped [Cid] of this [Link]
38    pub cid: Cid,
39    linked_type: PhantomData<T>,
40}
41
42impl<T> Copy for Link<T> where T: Clone {}
43
44impl<T> Debug for Link<T>
45where
46    T: Clone,
47{
48    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
49        f.debug_struct("Link")
50            .field("cid", &self.cid.to_string())
51            .field("linked_type", &self.linked_type)
52            .finish()
53    }
54}
55
56impl<T> Deref for Link<T>
57where
58    T: Clone,
59{
60    type Target = Cid;
61
62    fn deref(&self) -> &Self::Target {
63        &self.cid
64    }
65}
66
67impl<T> Hash for Link<T>
68where
69    T: Clone,
70{
71    fn hash<H: std::hash::Hasher>(&self, hasher: &mut H) {
72        Hash::hash(&self.cid, hasher)
73    }
74}
75
76impl<T> PartialEq for Link<T>
77where
78    T: Clone,
79{
80    fn eq(&self, other: &Self) -> bool {
81        self.cid == other.cid
82    }
83}
84
85impl<T> Eq for Link<T> where T: Clone {}
86
87impl<T> HamtHash for Link<T>
88where
89    T: Clone,
90{
91    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
92        self.cid.hash().hash(state);
93    }
94}
95
96impl<T> Link<T>
97where
98    T: Clone,
99{
100    /// Wrap a given [Cid] in a typed [Link]
101    pub fn new(cid: Cid) -> Self {
102        Link {
103            cid,
104            linked_type: PhantomData,
105        }
106    }
107}
108
109impl<T> Display for Link<T>
110where
111    T: Clone,
112{
113    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
114        Display::fmt(&self.cid, f)
115    }
116}
117
118impl<C: Codec, T> Encode<C> for Link<T>
119where
120    Cid: Encode<C>,
121    T: Clone,
122{
123    fn encode<W: Write>(&self, c: C, w: &mut W) -> Result<()> {
124        self.cid.encode(c, w)
125    }
126}
127
128impl<C: Codec, T> Decode<C> for Link<T>
129where
130    Cid: Decode<C>,
131    T: Clone,
132{
133    fn decode<R: Read + Seek>(c: C, r: &mut R) -> Result<Self> {
134        Ok(Self::new(Cid::decode(c, r)?))
135    }
136}
137
138impl<T> AsRef<Cid> for Link<T>
139where
140    T: Clone,
141{
142    fn as_ref(&self) -> &Cid {
143        &self.cid
144    }
145}
146
147impl<T> From<Cid> for Link<T>
148where
149    T: Clone,
150{
151    fn from(cid: Cid) -> Self {
152        Self::new(cid)
153    }
154}
155
156impl<T> From<&Cid> for Link<T>
157where
158    T: Clone,
159{
160    fn from(cid: &Cid) -> Self {
161        Self::new(*cid)
162    }
163}
164
165impl<T> From<Link<T>> for Cid
166where
167    T: Clone,
168{
169    fn from(link: Link<T>) -> Self {
170        link.cid
171    }
172}
173
174impl<T> FromStr for Link<T>
175where
176    T: Clone,
177{
178    type Err = <Cid as FromStr>::Err;
179
180    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
181        Ok(Cid::from_str(s)?.into())
182    }
183}
184
185impl<T> From<Link<T>> for String
186where
187    T: Clone,
188{
189    fn from(value: Link<T>) -> Self {
190        From::from(value.cid)
191    }
192}
193
194impl<T> Link<T>
195where
196    T: Serialize + DeserializeOwned + Clone + ConditionalSend,
197{
198    /// Given a [BlockStore], attempt to load a value for the [Cid] of this
199    /// [Link]. The loaded block will be interpretted as the type that is
200    /// attached to the [Cid] by this [Link], and then returned.
201    pub async fn load_from<S: BlockStore>(&self, store: &S) -> Result<T> {
202        match self.codec() {
203            codec_id if codec_id == u64::from(DagCborCodec) => {
204                store.load::<DagCborCodec, _>(self).await
205            }
206            codec_id if codec_id == u64::from(RawCodec) => store.load::<RawCodec, _>(self).await,
207            codec_id => Err(anyhow!("Unsupported codec {}", codec_id)),
208        }
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use cid::Cid;
215    use libipld_cbor::DagCborCodec;
216    use noosphere_storage::{BlockStore, MemoryStore};
217    use serde::{Deserialize, Serialize};
218    #[cfg(target_arch = "wasm32")]
219    use wasm_bindgen_test::wasm_bindgen_test;
220
221    use crate::data::MemoIpld;
222
223    use super::Link;
224
225    #[cfg(target_arch = "wasm32")]
226    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
227
228    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
229    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
230    async fn it_can_interpret_referenced_block_as_attached_type() {
231        let mut store = MemoryStore::default();
232        let cid = store
233            .save::<DagCborCodec, _>(&MemoIpld {
234                parent: None,
235                headers: vec![("Foo".into(), "Bar".into())],
236                body: Cid::default(),
237            })
238            .await
239            .unwrap();
240
241        let link = Link::<MemoIpld>::new(cid);
242
243        let memo = link.load_from(&store).await.unwrap();
244
245        assert_eq!(memo.get_first_header("Foo"), Some(String::from("Bar")))
246    }
247
248    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
249    #[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
250    async fn it_transparently_serializes_and_deserializes_as_a_cid() {
251        #[derive(Serialize, Deserialize)]
252        struct UsesLink {
253            pub link: Link<MemoIpld>,
254        }
255
256        #[derive(Serialize, Deserialize)]
257        struct UsesCid {
258            pub link: Cid,
259        }
260
261        let mut store = MemoryStore::default();
262
263        let memo_cid = store
264            .save::<DagCborCodec, _>(&MemoIpld {
265                parent: None,
266                headers: vec![("Foo".into(), "Bar".into())],
267                body: Cid::default(),
268            })
269            .await
270            .unwrap();
271
272        let uses_link_cid = store
273            .save::<DagCborCodec, _>(&UsesLink {
274                link: Link::new(memo_cid),
275            })
276            .await
277            .unwrap();
278
279        let loaded_uses_cid = store
280            .load::<DagCborCodec, UsesCid>(&uses_link_cid)
281            .await
282            .unwrap();
283
284        assert_eq!(loaded_uses_cid.link, memo_cid);
285
286        let loaded_uses_link = store
287            .load::<DagCborCodec, UsesLink>(&uses_link_cid)
288            .await
289            .unwrap();
290
291        assert_eq!(loaded_uses_link.link, Link::<MemoIpld>::new(memo_cid));
292    }
293}