| name | rdf-wrapper |
| description | Traverse sparq RDF graphs as native Rust objects with the opt-in sparq-wrapper crate: bind a focus Term to an owned or borrowed Store, follow outgoing/incoming NamedNode predicates with iterators, unwrap values, convert typed literals to str/i64/bool, mutate owned stores, and optionally use the unlanded distinct-result, typed-cardinality, literal-codec, typed-focus, and effective-change observation proposals. Use when Rust code should work with focus objects instead of raw triples or dictionary IDs; SHACL-to-Rust code generation is a later surface. |
Use sparq-wrapper
Add the opt-in crate explicitly:
[dependencies]
sparq-core = "0.1"
sparq-wrapper = "0.1"
oxrdf = "0.3"
Load a graph, borrow it, and traverse with typed predicates:
use oxrdf::NamedNode;
use sparq_core::Graph;
use sparq_wrapper::Store;
let graph = Graph::load_str(
"@prefix ex: <http://example.org/> . ex:alice ex:knows ex:bob . ex:bob ex:age 42 .",
"turtle",
)?;
let store = Store::borrowed(&graph);
let alice = NamedNode::new("http://example.org/alice")?;
let knows = NamedNode::new("http://example.org/knows")?;
let age = NamedNode::new("http://example.org/age")?;
let bob = store.node(alice).out(&knows).next().expect("friend");
assert_eq!(bob.out(&age).next().expect("age").as_i64()?, 42);
# Ok::<(), Box<dyn std::error::Error>>(())
.out() and .r#in() return NodeSet, an ExactSizeIterator<Item = Node>;
the raw identifier is Rust's required spelling for a method named in.
Call .values() on a traversal to yield owned oxrdf::Terms. An absent focus
or predicate is valid and yields an empty iterator. Node::dataset() exposes a
borrowed dataset wrapper; .graph() is the raw sparq_core::Graph escape hatch.
Choose ownership deliberately:
Store::borrowed(&graph) is read-only and tied to the graph's lifetime.
Store::owned(graph) and Store::new() own the graph and allow
insert/remove. Nodes borrow the store, so stop using them before a write
and reacquire them afterwards.
- Traversal addresses the default graph in M1. Reach named graphs through the
raw graph until a scoped-dataset surface lands.
Typed accessors are strict:
as_str() accepts xsd:string and rdf:langString.
as_i64() accepts the XML Schema integer family, enforces every derived
datatype's exact bounds (byte through unsignedLong), then checks that the
value is representable as i64.
as_bool() accepts only xsd:boolean, including true/false/1/0.
as_typed_literal() returns lexical form, datatype, and language.
All return Result<_, AccessError>; do not silently coerce a mismatched RDF
datatype.
Six explicitly experimental features implement proposals that remain unlanded
in rdfjs/wrapper:
sparq-wrapper = { version = "0.1", features = [
"proposed-distinct",
"proposed-cardinality",
"proposed-codecs",
"proposed-graph-scope",
"proposed-observe",
"proposed-typed-focus",
] }
proposed-graph-scope adds a read-many/write-one GraphScope based on
rdfjs/wrapper draft PR #95. Its reads are the deduplicated projection of
exactly the named graphs supplied to GraphScope::new; call
with_default_graph() to include the default graph explicitly. Scoped nodes
retain the projection for chained out/in traversal, while node- or
scope-level insert/remove operations affect only the configured named write
graph and leave copies elsewhere untouched.
use oxrdf::{Literal, NamedNode, Term};
use sparq_core::Graph;
use sparq_wrapper::proposed::graph_scope::GraphScope;
let mut graph = Graph::load_dataset(
"<http://example.org/alice> <http://example.org/tag> \"rdf\" <http://example.org/g1> .\n\
<http://example.org/alice> <http://example.org/tag> \"rdf\" <http://example.org/g2> .",
"nquads",
)?;
let alice = NamedNode::new("http://example.org/alice")?;
let tag = NamedNode::new("http://example.org/tag")?;
let g1 = Term::NamedNode(NamedNode::new("http://example.org/g1")?);
let g2 = Term::NamedNode(NamedNode::new("http://example.org/g2")?);
let scope = GraphScope::new(&mut graph, [g1.clone(), g2], g1);
let alice = scope.node(alice);
assert_eq!(alice.out(&tag).len(), 1);
alice.insert(tag, Literal::new_simple_literal("rust"))?;
# Ok::<(), Box<dyn std::error::Error>>(())
proposed-distinct adds Dataset::subjects_of / objects_of and yields each
term once (issue #25,
draft PR #88).
proposed-cardinality adds Node::required_out / optional_out and typed
CardinalityError data (draft PR #89).
Its sparq_wrapper::proposed::cardinality module also adds mapped
required, optional, and many reads plus the live_mapped write-through
collection (issue #8,
draft PR #92).
Use the mapped reads when a property has an explicit RDF cardinality. The
required and optional variants wrap M1 CardinalityError data in
CardinalityViewError; many returns a Vec, preserving every distinct RDF
term even when two terms map to equal Rust values. A mapper error is returned
without changing the store.
use oxrdf::{Literal, NamedNode, Term};
use sparq_wrapper::proposed::cardinality::{live_mapped, required};
use sparq_wrapper::Store;
let mut store = Store::new();
let alice = NamedNode::new("http://example.org/alice")?;
let name = NamedNode::new("http://example.org/name")?;
let tag = NamedNode::new("http://example.org/tag")?;
store.insert(
alice.clone(),
name.clone(),
Literal::new_simple_literal("Alice"),
)?;
let display_name = required(&store.node(alice.clone()), &name, |node| {
node.as_str().map(str::to_owned)
})?;
assert_eq!(display_name, "Alice");
{
let mut tags = live_mapped(
&mut store,
alice.clone(),
tag,
|node| node.as_str().map(str::to_owned),
|value: &String| Ok::<Term, std::convert::Infallible>(
Literal::new_simple_literal(value).into(),
),
);
assert!(tags.insert(&"rdf".to_owned())?);
assert_eq!(tags.values()?, vec!["rdf"]);
assert!(tags.remove(&"rdf".to_owned())?);
assert!(tags.is_empty());
}
# Ok::<(), Box<dyn std::error::Error>>(())
LiveMappedCollection holds a mutable store borrow for its lifetime. Its
values, len, is_empty, and contains methods query current triples;
insert, remove, and clear write through. insert and remove return
true only for an effective graph change. Encoding runs before each mutation,
so an encoder error leaves all existing triples intact.
proposed-observe exposes a self-contained proposed::observe::ObservableStore
for the effective-change subscription proposals in rdfjs/wrapper draft PRs #93
and #94. Dataset callbacks receive a typed ChangeEvent; LiveValues::subscribe
filters by subject and predicate and maps the changed RDF object into an
application ValueChange<T>. Duplicate adds and absent deletes stay silent,
and callbacks receive the committed graph only after the mutable graph borrow
has ended.
use oxrdf::{Literal, NamedNode};
use sparq_wrapper::proposed::observe::{ChangeKind, ObservableStore};
let mut store = ObservableStore::new();
let alice = NamedNode::new("http://example.org/alice")?;
let tag = NamedNode::new("http://example.org/tag")?;
let subscription = store.subscribe(|event, committed| {
assert!(matches!(event.kind, ChangeKind::Add | ChangeKind::Delete));
let _committed_triple_count = committed.len();
});
let mut tags = store.live_values(alice, tag);
assert!(tags.insert(Literal::new_simple_literal("rdf"))?);
assert!(!tags.insert(Literal::new_simple_literal("rdf"))?);
drop(tags);
assert!(store.unsubscribe(subscription));
# Ok::<(), Box<dyn std::error::Error>>(())
proposed-codecs exposes symmetric literal mappings in
sparq_wrapper::proposed::codecs (issue #7,
draft PR #90,
draft PR #91). encode_i128 and
decode_i128 round-trip the full Rust i128 range as exact xsd:integer
literals. The decoder accepts only that exact datatype and returns
CodecError::InvalidInteger for malformed or out-of-range lexical forms.
encode_lang_string validates a BCP47 language tag and produces an
rdf:langString; decode_lang_string returns an owned LangString containing
both value and language, so a round trip cannot discard the tag. Datatype,
integer, language-tag, and missing-language failures are represented by the
typed CodecError variants.
use oxrdf::Literal;
use sparq_wrapper::proposed::codecs::{
decode_i128, decode_lang_string, encode_i128, encode_lang_string, LangString,
};
let large = i128::from(i64::MAX) + 1;
let integer_literal = encode_i128(large);
assert_eq!(decode_i128(&integer_literal)?, large);
let label_literal = encode_lang_string("Y llyfrgellydd", "cy")?;
assert_eq!(
decode_lang_string(&label_literal)?,
LangString {
value: "Y llyfrgellydd".to_owned(),
language: "cy".to_owned(),
},
);
let plain = Literal::new_simple_literal("not language-tagged");
assert!(decode_lang_string(&plain).is_err());
# Ok::<(), Box<dyn std::error::Error>>(())
proposed-typed-focus adds the sparq_wrapper::proposed::typed_focus module.
Its NodeFactory binds one borrowed graph, store, or dataset view and can wrap
many terms without cloning the graph. Kind-specific constructors return a
TypedNode whose available traversals reflect the term's legal positions;
NodeFactory::term instead returns AnyNode, whose enum variant preserves the
concrete focus kind at run time.
use oxrdf::{Literal, NamedNode, Term};
use sparq_core::Graph;
use sparq_wrapper::proposed::typed_focus::{AnyNode, NodeFactory};
let mut graph = Graph::new();
let alice = NamedNode::new("http://example.org/alice")?;
let name = NamedNode::new("http://example.org/name")?;
graph.insert_triple(
alice.clone(),
name.clone(),
Literal::new_simple_literal("Alice"),
)?;
let factory = NodeFactory::new(&graph);
let subject = factory.iri(alice);
assert_eq!(subject.out(&name).len(), 1);
match factory.term(Term::Literal(Literal::new_simple_literal("Alice"))) {
AnyNode::Literal(value) => assert_eq!(value.r#in(&name).len(), 1),
_ => unreachable!("the factory preserves the concrete term kind"),
}
# Ok::<(), Box<dyn std::error::Error>>(())
Every typed focus supports incoming traversal because every RDF term may be an
object. Outgoing traversal is available only for SubjectFocus kinds, so code
such as factory.literal(value).out(&predicate) fails to compile. The
predicate-wide subjects() / objects() helpers are available only on the IRI
focus returned by NodeFactory::iri. Match an AnyNode variant to recover
those kind-specific methods, or call into_node() to erase the focus kind and
return to the untyped wrapper.
SHACL-to-Rust struct generation is not part of M1. Reuse sparq-shacl's
ShapesModel for that work; do not invent a second SHACL parser.