use super::category::Category;
#[derive(Debug, Clone, PartialEq)]
pub struct TraceRecord {
pub ontology: String,
pub operation: String,
pub detail: String,
pub status: TraceRecordStatus,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TraceRecordStatus {
Ok,
Warning,
Error,
}
#[derive(Debug, Clone)]
pub struct TracedMorphism<M> {
pub morphism: M,
pub trace: Vec<TraceRecord>,
}
pub struct TracedCategory<C: Category>(std::marker::PhantomData<C>);
impl<M: Clone> TracedMorphism<M> {
pub fn new(morphism: M, ontology: &str, operation: &str, detail: &str) -> Self {
Self {
morphism,
trace: vec![TraceRecord {
ontology: ontology.into(),
operation: operation.into(),
detail: detail.into(),
status: TraceRecordStatus::Ok,
}],
}
}
pub fn bare(morphism: M) -> Self {
Self {
morphism,
trace: Vec::new(),
}
}
pub fn record(&mut self, ontology: &str, operation: &str, detail: &str) {
self.trace.push(TraceRecord {
ontology: ontology.into(),
operation: operation.into(),
detail: detail.into(),
status: TraceRecordStatus::Ok,
});
}
pub fn warn(&mut self, ontology: &str, operation: &str, detail: &str) {
self.trace.push(TraceRecord {
ontology: ontology.into(),
operation: operation.into(),
detail: detail.into(),
status: TraceRecordStatus::Warning,
});
}
}
impl<C: Category> TracedCategory<C>
where
C::Morphism: Clone,
C::Object: Clone,
{
pub fn compose(
f: &TracedMorphism<C::Morphism>,
g: &TracedMorphism<C::Morphism>,
) -> Option<TracedMorphism<C::Morphism>> {
let composed = C::compose(&f.morphism, &g.morphism)?;
let mut trace = f.trace.clone();
trace.extend(g.trace.iter().cloned());
Some(TracedMorphism {
morphism: composed,
trace,
})
}
pub fn identity(obj: &C::Object) -> TracedMorphism<C::Morphism> {
TracedMorphism::bare(C::identity(obj))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::category::entity::Entity;
use crate::category::relationship::Relationship;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum TestObj {
A,
B,
C,
}
impl Entity for TestObj {
fn variants() -> Vec<Self> {
vec![Self::A, Self::B, Self::C]
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct TestMorph {
from: TestObj,
to: TestObj,
}
impl Relationship for TestMorph {
type Object = TestObj;
fn source(&self) -> TestObj {
self.from
}
fn target(&self) -> TestObj {
self.to
}
}
struct TestCat;
impl Category for TestCat {
type Object = TestObj;
type Morphism = TestMorph;
fn identity(obj: &TestObj) -> TestMorph {
TestMorph {
from: *obj,
to: *obj,
}
}
fn compose(f: &TestMorph, g: &TestMorph) -> Option<TestMorph> {
if f.to == g.from {
Some(TestMorph {
from: f.from,
to: g.to,
})
} else {
None
}
}
fn morphisms() -> Vec<TestMorph> {
vec![
TestMorph {
from: TestObj::A,
to: TestObj::B,
},
TestMorph {
from: TestObj::B,
to: TestObj::C,
},
]
}
}
#[test]
fn traced_compose_accumulates_records() {
let f = TracedMorphism::new(
TestMorph {
from: TestObj::A,
to: TestObj::B,
},
"TestOntology",
"step1",
"A → B",
);
let g = TracedMorphism::new(
TestMorph {
from: TestObj::B,
to: TestObj::C,
},
"TestOntology",
"step2",
"B → C",
);
let h = TracedCategory::<TestCat>::compose(&f, &g).unwrap();
assert_eq!(h.morphism.from, TestObj::A);
assert_eq!(h.morphism.to, TestObj::C);
assert_eq!(h.trace.len(), 2);
assert_eq!(h.trace[0].operation, "step1");
assert_eq!(h.trace[1].operation, "step2");
}
#[test]
fn traced_identity_has_no_trace() {
let id = TracedCategory::<TestCat>::identity(&TestObj::A);
assert_eq!(id.morphism.from, TestObj::A);
assert_eq!(id.morphism.to, TestObj::A);
assert!(id.trace.is_empty());
}
#[test]
fn traced_compose_with_identity_preserves_trace() {
let f = TracedMorphism::new(
TestMorph {
from: TestObj::A,
to: TestObj::B,
},
"Test",
"lookup",
"found",
);
let id = TracedCategory::<TestCat>::identity(&TestObj::B);
let h = TracedCategory::<TestCat>::compose(&f, &id).unwrap();
assert_eq!(h.trace.len(), 1); assert_eq!(h.trace[0].detail, "found");
}
#[test]
fn trace_records_have_status() {
let mut f = TracedMorphism::new(
TestMorph {
from: TestObj::A,
to: TestObj::B,
},
"Test",
"parse",
"success",
);
f.warn("Test", "parse", "ambiguous — multiple parses");
assert_eq!(f.trace.len(), 2);
assert_eq!(f.trace[0].status, TraceRecordStatus::Ok);
assert_eq!(f.trace[1].status, TraceRecordStatus::Warning);
}
#[test]
fn compose_incompatible_returns_none() {
let f = TracedMorphism::new(
TestMorph {
from: TestObj::A,
to: TestObj::B,
},
"Test",
"step1",
"A → B",
);
let g = TracedMorphism::new(
TestMorph {
from: TestObj::A,
to: TestObj::C,
},
"Test",
"step2",
"A → C",
);
assert!(TracedCategory::<TestCat>::compose(&f, &g).is_none());
}
}