diff --git a/ldp/src/container.rs b/ldp/src/container.rs new file mode 100644 index 0000000..308a822 --- /dev/null +++ b/ldp/src/container.rs @@ -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 { + match node { + vocab::ldp::BASIC_CONTAINER => Some(ContainerType::Basic), + vocab::ldp::DIRECT_CONTAINER => Some(ContainerType::Direct), + vocab::ldp::INDIRECT_CONTAINER => Some(ContainerType::Indirect), + _ => None, + } + } +} diff --git a/ldp/src/lib.rs b/ldp/src/lib.rs index 53116bc..686fe25 100644 --- a/ldp/src/lib.rs +++ b/ldp/src/lib.rs @@ -1,5 +1,6 @@ #![cfg_attr(doc, doc = include_str!("../README.md"))] +mod container; mod error; pub mod header; pub mod middleware; @@ -14,8 +15,11 @@ pub use oxigraph; pub use reqwest_middleware; pub use reqwest_middleware::reqwest; +pub use container::ContainerType; pub use error::{Error, Result}; pub use rdf_source::{ RdfSource, RdfSourceUpdateRequest, RdfSourceUpdateResponse, SerializationOptions, }; -pub use resource::{Resource, ResourceRequest, ResourceRequestBuilder, ResponseFormat}; +pub use resource::{ + Resource, ResourceRequest, ResourceRequestBuilder, ResourceType, ResponseFormat, +}; diff --git a/ldp/src/rdf_source.rs b/ldp/src/rdf_source.rs index ee508c6..b66d468 100644 --- a/ldp/src/rdf_source.rs +++ b/ldp/src/rdf_source.rs @@ -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 { pub(crate) origin: Url, - pub(crate) described_by: Option, + pub(crate) origin_type: Option, pub(crate) state_token: Option, + pub(crate) described_by: Option, + pub(crate) file_name: Option, + pub(crate) size: Option, pub(crate) dataset: D, } @@ -23,8 +27,11 @@ impl RdfSource { 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 RdfSource { &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 RdfSource { 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 { + self.size + } + /// The underlying dataset. pub fn dataset(&self) -> &D { &self.dataset diff --git a/ldp/src/resource.rs b/ldp/src/resource.rs index d22d501..de1b666 100644 --- a/ldp/src/resource.rs +++ b/ldp/src/resource.rs @@ -1,3 +1,4 @@ +use crate::container::ContainerType; use crate::rdf_source::RdfSource; use crate::vocab; 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::{ClientBuilder, ClientWithMiddleware, RequestBuilder}; use sfv::{Item, TokenRef}; +use std::collections::BTreeSet; use std::str::FromStr; use tracing::error; @@ -164,6 +166,34 @@ impl ResourceRequest { Err(crate::Error::LDPUnsupported) } + fn resource_type(response: &Response) -> Option { + 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::>(); + + // 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 { @@ -245,6 +275,8 @@ impl ResourceRequest { None }; + let resource_type = Self::resource_type(&response); + let url_to_get; let described_by = Self::extract_described_by(&response); if let Some(new_url) = &described_by @@ -286,14 +318,47 @@ impl ResourceRequest { origin: self.builder.url.clone(), described_by, state_token, + resource_type, format, - size, 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), +} + +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 { + 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. pub enum ResponseFormat { RdfFormat(RdfFormat), @@ -313,9 +378,10 @@ pub struct Resource { origin: Url, described_by: Option, state_token: Option, + resource_type: Option, format: ResponseFormat, - size: Option, file_name: Option, + size: Option, response: Response, } @@ -357,6 +423,14 @@ impl Resource { 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. /// ///
@@ -390,8 +464,11 @@ impl Resource { let dataset = quads.collect::>()?; 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 { diff --git a/ldp/src/vocab.rs b/ldp/src/vocab.rs index 29e108b..dd3931f 100644 --- a/ldp/src/vocab.rs +++ b/ldp/src/vocab.rs @@ -5,4 +5,14 @@ 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 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"); }