Make RdfSource generic over the dataset type
This commit is contained in:
Generated
+12
-2
@@ -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"
|
||||||
|
|||||||
@@ -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
@@ -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(())
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
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};
|
||||||
|
|||||||
+116
-16
@@ -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,23 +50,104 @@ 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 {
|
||||||
serializer.serialize_quad(quad)?;
|
if (options.filter)(TripleRef::from(quad)) {
|
||||||
|
serializer.serialize_quad(quad)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for quad in &self.dataset {
|
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.
|
/// 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
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user