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
Generated
+12 -2
View File
@@ -244,9 +244,9 @@ dependencies = [
[[package]] [[package]]
name = "either" name = "either"
version = "1.15.0" version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
[[package]] [[package]]
name = "errno" name = "errno"
@@ -687,6 +687,7 @@ dependencies = [
"oxigraph", "oxigraph",
"parse_link_header", "parse_link_header",
"reqwest-middleware", "reqwest-middleware",
"slotmap",
"thiserror", "thiserror",
"tracing", "tracing",
] ]
@@ -1308,6 +1309,15 @@ version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "slotmap"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038"
dependencies = [
"version_check",
]
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.15.1" version = "1.15.1"
+1
View File
@@ -17,6 +17,7 @@ color-eyre = "0.6"
oxigraph = "0.5" oxigraph = "0.5"
parse_link_header = "0.4" parse_link_header = "0.4"
reqwest-middleware = { version = "0.5", features = ["stream"] } reqwest-middleware = { version = "0.5", features = ["stream"] }
slotmap = "1.1"
thiserror = "2" thiserror = "2"
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
tracing = "0.1" tracing = "0.1"
+3 -2
View File
@@ -1,5 +1,6 @@
use ldp::ResourceRequestBuilder; use ldp::ResourceRequestBuilder;
use ldp::middleware::BasicAuthMiddleware; use ldp::middleware::BasicAuthMiddleware;
use ldp::oxigraph::model::Dataset;
use ldp::reqwest::{Client, Url}; use ldp::reqwest::{Client, Url};
use ldp::reqwest_middleware::ClientBuilder; use ldp::reqwest_middleware::ClientBuilder;
use tracing::debug; use tracing::debug;
@@ -28,9 +29,9 @@ async fn main() -> color_eyre::Result<()> {
.build(); .build();
let resource = request.send().await?; let resource = request.send().await?;
let rdf_source = resource.into_rdf_source().await?; let rdf_source = resource.into_rdf_source::<Dataset>().await?;
for quad in rdf_source.dataset() { for quad in rdf_source.dataset() {
debug!(?quad); println!("{:#?}", quad);
} }
Ok(()) Ok(())
+5
View File
@@ -8,6 +8,10 @@ license = "GPL-3.0-only"
keywords = ["ldp", "rdf", "sparql"] keywords = ["ldp", "rdf", "sparql"]
categories = ["database", "web-programming::http-client"] categories = ["database", "web-programming::http-client"]
[features]
default = ["keyed"]
keyed = ["dep:slotmap"]
[dependencies] [dependencies]
async-trait.workspace = true async-trait.workspace = true
base64.workspace = true base64.workspace = true
@@ -17,5 +21,6 @@ http.workspace = true
oxigraph.workspace = true oxigraph.workspace = true
parse_link_header.workspace = true parse_link_header.workspace = true
reqwest-middleware.workspace = true reqwest-middleware.workspace = true
slotmap = { optional = true, workspace = true }
thiserror.workspace = true thiserror.workspace = true
tracing.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. /// The response from the server was not in a format we understand.
#[error("Response was not in a supported RDF format")] #[error("Response was not in a supported RDF format")]
UnsupportedFormat, 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; mod resource;
pub mod vocab; pub mod vocab;
#[cfg(feature = "keyed")]
pub mod keyed;
pub use http; pub use http;
pub use oxigraph; pub use oxigraph;
pub use reqwest_middleware; pub use reqwest_middleware;
pub use reqwest_middleware::reqwest; pub use reqwest_middleware::reqwest;
pub use error::{Error, Result}; 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}; pub use resource::{Resource, ResourceRequest, ResourceRequestBuilder, ResponseFormat};
+114 -14
View File
@@ -1,20 +1,36 @@
use bytes::BufMut; use bytes::BufMut;
use http::{StatusCode, header}; use http::{StatusCode, header};
use oxigraph::io::{RdfFormat, RdfSerializer}; 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::ClientWithMiddleware;
use reqwest_middleware::reqwest::Url; use reqwest_middleware::reqwest::Url;
/// A LDP [RDF Source](https://www.w3.org/TR/ldp/#ldprs). /// A LDP [RDF Source](https://www.w3.org/TR/ldp/#ldprs).
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct RdfSource { pub struct RdfSource<D> {
pub(crate) origin: Url, pub(crate) origin: Url,
pub(crate) described_by: Option<Url>, pub(crate) described_by: Option<Url>,
pub(crate) state_token: Option<String>, 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. /// The original URL used to procure this RDF Source.
pub fn origin(&self) -> &Url { pub fn origin(&self) -> &Url {
&self.origin &self.origin
@@ -34,35 +50,119 @@ impl RdfSource {
self.state_token.as_deref() self.state_token.as_deref()
} }
/// The underlying Dataset. /// The underlying dataset.
pub fn dataset(&self) -> &Dataset { pub fn dataset(&self) -> &D {
&self.dataset &self.dataset
} }
/// Serializes the Dataset in to the provided format. /// A mutable reference to the underlying dataset.
pub fn serialize(&self, format: RdfFormat) -> crate::Result<bytes::Bytes> { pub fn dataset_mut(&mut self) -> &mut D {
let writer = bytes::BytesMut::new().writer(); &mut self.dataset
let mut serializer = RdfSerializer::from_format(format).for_writer(writer); }
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 { for quad in &self.dataset {
if (options.filter)(TripleRef::from(quad)) {
serializer.serialize_quad(quad)?; serializer.serialize_quad(quad)?;
} }
}
} else { } else {
for quad in &self.dataset { for quad in &self.dataset {
if (options.filter)(TripleRef::from(quad)) {
serializer.serialize_triple(quad)?; serializer.serialize_triple(quad)?;
} }
} }
}
let finished_writer = serializer.finish()?; let finished_writer = serializer.finish()?;
Ok(finished_writer.into_inner().freeze()) Ok(finished_writer.into_inner().freeze())
} }
/// Prepare an update request. /// 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 url = self.described_by.clone().unwrap_or(self.origin.clone());
let media_type = format.media_type().to_string(); let media_type = options.format.media_type().to_string();
let body = self.serialize(format)?; let body = self.serialize(options)?;
Ok(RdfSourceUpdateRequest { Ok(RdfSourceUpdateRequest {
url, url,
+16 -8
View File
@@ -3,7 +3,7 @@ use crate::vocab;
use bytes::Bytes; use bytes::Bytes;
use futures::Stream; use futures::Stream;
use oxigraph::io::{RdfFormat, RdfParser}; 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::reqwest::{Client, Response, StatusCode, Url, header};
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware, RequestBuilder}; use reqwest_middleware::{ClientBuilder, ClientWithMiddleware, RequestBuilder};
use tracing::error; use tracing::error;
@@ -12,17 +12,22 @@ use tracing::error;
/// ///
/// # Example /// # Example
/// ```rust /// ```rust
/// use ldp::ResourceRequestBuilder;
/// use ldp::oxigraph::model::Dataset;
/// use ldp::reqwest::{Client, Url}; /// use ldp::reqwest::{Client, Url};
/// use ldp::reqwest_middleware::ClientBuilder; /// use ldp::reqwest_middleware::ClientBuilder;
/// use ldp::ResourceRequestBuilder;
/// ///
/// let client = ClientBuilder::new(Client::new()).build();
/// let url = Url::parse("http://server/resource")?; /// 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) /// .follow_described_by(true)
/// .accept_all_rdf_formats() /// .accept_all_rdf_formats()
/// .send(); /// .build();
/// .await?; ///
/// 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)] #[derive(Clone, Debug)]
pub struct ResourceRequestBuilder { pub struct ResourceRequestBuilder {
@@ -253,14 +258,17 @@ impl Resource {
} }
/// Parse the response. /// 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 { if let ResponseFormat::RdfFormat(format) = self.format {
let graph_url = self.described_by.as_ref().unwrap_or(&self.origin); let graph_url = self.described_by.as_ref().unwrap_or(&self.origin);
let graph = GraphNameRef::NamedNode(NamedNodeRef::new_unchecked(graph_url.as_str())); let graph = GraphNameRef::NamedNode(NamedNodeRef::new_unchecked(graph_url.as_str()));
let parser = RdfParser::from_format(format).with_default_graph(graph); let parser = RdfParser::from_format(format).with_default_graph(graph);
let body = self.response.bytes().await?; let body = self.response.bytes().await?;
let quads = parser.for_slice(&body); let quads = parser.for_slice(&body);
let dataset = quads.filter_map(Result::ok).collect::<Dataset>(); let dataset = quads.collect::<Result<_, _>>()?;
Ok(RdfSource { Ok(RdfSource {
origin: self.origin, origin: self.origin,
described_by: self.described_by, described_by: self.described_by,