use std::collections::BTreeMap;
use crate::archive::Archive;
use crate::connection::GeneratorAction;
use crate::definition::Definition;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ApplyError {
UnsupportedAction { kind: &'static str },
}
impl core::fmt::Display for ApplyError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
ApplyError::UnsupportedAction { kind } => write!(
f,
"apply: only a Functor projection is interpreted; got {kind} \
(lens/adjunction/nat-trans replay is a tracked follow-up)"
),
}
}
}
impl std::error::Error for ApplyError {}
fn action_kind(action: &GeneratorAction) -> &'static str {
match action {
GeneratorAction::Functor { .. } => "Functor",
GeneratorAction::NaturalTransformation { .. } => "NaturalTransformation",
GeneratorAction::Lens { .. } => "Lens",
GeneratorAction::Adjunction { .. } => "Adjunction",
}
}
pub fn apply(action: &GeneratorAction, source: &Archive) -> Result<Archive, ApplyError> {
let GeneratorAction::Functor {
map_object,
map_morphism,
} = action
else {
return Err(ApplyError::UnsupportedAction {
kind: action_kind(action),
});
};
let on_vertex: BTreeMap<&str, &str> = map_object
.iter()
.map(|(s, t)| (s.as_str(), t.as_str()))
.collect();
let on_edge: BTreeMap<&str, &str> = map_morphism
.iter()
.map(|(s, t)| (s.as_str(), t.as_str()))
.collect();
let nodes = source
.nodes
.iter()
.map(|d| Definition {
kind: on_vertex
.get(d.kind.as_str())
.map_or_else(|| d.kind.clone(), |t| t.to_string()),
name: d.name.clone(),
edges: d
.edges
.iter()
.map(|(edge_kind, target)| {
(
on_edge
.get(edge_kind.as_str())
.map_or_else(|| edge_kind.clone(), |t| t.to_string()),
target.clone(),
)
})
.collect(),
axioms: d.axioms.clone(),
lexical: d.lexical.clone(),
})
.collect();
Ok(Archive {
nodes,
connections: source.connections.clone(),
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::definition::EdgeTarget;
fn synset(name: &str, hypernym: &str) -> Definition {
Definition {
kind: "Synset".into(),
name: name.into(),
edges: vec![("hypernym".into(), hypernym.into())],
axioms: vec![],
lexical: Some("a four-legged animal".into()),
}
}
fn wordnet_functor() -> GeneratorAction {
GeneratorAction::Functor {
map_object: vec![("Synset".into(), "ConceptNode".into())],
map_morphism: vec![
("hypernym".into(), "Subsumption".into()),
("holo_part".into(), "Parthood".into()),
],
}
}
#[test]
fn relabels_kinds_carries_identity_content() {
let source = Archive {
nodes: vec![synset("dog", "mammal")],
connections: vec![],
};
let target = apply(&wordnet_functor(), &source).unwrap();
let node = &target.nodes[0];
assert_eq!(
node.kind, "ConceptNode",
"node kind relabeled by map_object"
);
assert_eq!(node.name, "dog", "name (identity) carried unchanged");
assert_eq!(
node.edges,
vec![(
"Subsumption".to_string(),
EdgeTarget::Local("mammal".to_string())
)]
);
assert_eq!(
node.lexical.as_deref(),
Some("a four-legged animal"),
"the gloss rides the object map's image — it is the node's lexical"
);
}
#[test]
fn unmapped_kind_is_the_identity_image_not_dropped() {
let source = Archive {
nodes: vec![Definition {
kind: "Synset".into(),
name: "hot".into(),
edges: vec![("antonym".into(), "cold".into())],
axioms: vec![],
lexical: None,
}],
connections: vec![],
};
let target = apply(&wordnet_functor(), &source).unwrap();
assert_eq!(target.nodes[0].kind, "ConceptNode");
assert_eq!(
target.nodes[0].edges,
vec![("antonym".to_string(), EdgeTarget::Local("cold".to_string()))],
"an unmapped relation is carried as-is (representable), not dropped"
);
}
#[test]
fn re_emitting_a_different_functor_changes_the_projection() {
let source = Archive {
nodes: vec![synset("dog", "mammal")],
connections: vec![],
};
let remapped = GeneratorAction::Functor {
map_object: vec![("Synset".into(), "ConceptNode".into())],
map_morphism: vec![("hypernym".into(), "Parthood".into())],
};
let target = apply(&remapped, &source).unwrap();
assert_eq!(
target.nodes[0].edges,
vec![(
"Parthood".to_string(),
EdgeTarget::Local("mammal".to_string())
)]
);
}
#[test]
fn refuses_a_non_functor_action_fail_closed() {
let lens = GeneratorAction::Lens {
view: "Source".into(),
get: "parse".into(),
put: "generate".into(),
};
let source = Archive {
nodes: vec![synset("dog", "mammal")],
connections: vec![],
};
assert_eq!(
apply(&lens, &source).unwrap_err(),
ApplyError::UnsupportedAction { kind: "Lens" }
);
}
}