Compare commits

..
16 Commits
Author SHA256 Message Date
alex 4eca2c45df Lint 2026-09-02 22:28:56 -04:00
alex 2568c98bbc Bump dependencies 2026-09-02 22:14:19 -04:00
alex 3f053cb41c Add support for emitting Prefer header 2026-09-02 22:10:08 -04:00
alex 311608e37c Add Container vocabulary term 2026-09-02 18:37:59 -04:00
alex f267bdf8f1 Begin support for Resource names, sizes, and types 2026-09-02 18:16:03 -04:00
alex 49dfc9b4fc Add support for extracting the file name and size from HTTP response 2026-09-02 14:45:29 -04:00
alex ce65bc7739 Bump dependencies 2026-07-24 18:02:27 -04:00
alex 791c125290 Lint and format 2026-07-24 17:35:07 -04:00
alex ddf8014561 Gracefully handle HTTP 405 errors 2026-07-24 17:27:59 -04:00
alex 3be41f1733 Bump dependencies 2026-07-20 23:26:10 -04:00
alex 22153cd19a Add Span to repository traversal 2026-07-20 23:25:52 -04:00
alex 7c7a078f96 Change default predicate when adding a new Quad 2026-07-20 23:16:15 -04:00
alex c84d8cd28c Bump dependencies 2026-07-07 21:17:40 -04:00
alex 2367de77ca Add explicit lifetime for SerializationOptions 2026-07-07 21:13:36 -04:00
alex e98da77871 Bump dependencies 2026-07-06 17:02:30 -04:00
alex cdc38eb9f3 Add classes function for Datasets, reorg modules 2026-05-22 17:23:36 -04:00
16 changed files with 799 additions and 298 deletions
Generated
+357 -252
View File
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -9,14 +9,15 @@ members = [
ldp = { path = "ldp" }
async-trait = "0.1"
base64 = "0.22"
bytes = "1.11"
base64 = "0.23"
bytes = "1.12"
futures = "0.3"
http = "1.4"
http = "1.5"
color-eyre = "0.6"
oxigraph = "0.5"
parse_link_header = "0.4"
reqwest-middleware = { version = "0.5", features = ["stream"] }
sfv = "0.15"
slotmap = "1.1"
thiserror = "2"
tokio = { version = "1", features = ["full"] }
+4 -1
View File
@@ -4,9 +4,12 @@ version = "0.0.0"
edition = "2024"
publish = false
[[bin]]
name = "ldctl"
doc = false
[dependencies]
ldp.workspace = true
futures.workspace = true
color-eyre.workspace = true
tokio.workspace = true
tracing.workspace = true
+3 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "ldp"
version = "0.0.0"
version = "0.1.0"
edition = "2024"
description = "A library to assist with the creation and maintenance of remote RDF data via LDP"
readme = "README.md"
@@ -8,7 +8,7 @@ license = "GPL-3.0-only"
keywords = ["ldp", "rdf", "sparql"]
categories = ["database", "web-programming::http-client"]
homepage = "https://graphofliberty.org"
repository = "https://code.graphofliberty.org/graphofliberty/ldp"
repository = "https://code.graphofliberty.org/gl/ldp"
[features]
keyed = ["dep:slotmap"]
@@ -22,6 +22,7 @@ http.workspace = true
oxigraph.workspace = true
parse_link_header.workspace = true
reqwest-middleware.workspace = true
sfv.workspace = true
slotmap = { optional = true, workspace = true }
thiserror.workspace = true
tracing.workspace = true
+29
View File
@@ -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)]
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,
}
}
}
+3
View File
@@ -26,4 +26,7 @@ pub enum Error {
/// The RDF data failed to parse.
#[error(transparent)]
InvalidRdfSyntax(#[from] oxigraph::io::RdfSyntaxError),
#[error(transparent)]
ToStr(#[from] http::header::ToStrError),
}
-1
View File
@@ -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_IF_STATE_TOKEN: HeaderName = HeaderName::from_static("x-if-state-token");
pub const PREFER: HeaderName = HeaderName::from_static("prefer");
pub const PREFERENCE_APPLIED: HeaderName = HeaderName::from_static("preference-applied");
+11 -7
View File
@@ -1,23 +1,27 @@
#![cfg_attr(doc, doc = include_str!("../README.md"))]
mod container;
mod error;
pub mod header;
mod header;
pub mod middleware;
pub mod model;
mod prefer;
mod rdf_source;
mod resource;
pub mod traverse;
mod traverse;
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 container::ContainerType;
pub use error::{Error, Result};
pub use prefer::{Preference, header_for_preferences};
pub use rdf_source::{
RdfSource, RdfSourceUpdateRequest, RdfSourceUpdateResponse, SerializationOptions,
};
pub use resource::{Resource, ResourceRequest, ResourceRequestBuilder, ResponseFormat};
pub use resource::{
Resource, ResourceRequest, ResourceRequestBuilder, ResourceType, ResponseFormat,
};
pub use traverse::Traverse;
+17
View File
@@ -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 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)]
pub struct KeyedDataset<T> {
/// The underlying SlotMap.
pub quads: SlotMap<QuadKey, Quad>,
/// Application-specific information to be associated with the quads.
pub associated_data: SecondaryMap<QuadKey, T>,
}
+8
View File
@@ -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};
+59
View File
@@ -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
View File
@@ -1,3 +1,4 @@
use crate::resource::ResourceType;
use bytes::BufMut;
use http::{StatusCode, header};
use oxigraph::io::{RdfFormat, RdfSerializer};
@@ -11,8 +12,11 @@ use reqwest_middleware::reqwest::Url;
#[derive(Clone, Debug)]
pub struct RdfSource<D> {
pub(crate) origin: Url,
pub(crate) described_by: Option<Url>,
pub(crate) origin_type: Option<ResourceType>,
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,
}
@@ -23,8 +27,11 @@ impl<D: Default> RdfSource<D> {
pub fn new(origin: Url) -> Self {
Self {
origin,
described_by: None,
origin_type: None,
state_token: None,
described_by: None,
file_name: None,
size: None,
dataset: Default::default(),
}
}
@@ -36,11 +43,9 @@ impl<D> RdfSource<D> {
&self.origin
}
/// 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 type of Resource that was originally fetched.
pub fn origin_type(&self) -> Option<&ResourceType> {
self.origin_type.as_ref()
}
/// The state token of the resource.
@@ -50,6 +55,23 @@ impl<D> RdfSource<D> {
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.
pub fn dataset(&self) -> &D {
&self.dataset
@@ -72,7 +94,7 @@ impl<D> RdfSource<D> {
.unwrap_or(self.origin().as_str());
Quad::new(
NamedOrBlankNode::NamedNode(NamedNode::new_unchecked(self.origin.clone())),
vocab::rdf::VALUE,
vocab::rdf::TYPE,
Term::Literal(Literal::new_simple_literal("")),
GraphName::NamedNode(NamedNode::new_unchecked(graph_name)),
)
@@ -100,12 +122,12 @@ impl<D> RdfSource<D> {
}
/// Holds options related to serialization.
pub struct SerializationOptions {
pub struct SerializationOptions<'a> {
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.
pub fn from_format(format: RdfFormat) -> Self {
Self {
@@ -120,7 +142,7 @@ impl SerializationOptions {
#[must_use]
pub fn with_filter<F>(self, filter: F) -> Self
where
F: Fn(TripleRef<'_>) -> bool + 'static,
F: Fn(TripleRef<'a>) -> bool + 'static,
{
Self {
format: self.format,
@@ -134,7 +156,7 @@ 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> {
pub fn serialize(&'a self, options: SerializationOptions<'a>) -> crate::Result<bytes::Bytes> {
let writer = bytes::BytesMut::new().writer();
let mut serializer = RdfSerializer::from_format(options.format).for_writer(writer);
@@ -159,7 +181,7 @@ where
/// Prepare an update request.
pub fn to_update(
&'a self,
options: SerializationOptions,
options: SerializationOptions<'a>,
) -> crate::Result<RdfSourceUpdateRequest> {
let url = self.described_by.clone().unwrap_or(self.origin.clone());
let media_type = options.format.media_type().to_string();
+234 -17
View File
@@ -1,11 +1,16 @@
use crate::container::ContainerType;
use crate::rdf_source::RdfSource;
use crate::vocab;
use crate::{Preference, prefer, vocab};
use bytes::Bytes;
use futures::Stream;
use http::Method;
use oxigraph::io::{RdfFormat, RdfParser};
use oxigraph::model::{GraphNameRef, NamedNodeRef, Quad};
use reqwest_middleware::reqwest::{Client, Response, StatusCode, Url, header};
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware, RequestBuilder};
use sfv::{Item, TokenRef};
use std::collections::BTreeSet;
use std::str::FromStr;
use tracing::error;
/// Builds an HTTP request for a [Resource](https://www.w3.org/TR/ldp/#ldpr).
@@ -34,7 +39,10 @@ pub struct ResourceRequestBuilder {
client: ClientWithMiddleware,
url: Url,
follow_described_by: bool,
validate_support: bool,
formats: Vec<RdfFormat>,
include_preferences: Vec<Preference>,
omit_preferences: Vec<Preference>,
}
impl ResourceRequestBuilder {
@@ -52,17 +60,37 @@ impl ResourceRequestBuilder {
client,
url,
follow_described_by: true,
validate_support: true,
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]
pub fn follow_described_by(mut self, value: bool) -> Self {
self.follow_described_by = value;
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.
///
/// Repeated calls are additive. By default, all formats — even non-RDF formats — are accepted.
@@ -87,6 +115,18 @@ impl ResourceRequestBuilder {
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.
pub fn build(self) -> ResourceRequest {
ResourceRequest { builder: self }
@@ -125,7 +165,7 @@ impl ResourceRequest {
if response.status() == StatusCode::OK {
let headers = response.headers();
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) {
Ok(link_map) => {
if let Some(metadata_url) = link_map.get(&Some("type".to_string())) {
@@ -142,6 +182,34 @@ impl ResourceRequest {
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 {
let media_types = self.builder.formats.iter().map(|f| f.media_type());
for media_type in media_types {
@@ -150,19 +218,80 @@ impl ResourceRequest {
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.
///
/// 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 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.
/// 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> {
let request_builder = self.builder.client.head(self.builder.url.clone());
let mut response = request_builder.send().await?.error_for_status()?;
Self::ensure_ldp_support(&response)?;
let mut response = request_builder.send().await?;
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 described_by = Self::extract_described_by(&response);
@@ -177,8 +306,20 @@ impl ResourceRequest {
let mut request_builder = self.builder.client.get(url_to_get);
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()?;
Self::ensure_ldp_support(&response)?;
if self.builder.validate_support {
Self::ensure_ldp_support(&response)?;
}
let state_token = response
.headers()
@@ -202,12 +343,47 @@ impl ResourceRequest {
origin: self.builder.url.clone(),
described_by,
state_token,
resource_type,
format,
file_name,
size,
response,
})
}
}
/// The type of [Resource](https://www.w3.org/TR/ldp/#ldpr-resource).
#[derive(Clone, Debug)]
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.
pub enum ResponseFormat {
RdfFormat(RdfFormat),
@@ -217,17 +393,20 @@ pub enum ResponseFormat {
/// 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
/// to treat the response as opaque data (such as a media file), call [`Resource::into_stream`]. In
/// the spec, this is equivalent to a [NonRDFSource](https://www.w3.org/TR/ldp/#ldpnr).
/// 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 PDF or video file), call [`Resource::into_stream`].
/// 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`].
pub struct Resource {
origin: Url,
described_by: Option<Url>,
state_token: Option<String>,
resource_type: Option<ResourceType>,
format: ResponseFormat,
file_name: Option<String>,
size: Option<usize>,
response: Response,
}
@@ -244,24 +423,59 @@ impl Resource {
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 {
&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.
///
/// 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> {
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>> {
self.response.bytes_stream()
}
/// 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>>
where
D: FromIterator<Quad>,
@@ -275,8 +489,11 @@ impl Resource {
let dataset = quads.collect::<Result<_, _>>()?;
Ok(RdfSource {
origin: self.origin,
described_by: self.described_by,
origin_type: self.resource_type,
state_token: self.state_token,
described_by: self.described_by,
file_name: self.file_name,
size: self.size,
dataset,
})
} else {
+4
View File
@@ -7,6 +7,7 @@ use reqwest_middleware::reqwest::Url;
use std::collections::HashSet;
use std::pin::Pin;
use std::task::{Context, Poll, ready};
use tracing::debug_span;
/// Recursively traverse an entire repository.
///
@@ -21,6 +22,7 @@ pub struct Traverse<'a> {
visited_urls: HashSet<Url>,
pending_urls: HashSet<Url>,
parallelism: Option<usize>,
span: tracing::Span,
}
impl<'a> Traverse<'a> {
@@ -35,6 +37,7 @@ impl<'a> Traverse<'a> {
visited_urls: HashSet::new(),
pending_urls: HashSet::new(),
parallelism,
span: debug_span!("Traverse Repository", %root),
};
this.visited_urls.insert(root.clone());
this.stream
@@ -89,6 +92,7 @@ impl<'a> Stream for Traverse<'a> {
type Item = crate::Result<RdfSource<Dataset>>;
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)) {
Some(Ok(document)) => {
self.visited_urls.insert(document.origin().clone());
+19
View File
@@ -5,4 +5,23 @@ pub mod ldp {
NamedNodeRef::new_unchecked("http://www.w3.org/ns/ldp#contains");
pub const RESOURCE: NamedNodeRef<'_> =
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");
}