Compare commits
18
Commits
75069f52d6
...
master
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
95216f2e62
|
||
|
|
63bc705669
|
||
|
|
4eca2c45df
|
||
|
|
2568c98bbc
|
||
|
|
3f053cb41c
|
||
|
|
311608e37c
|
||
|
|
f267bdf8f1
|
||
|
|
49dfc9b4fc
|
||
|
|
ce65bc7739
|
||
|
|
791c125290
|
||
|
|
ddf8014561
|
||
|
|
3be41f1733
|
||
|
|
22153cd19a
|
||
|
|
7c7a078f96
|
||
|
|
c84d8cd28c
|
||
|
|
2367de77ca
|
||
|
|
e98da77871
|
||
|
|
cdc38eb9f3
|
Generated
+357
-252
File diff suppressed because it is too large
Load Diff
+4
-3
@@ -9,14 +9,15 @@ members = [
|
|||||||
ldp = { path = "ldp" }
|
ldp = { path = "ldp" }
|
||||||
|
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
base64 = "0.22"
|
base64 = "0.23"
|
||||||
bytes = "1.11"
|
bytes = "1.12"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
http = "1.4"
|
http = "1.5"
|
||||||
color-eyre = "0.6"
|
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"] }
|
||||||
|
sfv = "0.15"
|
||||||
slotmap = "1.1"
|
slotmap = "1.1"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|||||||
+4
-1
@@ -4,9 +4,12 @@ version = "0.0.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "ldctl"
|
||||||
|
doc = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ldp.workspace = true
|
ldp.workspace = true
|
||||||
futures.workspace = true
|
|
||||||
color-eyre.workspace = true
|
color-eyre.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
|
|||||||
+4
-2
@@ -1,14 +1,15 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ldp"
|
name = "ldp"
|
||||||
version = "0.0.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "A library to assist with the creation and maintenance of remote RDF data via LDP"
|
description = "A library to assist with the creation and maintenance of remote RDF data via LDP"
|
||||||
|
documentation = "https://docs.rs/ldp"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "GPL-3.0-only"
|
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"]
|
||||||
homepage = "https://graphofliberty.org"
|
homepage = "https://graphofliberty.org"
|
||||||
repository = "https://code.graphofliberty.org/graphofliberty/ldp"
|
repository = "https://code.graphofliberty.org/gl/ldp"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
keyed = ["dep:slotmap"]
|
keyed = ["dep:slotmap"]
|
||||||
@@ -22,6 +23,7 @@ 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
|
||||||
|
sfv.workspace = true
|
||||||
slotmap = { optional = true, workspace = true }
|
slotmap = { optional = true, workspace = true }
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
use crate::vocab;
|
||||||
|
use oxigraph::model::NamedNodeRef;
|
||||||
|
|
||||||
|
/// The type of [Container](https://www.w3.org/TR/ldp/#ldpc).
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum ContainerType {
|
||||||
|
Basic,
|
||||||
|
Direct,
|
||||||
|
Indirect,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContainerType {
|
||||||
|
pub fn as_named_node_ref(&self) -> NamedNodeRef<'_> {
|
||||||
|
match self {
|
||||||
|
ContainerType::Basic => vocab::ldp::BASIC_CONTAINER,
|
||||||
|
ContainerType::Direct => vocab::ldp::DIRECT_CONTAINER,
|
||||||
|
ContainerType::Indirect => vocab::ldp::INDIRECT_CONTAINER,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_named_node_ref(node: NamedNodeRef<'_>) -> Option<Self> {
|
||||||
|
match node {
|
||||||
|
vocab::ldp::BASIC_CONTAINER => Some(ContainerType::Basic),
|
||||||
|
vocab::ldp::DIRECT_CONTAINER => Some(ContainerType::Direct),
|
||||||
|
vocab::ldp::INDIRECT_CONTAINER => Some(ContainerType::Indirect),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,4 +26,7 @@ pub enum Error {
|
|||||||
/// The RDF data failed to parse.
|
/// The RDF data failed to parse.
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
InvalidRdfSyntax(#[from] oxigraph::io::RdfSyntaxError),
|
InvalidRdfSyntax(#[from] oxigraph::io::RdfSyntaxError),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
ToStr(#[from] http::header::ToStrError),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,4 +3,3 @@ use reqwest_middleware::reqwest::header::HeaderName;
|
|||||||
pub const X_STATE_TOKEN: HeaderName = HeaderName::from_static("x-state-token");
|
pub const X_STATE_TOKEN: HeaderName = HeaderName::from_static("x-state-token");
|
||||||
pub const X_IF_STATE_TOKEN: HeaderName = HeaderName::from_static("x-if-state-token");
|
pub const X_IF_STATE_TOKEN: HeaderName = HeaderName::from_static("x-if-state-token");
|
||||||
pub const PREFER: HeaderName = HeaderName::from_static("prefer");
|
pub const PREFER: HeaderName = HeaderName::from_static("prefer");
|
||||||
pub const PREFERENCE_APPLIED: HeaderName = HeaderName::from_static("preference-applied");
|
|
||||||
|
|||||||
+11
-7
@@ -1,23 +1,27 @@
|
|||||||
#![cfg_attr(doc, doc = include_str!("../README.md"))]
|
#![cfg_attr(doc, doc = include_str!("../README.md"))]
|
||||||
|
|
||||||
|
mod container;
|
||||||
mod error;
|
mod error;
|
||||||
pub mod header;
|
mod header;
|
||||||
pub mod middleware;
|
pub mod middleware;
|
||||||
|
pub mod model;
|
||||||
|
mod prefer;
|
||||||
mod rdf_source;
|
mod rdf_source;
|
||||||
mod resource;
|
mod resource;
|
||||||
pub mod traverse;
|
mod traverse;
|
||||||
pub mod vocab;
|
pub mod vocab;
|
||||||
|
|
||||||
#[cfg(feature = "keyed")]
|
|
||||||
pub mod keyed;
|
|
||||||
|
|
||||||
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 container::ContainerType;
|
||||||
pub use error::{Error, Result};
|
pub use error::{Error, Result};
|
||||||
|
pub use prefer::{Preference, header_for_preferences};
|
||||||
pub use rdf_source::{
|
pub use rdf_source::{
|
||||||
RdfSource, RdfSourceUpdateRequest, RdfSourceUpdateResponse, SerializationOptions,
|
RdfSource, RdfSourceUpdateRequest, RdfSourceUpdateResponse, SerializationOptions,
|
||||||
};
|
};
|
||||||
pub use resource::{Resource, ResourceRequest, ResourceRequestBuilder, ResponseFormat};
|
pub use resource::{
|
||||||
|
Resource, ResourceRequest, ResourceRequestBuilder, ResourceType, ResponseFormat,
|
||||||
|
};
|
||||||
|
pub use traverse::Traverse;
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
use crate::RdfSource;
|
||||||
|
use oxigraph::model::vocab::rdf;
|
||||||
|
use oxigraph::model::{Dataset, NamedNodeRef, NamedOrBlankNodeRef, TermRef};
|
||||||
|
|
||||||
|
impl RdfSource<Dataset> {
|
||||||
|
/// Return all the classes associated with this document.
|
||||||
|
pub fn classes(&self) -> impl Iterator<Item = NamedNodeRef<'_>> {
|
||||||
|
let subject =
|
||||||
|
NamedOrBlankNodeRef::NamedNode(NamedNodeRef::new_unchecked(self.origin.as_str()));
|
||||||
|
self.dataset
|
||||||
|
.quads_for_pattern(Some(subject), Some(rdf::TYPE), None, None)
|
||||||
|
.filter_map(|quad| match quad.object {
|
||||||
|
TermRef::NamedNode(node) => Some(node),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,21 @@
|
|||||||
use oxigraph::model::{Quad, QuadRef};
|
use oxigraph::model::{Quad, QuadRef};
|
||||||
use slotmap::{SecondaryMap, SlotMap, new_key_type};
|
use slotmap::{SecondaryMap, SlotMap, new_key_type};
|
||||||
|
|
||||||
new_key_type! { pub struct QuadKey; }
|
new_key_type! {
|
||||||
|
/// A type which uniquely identifies a quad.
|
||||||
|
pub struct QuadKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A set of quads stored in a [`SlotMap`].
|
||||||
|
///
|
||||||
|
/// This exists as a convenience for those who want to uniquely identify a quad and associate
|
||||||
|
/// application-specific data with it.
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct KeyedDataset<T> {
|
pub struct KeyedDataset<T> {
|
||||||
|
/// The underlying SlotMap.
|
||||||
pub quads: SlotMap<QuadKey, Quad>,
|
pub quads: SlotMap<QuadKey, Quad>,
|
||||||
|
|
||||||
|
/// Application-specific information to be associated with the quads.
|
||||||
pub associated_data: SecondaryMap<QuadKey, T>,
|
pub associated_data: SecondaryMap<QuadKey, T>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
//! This module contains functionality related to the content of RDF documents.
|
||||||
|
|
||||||
|
mod dataset;
|
||||||
|
#[cfg(feature = "keyed")]
|
||||||
|
mod keyed_dataset;
|
||||||
|
|
||||||
|
#[cfg(feature = "keyed")]
|
||||||
|
pub use keyed_dataset::{KeyedDataset, QuadKey};
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
use crate::vocab;
|
||||||
|
use http::{HeaderMap, HeaderValue};
|
||||||
|
use oxigraph::model::{NamedNode, NamedNodeRef};
|
||||||
|
|
||||||
|
/// [Request preferences](https://www.w3.org/TR/ldp/#prefer-parameters)
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum Preference {
|
||||||
|
Containment,
|
||||||
|
Membership,
|
||||||
|
MinimalContainer,
|
||||||
|
Other(NamedNode),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Preference {
|
||||||
|
pub fn as_named_node_ref(&self) -> NamedNodeRef<'_> {
|
||||||
|
match self {
|
||||||
|
Preference::Containment => vocab::ldp::PREFER_CONTAINMENT,
|
||||||
|
Preference::Membership => vocab::ldp::PREFER_MEMBERSHIP,
|
||||||
|
Preference::MinimalContainer => vocab::ldp::PREFER_MINIMAL_CONTAINER,
|
||||||
|
Preference::Other(preference) => preference.as_ref(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn header_for_preferences<'a, I, J>(include: I, omit: J) -> crate::Result<HeaderMap>
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = &'a Preference>,
|
||||||
|
J: IntoIterator<Item = &'a Preference>,
|
||||||
|
{
|
||||||
|
let include_str = include
|
||||||
|
.into_iter()
|
||||||
|
.map(|preference| preference.as_named_node_ref().as_str().to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
let omit_str = omit
|
||||||
|
.into_iter()
|
||||||
|
.map(|preference| preference.as_named_node_ref().as_str().to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
let mut value = String::from("return=representation");
|
||||||
|
|
||||||
|
if !include_str.is_empty() {
|
||||||
|
value = format!("{value}; include=\"{include_str}\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
if !omit_str.is_empty() {
|
||||||
|
value = format!("{value}; omit=\"{omit_str}\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
let header_value =
|
||||||
|
HeaderValue::from_str(&value).expect("Failed to produce Prefer header value");
|
||||||
|
|
||||||
|
Ok(HeaderMap::from_iter([(
|
||||||
|
crate::header::PREFER,
|
||||||
|
header_value,
|
||||||
|
)]))
|
||||||
|
}
|
||||||
+36
-14
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::resource::ResourceType;
|
||||||
use bytes::BufMut;
|
use bytes::BufMut;
|
||||||
use http::{StatusCode, header};
|
use http::{StatusCode, header};
|
||||||
use oxigraph::io::{RdfFormat, RdfSerializer};
|
use oxigraph::io::{RdfFormat, RdfSerializer};
|
||||||
@@ -11,8 +12,11 @@ use reqwest_middleware::reqwest::Url;
|
|||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct RdfSource<D> {
|
pub struct RdfSource<D> {
|
||||||
pub(crate) origin: Url,
|
pub(crate) origin: Url,
|
||||||
pub(crate) described_by: Option<Url>,
|
pub(crate) origin_type: Option<ResourceType>,
|
||||||
pub(crate) state_token: Option<String>,
|
pub(crate) state_token: Option<String>,
|
||||||
|
pub(crate) described_by: Option<Url>,
|
||||||
|
pub(crate) file_name: Option<String>,
|
||||||
|
pub(crate) size: Option<usize>,
|
||||||
pub(crate) dataset: D,
|
pub(crate) dataset: D,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,8 +27,11 @@ impl<D: Default> RdfSource<D> {
|
|||||||
pub fn new(origin: Url) -> Self {
|
pub fn new(origin: Url) -> Self {
|
||||||
Self {
|
Self {
|
||||||
origin,
|
origin,
|
||||||
described_by: None,
|
origin_type: None,
|
||||||
state_token: None,
|
state_token: None,
|
||||||
|
described_by: None,
|
||||||
|
file_name: None,
|
||||||
|
size: None,
|
||||||
dataset: Default::default(),
|
dataset: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,11 +43,9 @@ impl<D> RdfSource<D> {
|
|||||||
&self.origin
|
&self.origin
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The URL at which the RDF description of the resource is located.
|
/// The type of Resource that was originally fetched.
|
||||||
///
|
pub fn origin_type(&self) -> Option<&ResourceType> {
|
||||||
/// This is the URL at which updates are submitted.
|
self.origin_type.as_ref()
|
||||||
pub fn described_by(&self) -> Option<&Url> {
|
|
||||||
self.described_by.as_ref()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The state token of the resource.
|
/// The state token of the resource.
|
||||||
@@ -50,6 +55,23 @@ impl<D> RdfSource<D> {
|
|||||||
self.state_token.as_deref()
|
self.state_token.as_deref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The URL at which the RDF description of the resource is located.
|
||||||
|
///
|
||||||
|
/// This is the URL at which updates are submitted.
|
||||||
|
pub fn described_by(&self) -> Option<&Url> {
|
||||||
|
self.described_by.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The name of the file that was originally fetched.
|
||||||
|
pub fn origin_file_name(&self) -> Option<&str> {
|
||||||
|
self.file_name.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The size of the file that was originally fetched.
|
||||||
|
pub fn origin_size(&self) -> Option<usize> {
|
||||||
|
self.size
|
||||||
|
}
|
||||||
|
|
||||||
/// The underlying dataset.
|
/// The underlying dataset.
|
||||||
pub fn dataset(&self) -> &D {
|
pub fn dataset(&self) -> &D {
|
||||||
&self.dataset
|
&self.dataset
|
||||||
@@ -72,7 +94,7 @@ impl<D> RdfSource<D> {
|
|||||||
.unwrap_or(self.origin().as_str());
|
.unwrap_or(self.origin().as_str());
|
||||||
Quad::new(
|
Quad::new(
|
||||||
NamedOrBlankNode::NamedNode(NamedNode::new_unchecked(self.origin.clone())),
|
NamedOrBlankNode::NamedNode(NamedNode::new_unchecked(self.origin.clone())),
|
||||||
vocab::rdf::VALUE,
|
vocab::rdf::TYPE,
|
||||||
Term::Literal(Literal::new_simple_literal("")),
|
Term::Literal(Literal::new_simple_literal("")),
|
||||||
GraphName::NamedNode(NamedNode::new_unchecked(graph_name)),
|
GraphName::NamedNode(NamedNode::new_unchecked(graph_name)),
|
||||||
)
|
)
|
||||||
@@ -100,12 +122,12 @@ impl<D> RdfSource<D> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Holds options related to serialization.
|
/// Holds options related to serialization.
|
||||||
pub struct SerializationOptions {
|
pub struct SerializationOptions<'a> {
|
||||||
format: RdfFormat,
|
format: RdfFormat,
|
||||||
filter: Box<dyn Fn(TripleRef<'_>) -> bool>,
|
filter: Box<dyn Fn(TripleRef<'a>) -> bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SerializationOptions {
|
impl<'a> SerializationOptions<'a> {
|
||||||
/// Create a new set of serialization options with the provided format.
|
/// Create a new set of serialization options with the provided format.
|
||||||
pub fn from_format(format: RdfFormat) -> Self {
|
pub fn from_format(format: RdfFormat) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -120,7 +142,7 @@ impl SerializationOptions {
|
|||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn with_filter<F>(self, filter: F) -> Self
|
pub fn with_filter<F>(self, filter: F) -> Self
|
||||||
where
|
where
|
||||||
F: Fn(TripleRef<'_>) -> bool + 'static,
|
F: Fn(TripleRef<'a>) -> bool + 'static,
|
||||||
{
|
{
|
||||||
Self {
|
Self {
|
||||||
format: self.format,
|
format: self.format,
|
||||||
@@ -134,7 +156,7 @@ where
|
|||||||
&'a D: IntoIterator<Item = QuadRef<'a>>,
|
&'a D: IntoIterator<Item = QuadRef<'a>>,
|
||||||
{
|
{
|
||||||
/// Serializes the dataset in to the provided format.
|
/// Serializes the dataset in to the provided format.
|
||||||
pub fn serialize(&'a self, options: SerializationOptions) -> crate::Result<bytes::Bytes> {
|
pub fn serialize(&'a self, options: SerializationOptions<'a>) -> crate::Result<bytes::Bytes> {
|
||||||
let writer = bytes::BytesMut::new().writer();
|
let writer = bytes::BytesMut::new().writer();
|
||||||
let mut serializer = RdfSerializer::from_format(options.format).for_writer(writer);
|
let mut serializer = RdfSerializer::from_format(options.format).for_writer(writer);
|
||||||
|
|
||||||
@@ -159,7 +181,7 @@ where
|
|||||||
/// Prepare an update request.
|
/// Prepare an update request.
|
||||||
pub fn to_update(
|
pub fn to_update(
|
||||||
&'a self,
|
&'a self,
|
||||||
options: SerializationOptions,
|
options: SerializationOptions<'a>,
|
||||||
) -> crate::Result<RdfSourceUpdateRequest> {
|
) -> 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 = options.format.media_type().to_string();
|
let media_type = options.format.media_type().to_string();
|
||||||
|
|||||||
+233
-16
@@ -1,11 +1,16 @@
|
|||||||
|
use crate::container::ContainerType;
|
||||||
use crate::rdf_source::RdfSource;
|
use crate::rdf_source::RdfSource;
|
||||||
use crate::vocab;
|
use crate::{Preference, prefer, vocab};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures::Stream;
|
use futures::Stream;
|
||||||
|
use http::Method;
|
||||||
use oxigraph::io::{RdfFormat, RdfParser};
|
use oxigraph::io::{RdfFormat, RdfParser};
|
||||||
use oxigraph::model::{GraphNameRef, NamedNodeRef, Quad};
|
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 sfv::{Item, TokenRef};
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
use std::str::FromStr;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
/// Builds an HTTP request for a [Resource](https://www.w3.org/TR/ldp/#ldpr).
|
/// Builds an HTTP request for a [Resource](https://www.w3.org/TR/ldp/#ldpr).
|
||||||
@@ -34,7 +39,10 @@ pub struct ResourceRequestBuilder {
|
|||||||
client: ClientWithMiddleware,
|
client: ClientWithMiddleware,
|
||||||
url: Url,
|
url: Url,
|
||||||
follow_described_by: bool,
|
follow_described_by: bool,
|
||||||
|
validate_support: bool,
|
||||||
formats: Vec<RdfFormat>,
|
formats: Vec<RdfFormat>,
|
||||||
|
include_preferences: Vec<Preference>,
|
||||||
|
omit_preferences: Vec<Preference>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResourceRequestBuilder {
|
impl ResourceRequestBuilder {
|
||||||
@@ -52,17 +60,37 @@ impl ResourceRequestBuilder {
|
|||||||
client,
|
client,
|
||||||
url,
|
url,
|
||||||
follow_described_by: true,
|
follow_described_by: true,
|
||||||
|
validate_support: true,
|
||||||
formats: Vec::new(),
|
formats: Vec::new(),
|
||||||
|
include_preferences: Vec::new(),
|
||||||
|
omit_preferences: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If the HTTP response includes a [describedby](https://www.w3.org/TR/ldp/#link-relation-describedby) link rel, then it will be followed.
|
/// If the HTTP response includes a [describedby](https://www.w3.org/TR/ldp/#link-relation-describedby)
|
||||||
|
/// link rel, then it will be followed. The original URL may be accessed using [`Resource::origin`],
|
||||||
|
/// and the link rel may be accessed using [`Resource::described_by`].
|
||||||
|
///
|
||||||
|
/// Note: If your intention is to fetch a NonRDFSource (e.g. a PDF, video, etc.), this value
|
||||||
|
/// must be `false`.
|
||||||
|
///
|
||||||
|
/// Default value: `true`.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn follow_described_by(mut self, value: bool) -> Self {
|
pub fn follow_described_by(mut self, value: bool) -> Self {
|
||||||
self.follow_described_by = value;
|
self.follow_described_by = value;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Require the presence of a `Link: <http://www.w3.org/ns/ldp#Resource>; rel="type"` response header.
|
||||||
|
/// This is described in [section 4.2.1.4](https://www.w3.org/TR/ldp/#ldpr-resource) of the spec.
|
||||||
|
///
|
||||||
|
/// Default value: `true`.
|
||||||
|
#[must_use]
|
||||||
|
pub fn validate_support(mut self, value: bool) -> Self {
|
||||||
|
self.validate_support = value;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Restrict the request to the given RDF format.
|
/// Restrict the request to the given RDF format.
|
||||||
///
|
///
|
||||||
/// Repeated calls are additive. By default, all formats — even non-RDF formats — are accepted.
|
/// Repeated calls are additive. By default, all formats — even non-RDF formats — are accepted.
|
||||||
@@ -87,6 +115,18 @@ impl ResourceRequestBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Include a representation preference according to [section 7.2](https://www.w3.org/TR/ldp/#prefer-parameters).
|
||||||
|
pub fn include_preference(mut self, preference: Preference) -> Self {
|
||||||
|
self.include_preferences.push(preference);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Omit a representation preference according to [section 7.2](https://www.w3.org/TR/ldp/#prefer-parameters).
|
||||||
|
pub fn omit_preference(mut self, preference: Preference) -> Self {
|
||||||
|
self.omit_preferences.push(preference);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Build the request.
|
/// Build the request.
|
||||||
pub fn build(self) -> ResourceRequest {
|
pub fn build(self) -> ResourceRequest {
|
||||||
ResourceRequest { builder: self }
|
ResourceRequest { builder: self }
|
||||||
@@ -125,7 +165,7 @@ impl ResourceRequest {
|
|||||||
if response.status() == StatusCode::OK {
|
if response.status() == StatusCode::OK {
|
||||||
let headers = response.headers();
|
let headers = response.headers();
|
||||||
for link in headers.get_all(header::LINK) {
|
for link in headers.get_all(header::LINK) {
|
||||||
let link = link.to_str().unwrap_or("");
|
let link = link.to_str().unwrap_or_default();
|
||||||
match parse_link_header::parse(link) {
|
match parse_link_header::parse(link) {
|
||||||
Ok(link_map) => {
|
Ok(link_map) => {
|
||||||
if let Some(metadata_url) = link_map.get(&Some("type".to_string())) {
|
if let Some(metadata_url) = link_map.get(&Some("type".to_string())) {
|
||||||
@@ -142,6 +182,34 @@ impl ResourceRequest {
|
|||||||
Err(crate::Error::LDPUnsupported)
|
Err(crate::Error::LDPUnsupported)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resource_type(response: &Response) -> Option<ResourceType> {
|
||||||
|
let headers = response.headers();
|
||||||
|
let links = headers
|
||||||
|
.get_all(header::LINK)
|
||||||
|
.iter()
|
||||||
|
.map(|hv| hv.to_str().unwrap_or_default())
|
||||||
|
.filter_map(|link| parse_link_header::parse_with_rel(link).ok())
|
||||||
|
.filter_map(|link_map| link_map.get("type").map(|item| item.raw_uri.clone()))
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
|
||||||
|
// NB: The comparisons below must be done in this order, because multiple Link headers may
|
||||||
|
// be present with differing degrees of specificity. E.g. BasicContainer is more specific
|
||||||
|
// than RDFSource, but both are valid and may be present concomitantly.
|
||||||
|
if links.contains(vocab::ldp::BASIC_CONTAINER.as_str()) {
|
||||||
|
Some(ResourceType::RdfSource(Some(ContainerType::Basic)))
|
||||||
|
} else if links.contains(vocab::ldp::DIRECT_CONTAINER.as_str()) {
|
||||||
|
Some(ResourceType::RdfSource(Some(ContainerType::Direct)))
|
||||||
|
} else if links.contains(vocab::ldp::INDIRECT_CONTAINER.as_str()) {
|
||||||
|
Some(ResourceType::RdfSource(Some(ContainerType::Indirect)))
|
||||||
|
} else if links.contains(vocab::ldp::RDF_SOURCE.as_str()) {
|
||||||
|
Some(ResourceType::RdfSource(None))
|
||||||
|
} else if links.contains(vocab::ldp::NON_RDF_SOURCE.as_str()) {
|
||||||
|
Some(ResourceType::NonRdfSource)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn add_media_types(&self, mut request_builder: RequestBuilder) -> RequestBuilder {
|
fn add_media_types(&self, mut request_builder: RequestBuilder) -> RequestBuilder {
|
||||||
let media_types = self.builder.formats.iter().map(|f| f.media_type());
|
let media_types = self.builder.formats.iter().map(|f| f.media_type());
|
||||||
for media_type in media_types {
|
for media_type in media_types {
|
||||||
@@ -150,19 +218,80 @@ impl ResourceRequest {
|
|||||||
request_builder
|
request_builder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_method_allowed(response: &Response, method: Method) -> crate::Result<bool> {
|
||||||
|
for header in response.headers().get_all(header::ALLOW) {
|
||||||
|
for value in header.to_str()?.replace(" ", "").split(',') {
|
||||||
|
if value == method {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
/// Send the request.
|
/// Send the request.
|
||||||
///
|
///
|
||||||
/// There are two stages to this process. First, a HEAD request is made to determine whether
|
/// There are two stages to this process. First, a `HEAD` request is made to determine whether
|
||||||
/// the requested resource is described by an RDF graph at another location. If it is, and if
|
/// the requested resource is described by an RDF graph at another location. If it is, and if
|
||||||
/// the user permits it, then the URL in the
|
/// the user [permits it](`ResourceRequestBuilder::follow_described_by`), then the URL in the
|
||||||
/// [describedby](https://www.w3.org/TR/ldp/#link-relation-describedby) header is used.
|
/// [describedby](https://www.w3.org/TR/ldp/#link-relation-describedby) header is used.
|
||||||
/// Otherwise, the original URL is used.
|
/// Otherwise, the original URL is used.
|
||||||
///
|
///
|
||||||
/// During the second stage, a GET request is made. No parsing occurs at this time.
|
/// During the second stage, a `GET` request is made. **The response body is not consumed yet.**
|
||||||
|
/// To consume the response body, you must call either [`Resource::into_stream`] to receive the
|
||||||
|
/// raw bytes or [`Resource::into_rdf_source`] to parse the content and receive a [`RdfSource`].
|
||||||
|
///
|
||||||
|
/// If the `HEAD` method is not allowed on this URL (a violation of the LDP spec), the `Allow`
|
||||||
|
/// header is inspected for `GET`. If absent, this method will return an error.
|
||||||
pub async fn send(&self) -> crate::Result<Resource> {
|
pub async fn send(&self) -> crate::Result<Resource> {
|
||||||
let request_builder = self.builder.client.head(self.builder.url.clone());
|
let request_builder = self.builder.client.head(self.builder.url.clone());
|
||||||
let mut response = request_builder.send().await?.error_for_status()?;
|
let mut response = request_builder.send().await?;
|
||||||
Self::ensure_ldp_support(&response)?;
|
|
||||||
|
match response.error_for_status_ref() {
|
||||||
|
Ok(response) => {
|
||||||
|
if self.builder.validate_support {
|
||||||
|
Self::ensure_ldp_support(response)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) if err.status() == Some(StatusCode::METHOD_NOT_ALLOWED) => {
|
||||||
|
if !Self::is_method_allowed(&response, Method::GET)? {
|
||||||
|
return Err(err.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err => {
|
||||||
|
err?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let size = response
|
||||||
|
.headers()
|
||||||
|
.get(header::CONTENT_LENGTH)
|
||||||
|
.and_then(|hv| hv.to_str().ok())
|
||||||
|
.and_then(|et| usize::from_str(et).ok());
|
||||||
|
|
||||||
|
let content_disposition = response
|
||||||
|
.headers()
|
||||||
|
.get(header::CONTENT_DISPOSITION)
|
||||||
|
.and_then(|hv| hv.to_str().ok());
|
||||||
|
|
||||||
|
let file_name = if let Some(content_disposition) = content_disposition {
|
||||||
|
sfv::Parser::new(content_disposition)
|
||||||
|
.parse::<Item>()
|
||||||
|
.ok()
|
||||||
|
.and_then(|item| {
|
||||||
|
if item.bare_item.as_token() == Some(TokenRef::constant("attachment")) {
|
||||||
|
item.params
|
||||||
|
.get("filename")
|
||||||
|
.and_then(|item| item.as_string().map(|s| s.to_string()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let resource_type = Self::resource_type(&response);
|
||||||
|
|
||||||
let url_to_get;
|
let url_to_get;
|
||||||
let described_by = Self::extract_described_by(&response);
|
let described_by = Self::extract_described_by(&response);
|
||||||
@@ -177,8 +306,20 @@ impl ResourceRequest {
|
|||||||
let mut request_builder = self.builder.client.get(url_to_get);
|
let mut request_builder = self.builder.client.get(url_to_get);
|
||||||
request_builder = self.add_media_types(request_builder);
|
request_builder = self.add_media_types(request_builder);
|
||||||
|
|
||||||
|
if !self.builder.include_preferences.is_empty() || !self.builder.omit_preferences.is_empty()
|
||||||
|
{
|
||||||
|
let header = prefer::header_for_preferences(
|
||||||
|
&self.builder.include_preferences,
|
||||||
|
&self.builder.omit_preferences,
|
||||||
|
)?;
|
||||||
|
request_builder = request_builder.headers(header);
|
||||||
|
}
|
||||||
|
|
||||||
response = request_builder.send().await?.error_for_status()?;
|
response = request_builder.send().await?.error_for_status()?;
|
||||||
|
|
||||||
|
if self.builder.validate_support {
|
||||||
Self::ensure_ldp_support(&response)?;
|
Self::ensure_ldp_support(&response)?;
|
||||||
|
}
|
||||||
|
|
||||||
let state_token = response
|
let state_token = response
|
||||||
.headers()
|
.headers()
|
||||||
@@ -202,12 +343,47 @@ impl ResourceRequest {
|
|||||||
origin: self.builder.url.clone(),
|
origin: self.builder.url.clone(),
|
||||||
described_by,
|
described_by,
|
||||||
state_token,
|
state_token,
|
||||||
|
resource_type,
|
||||||
format,
|
format,
|
||||||
|
file_name,
|
||||||
|
size,
|
||||||
response,
|
response,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The type of [Resource](https://www.w3.org/TR/ldp/#ldpr-resource).
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum ResourceType {
|
||||||
|
NonRdfSource,
|
||||||
|
RdfSource(Option<ContainerType>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResourceType {
|
||||||
|
pub fn as_named_node_ref(&self) -> NamedNodeRef<'_> {
|
||||||
|
match self {
|
||||||
|
ResourceType::NonRdfSource => vocab::ldp::NON_RDF_SOURCE,
|
||||||
|
ResourceType::RdfSource(None) => vocab::ldp::RDF_SOURCE,
|
||||||
|
ResourceType::RdfSource(Some(container_type)) => container_type.as_named_node_ref(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_named_node_ref(node: NamedNodeRef<'_>) -> Option<Self> {
|
||||||
|
match node {
|
||||||
|
vocab::ldp::NON_RDF_SOURCE => Some(ResourceType::NonRdfSource),
|
||||||
|
vocab::ldp::RDF_SOURCE => Some(ResourceType::RdfSource(None)),
|
||||||
|
_ => {
|
||||||
|
let container_type = ContainerType::from_named_node_ref(node);
|
||||||
|
if container_type.is_some() {
|
||||||
|
Some(ResourceType::RdfSource(container_type))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The format of the response, as determined by the `Content-Type` HTTP header.
|
/// The format of the response, as determined by the `Content-Type` HTTP header.
|
||||||
pub enum ResponseFormat {
|
pub enum ResponseFormat {
|
||||||
RdfFormat(RdfFormat),
|
RdfFormat(RdfFormat),
|
||||||
@@ -217,17 +393,20 @@ pub enum ResponseFormat {
|
|||||||
|
|
||||||
/// A LDP [Resource](https://www.w3.org/TR/ldp/#ldpr).
|
/// A LDP [Resource](https://www.w3.org/TR/ldp/#ldpr).
|
||||||
///
|
///
|
||||||
/// A Resource on its own is not very useful, as it is a very abstract concept. If the intention is
|
/// A Resource on its own is not very useful, as it is a very abstract concept. If your intention is
|
||||||
/// to treat the response as opaque data (such as a media file), call [`Resource::into_stream`]. In
|
/// to treat the response as opaque data (such as a PDF or video file), call [`Resource::into_stream`].
|
||||||
/// the spec, this is equivalent to a [NonRDFSource](https://www.w3.org/TR/ldp/#ldpnr).
|
/// In the spec, this is equivalent to a [NonRDFSource](https://www.w3.org/TR/ldp/#ldpnr).
|
||||||
///
|
///
|
||||||
/// If the intention is to treat the response as RDF data to be parsed, call
|
/// If your intention is to treat the response as RDF data to be parsed, call
|
||||||
/// [`Resource::into_rdf_source`].
|
/// [`Resource::into_rdf_source`].
|
||||||
pub struct Resource {
|
pub struct Resource {
|
||||||
origin: Url,
|
origin: Url,
|
||||||
described_by: Option<Url>,
|
described_by: Option<Url>,
|
||||||
state_token: Option<String>,
|
state_token: Option<String>,
|
||||||
|
resource_type: Option<ResourceType>,
|
||||||
format: ResponseFormat,
|
format: ResponseFormat,
|
||||||
|
file_name: Option<String>,
|
||||||
|
size: Option<usize>,
|
||||||
response: Response,
|
response: Response,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,24 +423,59 @@ impl Resource {
|
|||||||
self.described_by.as_ref()
|
self.described_by.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The format of the response.
|
/// The format of the response, as reported by the `Content-Type` HTTP header.
|
||||||
pub fn format(&self) -> &ResponseFormat {
|
pub fn format(&self) -> &ResponseFormat {
|
||||||
&self.format
|
&self.format
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The size of the content, as reported by the `Content-Length` HTTP header.
|
||||||
|
///
|
||||||
|
/// Note: This value is extracted from the response headers of the HEAD request for the origin
|
||||||
|
/// URL.
|
||||||
|
pub fn size(&self) -> Option<usize> {
|
||||||
|
self.size
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The file name of the content, as reported by the `Content-Disposition` HTTP header.
|
||||||
|
/// Example:
|
||||||
|
/// ```
|
||||||
|
/// Content-Disposition: attachment; filename="new-king-james-version-en.pdf"
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Note: This value is extracted from the response headers of the HEAD request for the origin
|
||||||
|
/// URL.
|
||||||
|
pub fn file_name(&self) -> Option<&str> {
|
||||||
|
self.file_name.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The type of Resource, as reported by the `Link: <...> rel="type"` HTTP header.
|
||||||
|
///
|
||||||
|
/// Note: This value is extracted from the response headers of the HEAD request for the origin
|
||||||
|
/// URL.
|
||||||
|
pub fn resource_type(&self) -> Option<&ResourceType> {
|
||||||
|
self.resource_type.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
/// The state token, as extracted from the `X-State-Token` header.
|
/// The state token, as extracted from the `X-State-Token` header.
|
||||||
///
|
///
|
||||||
/// Note that this is a feature [specific to Fedora](https://fedora.info/2021/05/01/spec/#state-tokens).
|
/// <div class="warning">
|
||||||
|
/// This is a feature specific to <a href="https://fedora.info/2021/05/01/spec/#state-tokens">Fedora</a>.
|
||||||
|
/// </div>
|
||||||
pub fn state_token(&self) -> Option<&String> {
|
pub fn state_token(&self) -> Option<&String> {
|
||||||
self.state_token.as_ref()
|
self.state_token.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract the response as a stream of unparsed bytes.
|
/// Provide the response body as a stream of bytes.
|
||||||
pub fn into_stream(self) -> impl Stream<Item = reqwest_middleware::reqwest::Result<Bytes>> {
|
pub fn into_stream(self) -> impl Stream<Item = reqwest_middleware::reqwest::Result<Bytes>> {
|
||||||
self.response.bytes_stream()
|
self.response.bytes_stream()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse the response.
|
/// Parse the response.
|
||||||
|
///
|
||||||
|
/// The `D` type parameter can be any type capable of storing [`Quad`]s. If you intend to query
|
||||||
|
/// the dataset, use [`Dataset`](`oxigraph::model::Dataset`). If you want a stable ordering of the
|
||||||
|
/// quads, a [`KeyedDataset`](`crate::model::KeyedDataset`), [`Vec`], or similar data structure
|
||||||
|
/// may be used.
|
||||||
pub async fn into_rdf_source<D>(self) -> crate::Result<RdfSource<D>>
|
pub async fn into_rdf_source<D>(self) -> crate::Result<RdfSource<D>>
|
||||||
where
|
where
|
||||||
D: FromIterator<Quad>,
|
D: FromIterator<Quad>,
|
||||||
@@ -275,8 +489,11 @@ impl Resource {
|
|||||||
let dataset = quads.collect::<Result<_, _>>()?;
|
let dataset = quads.collect::<Result<_, _>>()?;
|
||||||
Ok(RdfSource {
|
Ok(RdfSource {
|
||||||
origin: self.origin,
|
origin: self.origin,
|
||||||
described_by: self.described_by,
|
origin_type: self.resource_type,
|
||||||
state_token: self.state_token,
|
state_token: self.state_token,
|
||||||
|
described_by: self.described_by,
|
||||||
|
file_name: self.file_name,
|
||||||
|
size: self.size,
|
||||||
dataset,
|
dataset,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use reqwest_middleware::reqwest::Url;
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::task::{Context, Poll, ready};
|
use std::task::{Context, Poll, ready};
|
||||||
|
use tracing::debug_span;
|
||||||
|
|
||||||
/// Recursively traverse an entire repository.
|
/// Recursively traverse an entire repository.
|
||||||
///
|
///
|
||||||
@@ -21,6 +22,7 @@ pub struct Traverse<'a> {
|
|||||||
visited_urls: HashSet<Url>,
|
visited_urls: HashSet<Url>,
|
||||||
pending_urls: HashSet<Url>,
|
pending_urls: HashSet<Url>,
|
||||||
parallelism: Option<usize>,
|
parallelism: Option<usize>,
|
||||||
|
span: tracing::Span,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Traverse<'a> {
|
impl<'a> Traverse<'a> {
|
||||||
@@ -35,6 +37,7 @@ impl<'a> Traverse<'a> {
|
|||||||
visited_urls: HashSet::new(),
|
visited_urls: HashSet::new(),
|
||||||
pending_urls: HashSet::new(),
|
pending_urls: HashSet::new(),
|
||||||
parallelism,
|
parallelism,
|
||||||
|
span: debug_span!("Traverse Repository", %root),
|
||||||
};
|
};
|
||||||
this.visited_urls.insert(root.clone());
|
this.visited_urls.insert(root.clone());
|
||||||
this.stream
|
this.stream
|
||||||
@@ -89,6 +92,7 @@ impl<'a> Stream for Traverse<'a> {
|
|||||||
type Item = crate::Result<RdfSource<Dataset>>;
|
type Item = crate::Result<RdfSource<Dataset>>;
|
||||||
|
|
||||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||||
|
let _enter = self.span.clone().entered();
|
||||||
let result = match ready!(self.stream.poll_next_unpin(cx)) {
|
let result = match ready!(self.stream.poll_next_unpin(cx)) {
|
||||||
Some(Ok(document)) => {
|
Some(Ok(document)) => {
|
||||||
self.visited_urls.insert(document.origin().clone());
|
self.visited_urls.insert(document.origin().clone());
|
||||||
|
|||||||
@@ -5,4 +5,23 @@ pub mod ldp {
|
|||||||
NamedNodeRef::new_unchecked("http://www.w3.org/ns/ldp#contains");
|
NamedNodeRef::new_unchecked("http://www.w3.org/ns/ldp#contains");
|
||||||
pub const RESOURCE: NamedNodeRef<'_> =
|
pub const RESOURCE: NamedNodeRef<'_> =
|
||||||
NamedNodeRef::new_unchecked("http://www.w3.org/ns/ldp#Resource");
|
NamedNodeRef::new_unchecked("http://www.w3.org/ns/ldp#Resource");
|
||||||
|
pub const NON_RDF_SOURCE: NamedNodeRef<'_> =
|
||||||
|
NamedNodeRef::new_unchecked("http://www.w3.org/ns/ldp#NonRDFSource");
|
||||||
|
pub const RDF_SOURCE: NamedNodeRef<'_> =
|
||||||
|
NamedNodeRef::new_unchecked("http://www.w3.org/ns/ldp#RDFSource");
|
||||||
|
pub const CONTAINER: NamedNodeRef<'_> =
|
||||||
|
NamedNodeRef::new_unchecked("http://www.w3.org/ns/ldp#Container");
|
||||||
|
pub const BASIC_CONTAINER: NamedNodeRef<'_> =
|
||||||
|
NamedNodeRef::new_unchecked("http://www.w3.org/ns/ldp#BasicContainer");
|
||||||
|
pub const DIRECT_CONTAINER: NamedNodeRef<'_> =
|
||||||
|
NamedNodeRef::new_unchecked("http://www.w3.org/ns/ldp#DirectContainer");
|
||||||
|
pub const INDIRECT_CONTAINER: NamedNodeRef<'_> =
|
||||||
|
NamedNodeRef::new_unchecked("http://www.w3.org/ns/ldp#IndirectContainer");
|
||||||
|
|
||||||
|
pub const PREFER_CONTAINMENT: NamedNodeRef<'_> =
|
||||||
|
NamedNodeRef::new_unchecked("http://www.w3.org/ns/ldp#PreferContainment");
|
||||||
|
pub const PREFER_MEMBERSHIP: NamedNodeRef<'_> =
|
||||||
|
NamedNodeRef::new_unchecked("http://www.w3.org/ns/ldp#PreferMembership");
|
||||||
|
pub const PREFER_MINIMAL_CONTAINER: NamedNodeRef<'_> =
|
||||||
|
NamedNodeRef::new_unchecked("http://www.w3.org/ns/ldp#PreferMinimalContainer");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user