use std::collections::BTreeSet;
use std::fmt;
pub const ONT_BASE: &str = "https://ont.paiml.dev/v1alpha1/";
pub const RDF_TYPE: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
pub const PROV_ENTITY: &str = "http://www.w3.org/ns/prov#Entity";
pub const XSD_STRING: &str = "http://www.w3.org/2001/XMLSchema#string";
pub const XSD_INTEGER: &str = "http://www.w3.org/2001/XMLSchema#integer";
pub const XSD_BOOLEAN: &str = "http://www.w3.org/2001/XMLSchema#boolean";
pub const XSD_DOUBLE: &str = "http://www.w3.org/2001/XMLSchema#double";
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum Term {
Iri(String),
Literal { value: String, datatype: String },
}
impl Term {
#[must_use]
pub fn iri(s: impl Into<String>) -> Self {
Self::Iri(s.into())
}
#[must_use]
pub fn string(s: impl Into<String>) -> Self {
Self::Literal {
value: s.into(),
datatype: XSD_STRING.to_string(),
}
}
#[must_use]
pub fn integer(n: u64) -> Self {
Self::Literal {
value: n.to_string(),
datatype: XSD_INTEGER.to_string(),
}
}
#[must_use]
pub fn boolean(b: bool) -> Self {
Self::Literal {
value: b.to_string(),
datatype: XSD_BOOLEAN.to_string(),
}
}
#[must_use]
pub fn double(x: f64) -> Self {
Self::Literal {
value: x.to_string(),
datatype: XSD_DOUBLE.to_string(),
}
}
#[must_use]
pub fn signed(n: i64) -> Self {
Self::Literal {
value: n.to_string(),
datatype: XSD_INTEGER.to_string(),
}
}
#[must_use]
pub fn as_iri(&self) -> Option<&str> {
match self {
Self::Iri(s) => Some(s),
Self::Literal { .. } => None,
}
}
#[must_use]
pub fn as_literal(&self) -> Option<(&str, &str)> {
match self {
Self::Iri(_) => None,
Self::Literal { value, datatype } => Some((value, datatype)),
}
}
}
impl fmt::Display for Term {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Iri(s) => write!(f, "<{s}>"),
Self::Literal { value, datatype } => {
write!(f, "\"{}\"^^<{datatype}>", escape_literal(value))
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Triple {
pub subject: String,
pub predicate: String,
pub object: Term,
}
#[derive(Debug, Default, Clone)]
pub struct Graph {
triples: BTreeSet<Triple>,
}
impl Graph {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn insert(
&mut self,
subject: impl Into<String>,
predicate: impl Into<String>,
object: Term,
) {
self.triples.insert(Triple {
subject: subject.into(),
predicate: predicate.into(),
object,
});
}
pub fn extend(&mut self, other: &Self) {
self.triples.extend(other.triples.iter().cloned());
}
#[must_use]
pub fn len(&self) -> usize {
self.triples.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.triples.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &Triple> {
self.triples.iter()
}
#[must_use]
pub fn instances_of(&self, class: &str) -> Vec<&str> {
self.triples
.iter()
.filter(|t| t.predicate == RDF_TYPE && t.object.as_iri() == Some(class))
.map(|t| t.subject.as_str())
.collect()
}
#[must_use]
pub fn objects(&self, subject: &str, predicate: &str) -> Vec<&Term> {
let from = Triple {
subject: subject.to_string(),
predicate: predicate.to_string(),
object: Term::Iri(String::new()),
};
self.triples
.range(from..)
.take_while(|t| t.subject == subject && t.predicate == predicate)
.map(|t| &t.object)
.collect()
}
#[must_use]
pub fn predicates_of(&self, subject: &str) -> BTreeSet<&str> {
let from = Triple {
subject: subject.to_string(),
predicate: String::new(),
object: Term::Iri(String::new()),
};
self.triples
.range(from..)
.take_while(|t| t.subject == subject)
.map(|t| t.predicate.as_str())
.collect()
}
#[must_use]
pub fn to_ntriples(&self) -> String {
let mut out = String::new();
for t in &self.triples {
out.push_str(&format!(
"<{}> <{}> {} .\n",
t.subject, t.predicate, t.object
));
}
out
}
}
#[must_use]
pub fn escape_literal(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
_ => out.push(c),
}
}
out
}
#[must_use]
pub fn iri(kind: &str, id: &str) -> String {
format!("{ONT_BASE}{kind}/{}", percent_encode(id))
}
#[must_use]
pub fn iri_path(kind: &str, segments: &[&str]) -> String {
let mut out = format!("{ONT_BASE}{kind}");
for s in segments {
out.push('/');
out.push_str(&percent_encode(s));
}
out
}
#[must_use]
pub fn ont(name: &str) -> String {
format!("{ONT_BASE}{name}")
}
fn percent_encode(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for b in s.bytes() {
match b {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' | b':' => {
out.push(b as char)
}
_ => out.push_str(&format!("%{b:02X}")),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serialization_is_byte_ordered_and_insertion_independent() {
let mut a = Graph::new();
a.insert(iri("contract", "z"), RDF_TYPE, Term::iri(ont("Contract")));
a.insert(iri("contract", "a"), ont("id"), Term::string("a"));
let mut b = Graph::new();
b.insert(iri("contract", "a"), ont("id"), Term::string("a"));
b.insert(iri("contract", "z"), RDF_TYPE, Term::iri(ont("Contract")));
assert_eq!(a.to_ntriples(), b.to_ntriples());
assert!(a
.to_ntriples()
.starts_with("<https://ont.paiml.dev/v1alpha1/contract/a>"));
assert!(a.to_ntriples().ends_with(" .\n"));
}
#[test]
fn there_is_no_blank_node_in_the_output() {
let mut g = Graph::new();
g.insert(iri("contract", "x"), ont("name"), Term::string("x"));
assert!(!g.to_ntriples().contains("_:"));
}
#[test]
fn literals_are_escaped_and_typed() {
let t = Term::string("say \"hi\"\n\\");
assert_eq!(
t.to_string(),
"\"say \\\"hi\\\"\\n\\\\\"^^<http://www.w3.org/2001/XMLSchema#string>"
);
assert_eq!(
Term::integer(7).to_string(),
"\"7\"^^<http://www.w3.org/2001/XMLSchema#integer>"
);
}
#[test]
fn ids_are_percent_encoded_in_the_iri() {
assert_eq!(
iri("contract", "a b/c"),
"https://ont.paiml.dev/v1alpha1/contract/a%20b%2Fc"
);
assert_eq!(
iri("symbol", "crate::f"),
"https://ont.paiml.dev/v1alpha1/symbol/crate::f"
);
}
#[test]
fn instances_and_objects_read_back() {
let mut g = Graph::new();
let c = iri("contract", "c");
g.insert(c.clone(), RDF_TYPE, Term::iri(ont("Contract")));
g.insert(
c.clone(),
ont("depends_on"),
Term::iri(iri("contract", "d")),
);
assert_eq!(g.instances_of(&ont("Contract")), vec![c.as_str()]);
assert_eq!(g.objects(&c, &ont("depends_on")).len(), 1);
assert_eq!(g.predicates_of(&c).len(), 2);
}
}