diff --git a/ldp/src/container.rs b/ldp/src/container.rs index 308a822..f6c9d57 100644 --- a/ldp/src/container.rs +++ b/ldp/src/container.rs @@ -10,7 +10,7 @@ pub enum ContainerType { } impl ContainerType { - pub fn as_named_node(&self) -> NamedNodeRef<'_> { + pub fn as_named_node_ref(&self) -> NamedNodeRef<'_> { match self { ContainerType::Basic => vocab::ldp::BASIC_CONTAINER, ContainerType::Direct => vocab::ldp::DIRECT_CONTAINER, @@ -18,7 +18,7 @@ impl ContainerType { } } - pub fn from_named_node(node: NamedNodeRef<'_>) -> Option { + pub fn from_named_node_ref(node: NamedNodeRef<'_>) -> Option { match node { vocab::ldp::BASIC_CONTAINER => Some(ContainerType::Basic), vocab::ldp::DIRECT_CONTAINER => Some(ContainerType::Direct), diff --git a/ldp/src/header.rs b/ldp/src/header.rs index 63042ac..8326284 100644 --- a/ldp/src/header.rs +++ b/ldp/src/header.rs @@ -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"); diff --git a/ldp/src/lib.rs b/ldp/src/lib.rs index 686fe25..a59c32d 100644 --- a/ldp/src/lib.rs +++ b/ldp/src/lib.rs @@ -2,24 +2,26 @@ 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; -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, ResourceType, ResponseFormat, }; +pub use traverse::Traverse; diff --git a/ldp/src/model/mod.rs b/ldp/src/model/mod.rs index c01753f..b52ecfa 100644 --- a/ldp/src/model/mod.rs +++ b/ldp/src/model/mod.rs @@ -1,3 +1,5 @@ +//! This module contains functionality related to the content of RDF documents. + mod dataset; #[cfg(feature = "keyed")] mod keyed_dataset; diff --git a/ldp/src/prefer.rs b/ldp/src/prefer.rs new file mode 100644 index 0000000..4b0724b --- /dev/null +++ b/ldp/src/prefer.rs @@ -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 +where + I: IntoIterator, + J: IntoIterator, +{ + let include_str = include + .into_iter() + .map(|preference| preference.as_named_node_ref().as_str().to_string()) + .collect::>() + .join(" "); + + let omit_str = omit + .into_iter() + .map(|preference| preference.as_named_node_ref().as_str().to_string()) + .collect::>() + .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, + )])) +} diff --git a/ldp/src/resource.rs b/ldp/src/resource.rs index de1b666..d99078f 100644 --- a/ldp/src/resource.rs +++ b/ldp/src/resource.rs @@ -1,6 +1,6 @@ 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; @@ -41,6 +41,8 @@ pub struct ResourceRequestBuilder { follow_described_by: bool, validate_support: bool, formats: Vec, + include_preferences: Vec, + omit_preferences: Vec, } impl ResourceRequestBuilder { @@ -60,6 +62,8 @@ impl ResourceRequestBuilder { follow_described_by: true, validate_support: true, formats: Vec::new(), + include_preferences: Vec::new(), + omit_preferences: Vec::new(), } } @@ -111,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 } @@ -290,6 +306,15 @@ 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()?; if self.builder.validate_support { @@ -335,20 +360,20 @@ pub enum ResourceType { } impl ResourceType { - pub fn as_named_node(&self) -> NamedNodeRef<'_> { + 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(), + ResourceType::RdfSource(Some(container_type)) => container_type.as_named_node_ref(), } } - pub fn from_named_node(node: NamedNodeRef<'_>) -> Option { + pub fn from_named_node_ref(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); + let container_type = ContainerType::from_named_node_ref(node); if container_type.is_some() { Some(ResourceType::RdfSource(container_type)) } else { diff --git a/ldp/src/vocab.rs b/ldp/src/vocab.rs index 2a2fc91..f0faf4d 100644 --- a/ldp/src/vocab.rs +++ b/ldp/src/vocab.rs @@ -17,4 +17,11 @@ pub mod ldp { 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"); }