use core::fmt;
use serde::de::{self, MapAccess, Visitor};
use serde::ser::SerializeMap;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::address::ContentAddress;
use crate::codec::{self, CodecError};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum EdgeTarget {
Local(String),
Grounded {
ontology: String,
atom: ContentAddress,
},
}
impl EdgeTarget {
pub fn local_name(&self) -> Option<&str> {
match self {
EdgeTarget::Local(name) => Some(name),
EdgeTarget::Grounded { .. } => None,
}
}
}
impl From<String> for EdgeTarget {
fn from(name: String) -> Self {
EdgeTarget::Local(name)
}
}
impl From<&str> for EdgeTarget {
fn from(name: &str) -> Self {
EdgeTarget::Local(name.to_string())
}
}
impl Serialize for EdgeTarget {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
EdgeTarget::Local(name) => serializer.serialize_str(name),
EdgeTarget::Grounded { ontology, atom } => {
let mut map = serializer.serialize_map(Some(2))?;
map.serialize_entry("atom", &atom.to_hex())?;
map.serialize_entry("ontology", ontology)?;
map.end()
}
}
}
}
impl<'de> Deserialize<'de> for EdgeTarget {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct EdgeTargetVisitor;
impl<'de> Visitor<'de> for EdgeTargetVisitor {
type Value = EdgeTarget;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("a local target name (string) or a grounded {ontology, atom} map")
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<EdgeTarget, E> {
Ok(EdgeTarget::Local(v.to_string()))
}
fn visit_string<E: de::Error>(self, v: String) -> Result<EdgeTarget, E> {
Ok(EdgeTarget::Local(v))
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<EdgeTarget, A::Error> {
let mut ontology: Option<String> = None;
let mut atom_hex: Option<String> = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"ontology" => ontology = Some(map.next_value()?),
"atom" => atom_hex = Some(map.next_value()?),
_ => {
let _: de::IgnoredAny = map.next_value()?;
}
}
}
let ontology = ontology.ok_or_else(|| de::Error::missing_field("ontology"))?;
let atom_hex = atom_hex.ok_or_else(|| de::Error::missing_field("atom"))?;
let atom = ContentAddress::from_hex(&atom_hex).ok_or_else(|| {
de::Error::custom("grounded edge atom is not a valid content address")
})?;
Ok(EdgeTarget::Grounded { ontology, atom })
}
}
deserializer.deserialize_any(EdgeTargetVisitor)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Definition {
pub kind: String,
pub name: String,
pub edges: Vec<(String, EdgeTarget)>,
pub axioms: Vec<String>,
pub lexical: Option<String>,
}
impl Definition {
pub fn address(&self) -> Result<ContentAddress, CodecError> {
let mut canon = self.clone();
canon.edges.sort();
canon.edges.dedup();
canon.axioms.sort();
canon.axioms.dedup();
codec::address_of(&canon)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn base() -> Definition {
Definition {
kind: "Concept".into(),
name: "Employer".into(),
edges: vec![("Subsumption".into(), "Agent".into())],
axioms: vec!["EmployerIsAgent".into()],
lexical: Some("employer".into()),
}
}
#[test]
fn identical_definitions_share_an_address() {
assert_eq!(base().address().unwrap(), base().address().unwrap());
}
#[test]
fn changing_an_edge_changes_the_address() {
let mut b = base();
b.edges = vec![("Subsumption".into(), "Person".into())]; assert_ne!(base().address().unwrap(), b.address().unwrap());
}
#[test]
fn changing_an_axiom_changes_the_address() {
let mut b = base();
b.axioms = vec!["EmployerHiresEmployee".into()];
assert_ne!(base().address().unwrap(), b.address().unwrap());
}
#[test]
fn changing_the_lexical_changes_the_address() {
let mut b = base();
b.lexical = Some("boss".into());
assert_ne!(base().address().unwrap(), b.address().unwrap());
}
#[test]
fn same_name_different_definition_does_not_collide() {
let mut b = base();
b.edges.push(("Opposition".into(), "Employee".into()));
assert_ne!(base().address().unwrap(), b.address().unwrap());
}
#[test]
fn address_is_order_independent() {
let mut a = base();
a.edges = vec![
("Subsumption".into(), "Agent".into()),
("Opposition".into(), "Employee".into()),
];
a.axioms = vec!["B".into(), "A".into()];
let mut b = base();
b.edges = vec![
("Opposition".into(), "Employee".into()),
("Subsumption".into(), "Agent".into()),
];
b.axioms = vec!["A".into(), "B".into()];
assert_eq!(a.address().unwrap(), b.address().unwrap());
}
#[test]
fn a_local_target_encodes_byte_identically_to_a_bare_string() {
let local = EdgeTarget::Local("Agent".to_string());
assert_eq!(
codec::canonical_encode(&local).unwrap(),
codec::canonical_encode(&"Agent".to_string()).unwrap(),
"EdgeTarget::Local must encode as a bare CBOR string"
);
}
#[test]
fn an_all_local_definition_address_is_unchanged_by_the_edge_target_type() {
#[derive(serde::Serialize)]
struct LegacyDefinition {
kind: String,
name: String,
edges: Vec<(String, String)>,
axioms: Vec<String>,
lexical: Option<String>,
}
let legacy = LegacyDefinition {
kind: "Concept".into(),
name: "Employer".into(),
edges: vec![("Subsumption".into(), "Agent".into())],
axioms: vec!["EmployerIsAgent".into()],
lexical: Some("employer".into()),
};
assert_eq!(
base().address().unwrap(),
codec::address_of(&legacy).unwrap(),
"an all-Local Definition must address identically to the pre-migration shape"
);
}
#[test]
fn a_grounded_target_round_trips_and_is_distinct_from_a_local_one() {
let atom = ContentAddress::of(b"a connected ontology's atom definition");
let grounded = EdgeTarget::Grounded {
ontology: "english_wordnet".to_string(),
atom,
};
let bytes = codec::canonical_encode(&grounded).unwrap();
let back: EdgeTarget = codec::canonical_decode(&bytes).unwrap();
assert_eq!(back, grounded, "a grounded target must round-trip");
assert_ne!(
bytes,
codec::canonical_encode(&EdgeTarget::Local("english_wordnet".to_string())).unwrap(),
"a grounded target must not encode like a local string of the same text"
);
}
#[test]
fn a_grounded_edge_changes_a_nodes_address() {
let atom = ContentAddress::of(b"some english form");
let mut b = base();
b.edges.push((
"denotes".to_string(),
EdgeTarget::Grounded {
ontology: "english_wordnet".to_string(),
atom,
},
));
assert_ne!(base().address().unwrap(), b.address().unwrap());
}
}