mnem_core/objects/
edge.rs1use std::collections::BTreeMap;
21
22use ipld_core::ipld::Ipld;
23use serde::{Deserialize, Deserializer, Serialize, Serializer};
24
25use crate::id::{EdgeId, NodeId};
26
27#[derive(Clone, Debug, PartialEq, Eq)]
29pub struct Edge {
30 pub id: EdgeId,
32 pub etype: String,
34 pub src: NodeId,
36 pub dst: NodeId,
38 pub props: BTreeMap<String, Ipld>,
40 pub extra: BTreeMap<String, Ipld>,
42}
43
44impl Edge {
45 pub const KIND: &'static str = "edge";
47
48 #[must_use]
50 pub fn new(id: EdgeId, etype: impl Into<String>, src: NodeId, dst: NodeId) -> Self {
51 Self {
52 id,
53 etype: etype.into(),
54 src,
55 dst,
56 props: BTreeMap::new(),
57 extra: BTreeMap::new(),
58 }
59 }
60
61 #[must_use]
63 pub fn with_prop(mut self, key: impl Into<String>, value: impl Into<Ipld>) -> Self {
64 self.props.insert(key.into(), value.into());
65 self
66 }
67}
68
69#[derive(Serialize, Deserialize)]
72struct EdgeWire {
73 #[serde(rename = "_kind")]
74 kind: String,
75 id: EdgeId,
76 etype: String,
77 src: NodeId,
78 dst: NodeId,
79 props: BTreeMap<String, Ipld>,
80 #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
81 extra: BTreeMap<String, Ipld>,
82}
83
84impl Serialize for Edge {
85 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
86 EdgeWire {
87 kind: Self::KIND.into(),
88 id: self.id,
89 etype: self.etype.clone(),
90 src: self.src,
91 dst: self.dst,
92 props: self.props.clone(),
93 extra: self.extra.clone(),
94 }
95 .serialize(serializer)
96 }
97}
98
99impl<'de> Deserialize<'de> for Edge {
100 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
101 let wire = EdgeWire::deserialize(deserializer)?;
102 if wire.kind != Self::KIND {
103 return Err(serde::de::Error::custom(format!(
104 "expected _kind='{}', got '{}'",
105 Self::KIND,
106 wire.kind
107 )));
108 }
109 Ok(Self {
110 id: wire.id,
111 etype: wire.etype,
112 src: wire.src,
113 dst: wire.dst,
114 props: wire.props,
115 extra: wire.extra,
116 })
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use crate::codec::{from_canonical_bytes, to_canonical_bytes};
124
125 fn sample() -> Edge {
126 Edge::new(
127 EdgeId::from_bytes_raw([3u8; 16]),
128 "knows",
129 NodeId::from_bytes_raw([1u8; 16]),
130 NodeId::from_bytes_raw([2u8; 16]),
131 )
132 .with_prop("since", Ipld::Integer(2020))
133 }
134
135 #[test]
136 fn edge_round_trip_byte_identity() {
137 let original = sample();
138 let bytes = to_canonical_bytes(&original).expect("encode");
139 let decoded: Edge = from_canonical_bytes(&bytes).expect("decode");
140 assert_eq!(original, decoded);
141 let bytes2 = to_canonical_bytes(&decoded).expect("re-encode");
142 assert_eq!(bytes, bytes2);
143 }
144
145 #[test]
146 fn edge_kind_rejection() {
147 let wire = EdgeWire {
148 kind: "node".into(),
149 id: EdgeId::from_bytes_raw([4u8; 16]),
150 etype: "x".into(),
151 src: NodeId::from_bytes_raw([1u8; 16]),
152 dst: NodeId::from_bytes_raw([2u8; 16]),
153 props: BTreeMap::new(),
154 extra: BTreeMap::new(),
155 };
156 let bytes = serde_ipld_dagcbor::to_vec(&wire).expect("encode");
157 let err = serde_ipld_dagcbor::from_slice::<Edge>(&bytes).unwrap_err();
158 assert!(err.to_string().contains("_kind"));
159 }
160}