Skip to main content

vertep_runtime/
ontology.rs

1//! The `vt:` runtime ontology module and its SHACL shapes.
2//!
3//! The module itself is `vertep-rdf/ontology/vt-runtime.ttl` (next to the chart module); the
4//! shapes ship with this crate. [`validate`] checks any runtime record (a journal entry as read
5//! back from a store, a snapshot, a timer, a dead letter, a mailbox row, a current-state
6//! graph) against them. The runtime's own queries use only `vt:` terms; [`rdfs_closure`] exists
7//! for tests and tools that want to see the PROV-O view an RDFS-aware store would answer.
8
9use std::collections::HashSet;
10use std::sync::OnceLock;
11
12use oxrdf::vocab::rdf;
13use oxrdf::{Graph, NamedNode, NamedOrBlankNode, Term, TermRef, Triple};
14use vertep_rdf::io::{RdfFormat, read_graph};
15use vertep_rdf::shacl::{Shapes, ValidationReport};
16use vertep_store::Entry;
17
18/// The runtime module of the `vt:` ontology, in Turtle.
19pub use vertep_rdf::RUNTIME_ONTOLOGY_TTL;
20
21/// The SHACL shapes of the runtime module, in Turtle.
22pub const RUNTIME_SHAPES_TTL: &str = include_str!("../shapes/vt-runtime-shapes.ttl");
23
24const RDFS_SUBCLASS: &str = "http://www.w3.org/2000/01/rdf-schema#subClassOf";
25const RDFS_SUBPROPERTY: &str = "http://www.w3.org/2000/01/rdf-schema#subPropertyOf";
26
27/// The runtime ontology module as a graph: classes for actors (virtual, invoked, stateless
28/// worker), addresses, mailboxes, activations, journal entries, macrosteps, snapshots, timers,
29/// migrations and dead letters, aligned with PROV-O by subclass and subproperty statements.
30// @tg: implements:: [[openspec:runtime-ontology#Runtime Vocabulary]]
31// @tg: implements:: [[openspec:runtime-ontology#PROV-O Alignment Without Dependency]]
32pub fn runtime_ontology() -> Graph {
33    read_graph(RUNTIME_ONTOLOGY_TTL.as_bytes(), RdfFormat::Turtle).unwrap_or_default()
34}
35
36/// The compiled runtime shapes.
37pub fn runtime_shapes() -> &'static Shapes {
38    static SHAPES: OnceLock<Shapes> = OnceLock::new();
39    SHAPES.get_or_init(|| {
40        let g = read_graph(RUNTIME_SHAPES_TTL.as_bytes(), RdfFormat::Turtle)
41            .unwrap_or_else(|e| panic!("the bundled runtime shapes parse: {e:?}"));
42        Shapes::from_graph(&g)
43            .unwrap_or_else(|e| panic!("the bundled runtime shapes compile: {e:?}"))
44    })
45}
46
47/// Validates runtime records against the runtime shapes.
48// @tg: implements:: [[openspec:runtime-ontology#Runtime SHACL Shapes]]
49pub fn validate(data: &Graph) -> ValidationReport {
50    runtime_shapes().validate(data)
51}
52
53/// Validates one journal entry, as read back from a journal store.
54pub fn validate_entry(entry: &Entry) -> ValidationReport {
55    validate(entry.triples())
56}
57
58/// `data` closed under the RDFS subclass and subproperty rules (rdfs5, rdfs7, rdfs9, rdfs11)
59/// of `ontology`: what an RDFS-aware store infers. The runtime never needs this.
60pub fn rdfs_closure(data: &Graph, ontology: &Graph) -> Graph {
61    let sub = |p: &str| -> Vec<(NamedNode, NamedNode)> {
62        let p = NamedNode::new_unchecked(p);
63        ontology
64            .triples_for_predicate(&p)
65            .filter_map(|t| match (t.subject, t.object) {
66                (oxrdf::NamedOrBlankNodeRef::NamedNode(a), TermRef::NamedNode(b)) => {
67                    Some((a.into_owned(), b.into_owned()))
68                }
69                _ => None,
70            })
71            .collect()
72    };
73    let closure = |pairs: Vec<(NamedNode, NamedNode)>| {
74        let mut set: HashSet<(NamedNode, NamedNode)> = pairs.into_iter().collect();
75        loop {
76            let mut added = Vec::new();
77            for (a, b) in &set {
78                for (c, d) in &set {
79                    if b == c && !set.contains(&(a.clone(), d.clone())) {
80                        added.push((a.clone(), d.clone()));
81                    }
82                }
83            }
84            if added.is_empty() {
85                break set;
86            }
87            set.extend(added);
88        }
89    };
90    let classes = closure(sub(RDFS_SUBCLASS));
91    let props = closure(sub(RDFS_SUBPROPERTY));
92    let mut out = data.clone();
93    let mut extra: Vec<Triple> = Vec::new();
94    for t in data.iter() {
95        let s: NamedOrBlankNode = t.subject.into_owned();
96        let p = t.predicate.into_owned();
97        let o: Term = t.object.into_owned();
98        for (a, b) in &props {
99            if *a == p {
100                extra.push(Triple::new(s.clone(), b.clone(), o.clone()));
101            }
102        }
103    }
104    for t in &extra {
105        out.insert(t);
106    }
107    let typed: Vec<(NamedOrBlankNode, NamedNode)> = out
108        .triples_for_predicate(rdf::TYPE)
109        .filter_map(|t| match t.object {
110            TermRef::NamedNode(c) => Some((t.subject.into_owned(), c.into_owned())),
111            _ => None,
112        })
113        .collect();
114    for (s, c) in typed {
115        for (a, b) in &classes {
116            if *a == c {
117                out.insert(&Triple::new(s.clone(), rdf::TYPE, b.clone()));
118            }
119        }
120    }
121    out
122}