1use std::collections::HashSet;
28
29use oxrdf::{vocab::rdf, vocab::xsd, Literal, NamedNode, Triple};
30use oxttl::TurtleSerializer;
31
32use crate::model::{when, Outcome, Run};
33use crate::name;
34
35const IK: &str = "https://ikigai-rs.dev/ns#";
37const PROV: &str = "http://www.w3.org/ns/prov#";
38const DCTERMS: &str = "http://purl.org/dc/terms/";
39const XSD: &str = "http://www.w3.org/2001/XMLSchema#";
40
41pub const OUTCOME_OK: &str = "urn:script:outcome:ok";
43pub const OUTCOME_FAILED: &str = "urn:script:outcome:failed";
45
46#[derive(Default)]
49pub(crate) struct Graph {
50 triples: Vec<Triple>,
51 seen: HashSet<Triple>,
52}
53
54fn node(text: &str) -> Option<NamedNode> {
56 NamedNode::new(text).ok()
57}
58
59fn term(namespace: &str, local: &str) -> NamedNode {
61 NamedNode::new_unchecked(format!("{namespace}{local}"))
62}
63
64fn date_time(millis: u64) -> Literal {
65 Literal::new_typed_literal(when(Some(millis)), xsd::DATE_TIME)
66}
67
68impl Graph {
69 fn add(&mut self, subject: &NamedNode, predicate: NamedNode, object: impl Into<oxrdf::Term>) {
70 let triple = Triple::new(subject.clone(), predicate, object.into());
71 if self.seen.insert(triple.clone()) {
72 self.triples.push(triple);
73 }
74 }
75
76 fn version(&mut self, script: &str, digest: &str) -> Option<NamedNode> {
78 let version = node(&name::version_iri(script, digest))?;
79 let of = node(&name::script_iri(script))?;
80 self.add(&version, term(PROV, "specializationOf"), of);
81 Some(version)
82 }
83
84 fn run(
86 &mut self,
87 iri: &str,
88 script: &str,
89 digest: &str,
90 outcome: &Outcome,
91 ended: Option<u64>,
92 ) -> Option<NamedNode> {
93 let subject = node(iri)?;
94 self.add(&subject, rdf::TYPE.into_owned(), term(PROV, "Activity"));
95 if let Some(version) = self.version(script, digest) {
96 self.add(&subject, term(PROV, "used"), version);
97 }
98 if let Some(ended) = ended {
99 self.add(&subject, term(PROV, "endedAtTime"), date_time(ended));
100 }
101 let outcome = match outcome {
102 Outcome::Running => None,
103 Outcome::Ok => Some(OUTCOME_OK),
104 Outcome::Failed { .. } => Some(OUTCOME_FAILED),
105 };
106 if let Some(outcome) = outcome.and_then(node) {
107 self.add(&subject, term(IK, "outcome"), outcome);
108 }
109 Some(subject)
110 }
111
112 pub(crate) fn record(&mut self, record: &Run) {
114 let Some(subject) = self.run(
115 &record.iri(),
116 &record.name,
117 &record.version,
118 &record.outcome,
119 record.ended,
120 ) else {
121 return;
122 };
123 if let Some(agent) = node(&record.principal) {
125 self.add(&subject, term(PROV, "wasAssociatedWith"), agent);
126 }
127 if let Some(started) = record.started {
128 self.add(&subject, term(PROV, "startedAtTime"), date_time(started));
129 }
130 }
131
132 pub(crate) fn script(&mut self, script: &str, head: Option<&str>, last: Option<&Run>) {
134 let (Some(catalog), Some(subject)) =
135 (node(name::CATALOG_IRI), node(&name::script_iri(script)))
136 else {
137 return;
138 };
139 self.add(&catalog, term(DCTERMS, "hasPart"), subject.clone());
140 self.add(
141 &subject,
142 term(DCTERMS, "identifier"),
143 Literal::new_simple_literal(script),
144 );
145 if let Some(digest) = head {
146 self.add(
147 &subject,
148 term(IK, "contentHash"),
149 Literal::new_simple_literal(digest),
150 );
151 self.version(script, digest);
152 }
153 if let Some(run) = last {
154 self.run(&run.iri(), script, &run.version, &run.outcome, run.ended);
155 }
156 }
157
158 pub(crate) fn turtle(self) -> std::io::Result<Vec<u8>> {
160 let mut out = TurtleSerializer::new()
161 .with_prefix("ik", IK)
162 .and_then(|s| s.with_prefix("prov", PROV))
163 .and_then(|s| s.with_prefix("dcterms", DCTERMS))
164 .and_then(|s| s.with_prefix("xsd", XSD))
165 .map_err(std::io::Error::other)?
166 .for_writer(Vec::new());
167 for triple in &self.triples {
168 out.serialize_triple(triple)?;
169 }
170 out.finish()
171 }
172}