Add documentation, refactor public-facing API

This commit is contained in:
2026-05-19 23:08:17 -04:00
parent f29b5af96d
commit be2fd42c5c
11 changed files with 228 additions and 300 deletions
Generated
+1 -212
View File
@@ -2,21 +2,6 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 version = 4
[[package]]
name = "addr2line"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
dependencies = [
"gimli",
]
[[package]]
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.1.4" version = "1.1.4"
@@ -49,21 +34,6 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "backtrace"
version = "0.3.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
dependencies = [
"addr2line",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
"windows-link",
]
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.22.1" version = "0.22.1"
@@ -155,33 +125,6 @@ dependencies = [
"libloading", "libloading",
] ]
[[package]]
name = "color-eyre"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d"
dependencies = [
"backtrace",
"color-spantrace",
"eyre",
"indenter",
"once_cell",
"owo-colors",
"tracing-error",
]
[[package]]
name = "color-spantrace"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427"
dependencies = [
"once_cell",
"owo-colors",
"tracing-core",
"tracing-error",
]
[[package]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.2.17" version = "0.2.17"
@@ -248,26 +191,6 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "eyre"
version = "0.6.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec"
dependencies = [
"indenter",
"once_cell",
]
[[package]] [[package]]
name = "find-msvc-tools" name = "find-msvc-tools"
version = "0.1.9" version = "0.1.9"
@@ -393,12 +316,6 @@ dependencies = [
"wasip2", "wasip2",
] ]
[[package]]
name = "gimli"
version = "0.32.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
[[package]] [[package]]
name = "glob" name = "glob"
version = "0.3.3" version = "0.3.3"
@@ -602,12 +519,6 @@ dependencies = [
"icu_properties", "icu_properties",
] ]
[[package]]
name = "indenter"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5"
[[package]] [[package]]
name = "ipnet" name = "ipnet"
version = "2.12.0" version = "2.12.0"
@@ -663,20 +574,9 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "ldctl"
version = "0.1.0"
dependencies = [
"color-eyre",
"futures",
"ldp",
"tokio",
"tracing",
]
[[package]] [[package]]
name = "ldp" name = "ldp"
version = "0.1.0" version = "0.0.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"base64", "base64",
@@ -749,15 +649,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
]
[[package]] [[package]]
name = "mio" name = "mio"
version = "1.2.0" version = "1.2.0"
@@ -779,27 +670,12 @@ dependencies = [
"minimal-lexical", "minimal-lexical",
] ]
[[package]]
name = "object"
version = "0.37.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.21.4" version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "owo-colors"
version = "4.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d"
[[package]] [[package]]
name = "oxigraph" name = "oxigraph"
version = "0.5.8" version = "0.5.8"
@@ -924,16 +800,6 @@ dependencies = [
"thiserror", "thiserror",
] ]
[[package]]
name = "parking_lot"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]] [[package]]
name = "parking_lot_core" name = "parking_lot_core"
version = "0.9.12" version = "0.9.12"
@@ -1171,12 +1037,6 @@ dependencies = [
"tower-service", "tower-service",
] ]
[[package]]
name = "rustc-demangle"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d"
[[package]] [[package]]
name = "rustc-hash" name = "rustc-hash"
version = "2.1.2" version = "2.1.2"
@@ -1252,31 +1112,12 @@ dependencies = [
"digest", "digest",
] ]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]] [[package]]
name = "shlex" name = "shlex"
version = "1.3.0" version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
dependencies = [
"errno",
"libc",
]
[[package]] [[package]]
name = "siphasher" name = "siphasher"
version = "1.0.3" version = "1.0.3"
@@ -1423,15 +1264,6 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "thread_local"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "tinystr" name = "tinystr"
version = "0.8.3" version = "0.8.3"
@@ -1448,28 +1280,13 @@ version = "1.52.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
dependencies = [ dependencies = [
"bytes",
"libc", "libc",
"mio", "mio",
"parking_lot",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry",
"socket2", "socket2",
"tokio-macros",
"windows-sys", "windows-sys",
] ]
[[package]]
name = "tokio-macros"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "tokio-util" name = "tokio-util"
version = "0.7.18" version = "0.7.18"
@@ -1557,28 +1374,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"valuable",
]
[[package]]
name = "tracing-error"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db"
dependencies = [
"tracing",
"tracing-subscriber",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
dependencies = [
"sharded-slab",
"thread_local",
"tracing-core",
] ]
[[package]] [[package]]
@@ -1617,12 +1412,6 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.5" version = "0.9.5"
-1
View File
@@ -2,7 +2,6 @@
resolver = "2" resolver = "2"
members = [ members = [
"ldp", "ldp",
"ldctl",
] ]
[workspace.dependencies] [workspace.dependencies]
-6
View File
@@ -1,6 +0,0 @@
[package]
name = "ldctl"
version = "0.1.0"
edition = "2024"
[dependencies]
-3
View File
@@ -1,3 +0,0 @@
fn main() {
println!("Hello, world!");
}
+6 -1
View File
@@ -1,7 +1,12 @@
[package] [package]
name = "ldp" name = "ldp"
version = "0.1.0" version = "0.0.0"
edition = "2024" edition = "2024"
description = "A library to assist with the creation and maintenance of remote RDF data via LDP"
readme = "README.md"
license = "GPL-3.0-only"
keywords = ["ldp", "rdf", "sparql"]
categories = ["database", "web-programming::http-client"]
[dependencies] [dependencies]
async-trait.workspace = true async-trait.workspace = true
+4
View File
@@ -0,0 +1,4 @@
# LDP
This library is intended to implement the [Linked Data Platform](https://www.w3.org/TR/ldp/) standard, to facilitate the
creation and maintenance of RDF data on remote servers.
+4 -6
View File
@@ -1,7 +1,9 @@
use thiserror::Error; use thiserror::Error;
/// The result type for the library.
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
/// The error type for the library.
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum Error { pub enum Error {
#[error(transparent)] #[error(transparent)]
@@ -13,15 +15,11 @@ pub enum Error {
#[error(transparent)] #[error(transparent)]
ReqwestMiddleware(#[from] reqwest_middleware::Error), ReqwestMiddleware(#[from] reqwest_middleware::Error),
#[error(transparent)] /// The server did not advertise LDP support.
InvalidHeaderValue(#[from] http::header::InvalidHeaderValue),
#[error("Server did not advertise LDP support")] #[error("Server did not advertise LDP support")]
LDPUnsupported, LDPUnsupported,
/// The response from the server was not in a format we understand.
#[error("Response was not in a supported RDF format")] #[error("Response was not in a supported RDF format")]
UnsupportedFormat, UnsupportedFormat,
#[error("Document has been modified since last fetch, and overwrite was not enabled")]
DocumentModified,
} }
+8 -7
View File
@@ -1,3 +1,5 @@
#![cfg_attr(doc, doc = include_str!("../README.md"))]
mod error; mod error;
pub mod header; pub mod header;
pub mod middleware; pub mod middleware;
@@ -5,12 +7,11 @@ mod rdf_source;
mod resource; mod resource;
pub mod vocab; pub mod vocab;
pub use http::{HeaderName, HeaderValue}; pub use http;
pub use oxigraph; pub use oxigraph;
pub use reqwest_middleware::ClientBuilder; pub use reqwest_middleware;
pub use reqwest_middleware::reqwest::Client; pub use reqwest_middleware::reqwest;
pub use reqwest_middleware::reqwest::Url;
pub use error::Result; pub use error::{Error, Result};
pub use rdf_source::RdfSource; pub use rdf_source::{RdfSource, RdfSourceUpdateRequest, RdfSourceUpdateResponse};
pub use resource::{Resource, ResourceRequestBuilder}; pub use resource::{Resource, ResourceRequest, ResourceRequestBuilder, ResponseFormat};
+1
View File
@@ -4,6 +4,7 @@ use reqwest_middleware::reqwest::header::HeaderValue;
use reqwest_middleware::reqwest::{Request, Response, header}; use reqwest_middleware::reqwest::{Request, Response, header};
use reqwest_middleware::{Middleware, Next}; use reqwest_middleware::{Middleware, Next};
/// HTTP Basic Authentication
pub struct BasicAuthMiddleware { pub struct BasicAuthMiddleware {
username: String, username: String,
password: Option<String>, password: Option<String>,
+83 -28
View File
@@ -1,11 +1,12 @@
use crate::{Url, error};
use bytes::BufMut; use bytes::BufMut;
use http::{HeaderValue, StatusCode, header}; use http::{StatusCode, header};
use oxigraph::io::{RdfFormat, RdfSerializer}; use oxigraph::io::{RdfFormat, RdfSerializer};
use oxigraph::model::Dataset; use oxigraph::model::Dataset;
use reqwest_middleware::ClientWithMiddleware; use reqwest_middleware::ClientWithMiddleware;
use reqwest_middleware::reqwest::Request; use reqwest_middleware::reqwest::Url;
/// A LDP [RDF Source](https://www.w3.org/TR/ldp/#ldprs).
#[derive(Clone, Debug)]
pub struct RdfSource { pub struct RdfSource {
pub(crate) origin: Url, pub(crate) origin: Url,
pub(crate) described_by: Option<Url>, pub(crate) described_by: Option<Url>,
@@ -14,22 +15,31 @@ pub struct RdfSource {
} }
impl RdfSource { impl RdfSource {
/// The original URL used to procure this RDF Source.
pub fn origin(&self) -> &Url { pub fn origin(&self) -> &Url {
&self.origin &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> { pub fn described_by(&self) -> Option<&Url> {
self.described_by.as_ref() self.described_by.as_ref()
} }
/// The state token of the resource.
///
/// This is used for optimistic locking.
pub fn state_token(&self) -> Option<&str> { pub fn state_token(&self) -> Option<&str> {
self.state_token.as_deref() self.state_token.as_deref()
} }
/// The underlying Dataset.
pub fn dataset(&self) -> &Dataset { pub fn dataset(&self) -> &Dataset {
&self.dataset &self.dataset
} }
/// Serializes the Dataset in to the provided format.
pub fn serialize(&self, format: RdfFormat) -> crate::Result<bytes::Bytes> { pub fn serialize(&self, format: RdfFormat) -> crate::Result<bytes::Bytes> {
let writer = bytes::BytesMut::new().writer(); let writer = bytes::BytesMut::new().writer();
let mut serializer = RdfSerializer::from_format(format).for_writer(writer); let mut serializer = RdfSerializer::from_format(format).for_writer(writer);
@@ -48,40 +58,85 @@ impl RdfSource {
Ok(finished_writer.into_inner().freeze()) Ok(finished_writer.into_inner().freeze())
} }
pub fn to_request( /// Prepare an update request.
&self, pub fn to_update(&self, format: RdfFormat) -> crate::Result<RdfSourceUpdateRequest> {
client: ClientWithMiddleware,
format: RdfFormat,
) -> crate::Result<Request> {
let url = self.described_by.clone().unwrap_or(self.origin.clone()); let url = self.described_by.clone().unwrap_or(self.origin.clone());
let media_type = format.media_type().to_string();
let body = self.serialize(format)?; let body = self.serialize(format)?;
Ok(client
.put(url) Ok(RdfSourceUpdateRequest {
.header(header::CONTENT_TYPE, format.media_type()) url,
.body(body) state_token: self.state_token.clone(),
.build()?) media_type,
body,
})
} }
}
/// An update request.
pub struct RdfSourceUpdateRequest {
url: Url,
state_token: Option<String>,
media_type: String,
body: bytes::Bytes,
}
/// An update response.
///
/// If the document was modified since it was last fetched, and if the user set `overwrite` to
/// `false`, then the request will be returned back to the user so it can be re-submitted.
pub enum RdfSourceUpdateResponse {
/// The update succeeded.
Success,
/// The update failed specifically because of optimistic locking, and `overwrite` was disabled.
/// The original request is preserved here to allow the user to cheaply re-submit the request
/// with `overwrite` set to `true`.
DocumentModified(RdfSourceUpdateRequest),
}
impl RdfSourceUpdateRequest {
/// Send the update request.
///
/// If `overwrite` is `true`, then optimistic locking is disabled. In the event of a failed
/// update, the original request is preserved to permit the user to cheaply re-submit it. This
/// avoids unnecessary cloning/serialization.
///
/// Optimistic locking is implemented via the `X-State-Token` and `X-If-State-Token` HTTP
/// headers. The [412 Precondition Failed](https://http.dev/412) status code is used to
/// determine whether the update failed specifically because of optimistic locking.
pub async fn send( pub async fn send(
&self, self,
client: ClientWithMiddleware, client: ClientWithMiddleware,
mut request: Request,
overwrite: bool, overwrite: bool,
) -> crate::Result<()> { ) -> crate::Result<RdfSourceUpdateResponse> {
if !overwrite && let Some(state_token) = &self.state_token { if overwrite {
let value = HeaderValue::from_str(state_token.as_str())?; client
request .put(self.url)
.headers_mut() .header(header::CONTENT_TYPE, self.media_type)
.insert(crate::header::X_IF_STATE_TOKEN, value); .body(self.body)
} .send()
.await?
.error_for_status()?;
Ok(RdfSourceUpdateResponse::Success)
} else {
let mut builder = client
.put(self.url.clone())
.header(header::CONTENT_TYPE, self.media_type.clone());
let response = client.execute(request).await?; if let Some(state_token) = &self.state_token {
builder = builder.header(crate::header::X_IF_STATE_TOKEN, state_token.as_str());
}
match response.status() { let response = builder.body(self.body.clone()).send().await?;
StatusCode::PRECONDITION_FAILED => Err(error::Error::DocumentModified),
_ => { match response.status() {
response.error_for_status()?; StatusCode::PRECONDITION_FAILED => {
Ok(()) Ok(RdfSourceUpdateResponse::DocumentModified(self))
}
_ => {
response.error_for_status()?;
Ok(RdfSourceUpdateResponse::Success)
}
} }
} }
} }
+121 -36
View File
@@ -1,5 +1,5 @@
use crate::rdf_source::RdfSource; use crate::rdf_source::RdfSource;
use crate::{error, vocab}; use crate::vocab;
use bytes::Bytes; use bytes::Bytes;
use futures::Stream; use futures::Stream;
use oxigraph::io::{RdfFormat, RdfParser}; use oxigraph::io::{RdfFormat, RdfParser};
@@ -8,6 +8,23 @@ use reqwest_middleware::reqwest::{Client, Response, StatusCode, Url, header};
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware, RequestBuilder}; use reqwest_middleware::{ClientBuilder, ClientWithMiddleware, RequestBuilder};
use tracing::error; use tracing::error;
/// Builds an HTTP request for a [Resource](https://www.w3.org/TR/ldp/#ldpr).
///
/// # Example
/// ```rust
/// use ldp::reqwest::{Client, Url};
/// use ldp::reqwest_middleware::ClientBuilder;
/// use ldp::ResourceRequestBuilder;
///
/// let client = ClientBuilder::new(Client::new()).build();
/// let url = Url::parse("http://server/resource")?;
/// let resource = ResourceRequestBuilder::with_client_and_url(client.clone(), url)
/// .follow_described_by(true)
/// .accept_all_rdf_formats()
/// .send();
/// .await?;
/// ```
#[derive(Clone, Debug)]
pub struct ResourceRequestBuilder { pub struct ResourceRequestBuilder {
client: ClientWithMiddleware, client: ClientWithMiddleware,
url: Url, url: Url,
@@ -16,10 +33,14 @@ pub struct ResourceRequestBuilder {
} }
impl ResourceRequestBuilder { impl ResourceRequestBuilder {
/// Creates a new request.
///
/// A reqwest client will be created and managed by this library.
pub fn new(url: Url) -> Self { pub fn new(url: Url) -> Self {
Self::with_client_and_url(ClientBuilder::new(Client::new()).build(), url) Self::with_client_and_url(ClientBuilder::new(Client::new()).build(), url)
} }
/// Creates a new request with the given HTTP client.
pub fn with_client_and_url(client: ClientWithMiddleware, url: Url) -> Self { pub fn with_client_and_url(client: ClientWithMiddleware, url: Url) -> Self {
Self { Self {
client, client,
@@ -29,17 +50,23 @@ impl ResourceRequestBuilder {
} }
} }
/// If the HTTP response includes a [describedby](https://www.w3.org/TR/ldp/#link-relation-describedby) link rel, then it will be followed.
pub fn follow_described_by(mut self, value: bool) -> Self { pub fn follow_described_by(mut self, value: bool) -> Self {
self.follow_described_by = value; self.follow_described_by = value;
self self
} }
pub fn allow_format(mut self, format: RdfFormat) -> Self { /// Restrict the request to the given RDF format.
///
/// Repeated calls are additive. By default, all formats — even non-RDF formats — are accepted.
/// If your intention is to process non-RDF data, then this should not be called.
pub fn accept_rdf_format(mut self, format: RdfFormat) -> Self {
self.formats.push(format); self.formats.push(format);
self self
} }
pub fn allow_only_supported_formats(mut self) -> Self { /// Restrict the request to all the formats supported by the underlying parser.
pub fn accept_all_rdf_formats(mut self) -> Self {
self.formats = vec![ self.formats = vec![
RdfFormat::N3, RdfFormat::N3,
RdfFormat::NQuads, RdfFormat::NQuads,
@@ -51,20 +78,19 @@ impl ResourceRequestBuilder {
self self
} }
pub async fn send(self) -> crate::Result<Resource> { /// Build the request.
Resource::from_builder(self).await pub fn build(self) -> ResourceRequest {
ResourceRequest { builder: self }
} }
} }
pub struct Resource { /// A request for a LDP [Resource](https://www.w3.org/TR/ldp/#ldpr).
origin: Url, #[derive(Clone, Debug)]
described_by: Option<Url>, pub struct ResourceRequest {
state_token: Option<String>, builder: ResourceRequestBuilder,
format: Option<RdfFormat>,
response: Response,
} }
impl Resource { impl ResourceRequest {
/// Described by example: /// Described by example:
/// Link: <http://fedora.quill.lan/rest/E2/fcr:metadata>; rel="describedby" /// Link: <http://fedora.quill.lan/rest/E2/fcr:metadata>; rel="describedby"
fn extract_described_by(response: &Response) -> Option<Url> { fn extract_described_by(response: &Response) -> Option<Url> {
@@ -104,41 +130,46 @@ impl Resource {
} }
} }
} }
Err(error::Error::LDPUnsupported) Err(crate::Error::LDPUnsupported)
} }
fn add_media_types( fn add_media_types(&self, mut request_builder: RequestBuilder) -> RequestBuilder {
formats: Vec<RdfFormat>, let media_types = self.builder.formats.iter().map(|f| f.media_type());
mut request_builder: RequestBuilder,
) -> RequestBuilder {
let media_types = formats.iter().map(|f| f.media_type());
for media_type in media_types { for media_type in media_types {
request_builder = request_builder.header(header::ACCEPT, media_type); request_builder = request_builder.header(header::ACCEPT, media_type);
} }
request_builder request_builder
} }
pub async fn from_builder(builder: ResourceRequestBuilder) -> crate::Result<Self> { /// Send the request.
let request_builder = builder.client.head(builder.url.clone()); ///
/// 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
/// [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.
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()?; let mut response = request_builder.send().await?.error_for_status()?;
Resource::ensure_ldp_support(&response)?; Self::ensure_ldp_support(&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
&& builder.follow_described_by && self.builder.follow_described_by
{ {
url_to_get = new_url.clone(); url_to_get = new_url.clone();
} else { } else {
url_to_get = builder.url.clone(); url_to_get = self.builder.url.clone();
} }
let mut request_builder = builder.client.get(url_to_get); let mut request_builder = self.builder.client.get(url_to_get);
if !builder.formats.is_empty() { request_builder = self.add_media_types(request_builder);
request_builder = Self::add_media_types(builder.formats, request_builder);
}
response = request_builder.send().await?.error_for_status()?; response = request_builder.send().await?.error_for_status()?;
Resource::ensure_ldp_support(&response)?; Self::ensure_ldp_support(&response)?;
let state_token = response let state_token = response
.headers() .headers()
@@ -148,28 +179,82 @@ impl Resource {
let format = response let format = response
.headers() .headers()
.get(header::CONTENT_TYPE) .get(header::CONTENT_TYPE)
.map(|hv| hv.to_str().unwrap_or_default()) .and_then(|hv| hv.to_str().ok())
.and_then(RdfFormat::from_media_type); .map(|value| {
if let Some(format) = RdfFormat::from_media_type(value) {
ResponseFormat::RdfFormat(format)
} else {
ResponseFormat::Other(value.to_string())
}
})
.unwrap_or(ResponseFormat::Unspecified);
Ok(Self { Ok(Resource {
origin: builder.url, origin: self.builder.url.clone(),
described_by, described_by,
state_token, state_token,
format, format,
response, response,
}) })
} }
}
pub fn format(&self) -> Option<RdfFormat> { /// The format of the response, as determined by the `Content-Type` HTTP header.
self.format pub enum ResponseFormat {
RdfFormat(RdfFormat),
Other(String),
Unspecified,
}
/// 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).
///
/// If the 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>,
format: ResponseFormat,
response: Response,
}
impl Resource {
/// The original URL used to make the request.
pub fn origin(&self) -> &Url {
&self.origin
} }
/// The URL used to describe the Resource, which may be different from the Resource itself.
///
/// This is useful because it allows RDF data to be attached to non-RDF data, such as a video.
pub fn described_by(&self) -> Option<&Url> {
self.described_by.as_ref()
}
/// The format of the response.
pub fn format(&self) -> &ResponseFormat {
&self.format
}
/// 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).
pub fn state_token(&self) -> Option<&String> {
self.state_token.as_ref()
}
/// Extract the response as a stream of unparsed bytes.
pub fn into_stream(self) -> impl Stream<Item = reqwest_middleware::reqwest::Result<Bytes>> { pub fn into_stream(self) -> impl Stream<Item = reqwest_middleware::reqwest::Result<Bytes>> {
self.response.bytes_stream() self.response.bytes_stream()
} }
/// Parse the response.
pub async fn into_rdf_source(self) -> crate::Result<RdfSource> { pub async fn into_rdf_source(self) -> crate::Result<RdfSource> {
if let Some(format) = self.format { if let ResponseFormat::RdfFormat(format) = self.format {
let graph_url = self.described_by.as_ref().unwrap_or(&self.origin); let graph_url = self.described_by.as_ref().unwrap_or(&self.origin);
let graph = GraphNameRef::NamedNode(NamedNodeRef::new_unchecked(graph_url.as_str())); let graph = GraphNameRef::NamedNode(NamedNodeRef::new_unchecked(graph_url.as_str()));
let parser = RdfParser::from_format(format).with_default_graph(graph); let parser = RdfParser::from_format(format).with_default_graph(graph);
@@ -183,7 +268,7 @@ impl Resource {
dataset, dataset,
}) })
} else { } else {
Err(error::Error::UnsupportedFormat) Err(crate::Error::UnsupportedFormat)
} }
} }
} }