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#[derive(Ord, PartialOrd, Serialize, Deserialize, Clone)]
29#[serde(from = "Cid", into = "Cid")]
32#[repr(transparent)]
33pub struct Link<T>
34where
35 T: Clone,
36{
37 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 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 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}