Begin support for Resource names, sizes, and types

This commit is contained in:
2026-09-02 18:16:03 -04:00
parent 49dfc9b4fc
commit f267bdf8f1
5 changed files with 153 additions and 11 deletions
+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(&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(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,
}
}
}
+5 -1
View File
@@ -1,5 +1,6 @@
#![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; pub mod header;
pub mod middleware; pub mod middleware;
@@ -14,8 +15,11 @@ 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 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,
};
+29 -7
View File
@@ -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
+80 -3
View File
@@ -1,3 +1,4 @@
use crate::container::ContainerType;
use crate::rdf_source::RdfSource; use crate::rdf_source::RdfSource;
use crate::vocab; use crate::vocab;
use bytes::Bytes; use bytes::Bytes;
@@ -8,6 +9,7 @@ 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 sfv::{Item, TokenRef};
use std::collections::BTreeSet;
use std::str::FromStr; use std::str::FromStr;
use tracing::error; use tracing::error;
@@ -164,6 +166,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 {
@@ -245,6 +275,8 @@ impl ResourceRequest {
None 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);
if let Some(new_url) = &described_by if let Some(new_url) = &described_by
@@ -286,14 +318,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,
size,
file_name, file_name,
size,
response, 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(&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(),
}
}
pub fn from_named_node(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(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),
@@ -313,9 +378,10 @@ 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,
size: Option<usize>,
file_name: Option<String>, file_name: Option<String>,
size: Option<usize>,
response: Response, response: Response,
} }
@@ -357,6 +423,14 @@ impl Resource {
self.file_name.as_deref() 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.
/// ///
/// <div class="warning"> /// <div class="warning">
@@ -390,8 +464,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 {
+10
View File
@@ -5,4 +5,14 @@ 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 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");
} }