Add support for emitting Prefer header

This commit is contained in:
2026-09-02 22:10:08 -04:00
parent 311608e37c
commit 3f053cb41c
7 changed files with 105 additions and 11 deletions
+2 -2
View File
@@ -10,7 +10,7 @@ pub enum ContainerType {
} }
impl ContainerType { impl ContainerType {
pub fn as_named_node(&self) -> NamedNodeRef<'_> { pub fn as_named_node_ref(&self) -> NamedNodeRef<'_> {
match self { match self {
ContainerType::Basic => vocab::ldp::BASIC_CONTAINER, ContainerType::Basic => vocab::ldp::BASIC_CONTAINER,
ContainerType::Direct => vocab::ldp::DIRECT_CONTAINER, ContainerType::Direct => vocab::ldp::DIRECT_CONTAINER,
@@ -18,7 +18,7 @@ impl ContainerType {
} }
} }
pub fn from_named_node(node: NamedNodeRef<'_>) -> Option<Self> { pub fn from_named_node_ref(node: NamedNodeRef<'_>) -> Option<Self> {
match node { match node {
vocab::ldp::BASIC_CONTAINER => Some(ContainerType::Basic), vocab::ldp::BASIC_CONTAINER => Some(ContainerType::Basic),
vocab::ldp::DIRECT_CONTAINER => Some(ContainerType::Direct), vocab::ldp::DIRECT_CONTAINER => Some(ContainerType::Direct),
-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_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");
+5 -3
View File
@@ -2,24 +2,26 @@
mod container; mod container;
mod error; mod error;
pub mod header; mod header;
pub mod middleware; pub mod middleware;
pub mod model; 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;
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 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::{ pub use resource::{
Resource, ResourceRequest, ResourceRequestBuilder, ResourceType, ResponseFormat, Resource, ResourceRequest, ResourceRequestBuilder, ResourceType, ResponseFormat,
}; };
pub use traverse::Traverse;
+2
View File
@@ -1,3 +1,5 @@
//! This module contains functionality related to the content of RDF documents.
mod dataset; mod dataset;
#[cfg(feature = "keyed")] #[cfg(feature = "keyed")]
mod keyed_dataset; mod keyed_dataset;
+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,
)]))
}
+30 -5
View File
@@ -1,6 +1,6 @@
use crate::container::ContainerType; 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 http::Method;
@@ -41,6 +41,8 @@ pub struct ResourceRequestBuilder {
follow_described_by: bool, follow_described_by: bool,
validate_support: bool, validate_support: bool,
formats: Vec<RdfFormat>, formats: Vec<RdfFormat>,
include_preferences: Vec<Preference>,
omit_preferences: Vec<Preference>,
} }
impl ResourceRequestBuilder { impl ResourceRequestBuilder {
@@ -60,6 +62,8 @@ impl ResourceRequestBuilder {
follow_described_by: true, follow_described_by: true,
validate_support: true, validate_support: true,
formats: Vec::new(), formats: Vec::new(),
include_preferences: Vec::new(),
omit_preferences: Vec::new(),
} }
} }
@@ -111,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 }
@@ -290,6 +306,15 @@ 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 { if self.builder.validate_support {
@@ -335,20 +360,20 @@ pub enum ResourceType {
} }
impl ResourceType { impl ResourceType {
pub fn as_named_node(&self) -> NamedNodeRef<'_> { pub fn as_named_node_ref(&self) -> NamedNodeRef<'_> {
match self { match self {
ResourceType::NonRdfSource => vocab::ldp::NON_RDF_SOURCE, ResourceType::NonRdfSource => vocab::ldp::NON_RDF_SOURCE,
ResourceType::RdfSource(None) => vocab::ldp::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<Self> { pub fn from_named_node_ref(node: NamedNodeRef<'_>) -> Option<Self> {
match node { match node {
vocab::ldp::NON_RDF_SOURCE => Some(ResourceType::NonRdfSource), vocab::ldp::NON_RDF_SOURCE => Some(ResourceType::NonRdfSource),
vocab::ldp::RDF_SOURCE => Some(ResourceType::RdfSource(None)), 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() { if container_type.is_some() {
Some(ResourceType::RdfSource(container_type)) Some(ResourceType::RdfSource(container_type))
} else { } else {
+7
View File
@@ -17,4 +17,11 @@ pub mod ldp {
NamedNodeRef::new_unchecked("http://www.w3.org/ns/ldp#DirectContainer"); NamedNodeRef::new_unchecked("http://www.w3.org/ns/ldp#DirectContainer");
pub const INDIRECT_CONTAINER: NamedNodeRef<'_> = pub const INDIRECT_CONTAINER: NamedNodeRef<'_> =
NamedNodeRef::new_unchecked("http://www.w3.org/ns/ldp#IndirectContainer"); 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");
} }