Skip to main content

Crate purrdf_events

Crate purrdf_events 

Source
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 / annotation MAY reference an EventTermId whose term declaration has not yet arrived. References are resolved at finish, never eagerly. A source that happens to declare every term before referencing it advertises this via RdfEventSource::declares_before_reference.
  • At most one declaration per id per drive. An EventTermId is unique across the WHOLE drive: declaring the same id twice anywhere (in any scope, open or default) is an EventError::RedeclaredId — there is no last-writer-wins.
  • EventTermId is drive-global; ScopeId namespaces blank-node labels only. The id space is global to one ingestion drive — every reference (EventQuad, EventTriple, reifier, annotation) carries a bare EventTermId that resolves against that single global space, so a buffered row never needs a scope to disambiguate which declaration it names. A ScopeId scopes 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-unique EventTermId. close_scope seals a scope so no NEW blank may be declared under it; already-declared ids stay referenceable by their global EventTermId. Declaring a blank under a sealed or never-opened scope is EventError::ClosedScope.
  • Unresolved at finish is a hard error. Any EventTermId still undeclared when finish runs is EventError::Unresolved — never a silent drop or degraded fallback (no-optionality doctrine).
  • Cancellation must not freeze. A sink MAY return ControlFlow::Break from 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 EventTriple terms may nest, but a sink MUST bound resolution depth by MAX_TERM_NESTING_DEPTH (16), hard-failing with EventError::NestingDepthExceeded rather than recursing without bound.
  • Ill-typed literals are preserved, never auto-rejected. A EventTerm::Literal with 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§

EventQuad
One quad row: an (s, p, o) statement plus an optional graph name. g == None names the default graph.
EventTermId
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 EventTermId that resolves against that one space regardless of scope (a ScopeId namespaces blank-node labels only, never the id space). Ids MAY be forward-referenced (used by a quad before their term declaration arrives) and are resolved to the sink’s own identity at finish. Each id may be declared at most once per drive — redeclaring it anywhere is EventError::RedeclaredId.
EventTriple
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 ScopeId does NOT scope EventTermIds (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 is ScopeId::DEFAULT; a source opens further scopes (e.g. one per GTS segment) via RdfEventSink::open_scope and seals them with RdfEventSink::close_scope (after which no new blank may be declared under that scope, though already-declared ids remain referenceable globally).
SourceSpan
A droppable diagnostic source-location hint. A sink MAY ignore it entirely; it exists only to carry parser positions through to downstream diagnostics.

Enums§

EventError
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).
EventTerm
The value of one declared term, borrowed for the duration of the term call. Self-contained: an EventTerm::Literal carries its datatype as a borrowed IRI string (by value), never an id.
TextDirection
RDF 1.2 base direction for directional language-tagged literals. Mirrors the IR engine’s RdfTextDirection by 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_DEPTH in the IR engine. A cyclic or absurdly nested triple term hard-fails (EventError::NestingDepthExceeded) rather than recursing without bound.

Traits§

RdfEventSink
The permissive ingestion sink: a receiver of an RDF 1.2 event stream that an arbitrary RdfEventSource drives into it.
RdfEventSource
The permissive ingestion source: something that can drive an RDF 1.2 event stream into any RdfEventSink.