Make RdfSource generic over the dataset type

This commit is contained in:
2026-05-20 23:45:30 -04:00
parent 13daececb6
commit 8a6fe06c27
9 changed files with 222 additions and 29 deletions
+5
View File
@@ -8,6 +8,10 @@ license = "GPL-3.0-only"
keywords = ["ldp", "rdf", "sparql"]
categories = ["database", "web-programming::http-client"]
[features]
default = ["keyed"]
keyed = ["dep:slotmap"]
[dependencies]
async-trait.workspace = true
base64.workspace = true
@@ -17,5 +21,6 @@ http.workspace = true
oxigraph.workspace = true
parse_link_header.workspace = true
reqwest-middleware.workspace = true
slotmap = { optional = true, workspace = true }
thiserror.workspace = true
tracing.workspace = true
+4
View File
@@ -22,4 +22,8 @@ pub enum Error {
/// The response from the server was not in a format we understand.
#[error("Response was not in a supported RDF format")]
UnsupportedFormat,
/// The RDF data failed to parse.
#[error(transparent)]
InvalidRdfSyntax(#[from] oxigraph::io::RdfSyntaxError),
}
+59
View File
@@ -0,0 +1,59 @@
use oxigraph::model::{Quad, QuadRef};
use slotmap::{SecondaryMap, SlotMap, new_key_type};
new_key_type! { pub struct QuadKey; }
#[derive(Clone, Debug, Default)]
pub struct KeyedDataset<T> {
pub quads: SlotMap<QuadKey, Quad>,
pub associated_data: SecondaryMap<QuadKey, T>,
}
impl<T> FromIterator<Quad> for KeyedDataset<T> {
fn from_iter<U: IntoIterator<Item = Quad>>(iter: U) -> Self {
let mut quads = SlotMap::with_key();
for quad in iter {
quads.insert(quad);
}
Self {
quads,
associated_data: SecondaryMap::new(),
}
}
}
impl<'a, T> IntoIterator for &'a KeyedDataset<T> {
type Item = QuadRef<'a>;
type IntoIter = std::iter::Map<slotmap::basic::Values<'a, QuadKey, Quad>, fn(&Quad) -> QuadRef>;
fn into_iter(self) -> Self::IntoIter {
self.quads.values().map(|quad| quad.as_ref())
}
}
impl<T> KeyedDataset<T> {
/// Extend the dataset with the provided quads, returning a QuadKey for each insertion.
pub fn extend(&mut self, quads: impl Iterator<Item = Quad>) -> impl Iterator<Item = QuadKey> {
quads.map(|quad| self.quads.insert(quad))
}
/// Remove a quad and its associated value, if present.
pub fn remove(&mut self, key: QuadKey) {
self.quads.remove(key);
self.associated_data.remove(key);
}
/// Remove all quads and all associated data.
pub fn clear(&mut self) {
self.quads.clear();
self.associated_data.clear();
}
/// Iterate over both the quads and the associated data. Quads with no corresponding associated
/// data are filtered out.
pub fn iter_both(&self) -> impl Iterator<Item = (QuadKey, &Quad, &T)> {
self.quads
.iter()
.filter_map(|(key, quad)| self.associated_data.get(key).map(|ad| (key, quad, ad)))
}
}
+6 -1
View File
@@ -7,11 +7,16 @@ mod rdf_source;
mod resource;
pub mod vocab;
#[cfg(feature = "keyed")]
pub mod keyed;
pub use http;
pub use oxigraph;
pub use reqwest_middleware;
pub use reqwest_middleware::reqwest;
pub use error::{Error, Result};
pub use rdf_source::{RdfSource, RdfSourceUpdateRequest, RdfSourceUpdateResponse};
pub use rdf_source::{
RdfSource, RdfSourceUpdateRequest, RdfSourceUpdateResponse, SerializationOptions,
};
pub use resource::{Resource, ResourceRequest, ResourceRequestBuilder, ResponseFormat};
+116 -16
View File
@@ -1,20 +1,36 @@
use bytes::BufMut;
use http::{StatusCode, header};
use oxigraph::io::{RdfFormat, RdfSerializer};
use oxigraph::model::Dataset;
use oxigraph::model::{
GraphName, Literal, NamedNode, NamedOrBlankNode, Quad, QuadRef, Term, Triple, TripleRef, vocab,
};
use reqwest_middleware::ClientWithMiddleware;
use reqwest_middleware::reqwest::Url;
/// A LDP [RDF Source](https://www.w3.org/TR/ldp/#ldprs).
#[derive(Clone, Debug)]
pub struct RdfSource {
pub struct RdfSource<D> {
pub(crate) origin: Url,
pub(crate) described_by: Option<Url>,
pub(crate) state_token: Option<String>,
pub(crate) dataset: Dataset,
pub(crate) dataset: D,
}
impl RdfSource {
impl<D: Default> RdfSource<D> {
/// Create a new, empty, RdfSource with the given URL.
///
/// The subject of new quads will use this URL.
pub fn new(origin: Url) -> Self {
Self {
origin,
described_by: None,
state_token: None,
dataset: Default::default(),
}
}
}
impl<D> RdfSource<D> {
/// The original URL used to procure this RDF Source.
pub fn origin(&self) -> &Url {
&self.origin
@@ -34,23 +50,104 @@ impl RdfSource {
self.state_token.as_deref()
}
/// The underlying Dataset.
pub fn dataset(&self) -> &Dataset {
/// The underlying dataset.
pub fn dataset(&self) -> &D {
&self.dataset
}
/// Serializes the Dataset in to the provided format.
pub fn serialize(&self, format: RdfFormat) -> crate::Result<bytes::Bytes> {
let writer = bytes::BytesMut::new().writer();
let mut serializer = RdfSerializer::from_format(format).for_writer(writer);
/// A mutable reference to the underlying dataset.
pub fn dataset_mut(&mut self) -> &mut D {
&mut self.dataset
}
if format.supports_datasets() {
/// Create a new quad, using the origin as the subject.
///
/// The graph name is the `describedby` value, if present. If not present, the graph name is the
/// origin.
pub fn new_quad(&self) -> Quad {
let graph_name = self
.described_by
.as_ref()
.map(|db| db.as_str())
.unwrap_or(self.origin().as_str());
Quad::new(
NamedOrBlankNode::NamedNode(NamedNode::new_unchecked(self.origin.clone())),
vocab::rdf::VALUE,
Term::Literal(Literal::new_simple_literal("")),
GraphName::NamedNode(NamedNode::new_unchecked(graph_name)),
)
}
/// Create a new quad, using the given triple as a template.
///
/// The graph name is the `describedby` value, if present. If not present, the graph name is the
/// origin.
pub fn quad_from_triple(&self, triple: Triple) -> Quad {
let graph_name = GraphName::NamedNode(NamedNode::new_unchecked(
self.described_by
.as_ref()
.map(|db| db.as_str())
.unwrap_or(self.origin().as_str()),
));
Quad::new(
triple.subject,
triple.predicate,
triple.object,
graph_name.clone(),
)
}
}
/// Holds options related to serialization.
pub struct SerializationOptions {
format: RdfFormat,
filter: Box<dyn Fn(TripleRef<'_>) -> bool>,
}
impl SerializationOptions {
/// Create a new set of serialization options with the provided format.
pub fn from_format(format: RdfFormat) -> Self {
Self {
format,
filter: Box::new(|_| true),
}
}
/// Filter triples that match the predicate.
///
/// A return value of `true` means that it the triple ought to be included in the serialization.
pub fn with_filter<F>(self, filter: F) -> Self
where
F: Fn(TripleRef<'_>) -> bool + 'static,
{
Self {
format: self.format,
filter: Box::new(filter),
}
}
}
impl<'a, D: 'a> RdfSource<D>
where
&'a D: IntoIterator<Item = QuadRef<'a>>,
{
/// Serializes the dataset in to the provided format.
pub fn serialize(&'a self, options: SerializationOptions) -> crate::Result<bytes::Bytes> {
let writer = bytes::BytesMut::new().writer();
let mut serializer = RdfSerializer::from_format(options.format).for_writer(writer);
if options.format.supports_datasets() {
for quad in &self.dataset {
serializer.serialize_quad(quad)?;
if (options.filter)(TripleRef::from(quad)) {
serializer.serialize_quad(quad)?;
}
}
} else {
for quad in &self.dataset {
serializer.serialize_triple(quad)?;
if (options.filter)(TripleRef::from(quad)) {
serializer.serialize_triple(quad)?;
}
}
}
@@ -59,10 +156,13 @@ impl RdfSource {
}
/// Prepare an update request.
pub fn to_update(&self, format: RdfFormat) -> crate::Result<RdfSourceUpdateRequest> {
pub fn to_update(
&'a self,
options: SerializationOptions,
) -> crate::Result<RdfSourceUpdateRequest> {
let url = self.described_by.clone().unwrap_or(self.origin.clone());
let media_type = format.media_type().to_string();
let body = self.serialize(format)?;
let media_type = options.format.media_type().to_string();
let body = self.serialize(options)?;
Ok(RdfSourceUpdateRequest {
url,
+16 -8
View File
@@ -3,7 +3,7 @@ use crate::vocab;
use bytes::Bytes;
use futures::Stream;
use oxigraph::io::{RdfFormat, RdfParser};
use oxigraph::model::{Dataset, GraphNameRef, NamedNodeRef};
use oxigraph::model::{GraphNameRef, NamedNodeRef, Quad};
use reqwest_middleware::reqwest::{Client, Response, StatusCode, Url, header};
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware, RequestBuilder};
use tracing::error;
@@ -12,17 +12,22 @@ use tracing::error;
///
/// # Example
/// ```rust
/// use ldp::ResourceRequestBuilder;
/// use ldp::oxigraph::model::Dataset;
/// use ldp::reqwest::{Client, Url};
/// use ldp::reqwest_middleware::ClientBuilder;
/// use ldp::ResourceRequestBuilder;
///
/// let client = ClientBuilder::new(Client::new()).build();
/// let url = Url::parse("http://server/resource")?;
/// let resource = ResourceRequestBuilder::with_client_and_url(client.clone(), url)
/// let request = ResourceRequestBuilder::new(url)
/// .follow_described_by(true)
/// .accept_all_rdf_formats()
/// .send();
/// .await?;
/// .build();
///
/// let resource = request.send().await?;
/// let rdf_source = resource.into_rdf_source::<Dataset>().await?;
/// for quad in rdf_source.dataset() {
/// println!("{:?}", quad);
/// }
/// ```
#[derive(Clone, Debug)]
pub struct ResourceRequestBuilder {
@@ -253,14 +258,17 @@ impl Resource {
}
/// Parse the response.
pub async fn into_rdf_source(self) -> crate::Result<RdfSource> {
pub async fn into_rdf_source<D>(self) -> crate::Result<RdfSource<D>>
where
D: FromIterator<Quad>,
{
if let ResponseFormat::RdfFormat(format) = self.format {
let graph_url = self.described_by.as_ref().unwrap_or(&self.origin);
let graph = GraphNameRef::NamedNode(NamedNodeRef::new_unchecked(graph_url.as_str()));
let parser = RdfParser::from_format(format).with_default_graph(graph);
let body = self.response.bytes().await?;
let quads = parser.for_slice(&body);
let dataset = quads.filter_map(Result::ok).collect::<Dataset>();
let dataset = quads.collect::<Result<_, _>>()?;
Ok(RdfSource {
origin: self.origin,
described_by: self.described_by,