use std::collections::HashSet;
use oxrdf::dataset::CanonicalizationAlgorithm;
use oxrdf::vocab::xsd;
use oxrdf::{
Dataset, Graph, GraphName, GraphNameRef, Literal, NamedNode, NamedOrBlankNode, Quad, Term,
Triple,
};
use crate::error::{AppendError, StoreError};
use crate::vocab;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Position(u64);
impl Position {
pub const ZERO: Position = Position(0);
pub const fn new(n: u64) -> Self {
Position(n)
}
pub const fn get(self) -> u64 {
self.0
}
pub const fn next(self) -> Self {
Position(self.0 + 1)
}
pub const fn advance(self, n: u64) -> Self {
Position(self.0 + n)
}
}
impl From<u64> for Position {
fn from(n: u64) -> Self {
Position(n)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Entry {
name: NamedNode,
triples: Graph,
}
impl Entry {
pub fn new(name: NamedNode, triples: impl IntoIterator<Item = Triple>) -> Self {
Entry {
name,
triples: triples.into_iter().collect(),
}
}
pub fn from_dataset(dataset: &Dataset) -> Result<Self, StoreError> {
let mut name: Option<NamedNode> = None;
let mut triples = Graph::new();
for q in dataset {
let g = match q.graph_name {
GraphNameRef::NamedNode(n) => n.into_owned(),
GraphNameRef::BlankNode(_) => {
return Err(StoreError::invalid(
"an entry graph must be named by an IRI",
));
}
GraphNameRef::DefaultGraph => {
return Err(StoreError::invalid(
"an entry has no statements in the default graph",
));
}
};
match &name {
None => name = Some(g),
Some(n) if *n != g => {
return Err(StoreError::invalid(
"an entry is one named graph, found several",
));
}
Some(_) => {}
}
triples.insert(&Triple::new(
q.subject.into_owned(),
q.predicate.into_owned(),
q.object.into_owned(),
));
}
let name = name.ok_or_else(|| StoreError::invalid("an entry has at least one quad"))?;
Ok(Entry { name, triples })
}
pub fn name(&self) -> &NamedNode {
&self.name
}
pub fn triples(&self) -> &Graph {
&self.triples
}
pub fn quads(&self) -> Vec<Quad> {
let g = GraphName::NamedNode(self.name.clone());
self.triples
.iter()
.map(|t| t.into_owned().in_graph(g.clone()))
.collect()
}
pub fn to_dataset(&self) -> Dataset {
self.quads().into_iter().collect()
}
pub fn position(&self) -> Option<Position> {
let p = vocab::node(vocab::POSITION);
self.triples
.objects_for_subject_predicate(&self.name, &p)
.find_map(|o| match o {
oxrdf::TermRef::Literal(l) if l.datatype() == xsd::INTEGER => {
l.value().parse::<u64>().ok().map(Position)
}
_ => None,
})
}
pub fn stamp(&self, actor: &NamedNode, position: Position) -> Result<Entry, StoreError> {
let subject = NamedOrBlankNode::from(self.name.clone());
let actor_p = vocab::node(vocab::ACTOR);
let pos_p = vocab::node(vocab::POSITION);
let pos = Literal::new_typed_literal(position.get().to_string(), xsd::INTEGER);
for o in self
.triples
.objects_for_subject_predicate(&self.name, &actor_p)
{
if o != actor.as_ref().into() {
return Err(StoreError::invalid(format!(
"entry {} states actor {o}, appended to {actor}",
self.name
)));
}
}
for o in self
.triples
.objects_for_subject_predicate(&self.name, &pos_p)
{
if o != pos.as_ref().into() {
return Err(StoreError::invalid(format!(
"entry {} states position {o}, appended at {}",
self.name,
position.get()
)));
}
}
let mut triples = self.triples.clone();
triples.insert(&Triple::new(
subject.clone(),
vocab::node(vocab::RDF_TYPE),
vocab::node(vocab::JOURNAL_ENTRY),
));
triples.insert(&Triple::new(subject.clone(), actor_p, actor.clone()));
triples.insert(&Triple::new(subject, pos_p, pos));
Ok(Entry {
name: self.name.clone(),
triples,
})
}
pub fn is_isomorphic(&self, other: &Entry) -> bool {
if self.name != other.name || self.triples.len() != other.triples.len() {
return false;
}
let mut a = self.triples.clone();
let mut b = other.triples.clone();
a.canonicalize(CanonicalizationAlgorithm::default());
b.canonicalize(CanonicalizationAlgorithm::default());
a == b
}
pub fn has_blank_nodes(&self) -> bool {
self.triples.iter().any(|t| {
matches!(t.subject, oxrdf::NamedOrBlankNodeRef::BlankNode(_))
|| matches!(t.object, oxrdf::TermRef::BlankNode(_))
})
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct JournalRecord {
pub position: Position,
pub entry: Entry,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct FencingToken {
pub scope: NamedNode,
pub epoch: u64,
}
impl FencingToken {
pub fn new(scope: NamedNode, epoch: u64) -> Self {
FencingToken { scope, epoch }
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct StateDelta {
pub retract: Vec<Triple>,
pub assert: Vec<Triple>,
}
impl StateDelta {
pub fn is_empty(&self) -> bool {
self.retract.is_empty() && self.assert.is_empty()
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct AppendMeta {
pub fencing: Option<FencingToken>,
pub state: Option<StateDelta>,
}
impl AppendMeta {
pub fn none() -> Self {
AppendMeta::default()
}
pub fn with_fencing(mut self, token: FencingToken) -> Self {
self.fencing = Some(token);
self
}
pub fn with_state(mut self, delta: StateDelta) -> Self {
self.state = Some(delta);
self
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct JournalCapabilities {
pub fencing: bool,
pub state_in_append: bool,
}
pub trait JournalStore: Send + Sync {
fn capabilities(&self) -> JournalCapabilities;
fn supports_fencing(&self) -> bool {
self.capabilities().fencing
}
fn append(
&self,
actor: &NamedNode,
expected: Position,
entries: &[Entry],
meta: &AppendMeta,
) -> Result<Position, AppendError>;
fn read(&self, actor: &NamedNode, from: Position) -> Result<Vec<JournalRecord>, StoreError>;
fn head(&self, actor: &NamedNode) -> Result<Position, StoreError>;
fn actors(&self) -> Result<Vec<NamedNode>, StoreError>;
}
impl<J: JournalStore + ?Sized> JournalStore for std::sync::Arc<J> {
fn capabilities(&self) -> JournalCapabilities {
(**self).capabilities()
}
fn append(
&self,
actor: &NamedNode,
expected: Position,
entries: &[Entry],
meta: &AppendMeta,
) -> Result<Position, AppendError> {
(**self).append(actor, expected, entries, meta)
}
fn read(&self, actor: &NamedNode, from: Position) -> Result<Vec<JournalRecord>, StoreError> {
(**self).read(actor, from)
}
fn head(&self, actor: &NamedNode) -> Result<Position, StoreError> {
(**self).head(actor)
}
fn actors(&self) -> Result<Vec<NamedNode>, StoreError> {
(**self).actors()
}
}
pub fn prepare_append(
actor: &NamedNode,
expected: Position,
entries: &[Entry],
meta: &AppendMeta,
caps: JournalCapabilities,
) -> Result<Vec<Entry>, AppendError> {
if entries.is_empty() {
return Err(AppendError::Invalid(
"an append has at least one entry".into(),
));
}
if meta.fencing.is_some() && !caps.fencing {
return Err(AppendError::Unsupported(
"this journal store does not enforce fencing tokens".into(),
));
}
if let Some(delta) = &meta.state {
if !caps.state_in_append {
return Err(AppendError::Unsupported(
"this journal store does not commit state changes with entries".into(),
));
}
let blank = delta.retract.iter().chain(&delta.assert).any(|t| {
matches!(t.subject, NamedOrBlankNode::BlankNode(_))
|| matches!(t.object, Term::BlankNode(_))
});
if blank {
return Err(AppendError::Invalid(
"current-state statements cannot contain blank nodes".into(),
));
}
}
let mut seen = HashSet::new();
let mut out = Vec::with_capacity(entries.len());
for (i, e) in entries.iter().enumerate() {
if e.name() == actor {
return Err(AppendError::Invalid(format!(
"entry {} is named like its actor",
e.name()
)));
}
if !seen.insert(e.name().clone()) {
return Err(AppendError::Invalid(format!(
"entry {} appears twice in one append",
e.name()
)));
}
out.push(e.stamp(actor, expected.advance(i as u64 + 1))?);
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
fn iri(s: &str) -> NamedNode {
NamedNode::new(format!("urn:test:{s}")).unwrap()
}
#[test]
fn dataset_round_trip_and_shape_checks() {
let e = Entry::new(iri("e1"), [Triple::new(iri("a"), iri("p"), iri("b"))]);
let ds = e.to_dataset();
assert_eq!(Entry::from_dataset(&ds).unwrap(), e);
let mut two = ds.clone();
two.insert(&Quad::new(iri("a"), iri("p"), iri("c"), iri("e2")));
assert!(Entry::from_dataset(&two).is_err());
let mut default = ds;
default.insert(&Quad::new(
iri("a"),
iri("p"),
iri("c"),
GraphName::DefaultGraph,
));
assert!(Entry::from_dataset(&default).is_err());
assert!(Entry::from_dataset(&Dataset::new()).is_err());
}
#[test]
fn stamp_adds_type_actor_and_position_and_rejects_contradictions() {
let e = Entry::new(iri("e1"), [Triple::new(iri("a"), iri("p"), iri("b"))]);
let s = e.stamp(&iri("actor"), Position::new(7)).unwrap();
assert_eq!(s.triples().len(), 4);
assert_eq!(s.position(), Some(Position::new(7)));
assert_eq!(s.stamp(&iri("actor"), Position::new(7)).unwrap(), s);
assert!(s.stamp(&iri("actor"), Position::new(8)).is_err());
assert!(s.stamp(&iri("other"), Position::new(7)).is_err());
}
#[test]
fn prepare_append_rejects_bad_requests() {
let caps = JournalCapabilities::default();
let a = iri("actor");
let e = Entry::new(iri("e1"), [Triple::new(iri("a"), iri("p"), iri("b"))]);
assert!(matches!(
prepare_append(&a, Position::ZERO, &[], &AppendMeta::none(), caps),
Err(AppendError::Invalid(_))
));
assert!(matches!(
prepare_append(
&a,
Position::ZERO,
&[e.clone(), e.clone()],
&AppendMeta::none(),
caps
),
Err(AppendError::Invalid(_))
));
let fenced = AppendMeta::none().with_fencing(FencingToken::new(iri("shard"), 1));
assert!(matches!(
prepare_append(&a, Position::ZERO, std::slice::from_ref(&e), &fenced, caps),
Err(AppendError::Unsupported(_))
));
let stamped = prepare_append(&a, Position::new(3), &[e], &AppendMeta::none(), caps)
.unwrap()
.remove(0);
assert_eq!(stamped.position(), Some(Position::new(4)));
}
}