1use std::collections::BTreeMap;
36
37use ipld_core::ipld::Ipld;
38use serde::{Deserialize, Deserializer, Serialize, Serializer};
39
40use crate::id::{Cid, NodeId};
41
42#[derive(Clone, Debug, Default, PartialEq, Eq)]
45pub struct IndexSet {
46 pub nodes_by_label: BTreeMap<String, Cid>,
49
50 pub nodes_by_prop: BTreeMap<String, BTreeMap<String, Cid>>,
54
55 pub outgoing: Option<Cid>,
63
64 pub incoming: Option<Cid>,
71
72 pub extra: BTreeMap<String, Ipld>,
74}
75
76impl IndexSet {
77 pub const KIND: &'static str = "index_set";
79}
80
81#[derive(Clone, Debug, Default, PartialEq, Eq)]
85pub struct AdjacencyBucket {
86 pub edges: Vec<AdjacencyEntry>,
89 pub extra: BTreeMap<String, Ipld>,
91}
92
93impl AdjacencyBucket {
94 pub const KIND: &'static str = "adjacency_bucket";
96}
97
98#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
105pub struct AdjacencyEntry {
106 pub label: String,
108 pub edge: Cid,
110}
111
112#[derive(Clone, Debug, Default, PartialEq, Eq)]
122pub struct IncomingAdjacencyBucket {
123 pub edges: Vec<IncomingAdjacencyEntry>,
126 pub extra: BTreeMap<String, Ipld>,
128}
129
130impl IncomingAdjacencyBucket {
131 pub const KIND: &'static str = "incoming_adjacency_bucket";
133}
134
135#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
143pub struct IncomingAdjacencyEntry {
144 pub label: String,
146 pub src: NodeId,
149 pub edge: Cid,
152}
153
154#[derive(Serialize, Deserialize)]
163struct IndexSetWire {
164 #[serde(rename = "_kind")]
165 kind: String,
166 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
167 nodes_by_label: BTreeMap<String, Cid>,
168 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
169 nodes_by_prop: BTreeMap<String, BTreeMap<String, Cid>>,
170 #[serde(default, skip_serializing_if = "Option::is_none", alias = "adjacency")]
172 outgoing: Option<Cid>,
173 #[serde(default, skip_serializing_if = "Option::is_none")]
174 incoming: Option<Cid>,
175 #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
176 extra: BTreeMap<String, Ipld>,
177}
178
179impl Serialize for IndexSet {
180 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
181 IndexSetWire {
182 kind: Self::KIND.into(),
183 nodes_by_label: self.nodes_by_label.clone(),
184 nodes_by_prop: self.nodes_by_prop.clone(),
185 outgoing: self.outgoing.clone(),
186 incoming: self.incoming.clone(),
187 extra: self.extra.clone(),
188 }
189 .serialize(serializer)
190 }
191}
192
193impl<'de> Deserialize<'de> for IndexSet {
194 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
195 let w = IndexSetWire::deserialize(deserializer)?;
196 if w.kind != Self::KIND {
197 return Err(serde::de::Error::custom(format!(
198 "expected _kind='{}', got '{}'",
199 Self::KIND,
200 w.kind
201 )));
202 }
203 Ok(Self {
204 nodes_by_label: w.nodes_by_label,
205 nodes_by_prop: w.nodes_by_prop,
206 outgoing: w.outgoing,
207 incoming: w.incoming,
208 extra: w.extra,
209 })
210 }
211}
212
213#[derive(Serialize, Deserialize)]
216struct AdjacencyBucketWire {
217 #[serde(rename = "_kind")]
218 kind: String,
219 edges: Vec<AdjacencyEntry>,
220 #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
221 extra: BTreeMap<String, Ipld>,
222}
223
224impl Serialize for AdjacencyBucket {
225 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
226 AdjacencyBucketWire {
227 kind: Self::KIND.into(),
228 edges: self.edges.clone(),
229 extra: self.extra.clone(),
230 }
231 .serialize(serializer)
232 }
233}
234
235impl<'de> Deserialize<'de> for AdjacencyBucket {
236 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
237 let w = AdjacencyBucketWire::deserialize(deserializer)?;
238 if w.kind != Self::KIND {
239 return Err(serde::de::Error::custom(format!(
240 "expected _kind='{}', got '{}'",
241 Self::KIND,
242 w.kind
243 )));
244 }
245 Ok(Self {
246 edges: w.edges,
247 extra: w.extra,
248 })
249 }
250}
251
252#[derive(Serialize, Deserialize)]
255struct IncomingAdjacencyBucketWire {
256 #[serde(rename = "_kind")]
257 kind: String,
258 edges: Vec<IncomingAdjacencyEntry>,
259 #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
260 extra: BTreeMap<String, Ipld>,
261}
262
263impl Serialize for IncomingAdjacencyBucket {
264 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
265 IncomingAdjacencyBucketWire {
266 kind: Self::KIND.into(),
267 edges: self.edges.clone(),
268 extra: self.extra.clone(),
269 }
270 .serialize(serializer)
271 }
272}
273
274impl<'de> Deserialize<'de> for IncomingAdjacencyBucket {
275 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
276 let w = IncomingAdjacencyBucketWire::deserialize(deserializer)?;
277 if w.kind != Self::KIND {
278 return Err(serde::de::Error::custom(format!(
279 "expected _kind='{}', got '{}'",
280 Self::KIND,
281 w.kind
282 )));
283 }
284 Ok(Self {
285 edges: w.edges,
286 extra: w.extra,
287 })
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294 use crate::codec::{from_canonical_bytes, to_canonical_bytes};
295 use crate::id::{CODEC_RAW, Multihash};
296
297 fn raw(n: u32) -> Cid {
298 Cid::new(CODEC_RAW, Multihash::sha2_256(&n.to_be_bytes()))
299 }
300
301 #[test]
302 fn index_set_round_trip() {
303 let mut set = IndexSet::default();
304 set.nodes_by_label.insert("Person".into(), raw(1));
305 set.nodes_by_label.insert("Document".into(), raw(2));
306 let mut person_props = BTreeMap::new();
307 person_props.insert("name".into(), raw(3));
308 set.nodes_by_prop.insert("Person".into(), person_props);
309 set.outgoing = Some(raw(4));
310 set.incoming = Some(raw(5));
311
312 let bytes = to_canonical_bytes(&set).unwrap();
313 let decoded: IndexSet = from_canonical_bytes(&bytes).unwrap();
314 assert_eq!(set, decoded);
315 }
316
317 #[test]
318 fn empty_index_set_round_trips() {
319 let set = IndexSet::default();
320 let bytes = to_canonical_bytes(&set).unwrap();
321 let decoded: IndexSet = from_canonical_bytes(&bytes).unwrap();
322 assert_eq!(set, decoded);
323 }
324
325 #[test]
326 fn index_set_decodes_legacy_adjacency_alias() {
327 #[derive(Serialize)]
331 struct LegacyWire {
332 #[serde(rename = "_kind")]
333 kind: String,
334 adjacency: Cid,
335 }
336 let legacy = LegacyWire {
337 kind: "index_set".into(),
338 adjacency: raw(42),
339 };
340 let bytes = to_canonical_bytes(&legacy).unwrap();
341 let decoded: IndexSet = from_canonical_bytes(&bytes).unwrap();
342 assert_eq!(decoded.outgoing, Some(raw(42)));
343 assert!(decoded.incoming.is_none());
344 }
345
346 #[test]
347 fn adjacency_bucket_round_trip() {
348 let b = AdjacencyBucket {
349 edges: vec![
350 AdjacencyEntry {
351 label: "knows".into(),
352 edge: raw(10),
353 },
354 AdjacencyEntry {
355 label: "works_at".into(),
356 edge: raw(11),
357 },
358 ],
359 extra: BTreeMap::new(),
360 };
361 let bytes = to_canonical_bytes(&b).unwrap();
362 let decoded: AdjacencyBucket = from_canonical_bytes(&bytes).unwrap();
363 assert_eq!(b, decoded);
364 }
365
366 #[test]
367 fn incoming_adjacency_bucket_round_trip() {
368 let b = IncomingAdjacencyBucket {
369 edges: vec![
370 IncomingAdjacencyEntry {
371 label: "knows".into(),
372 src: NodeId::from_bytes_raw([1u8; 16]),
373 edge: raw(10),
374 },
375 IncomingAdjacencyEntry {
376 label: "works_at".into(),
377 src: NodeId::from_bytes_raw([2u8; 16]),
378 edge: raw(11),
379 },
380 ],
381 extra: BTreeMap::new(),
382 };
383 let bytes = to_canonical_bytes(&b).unwrap();
384 let decoded: IncomingAdjacencyBucket = from_canonical_bytes(&bytes).unwrap();
385 assert_eq!(b, decoded);
386 }
387
388 #[test]
389 fn wrong_kind_rejected() {
390 let wire = AdjacencyBucketWire {
391 kind: "not_adjacency".into(),
392 edges: vec![],
393 extra: BTreeMap::new(),
394 };
395 let bytes = serde_ipld_dagcbor::to_vec(&wire).unwrap();
396 let err = serde_ipld_dagcbor::from_slice::<AdjacencyBucket>(&bytes).unwrap_err();
397 assert!(err.to_string().contains("_kind"));
398 }
399
400 #[test]
401 fn incoming_bucket_wrong_kind_rejected() {
402 let wire = IncomingAdjacencyBucketWire {
403 kind: "not_incoming".into(),
404 edges: vec![],
405 extra: BTreeMap::new(),
406 };
407 let bytes = serde_ipld_dagcbor::to_vec(&wire).unwrap();
408 let err = serde_ipld_dagcbor::from_slice::<IncomingAdjacencyBucket>(&bytes).unwrap_err();
409 assert!(err.to_string().contains("_kind"));
410 }
411}