use std::collections::BTreeMap;
use ipld_core::ipld::Ipld;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::id::{Cid, NodeId};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct IndexSet {
pub nodes_by_label: BTreeMap<String, Cid>,
pub nodes_by_prop: BTreeMap<String, BTreeMap<String, Cid>>,
pub outgoing: Option<Cid>,
pub incoming: Option<Cid>,
pub extra: BTreeMap<String, Ipld>,
}
impl IndexSet {
pub const KIND: &'static str = "index_set";
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct AdjacencyBucket {
pub edges: Vec<AdjacencyEntry>,
pub extra: BTreeMap<String, Ipld>,
}
impl AdjacencyBucket {
pub const KIND: &'static str = "adjacency_bucket";
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AdjacencyEntry {
pub label: String,
pub edge: Cid,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct IncomingAdjacencyBucket {
pub edges: Vec<IncomingAdjacencyEntry>,
pub extra: BTreeMap<String, Ipld>,
}
impl IncomingAdjacencyBucket {
pub const KIND: &'static str = "incoming_adjacency_bucket";
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct IncomingAdjacencyEntry {
pub label: String,
pub src: NodeId,
pub edge: Cid,
}
#[derive(Serialize, Deserialize)]
struct IndexSetWire {
#[serde(rename = "_kind")]
kind: String,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
nodes_by_label: BTreeMap<String, Cid>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
nodes_by_prop: BTreeMap<String, BTreeMap<String, Cid>>,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "adjacency")]
outgoing: Option<Cid>,
#[serde(default, skip_serializing_if = "Option::is_none")]
incoming: Option<Cid>,
#[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
extra: BTreeMap<String, Ipld>,
}
impl Serialize for IndexSet {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
IndexSetWire {
kind: Self::KIND.into(),
nodes_by_label: self.nodes_by_label.clone(),
nodes_by_prop: self.nodes_by_prop.clone(),
outgoing: self.outgoing.clone(),
incoming: self.incoming.clone(),
extra: self.extra.clone(),
}
.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for IndexSet {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let w = IndexSetWire::deserialize(deserializer)?;
if w.kind != Self::KIND {
return Err(serde::de::Error::custom(format!(
"expected _kind='{}', got '{}'",
Self::KIND,
w.kind
)));
}
Ok(Self {
nodes_by_label: w.nodes_by_label,
nodes_by_prop: w.nodes_by_prop,
outgoing: w.outgoing,
incoming: w.incoming,
extra: w.extra,
})
}
}
#[derive(Serialize, Deserialize)]
struct AdjacencyBucketWire {
#[serde(rename = "_kind")]
kind: String,
edges: Vec<AdjacencyEntry>,
#[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
extra: BTreeMap<String, Ipld>,
}
impl Serialize for AdjacencyBucket {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
AdjacencyBucketWire {
kind: Self::KIND.into(),
edges: self.edges.clone(),
extra: self.extra.clone(),
}
.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for AdjacencyBucket {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let w = AdjacencyBucketWire::deserialize(deserializer)?;
if w.kind != Self::KIND {
return Err(serde::de::Error::custom(format!(
"expected _kind='{}', got '{}'",
Self::KIND,
w.kind
)));
}
Ok(Self {
edges: w.edges,
extra: w.extra,
})
}
}
#[derive(Serialize, Deserialize)]
struct IncomingAdjacencyBucketWire {
#[serde(rename = "_kind")]
kind: String,
edges: Vec<IncomingAdjacencyEntry>,
#[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
extra: BTreeMap<String, Ipld>,
}
impl Serialize for IncomingAdjacencyBucket {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
IncomingAdjacencyBucketWire {
kind: Self::KIND.into(),
edges: self.edges.clone(),
extra: self.extra.clone(),
}
.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for IncomingAdjacencyBucket {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let w = IncomingAdjacencyBucketWire::deserialize(deserializer)?;
if w.kind != Self::KIND {
return Err(serde::de::Error::custom(format!(
"expected _kind='{}', got '{}'",
Self::KIND,
w.kind
)));
}
Ok(Self {
edges: w.edges,
extra: w.extra,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::codec::{from_canonical_bytes, to_canonical_bytes};
use crate::id::{CODEC_RAW, Multihash};
fn raw(n: u32) -> Cid {
Cid::new(CODEC_RAW, Multihash::sha2_256(&n.to_be_bytes()))
}
#[test]
fn index_set_round_trip() {
let mut set = IndexSet::default();
set.nodes_by_label.insert("Person".into(), raw(1));
set.nodes_by_label.insert("Document".into(), raw(2));
let mut person_props = BTreeMap::new();
person_props.insert("name".into(), raw(3));
set.nodes_by_prop.insert("Person".into(), person_props);
set.outgoing = Some(raw(4));
set.incoming = Some(raw(5));
let bytes = to_canonical_bytes(&set).unwrap();
let decoded: IndexSet = from_canonical_bytes(&bytes).unwrap();
assert_eq!(set, decoded);
}
#[test]
fn empty_index_set_round_trips() {
let set = IndexSet::default();
let bytes = to_canonical_bytes(&set).unwrap();
let decoded: IndexSet = from_canonical_bytes(&bytes).unwrap();
assert_eq!(set, decoded);
}
#[test]
fn index_set_decodes_legacy_adjacency_alias() {
#[derive(Serialize)]
struct LegacyWire {
#[serde(rename = "_kind")]
kind: String,
adjacency: Cid,
}
let legacy = LegacyWire {
kind: "index_set".into(),
adjacency: raw(42),
};
let bytes = to_canonical_bytes(&legacy).unwrap();
let decoded: IndexSet = from_canonical_bytes(&bytes).unwrap();
assert_eq!(decoded.outgoing, Some(raw(42)));
assert!(decoded.incoming.is_none());
}
#[test]
fn adjacency_bucket_round_trip() {
let b = AdjacencyBucket {
edges: vec![
AdjacencyEntry {
label: "knows".into(),
edge: raw(10),
},
AdjacencyEntry {
label: "works_at".into(),
edge: raw(11),
},
],
extra: BTreeMap::new(),
};
let bytes = to_canonical_bytes(&b).unwrap();
let decoded: AdjacencyBucket = from_canonical_bytes(&bytes).unwrap();
assert_eq!(b, decoded);
}
#[test]
fn incoming_adjacency_bucket_round_trip() {
let b = IncomingAdjacencyBucket {
edges: vec![
IncomingAdjacencyEntry {
label: "knows".into(),
src: NodeId::from_bytes_raw([1u8; 16]),
edge: raw(10),
},
IncomingAdjacencyEntry {
label: "works_at".into(),
src: NodeId::from_bytes_raw([2u8; 16]),
edge: raw(11),
},
],
extra: BTreeMap::new(),
};
let bytes = to_canonical_bytes(&b).unwrap();
let decoded: IncomingAdjacencyBucket = from_canonical_bytes(&bytes).unwrap();
assert_eq!(b, decoded);
}
#[test]
fn wrong_kind_rejected() {
let wire = AdjacencyBucketWire {
kind: "not_adjacency".into(),
edges: vec![],
extra: BTreeMap::new(),
};
let bytes = serde_ipld_dagcbor::to_vec(&wire).unwrap();
let err = serde_ipld_dagcbor::from_slice::<AdjacencyBucket>(&bytes).unwrap_err();
assert!(err.to_string().contains("_kind"));
}
#[test]
fn incoming_bucket_wrong_kind_rejected() {
let wire = IncomingAdjacencyBucketWire {
kind: "not_incoming".into(),
edges: vec![],
extra: BTreeMap::new(),
};
let bytes = serde_ipld_dagcbor::to_vec(&wire).unwrap();
let err = serde_ipld_dagcbor::from_slice::<IncomingAdjacencyBucket>(&bytes).unwrap_err();
assert!(err.to_string().contains("_kind"));
}
}