Expand description
The permissive RDF 1.2 ingestion protocol (purrdf P6): the neutral event seam that an RDF source (a parser, a GTS reader, a frozen-dataset replayer) uses to push a dataset into a sink (an IR builder, a serializer) WITHOUT either side knowing the other’s concrete types.
This crate has zero dependencies on purpose. It is the contract that both the
IR engine (purrdf-core) and the GTS container (purrdf-gts) depend ON — so a
pure parse→serialize path that touches only these traits stays under the
workspace MIT OR Apache-2.0 license. Its value types (EventTerm,
EventQuad, …) are therefore self-contained:
it carries its OWN EventTermId term ids, not the engine’s dataset-local
TermId.
§The dual
RdfEventSink is the INGESTION direction: an external, possibly out-of-order
event stream folds into something. Its dual lives in purrdf-core as
RdfDatasetVisitor — the infallible OUTPUT visitor that walks an already-frozen
dataset out as events. Ingestion is fallible (forward references, cancellation,
and unresolved-at-finish are all real outcomes); the output visitor is not.
§Protocol semantics
These are the rules a RdfEventSink implementer MUST honor and an
RdfEventSource MAY rely on:
- Forward references are allowed. A
quad/reifier/annotationMAY reference anEventTermIdwhosetermdeclaration has not yet arrived. References are resolved atfinish, never eagerly. A source that happens to declare every term before referencing it advertises this viaRdfEventSource::declares_before_reference. - At most one declaration per id per drive. An
EventTermIdis unique across the WHOLE drive: declaring the same id twice anywhere (in any scope, open or default) is anEventError::RedeclaredId— there is no last-writer-wins. EventTermIdis drive-global;ScopeIdnamespaces blank-node labels only. The id space is global to one ingestion drive — every reference (EventQuad,EventTriple,reifier,annotation) carries a bareEventTermIdthat resolves against that single global space, so a buffered row never needs a scope to disambiguate which declaration it names. AScopeIdscopes blank-node label identity ONLY: the same blank label in two different scopes names two different nodes (mirroring per-segment blank scope in GTS), yet each still gets its own globally-uniqueEventTermId.close_scopeseals a scope so no NEW blank may be declared under it; already-declared ids stay referenceable by their globalEventTermId. Declaring a blank under a sealed or never-opened scope isEventError::ClosedScope.- Unresolved at finish is a hard error. Any
EventTermIdstill undeclared whenfinishruns isEventError::Unresolved— never a silent drop or degraded fallback (no-optionality doctrine). - Cancellation must not freeze. A sink MAY return
ControlFlow::Breakfrom any event to cancel the drive; the source stops immediately and the sink’s partial state MUST NOT be frozen into a result. - Triple-term nesting is depth-bounded. Reified
EventTripleterms may nest, but a sink MUST bound resolution depth byMAX_TERM_NESTING_DEPTH(16), hard-failing withEventError::NestingDepthExceededrather than recursing without bound. - Ill-typed literals are preserved, never auto-rejected. A
EventTerm::Literalwith a malformed lexical form for its datatype is carried through and MAY be flagged downstream; the protocol never rejects it at ingestion.
§Examples
A tiny source driving one triple into a counting sink — neither side knows the other’s concrete type. Note the forward reference: the quad arrives BEFORE the term declarations it names, which the protocol explicitly allows.
use core::ops::ControlFlow;
use purrdf_events::{
EventError, EventQuad, EventTerm, EventTermId, EventTriple, RdfEventSink,
RdfEventSource, ScopeId,
};
/// A sink that just counts what it receives.
#[derive(Default)]
struct CountingSink {
terms: usize,
quads: usize,
finished: bool,
}
impl RdfEventSink for CountingSink {
fn term(
&mut self,
_id: EventTermId,
_term: EventTerm<'_>,
) -> Result<ControlFlow<()>, EventError> {
self.terms += 1;
Ok(ControlFlow::Continue(()))
}
fn quad(&mut self, _q: EventQuad) -> Result<ControlFlow<()>, EventError> {
self.quads += 1;
Ok(ControlFlow::Continue(()))
}
fn reifier(
&mut self,
_reifier: EventTermId,
_triple: EventTriple,
) -> Result<ControlFlow<()>, EventError> {
Ok(ControlFlow::Continue(()))
}
fn annotation(
&mut self,
_reifier: EventTermId,
_p: EventTermId,
_o: EventTermId,
) -> Result<ControlFlow<()>, EventError> {
Ok(ControlFlow::Continue(()))
}
fn open_scope(&mut self) -> Result<ScopeId, EventError> {
Ok(ScopeId::DEFAULT)
}
fn close_scope(&mut self, _scope: ScopeId) -> Result<ControlFlow<()>, EventError> {
Ok(ControlFlow::Continue(()))
}
fn finish(&mut self) -> Result<(), EventError> {
self.finished = true;
Ok(())
}
}
/// A source replaying one hard-coded triple.
struct OneTriple;
impl RdfEventSource for OneTriple {
fn drive<S: RdfEventSink + ?Sized>(&self, sink: &mut S) -> Result<(), EventError> {
let (s, p, o) = (EventTermId(0), EventTermId(1), EventTermId(2));
// Forward reference: the quad may precede its term declarations.
if sink.quad(EventQuad { s, p, o, g: None })? == ControlFlow::Break(()) {
return Ok(());
}
sink.term(s, EventTerm::Iri("http://example.org/alice"))?;
sink.term(p, EventTerm::Iri("http://example.org/knows"))?;
sink.term(o, EventTerm::Iri("http://example.org/bob"))?;
sink.finish()
}
}
let mut sink = CountingSink::default();
OneTriple.drive(&mut sink)?;
assert_eq!(sink.terms, 3);
assert_eq!(sink.quads, 1);
assert!(sink.finished);Structs§
- Event
Quad - One quad row: an (s, p, o) statement plus an optional graph name.
g == Nonenames the default graph. - Event
Term Id - A protocol-local, drive-global term id. Ids are minted by the source and are
meaningful within one ingestion drive, where they form a SINGLE global id space:
every reference carries a bare
EventTermIdthat resolves against that one space regardless of scope (aScopeIdnamespaces blank-node labels only, never the id space). Ids MAY be forward-referenced (used by a quad before theirtermdeclaration arrives) and are resolved to the sink’s own identity atfinish. Each id may be declared at most once per drive — redeclaring it anywhere isEventError::RedeclaredId. - Event
Triple - A reified statement — a triple (s, p, o) of
EventTermIds, NOT a quad. - ScopeId
- A blank-node label namespace, local to one ingestion drive. A
ScopeIddoes NOT scopeEventTermIds (those are drive-global); it scopes blank-node label identity ONLY, so the same blank label in different scopes names different nodes. The default scope isScopeId::DEFAULT; a source opens further scopes (e.g. one per GTS segment) viaRdfEventSink::open_scopeand seals them withRdfEventSink::close_scope(after which no new blank may be declared under that scope, though already-declared ids remain referenceable globally). - Source
Span - A droppable diagnostic source-location hint. A sink MAY ignore it entirely; it exists only to carry parser positions through to downstream diagnostics.
Enums§
- Event
Error - The concrete ingestion error type. Deliberately not generic: the seam stays object-safe and the error space is fixed by the protocol (see the module docs).
- Event
Term - The value of one declared term, borrowed for the duration of the
termcall. Self-contained: anEventTerm::Literalcarries its datatype as a borrowed IRI string (by value), never an id. - Text
Direction - RDF 1.2 base direction for directional language-tagged literals. Mirrors the IR
engine’s
RdfTextDirectionby value so this crate stays dependency-free.
Constants§
- MAX_
TERM_ NESTING_ DEPTH - Depth bound for resolving nested reified-triple terms, mirroring
MAX_GTS_TERM_NESTING_DEPTHin the IR engine. A cyclic or absurdly nested triple term hard-fails (EventError::NestingDepthExceeded) rather than recursing without bound.
Traits§
- RdfEvent
Sink - The permissive ingestion sink: a receiver of an RDF 1.2 event stream that an
arbitrary
RdfEventSourcedrives into it. - RdfEvent
Source - The permissive ingestion source: something that can drive an RDF 1.2 event
stream into any
RdfEventSink.