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	#[error("literal type mismatch")]
121	LiteralTypeMismatch {
122		/// Error context.
123		context: ContextIris,
124		/// Expected literal type, if the type accepts only one.
125		expected: Option<IriBuf>,
126		/// Literal type actually found.
127		found: IriBuf,
128	},
129
130	/// Resource has a literal representation of the correct type, but the
131	/// lexical value could not be successfully parsed.
132	#[error("invalid literal")]
133	InvalidLiteral(ContextIris),
134
135	/// Missing required value.
136	#[error("missing required value")]
137	MissingRequiredValue(ContextIris),
138
139	/// Too many values.
140	#[error("too many values")]
141	TooManyValues(ContextIris),
142
143	/// Generic error for invalid subjects.
144	#[error("invalid subject")]
145	InvalidSubject {
146		/// Error context.
147		context: ContextIris,
148		/// Subject IRI, if it has one.
149		subject: Option<IriBuf>,
150	},
151}
152
153impl FromLinkedDataError {
154	/// Returns the context in which this error was raised.
155	pub fn context(&self) -> &ContextIris {
156		match self {
157			Self::ExpectedIri(c) => c,
158			Self::UnsupportedIri { context, .. } => context,
159			Self::ExpectedLiteral(c) => c,
160			Self::LiteralTypeMismatch { context, .. } => context,
161			Self::InvalidLiteral(c) => c,
162			Self::MissingRequiredValue(c) => c,
163			Self::TooManyValues(c) => c,
164			Self::InvalidSubject { context, .. } => context,
165		}
166	}
167}
168
169/// Linked-Data type.
170///
171/// A Linked-Data type represents an RDF dataset which can be visited using the
172/// [`visit`](Self::visit) method.
173pub trait LinkedData<I: Interpretation = ()> {
174	/// Visit the RDF dataset represented by this type.
175	fn visit<S>(&self, visitor: S) -> Result<S::Ok, S::Error>
176	where
177		S: Visitor<I>;
178}
179
180impl<I: Interpretation, T: ?Sized + LinkedData<I>> LinkedData<I> for &T {
181	fn visit<S>(&self, visitor: S) -> Result<S::Ok, S::Error>
182	where
183		S: Visitor<I>,
184	{
185		T::visit(self, visitor)
186	}
187}
188
189impl<I: Interpretation, T: ?Sized + LinkedData<I>> LinkedData<I> for Box<T> {
190	fn visit<S>(&self, visitor: S) -> Result<S::Ok, S::Error>
191	where
192		S: Visitor<I>,
193	{
194		T::visit(self, visitor)
195	}
196}
197
198impl<I: Interpretation> LinkedData<I> for IriBuf {
199	fn visit<S>(&self, visitor: S) -> Result<S::Ok, S::Error>
200	where
201		S: Visitor<I>,
202	{
203		visitor.end()
204	}
205}
206
207/// RDF dataset visitor.
208pub trait Visitor<I: Interpretation = ()> {
209	/// Type of the value returned by the visitor when the dataset has been
210	/// entirely visited.
211	type Ok;
212
213	/// Error type.
214	type Error;
215
216	/// Visits the default graph of the dataset.
217	fn default_graph<T>(&mut self, value: &T) -> Result<(), Self::Error>
218	where
219		T: ?Sized + LinkedDataGraph<I>;
220
221	/// Visits a named graph of the dataset.
222	fn named_graph<T>(&mut self, value: &T) -> Result<(), Self::Error>
223	where
224		T: ?Sized + LinkedDataResource<I> + LinkedDataGraph<I>;
225
226	/// Ends the dataset visit.
227	fn end(self) -> Result<Self::Ok, Self::Error>;
228}
229
230/// Any mutable reference to a visitor is itself a visitor.
231impl<I: Interpretation, S: Visitor<I>> Visitor<I> for &mut S {
232	type Ok = ();
233	type Error = S::Error;
234
235	fn default_graph<T>(&mut self, value: &T) -> Result<(), Self::Error>
236	where
237		T: ?Sized + LinkedDataGraph<I>,
238	{
239		S::default_graph(self, value)
240	}
241
242	fn named_graph<T>(&mut self, value: &T) -> Result<(), Self::Error>
243	where
244		T: ?Sized + LinkedDataResource<I> + LinkedDataGraph<I>,
245	{
246		S::named_graph(self, value)
247	}
248
249	fn end(self) -> Result<Self::Ok, Self::Error> {
250		Ok(())
251	}
252}
253
254#[derive(Educe)]
255#[educe(Debug(bound = "I::Resource: core::fmt::Debug"), Clone, Copy)]
256/// Reference to a resource, either interpreted or lexical.
257pub enum ResourceOrIriRef<'a, I: Interpretation> {
258	/// A resource of the interpretation.
259	Resource(&'a I::Resource),
260	/// An IRI, outside of any interpretation.
261	Iri(Iri<&'a str>),
262	/// An anonymous resource, with no identifier.
263	Anonymous,
264}
265
266impl<'a, I: Interpretation> ResourceOrIriRef<'a, I> {
267	/// Resolves this reference into an IRI, if it has one.
268	pub fn into_iri(self, interpretation: &I) -> Option<IriBuf>
269	where
270		I: ReverseInterpretation,
271	{
272		match self {
273			Self::Resource(r) => interpretation.iris_of(r).next().map(|i| i.into_owned()),
274			Self::Iri(i) => Some(i.into()),
275			Self::Anonymous => None,
276		}
277	}
278}
279
280#[derive(Educe)]
281#[educe(Debug(bound = "I::Resource: core::fmt::Debug"), Clone, Copy)]
282/// Position a value occupies while being visited, used to report errors.
283#[derive(Default)]
284pub enum Context<'a, I: Interpretation> {
285	/// The value is a subject.
286	#[default]
287	Subject,
288	/// The value is a predicate of the given subject.
289	Predicate {
290		/// Subject the predicate belongs to.
291		subject: ResourceOrIriRef<'a, I>,
292	},
293	/// The value is an object of the given subject and predicate.
294	Object {
295		/// Subject the object belongs to.
296		subject: ResourceOrIriRef<'a, I>,
297		/// Predicate the object belongs to.
298		predicate: ResourceOrIriRef<'a, I>,
299	},
300}
301
302impl<'a, I: Interpretation> Context<'a, I> {
303	/// Moves this context into the predicate position of `subject`.
304	pub fn with_subject(self, subject: &'a I::Resource) -> Self {
305		Self::Predicate {
306			subject: ResourceOrIriRef::Resource(subject),
307		}
308	}
309
310	/// Moves this context into the object position of `predicate`.
311	pub fn with_predicate(self, predicate: &'a I::Resource) -> Self {
312		match self {
313			Self::Predicate { subject } => Self::Object {
314				subject,
315				predicate: ResourceOrIriRef::Resource(predicate),
316			},
317			_ => Self::Subject,
318		}
319	}
320
321	/// Moves this context into the object position of the `predicate` IRI.
322	pub fn with_predicate_iri(self, predicate: Iri<&'a str>) -> Self {
323		match self {
324			Self::Predicate { subject } => Self::Object {
325				subject,
326				predicate: ResourceOrIriRef::Iri(predicate),
327			},
328			_ => Self::Subject,
329		}
330	}
331
332	/// Moves this context into the object position of an anonymous predicate.
333	pub fn with_anonymous_predicate(self) -> Self {
334		match self {
335			Self::Predicate { subject } => Self::Object {
336				subject,
337				predicate: ResourceOrIriRef::Anonymous,
338			},
339			_ => Self::Subject,
340		}
341	}
342
343	/// Resolves the resources of this context into IRIs.
344	pub fn into_iris(self, interpretation: &I) -> ContextIris
345	where
346		I: ReverseInterpretation,
347	{
348		match self {
349			Self::Subject => ContextIris::Subject,
350			Self::Predicate { subject } => ContextIris::Predicate {
351				subject: subject.into_iri(interpretation),
352			},
353			Self::Object { subject, predicate } => ContextIris::Object {
354				subject: subject.into_iri(interpretation),
355				predicate: predicate.into_iri(interpretation),
356			},
357		}
358	}
359}
360
361#[derive(Debug, Clone)]
362/// Error context with its resources resolved into IRIs.
363pub enum ContextIris {
364	/// The value is a subject.
365	Subject,
366	/// The value is a predicate of the given subject.
367	Predicate {
368		/// Subject IRI, if it has one.
369		subject: Option<IriBuf>,
370	},
371	/// The value is an object of the given subject and predicate.
372	Object {
373		/// Subject IRI, if it has one.
374		subject: Option<IriBuf>,
375		/// Predicate IRI, if it has one.
376		predicate: Option<IriBuf>,
377	},
378}
379
380/// Type that can be deserialized from an RDF dataset.
381pub trait LinkedDataDeserialize<I: Interpretation>: Sized
382where
383	I::Resource: rdfx::Resource,
384{
385	/// Deserializes a value from `dataset`, reporting errors against
386	/// `context`.
387	fn deserialize_dataset_in(
388		interpretation: &I,
389		dataset: &(
390		     impl rdfx::dataset::TraversableDataset<Subject = I::Resource>
391		     + PatternMatchingDataset<Subject = I::Resource>
392		 ),
393		context: Context<I>,
394	) -> Result<Self, FromLinkedDataError>;
395
396	/// Deserializes a value from `dataset`.
397	fn deserialize_dataset(
398		interpretation: &I,
399		dataset: &(
400		     impl rdfx::dataset::TraversableDataset<Subject = I::Resource>
401		     + PatternMatchingDataset<Subject = I::Resource>
402		 ),
403	) -> Result<Self, FromLinkedDataError> {
404		Self::deserialize_dataset_in(interpretation, dataset, Context::default())
405	}
406}