use std::collections::HashSet;
use oxrdf::{vocab::rdf, vocab::xsd, Literal, NamedNode, Triple};
use oxttl::TurtleSerializer;
use crate::model::{when, Outcome, Run};
use crate::name;
const IK: &str = "https://ikigai-rs.dev/ns#";
const PROV: &str = "http://www.w3.org/ns/prov#";
const DCTERMS: &str = "http://purl.org/dc/terms/";
const XSD: &str = "http://www.w3.org/2001/XMLSchema#";
pub const OUTCOME_OK: &str = "urn:script:outcome:ok";
pub const OUTCOME_FAILED: &str = "urn:script:outcome:failed";
#[derive(Default)]
pub(crate) struct Graph {
triples: Vec<Triple>,
seen: HashSet<Triple>,
}
fn node(text: &str) -> Option<NamedNode> {
NamedNode::new(text).ok()
}
fn term(namespace: &str, local: &str) -> NamedNode {
NamedNode::new_unchecked(format!("{namespace}{local}"))
}
fn date_time(millis: u64) -> Literal {
Literal::new_typed_literal(when(Some(millis)), xsd::DATE_TIME)
}
impl Graph {
fn add(&mut self, subject: &NamedNode, predicate: NamedNode, object: impl Into<oxrdf::Term>) {
let triple = Triple::new(subject.clone(), predicate, object.into());
if self.seen.insert(triple.clone()) {
self.triples.push(triple);
}
}
fn version(&mut self, script: &str, digest: &str) -> Option<NamedNode> {
let version = node(&name::version_iri(script, digest))?;
let of = node(&name::script_iri(script))?;
self.add(&version, term(PROV, "specializationOf"), of);
Some(version)
}
fn run(
&mut self,
iri: &str,
script: &str,
digest: &str,
outcome: &Outcome,
ended: Option<u64>,
) -> Option<NamedNode> {
let subject = node(iri)?;
self.add(&subject, rdf::TYPE.into_owned(), term(PROV, "Activity"));
if let Some(version) = self.version(script, digest) {
self.add(&subject, term(PROV, "used"), version);
}
if let Some(ended) = ended {
self.add(&subject, term(PROV, "endedAtTime"), date_time(ended));
}
let outcome = match outcome {
Outcome::Running => None,
Outcome::Ok => Some(OUTCOME_OK),
Outcome::Failed { .. } => Some(OUTCOME_FAILED),
};
if let Some(outcome) = outcome.and_then(node) {
self.add(&subject, term(IK, "outcome"), outcome);
}
Some(subject)
}
pub(crate) fn record(&mut self, record: &Run) {
let Some(subject) = self.run(
&record.iri(),
&record.name,
&record.version,
&record.outcome,
record.ended,
) else {
return;
};
if let Some(agent) = node(&record.principal) {
self.add(&subject, term(PROV, "wasAssociatedWith"), agent);
}
if let Some(started) = record.started {
self.add(&subject, term(PROV, "startedAtTime"), date_time(started));
}
}
pub(crate) fn script(&mut self, script: &str, head: Option<&str>, last: Option<&Run>) {
let (Some(catalog), Some(subject)) =
(node(name::CATALOG_IRI), node(&name::script_iri(script)))
else {
return;
};
self.add(&catalog, term(DCTERMS, "hasPart"), subject.clone());
self.add(
&subject,
term(DCTERMS, "identifier"),
Literal::new_simple_literal(script),
);
if let Some(digest) = head {
self.add(
&subject,
term(IK, "contentHash"),
Literal::new_simple_literal(digest),
);
self.version(script, digest);
}
if let Some(run) = last {
self.run(&run.iri(), script, &run.version, &run.outcome, run.ended);
}
}
pub(crate) fn turtle(self) -> std::io::Result<Vec<u8>> {
let mut out = TurtleSerializer::new()
.with_prefix("ik", IK)
.and_then(|s| s.with_prefix("prov", PROV))
.and_then(|s| s.with_prefix("dcterms", DCTERMS))
.and_then(|s| s.with_prefix("xsd", XSD))
.map_err(std::io::Error::other)?
.for_writer(Vec::new());
for triple in &self.triples {
out.serialize_triple(triple)?;
}
out.finish()
}
}