Make RdfSource generic over the dataset type
This commit is contained in:
Generated
+12
-2
@@ -244,9 +244,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
@@ -687,6 +687,7 @@ dependencies = [
|
||||
"oxigraph",
|
||||
"parse_link_header",
|
||||
"reqwest-middleware",
|
||||
"slotmap",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
]
|
||||
@@ -1308,6 +1309,15 @@ version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
|
||||
@@ -17,6 +17,7 @@ color-eyre = "0.6"
|
||||
oxigraph = "0.5"
|
||||
parse_link_header = "0.4"
|
||||
reqwest-middleware = { version = "0.5", features = ["stream"] }
|
||||
slotmap = "1.1"
|
||||
thiserror = "2"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tracing = "0.1"
|
||||
|
||||
+3
-2
@@ -1,5 +1,6 @@
|
||||
use ldp::ResourceRequestBuilder;
|
||||
use ldp::middleware::BasicAuthMiddleware;
|
||||
use ldp::oxigraph::model::Dataset;
|
||||
use ldp::reqwest::{Client, Url};
|
||||
use ldp::reqwest_middleware::ClientBuilder;
|
||||
use tracing::debug;
|
||||
@@ -28,9 +29,9 @@ async fn main() -> color_eyre::Result<()> {
|
||||
.build();
|
||||
|
||||
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() {
|
||||
debug!(?quad);
|
||||
println!("{:#?}", quad);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -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
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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
@@ -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};
|
||||
|
||||
+114
-14
@@ -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,35 +50,119 @@ 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 {
|
||||
if (options.filter)(TripleRef::from(quad)) {
|
||||
serializer.serialize_quad(quad)?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for quad in &self.dataset {
|
||||
if (options.filter)(TripleRef::from(quad)) {
|
||||
serializer.serialize_triple(quad)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let finished_writer = serializer.finish()?;
|
||||
Ok(finished_writer.into_inner().freeze())
|
||||
}
|
||||
|
||||
/// 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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user