1use core::fmt;
19
20use serde::de::{self, MapAccess, Visitor};
21use serde::ser::SerializeMap;
22use serde::{Deserialize, Deserializer, Serialize, Serializer};
23
24use crate::address::ContentAddress;
25use crate::codec::{self, CodecError};
26
27#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
46pub enum EdgeTarget {
47 Local(String),
49 Grounded {
52 ontology: String,
54 atom: ContentAddress,
56 },
57}
58
59impl EdgeTarget {
60 pub fn local_name(&self) -> Option<&str> {
64 match self {
65 EdgeTarget::Local(name) => Some(name),
66 EdgeTarget::Grounded { .. } => None,
67 }
68 }
69}
70
71impl From<String> for EdgeTarget {
72 fn from(name: String) -> Self {
73 EdgeTarget::Local(name)
74 }
75}
76
77impl From<&str> for EdgeTarget {
78 fn from(name: &str) -> Self {
79 EdgeTarget::Local(name.to_string())
80 }
81}
82
83impl Serialize for EdgeTarget {
84 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
85 match self {
86 EdgeTarget::Local(name) => serializer.serialize_str(name),
88 EdgeTarget::Grounded { ontology, atom } => {
89 let mut map = serializer.serialize_map(Some(2))?;
90 map.serialize_entry("atom", &atom.to_hex())?;
91 map.serialize_entry("ontology", ontology)?;
92 map.end()
93 }
94 }
95 }
96}
97
98impl<'de> Deserialize<'de> for EdgeTarget {
99 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
100 struct EdgeTargetVisitor;
101
102 impl<'de> Visitor<'de> for EdgeTargetVisitor {
103 type Value = EdgeTarget;
104
105 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
106 f.write_str("a local target name (string) or a grounded {ontology, atom} map")
107 }
108
109 fn visit_str<E: de::Error>(self, v: &str) -> Result<EdgeTarget, E> {
110 Ok(EdgeTarget::Local(v.to_string()))
111 }
112
113 fn visit_string<E: de::Error>(self, v: String) -> Result<EdgeTarget, E> {
114 Ok(EdgeTarget::Local(v))
115 }
116
117 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<EdgeTarget, A::Error> {
118 let mut ontology: Option<String> = None;
119 let mut atom_hex: Option<String> = None;
120 while let Some(key) = map.next_key::<String>()? {
121 match key.as_str() {
122 "ontology" => ontology = Some(map.next_value()?),
123 "atom" => atom_hex = Some(map.next_value()?),
124 _ => {
125 let _: de::IgnoredAny = map.next_value()?;
126 }
127 }
128 }
129 let ontology = ontology.ok_or_else(|| de::Error::missing_field("ontology"))?;
130 let atom_hex = atom_hex.ok_or_else(|| de::Error::missing_field("atom"))?;
131 let atom = ContentAddress::from_hex(&atom_hex).ok_or_else(|| {
132 de::Error::custom("grounded edge atom is not a valid content address")
133 })?;
134 Ok(EdgeTarget::Grounded { ontology, atom })
135 }
136 }
137
138 deserializer.deserialize_any(EdgeTargetVisitor)
139 }
140}
141
142#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
147pub struct Definition {
148 pub kind: String,
151 pub name: String,
153 pub edges: Vec<(String, EdgeTarget)>,
159 pub axioms: Vec<String>,
161 pub lexical: Option<String>,
164}
165
166impl Definition {
167 pub fn address(&self) -> Result<ContentAddress, CodecError> {
173 let mut canon = self.clone();
174 canon.edges.sort();
175 canon.edges.dedup();
176 canon.axioms.sort();
177 canon.axioms.dedup();
178 codec::address_of(&canon)
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 fn base() -> Definition {
187 Definition {
188 kind: "Concept".into(),
189 name: "Employer".into(),
190 edges: vec![("Subsumption".into(), "Agent".into())],
191 axioms: vec!["EmployerIsAgent".into()],
192 lexical: Some("employer".into()),
193 }
194 }
195
196 #[test]
197 fn identical_definitions_share_an_address() {
198 assert_eq!(base().address().unwrap(), base().address().unwrap());
199 }
200
201 #[test]
202 fn changing_an_edge_changes_the_address() {
203 let mut b = base();
204 b.edges = vec![("Subsumption".into(), "Person".into())]; assert_ne!(base().address().unwrap(), b.address().unwrap());
206 }
207
208 #[test]
209 fn changing_an_axiom_changes_the_address() {
210 let mut b = base();
211 b.axioms = vec!["EmployerHiresEmployee".into()];
212 assert_ne!(base().address().unwrap(), b.address().unwrap());
213 }
214
215 #[test]
216 fn changing_the_lexical_changes_the_address() {
217 let mut b = base();
218 b.lexical = Some("boss".into());
219 assert_ne!(base().address().unwrap(), b.address().unwrap());
220 }
221
222 #[test]
223 fn same_name_different_definition_does_not_collide() {
224 let mut b = base();
226 b.edges.push(("Opposition".into(), "Employee".into()));
227 assert_ne!(base().address().unwrap(), b.address().unwrap());
228 }
229
230 #[test]
231 fn address_is_order_independent() {
232 let mut a = base();
233 a.edges = vec![
234 ("Subsumption".into(), "Agent".into()),
235 ("Opposition".into(), "Employee".into()),
236 ];
237 a.axioms = vec!["B".into(), "A".into()];
238 let mut b = base();
239 b.edges = vec![
240 ("Opposition".into(), "Employee".into()),
241 ("Subsumption".into(), "Agent".into()),
242 ];
243 b.axioms = vec!["A".into(), "B".into()];
244 assert_eq!(a.address().unwrap(), b.address().unwrap());
245 }
246
247 #[test]
250 fn a_local_target_encodes_byte_identically_to_a_bare_string() {
251 let local = EdgeTarget::Local("Agent".to_string());
255 assert_eq!(
256 codec::canonical_encode(&local).unwrap(),
257 codec::canonical_encode(&"Agent".to_string()).unwrap(),
258 "EdgeTarget::Local must encode as a bare CBOR string"
259 );
260 }
261
262 #[test]
263 fn an_all_local_definition_address_is_unchanged_by_the_edge_target_type() {
264 #[derive(serde::Serialize)]
269 struct LegacyDefinition {
270 kind: String,
271 name: String,
272 edges: Vec<(String, String)>,
273 axioms: Vec<String>,
274 lexical: Option<String>,
275 }
276 let legacy = LegacyDefinition {
277 kind: "Concept".into(),
278 name: "Employer".into(),
279 edges: vec![("Subsumption".into(), "Agent".into())],
280 axioms: vec!["EmployerIsAgent".into()],
281 lexical: Some("employer".into()),
282 };
283 assert_eq!(
284 base().address().unwrap(),
285 codec::address_of(&legacy).unwrap(),
286 "an all-Local Definition must address identically to the pre-migration shape"
287 );
288 }
289
290 #[test]
291 fn a_grounded_target_round_trips_and_is_distinct_from_a_local_one() {
292 let atom = ContentAddress::of(b"a connected ontology's atom definition");
295 let grounded = EdgeTarget::Grounded {
296 ontology: "english_wordnet".to_string(),
297 atom,
298 };
299 let bytes = codec::canonical_encode(&grounded).unwrap();
300 let back: EdgeTarget = codec::canonical_decode(&bytes).unwrap();
301 assert_eq!(back, grounded, "a grounded target must round-trip");
302 assert_ne!(
303 bytes,
304 codec::canonical_encode(&EdgeTarget::Local("english_wordnet".to_string())).unwrap(),
305 "a grounded target must not encode like a local string of the same text"
306 );
307 }
308
309 #[test]
310 fn a_grounded_edge_changes_a_nodes_address() {
311 let atom = ContentAddress::of(b"some english form");
314 let mut b = base();
315 b.edges.push((
316 "denotes".to_string(),
317 EdgeTarget::Grounded {
318 ontology: "english_wordnet".to_string(),
319 atom,
320 },
321 ));
322 assert_ne!(base().address().unwrap(), b.address().unwrap());
323 }
324}