Skip to main content

ld_core/
lib.rs

1//! This library provides primitive traits to serialize and deserialize
2//! Linked-Data types. It is shipped with derive macros (using the `derive`
3//! feature) that can automatically implement those primitives for you.
4//!
5//! A value is described as a set of RDF quads: [`LinkedDataSubject`] says
6//! which properties a subject carries, [`LinkedDataPredicateObjects`] which
7//! objects a predicate points at, and [`LinkedDataGraph`] which graph they
8//! belong to. The derive macros implement all of them from `#[ld(...)]`
9//! attributes.
10//!
11//! # Example
12//!
13//! ```
14//! use iri_rs::IriBuf;
15//! use ld_core::rdfx::{RdfDisplay, generator};
16//! use ld_core::{Deserialize, Serialize, to_quads};
17//!
18//! #[derive(Serialize, Deserialize)]
19//! #[ld(prefix("ex" = "http://example.org/"))]
20//! struct Foo {
21//!     #[ld(id)]
22//!     id: IriBuf,
23//!
24//!     #[ld("ex:name")]
25//!     name: String,
26//!
27//!     #[ld("ex:email")]
28//!     email: String,
29//! }
30//!
31//! let value = Foo {
32//!     id: IriBuf::new("http://example.org/JohnSmith".to_owned()).unwrap(),
33//!     name: "John Smith".to_owned(),
34//!     email: "john.smith@example.org".to_owned(),
35//! };
36//!
37//! let quads = to_quads(generator::Blank::new(), &value).unwrap();
38//! let output: Vec<String> = quads.iter().map(|q| format!("{} .", q.rdf_display())).collect();
39//!
40//! assert!(output.contains(
41//!     &r#"<http://example.org/JohnSmith> <http://example.org/name> "John Smith" ."#.to_owned()
42//! ));
43//! ```
44//!
45//! # Feature flags
46//!
47//! - `derive` (default) — the [`Serialize`] and [`Deserialize`] derive macros.
48//! - `serde` (default) — `serde` interop, including the [`json_literal!`]
49//!   macro that maps a `serde` type onto an `rdf:JSON` literal.
50use educe::Educe;
51use iri_rs::{Iri, IriBuf};
52#[cfg(feature = "derive")]
53pub use ld_core_derive::{Deserialize, Serialize};
54use rdfx::{
55	Interpretation, dataset::PatternMatchingDataset, interpretation::ReverseInterpretation,
56};
57
58#[doc(hidden)]
59pub use iri_rs;
60
61#[doc(hidden)]
62pub use rdfx;
63
64#[doc(hidden)]
65pub use xsd_rs;
66
67#[doc(hidden)]
68pub use jstrict;
69
70mod anonymous;
71mod datatypes;
72mod graph;
73mod r#impl;
74mod macros;
75mod predicate;
76mod quads;
77mod rdf;
78mod reference;
79mod resource;
80mod subject;
81
82pub use anonymous::*;
83pub use graph::*;
84pub use predicate::*;
85pub use quads::{
86	IntoQuadsError, to_interpreted_graph_quads, to_interpreted_quads, to_interpreted_subject_quads,
87	to_lexical_quads, to_lexical_quads_with, to_lexical_subject_quads,
88	to_lexical_subject_quads_with, to_quads, to_quads_with,
89};
90pub use rdf::*;
91pub use reference::*;
92pub use resource::*;
93pub use subject::*;
94
95#[derive(Debug, thiserror::Error)]
96/// Error raised while deserializing a value from Linked Data.
97pub enum FromLinkedDataError {
98	/// Resource has no IRI representation.
99	#[error("expected IRI")]
100	ExpectedIri(ContextIris),
101
102	#[error("unsupported IRI `{found}`")]
103	/// Resource is identified by an IRI the type does not accept.
104	UnsupportedIri {
105		/// Error context.
106		context: ContextIris,
107
108		/// Unsupported IRI.
109		found: IriBuf,
110
111		/// Optional hint listing the supported IRIs.
112		supported: Option<Vec<IriBuf>>,
113	},
114
115	/// Resource has no literal representation.
116	#[error("expected literal")]
117	ExpectedLiteral(ContextIris),
118
119	/// Resource has literal representations, but none of the expected type.
120	///
121	/// The type IRIs are boxed: this is the only variant carrying two of them,
122	/// and inline they would make it the largest variant by a wide margin, which
123	/// sets the size of the whole enum.
124	#[error("literal type mismatch")]
125	LiteralTypeMismatch {
126		/// Error context.
127		context: ContextIris,
128		/// Expected literal type, if the type accepts only one.
129		expected: Option<Box<IriBuf>>,
130		/// Literal type actually found.
131		found: Box<IriBuf>,
132	},
133
134	/// Resource has a literal representation of the correct type, but the
135	/// lexical value could not be successfully parsed.
136	#[error("invalid literal")]
137	InvalidLiteral(ContextIris),
138
139	/// Missing required value.
140	#[error("missing required value")]
141	MissingRequiredValue(ContextIris),
142
143	/// Too many values.
144	#[error("too many values")]
145	TooManyValues(ContextIris),
146
147	/// Generic error for invalid subjects.
148	#[error("invalid subject")]
149	InvalidSubject {
150		/// Error context.
151		context: ContextIris,
152		/// Subject IRI, if it has one.
153		subject: Option<IriBuf>,
154	},
155}
156
157impl FromLinkedDataError {
158	/// Returns the context in which this error was raised.
159	pub fn context(&self) -> &ContextIris {
160		match self {
161			Self::ExpectedIri(c) => c,
162			Self::UnsupportedIri { context, .. } => context,
163			Self::ExpectedLiteral(c) => c,
164			Self::LiteralTypeMismatch { context, .. } => context,
165			Self::InvalidLiteral(c) => c,
166			Self::MissingRequiredValue(c) => c,
167			Self::TooManyValues(c) => c,
168			Self::InvalidSubject { context, .. } => context,
169		}
170	}
171}
172
173/// Linked-Data type.
174///
175/// A Linked-Data type represents an RDF dataset which can be visited using the
176/// [`visit`](Self::visit) method.
177pub trait LinkedData<I: Interpretation = ()> {
178	/// Visit the RDF dataset represented by this type.
179	fn visit<S>(&self, visitor: S) -> Result<S::Ok, S::Error>
180	where
181		S: Visitor<I>;
182}
183
184impl<I: Interpretation, T: ?Sized + LinkedData<I>> LinkedData<I> for &T {
185	fn visit<S>(&self, visitor: S) -> Result<S::Ok, S::Error>
186	where
187		S: Visitor<I>,
188	{
189		T::visit(self, visitor)
190	}
191}
192
193impl<I: Interpretation, T: ?Sized + LinkedData<I>> LinkedData<I> for Box<T> {
194	fn visit<S>(&self, visitor: S) -> Result<S::Ok, S::Error>
195	where
196		S: Visitor<I>,
197	{
198		T::visit(self, visitor)
199	}
200}
201
202impl<I: Interpretation> LinkedData<I> for IriBuf {
203	fn visit<S>(&self, visitor: S) -> Result<S::Ok, S::Error>
204	where
205		S: Visitor<I>,
206	{
207		visitor.end()
208	}
209}
210
211/// RDF dataset visitor.
212pub trait Visitor<I: Interpretation = ()> {
213	/// Type of the value returned by the visitor when the dataset has been
214	/// entirely visited.
215	type Ok;
216
217	/// Error type.
218	type Error;
219
220	/// Visits the default graph of the dataset.
221	fn default_graph<T>(&mut self, value: &T) -> Result<(), Self::Error>
222	where
223		T: ?Sized + LinkedDataGraph<I>;
224
225	/// Visits a named graph of the dataset.
226	fn named_graph<T>(&mut self, value: &T) -> Result<(), Self::Error>
227	where
228		T: ?Sized + LinkedDataResource<I> + LinkedDataGraph<I>;
229
230	/// Ends the dataset visit.
231	fn end(self) -> Result<Self::Ok, Self::Error>;
232}
233
234/// Any mutable reference to a visitor is itself a visitor.
235impl<I: Interpretation, S: Visitor<I>> Visitor<I> for &mut S {
236	type Ok = ();
237	type Error = S::Error;
238
239	fn default_graph<T>(&mut self, value: &T) -> Result<(), Self::Error>
240	where
241		T: ?Sized + LinkedDataGraph<I>,
242	{
243		S::default_graph(self, value)
244	}
245
246	fn named_graph<T>(&mut self, value: &T) -> Result<(), Self::Error>
247	where
248		T: ?Sized + LinkedDataResource<I> + LinkedDataGraph<I>,
249	{
250		S::named_graph(self, value)
251	}
252
253	fn end(self) -> Result<Self::Ok, Self::Error> {
254		Ok(())
255	}
256}
257
258#[derive(Educe)]
259#[educe(Debug(bound = "I::Resource: core::fmt::Debug"), Clone, Copy)]
260/// Reference to a resource, either interpreted or lexical.
261pub enum ResourceOrIriRef<'a, I: Interpretation> {
262	/// A resource of the interpretation.
263	Resource(&'a I::Resource),
264	/// An IRI, outside of any interpretation.
265	Iri(Iri<&'a str>),
266	/// An anonymous resource, with no identifier.
267	Anonymous,
268}
269
270impl<'a, I: Interpretation> ResourceOrIriRef<'a, I> {
271	/// Resolves this reference into an IRI, if it has one.
272	pub fn into_iri(self, interpretation: &I) -> Option<IriBuf>
273	where
274		I: ReverseInterpretation,
275	{
276		match self {
277			Self::Resource(r) => interpretation.iris_of(r).next().map(|i| i.into_owned()),
278			Self::Iri(i) => Some(i.into()),
279			Self::Anonymous => None,
280		}
281	}
282}
283
284#[derive(Educe)]
285#[educe(Debug(bound = "I::Resource: core::fmt::Debug"), Clone, Copy)]
286/// Position a value occupies while being visited, used to report errors.
287#[derive(Default)]
288pub enum Context<'a, I: Interpretation> {
289	/// The value is a subject.
290	#[default]
291	Subject,
292	/// The value is a predicate of the given subject.
293	Predicate {
294		/// Subject the predicate belongs to.
295		subject: ResourceOrIriRef<'a, I>,
296	},
297	/// The value is an object of the given subject and predicate.
298	Object {
299		/// Subject the object belongs to.
300		subject: ResourceOrIriRef<'a, I>,
301		/// Predicate the object belongs to.
302		predicate: ResourceOrIriRef<'a, I>,
303	},
304}
305
306impl<'a, I: Interpretation> Context<'a, I> {
307	/// Moves this context into the predicate position of `subject`.
308	pub fn with_subject(self, subject: &'a I::Resource) -> Self {
309		Self::Predicate {
310			subject: ResourceOrIriRef::Resource(subject),
311		}
312	}
313
314	/// Moves this context into the object position of `predicate`.
315	pub fn with_predicate(self, predicate: &'a I::Resource) -> Self {
316		match self {
317			Self::Predicate { subject } => Self::Object {
318				subject,
319				predicate: ResourceOrIriRef::Resource(predicate),
320			},
321			_ => Self::Subject,
322		}
323	}
324
325	/// Moves this context into the object position of the `predicate` IRI.
326	pub fn with_predicate_iri(self, predicate: Iri<&'a str>) -> Self {
327		match self {
328			Self::Predicate { subject } => Self::Object {
329				subject,
330				predicate: ResourceOrIriRef::Iri(predicate),
331			},
332			_ => Self::Subject,
333		}
334	}
335
336	/// Moves this context into the object position of an anonymous predicate.
337	pub fn with_anonymous_predicate(self) -> Self {
338		match self {
339			Self::Predicate { subject } => Self::Object {
340				subject,
341				predicate: ResourceOrIriRef::Anonymous,
342			},
343			_ => Self::Subject,
344		}
345	}
346
347	/// Resolves the resources of this context into IRIs.
348	pub fn into_iris(self, interpretation: &I) -> ContextIris
349	where
350		I: ReverseInterpretation,
351	{
352		match self {
353			Self::Subject => ContextIris::Subject,
354			Self::Predicate { subject } => ContextIris::Predicate {
355				subject: subject.into_iri(interpretation).map(Box::new),
356			},
357			Self::Object { subject, predicate } => ContextIris::Object {
358				subject: subject.into_iri(interpretation).map(Box::new),
359				predicate: predicate.into_iri(interpretation).map(Box::new),
360			},
361		}
362	}
363}
364
365#[derive(Debug, Clone)]
366/// Error context with its resources resolved into IRIs.
367///
368/// The IRIs are boxed because this type is carried by every
369/// [`FromLinkedDataError`] variant, and therefore sits in the `Err` slot of
370/// every deserialization `Result`. An inline [`IriBuf`] is 56 bytes, which
371/// would make the error — and so every `Result` returned on the success path
372/// too — several times larger than the values being deserialized.
373pub enum ContextIris {
374	/// The value is a subject.
375	Subject,
376	/// The value is a predicate of the given subject.
377	Predicate {
378		/// Subject IRI, if it has one.
379		subject: Option<Box<IriBuf>>,
380	},
381	/// The value is an object of the given subject and predicate.
382	Object {
383		/// Subject IRI, if it has one.
384		subject: Option<Box<IriBuf>>,
385		/// Predicate IRI, if it has one.
386		predicate: Option<Box<IriBuf>>,
387	},
388}
389
390/// Type that can be deserialized from an RDF dataset.
391pub trait LinkedDataDeserialize<I: Interpretation>: Sized
392where
393	I::Resource: rdfx::Resource,
394{
395	/// Deserializes a value from `dataset`, reporting errors against
396	/// `context`.
397	fn deserialize_dataset_in(
398		interpretation: &I,
399		dataset: &(
400		     impl rdfx::dataset::TraversableDataset<Subject = I::Resource>
401		     + PatternMatchingDataset<Subject = I::Resource>
402		 ),
403		context: Context<I>,
404	) -> Result<Self, FromLinkedDataError>;
405
406	/// Deserializes a value from `dataset`.
407	fn deserialize_dataset(
408		interpretation: &I,
409		dataset: &(
410		     impl rdfx::dataset::TraversableDataset<Subject = I::Resource>
411		     + PatternMatchingDataset<Subject = I::Resource>
412		 ),
413	) -> Result<Self, FromLinkedDataError> {
414		Self::deserialize_dataset_in(interpretation, dataset, Context::default())
415	}
416}