1use std::collections::HashSet;
4
5use oxrdf::dataset::CanonicalizationAlgorithm;
6use oxrdf::vocab::xsd;
7use oxrdf::{
8 Dataset, Graph, GraphName, GraphNameRef, Literal, NamedNode, NamedOrBlankNode, Quad, Term,
9 Triple,
10};
11
12use crate::error::{AppendError, StoreError};
13use crate::vocab;
14
15#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
21pub struct Position(u64);
22
23impl Position {
24 pub const ZERO: Position = Position(0);
26
27 pub const fn new(n: u64) -> Self {
29 Position(n)
30 }
31
32 pub const fn get(self) -> u64 {
34 self.0
35 }
36
37 pub const fn next(self) -> Self {
39 Position(self.0 + 1)
40 }
41
42 pub const fn advance(self, n: u64) -> Self {
44 Position(self.0 + n)
45 }
46}
47
48impl From<u64> for Position {
49 fn from(n: u64) -> Self {
50 Position(n)
51 }
52}
53
54#[derive(Clone, Debug, PartialEq, Eq)]
61pub struct Entry {
62 name: NamedNode,
63 triples: Graph,
64}
65
66impl Entry {
67 pub fn new(name: NamedNode, triples: impl IntoIterator<Item = Triple>) -> Self {
69 Entry {
70 name,
71 triples: triples.into_iter().collect(),
72 }
73 }
74
75 pub fn from_dataset(dataset: &Dataset) -> Result<Self, StoreError> {
82 let mut name: Option<NamedNode> = None;
83 let mut triples = Graph::new();
84 for q in dataset {
85 let g = match q.graph_name {
86 GraphNameRef::NamedNode(n) => n.into_owned(),
87 GraphNameRef::BlankNode(_) => {
88 return Err(StoreError::invalid(
89 "an entry graph must be named by an IRI",
90 ));
91 }
92 GraphNameRef::DefaultGraph => {
93 return Err(StoreError::invalid(
94 "an entry has no statements in the default graph",
95 ));
96 }
97 };
98 match &name {
99 None => name = Some(g),
100 Some(n) if *n != g => {
101 return Err(StoreError::invalid(
102 "an entry is one named graph, found several",
103 ));
104 }
105 Some(_) => {}
106 }
107 triples.insert(&Triple::new(
108 q.subject.into_owned(),
109 q.predicate.into_owned(),
110 q.object.into_owned(),
111 ));
112 }
113 let name = name.ok_or_else(|| StoreError::invalid("an entry has at least one quad"))?;
114 Ok(Entry { name, triples })
115 }
116
117 pub fn name(&self) -> &NamedNode {
119 &self.name
120 }
121
122 pub fn triples(&self) -> &Graph {
124 &self.triples
125 }
126
127 pub fn quads(&self) -> Vec<Quad> {
129 let g = GraphName::NamedNode(self.name.clone());
130 self.triples
131 .iter()
132 .map(|t| t.into_owned().in_graph(g.clone()))
133 .collect()
134 }
135
136 pub fn to_dataset(&self) -> Dataset {
138 self.quads().into_iter().collect()
139 }
140
141 pub fn position(&self) -> Option<Position> {
143 let p = vocab::node(vocab::POSITION);
144 self.triples
145 .objects_for_subject_predicate(&self.name, &p)
146 .find_map(|o| match o {
147 oxrdf::TermRef::Literal(l) if l.datatype() == xsd::INTEGER => {
148 l.value().parse::<u64>().ok().map(Position)
149 }
150 _ => None,
151 })
152 }
153
154 pub fn stamp(&self, actor: &NamedNode, position: Position) -> Result<Entry, StoreError> {
161 let subject = NamedOrBlankNode::from(self.name.clone());
162 let actor_p = vocab::node(vocab::ACTOR);
163 let pos_p = vocab::node(vocab::POSITION);
164 let pos = Literal::new_typed_literal(position.get().to_string(), xsd::INTEGER);
165 for o in self
166 .triples
167 .objects_for_subject_predicate(&self.name, &actor_p)
168 {
169 if o != actor.as_ref().into() {
170 return Err(StoreError::invalid(format!(
171 "entry {} states actor {o}, appended to {actor}",
172 self.name
173 )));
174 }
175 }
176 for o in self
177 .triples
178 .objects_for_subject_predicate(&self.name, &pos_p)
179 {
180 if o != pos.as_ref().into() {
181 return Err(StoreError::invalid(format!(
182 "entry {} states position {o}, appended at {}",
183 self.name,
184 position.get()
185 )));
186 }
187 }
188 let mut triples = self.triples.clone();
189 triples.insert(&Triple::new(
190 subject.clone(),
191 vocab::node(vocab::RDF_TYPE),
192 vocab::node(vocab::JOURNAL_ENTRY),
193 ));
194 triples.insert(&Triple::new(subject.clone(), actor_p, actor.clone()));
195 triples.insert(&Triple::new(subject, pos_p, pos));
196 Ok(Entry {
197 name: self.name.clone(),
198 triples,
199 })
200 }
201
202 pub fn is_isomorphic(&self, other: &Entry) -> bool {
205 if self.name != other.name || self.triples.len() != other.triples.len() {
206 return false;
207 }
208 let mut a = self.triples.clone();
209 let mut b = other.triples.clone();
210 a.canonicalize(CanonicalizationAlgorithm::default());
211 b.canonicalize(CanonicalizationAlgorithm::default());
212 a == b
213 }
214
215 pub fn has_blank_nodes(&self) -> bool {
217 self.triples.iter().any(|t| {
218 matches!(t.subject, oxrdf::NamedOrBlankNodeRef::BlankNode(_))
219 || matches!(t.object, oxrdf::TermRef::BlankNode(_))
220 })
221 }
222}
223
224#[derive(Clone, Debug, PartialEq, Eq)]
226pub struct JournalRecord {
227 pub position: Position,
229 pub entry: Entry,
231}
232
233#[derive(Clone, Debug, PartialEq, Eq, Hash)]
238pub struct FencingToken {
239 pub scope: NamedNode,
241 pub epoch: u64,
243}
244
245impl FencingToken {
246 pub fn new(scope: NamedNode, epoch: u64) -> Self {
248 FencingToken { scope, epoch }
249 }
250}
251
252#[derive(Clone, Debug, Default, PartialEq, Eq)]
259pub struct StateDelta {
260 pub retract: Vec<Triple>,
262 pub assert: Vec<Triple>,
264}
265
266impl StateDelta {
267 pub fn is_empty(&self) -> bool {
269 self.retract.is_empty() && self.assert.is_empty()
270 }
271}
272
273#[derive(Clone, Debug, Default, PartialEq, Eq)]
275pub struct AppendMeta {
276 pub fencing: Option<FencingToken>,
278 pub state: Option<StateDelta>,
280}
281
282impl AppendMeta {
283 pub fn none() -> Self {
285 AppendMeta::default()
286 }
287
288 pub fn with_fencing(mut self, token: FencingToken) -> Self {
290 self.fencing = Some(token);
291 self
292 }
293
294 pub fn with_state(mut self, delta: StateDelta) -> Self {
296 self.state = Some(delta);
297 self
298 }
299}
300
301#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
303pub struct JournalCapabilities {
304 pub fencing: bool,
307 pub state_in_append: bool,
309}
310
311pub trait JournalStore: Send + Sync {
316 fn capabilities(&self) -> JournalCapabilities;
318
319 fn supports_fencing(&self) -> bool {
321 self.capabilities().fencing
322 }
323
324 fn append(
337 &self,
338 actor: &NamedNode,
339 expected: Position,
340 entries: &[Entry],
341 meta: &AppendMeta,
342 ) -> Result<Position, AppendError>;
343
344 fn read(&self, actor: &NamedNode, from: Position) -> Result<Vec<JournalRecord>, StoreError>;
351
352 fn head(&self, actor: &NamedNode) -> Result<Position, StoreError>;
358
359 fn actors(&self) -> Result<Vec<NamedNode>, StoreError>;
365}
366
367impl<J: JournalStore + ?Sized> JournalStore for std::sync::Arc<J> {
368 fn capabilities(&self) -> JournalCapabilities {
369 (**self).capabilities()
370 }
371 fn append(
372 &self,
373 actor: &NamedNode,
374 expected: Position,
375 entries: &[Entry],
376 meta: &AppendMeta,
377 ) -> Result<Position, AppendError> {
378 (**self).append(actor, expected, entries, meta)
379 }
380 fn read(&self, actor: &NamedNode, from: Position) -> Result<Vec<JournalRecord>, StoreError> {
381 (**self).read(actor, from)
382 }
383 fn head(&self, actor: &NamedNode) -> Result<Position, StoreError> {
384 (**self).head(actor)
385 }
386 fn actors(&self) -> Result<Vec<NamedNode>, StoreError> {
387 (**self).actors()
388 }
389}
390
391pub fn prepare_append(
401 actor: &NamedNode,
402 expected: Position,
403 entries: &[Entry],
404 meta: &AppendMeta,
405 caps: JournalCapabilities,
406) -> Result<Vec<Entry>, AppendError> {
407 if entries.is_empty() {
408 return Err(AppendError::Invalid(
409 "an append has at least one entry".into(),
410 ));
411 }
412 if meta.fencing.is_some() && !caps.fencing {
413 return Err(AppendError::Unsupported(
414 "this journal store does not enforce fencing tokens".into(),
415 ));
416 }
417 if let Some(delta) = &meta.state {
418 if !caps.state_in_append {
419 return Err(AppendError::Unsupported(
420 "this journal store does not commit state changes with entries".into(),
421 ));
422 }
423 let blank = delta.retract.iter().chain(&delta.assert).any(|t| {
424 matches!(t.subject, NamedOrBlankNode::BlankNode(_))
425 || matches!(t.object, Term::BlankNode(_))
426 });
427 if blank {
428 return Err(AppendError::Invalid(
429 "current-state statements cannot contain blank nodes".into(),
430 ));
431 }
432 }
433 let mut seen = HashSet::new();
434 let mut out = Vec::with_capacity(entries.len());
435 for (i, e) in entries.iter().enumerate() {
436 if e.name() == actor {
437 return Err(AppendError::Invalid(format!(
438 "entry {} is named like its actor",
439 e.name()
440 )));
441 }
442 if !seen.insert(e.name().clone()) {
443 return Err(AppendError::Invalid(format!(
444 "entry {} appears twice in one append",
445 e.name()
446 )));
447 }
448 out.push(e.stamp(actor, expected.advance(i as u64 + 1))?);
449 }
450 Ok(out)
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456
457 fn iri(s: &str) -> NamedNode {
458 NamedNode::new(format!("urn:test:{s}")).unwrap()
459 }
460
461 #[test]
462 fn dataset_round_trip_and_shape_checks() {
463 let e = Entry::new(iri("e1"), [Triple::new(iri("a"), iri("p"), iri("b"))]);
464 let ds = e.to_dataset();
465 assert_eq!(Entry::from_dataset(&ds).unwrap(), e);
466
467 let mut two = ds.clone();
468 two.insert(&Quad::new(iri("a"), iri("p"), iri("c"), iri("e2")));
469 assert!(Entry::from_dataset(&two).is_err());
470 let mut default = ds;
471 default.insert(&Quad::new(
472 iri("a"),
473 iri("p"),
474 iri("c"),
475 GraphName::DefaultGraph,
476 ));
477 assert!(Entry::from_dataset(&default).is_err());
478 assert!(Entry::from_dataset(&Dataset::new()).is_err());
479 }
480
481 #[test]
482 fn stamp_adds_type_actor_and_position_and_rejects_contradictions() {
483 let e = Entry::new(iri("e1"), [Triple::new(iri("a"), iri("p"), iri("b"))]);
484 let s = e.stamp(&iri("actor"), Position::new(7)).unwrap();
485 assert_eq!(s.triples().len(), 4);
486 assert_eq!(s.position(), Some(Position::new(7)));
487 assert_eq!(s.stamp(&iri("actor"), Position::new(7)).unwrap(), s);
489 assert!(s.stamp(&iri("actor"), Position::new(8)).is_err());
490 assert!(s.stamp(&iri("other"), Position::new(7)).is_err());
491 }
492
493 #[test]
494 fn prepare_append_rejects_bad_requests() {
495 let caps = JournalCapabilities::default();
496 let a = iri("actor");
497 let e = Entry::new(iri("e1"), [Triple::new(iri("a"), iri("p"), iri("b"))]);
498 assert!(matches!(
499 prepare_append(&a, Position::ZERO, &[], &AppendMeta::none(), caps),
500 Err(AppendError::Invalid(_))
501 ));
502 assert!(matches!(
503 prepare_append(
504 &a,
505 Position::ZERO,
506 &[e.clone(), e.clone()],
507 &AppendMeta::none(),
508 caps
509 ),
510 Err(AppendError::Invalid(_))
511 ));
512 let fenced = AppendMeta::none().with_fencing(FencingToken::new(iri("shard"), 1));
513 assert!(matches!(
514 prepare_append(&a, Position::ZERO, std::slice::from_ref(&e), &fenced, caps),
515 Err(AppendError::Unsupported(_))
516 ));
517 let stamped = prepare_append(&a, Position::new(3), &[e], &AppendMeta::none(), caps)
518 .unwrap()
519 .remove(0);
520 assert_eq!(stamped.position(), Some(Position::new(4)));
521 }
522}