Add documentation, refactor public-facing API
This commit is contained in:
Generated
+1
-212
@@ -2,21 +2,6 @@
|
||||
# It is not intended for manual editing.
|
||||
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]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
@@ -49,21 +34,6 @@ version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
@@ -155,33 +125,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
@@ -248,26 +191,6 @@ version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
@@ -393,12 +316,6 @@ dependencies = [
|
||||
"wasip2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.32.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.3"
|
||||
@@ -602,12 +519,6 @@ dependencies = [
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indenter"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5"
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.12.0"
|
||||
@@ -663,20 +574,9 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "ldctl"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"color-eyre",
|
||||
"futures",
|
||||
"ldp",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ldp"
|
||||
version = "0.1.0"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64",
|
||||
@@ -749,15 +649,6 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "mio"
|
||||
version = "1.2.0"
|
||||
@@ -779,27 +670,12 @@ dependencies = [
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.37.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "owo-colors"
|
||||
version = "4.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d"
|
||||
|
||||
[[package]]
|
||||
name = "oxigraph"
|
||||
version = "0.5.8"
|
||||
@@ -924,16 +800,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.12"
|
||||
@@ -1171,12 +1037,6 @@ dependencies = [
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.2"
|
||||
@@ -1252,31 +1112,12 @@ dependencies = [
|
||||
"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]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "siphasher"
|
||||
version = "1.0.3"
|
||||
@@ -1423,15 +1264,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "tinystr"
|
||||
version = "0.8.3"
|
||||
@@ -1448,28 +1280,13 @@ version = "1.52.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"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]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.18"
|
||||
@@ -1557,28 +1374,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||
dependencies = [
|
||||
"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]]
|
||||
@@ -1617,12 +1412,6 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
resolver = "2"
|
||||
members = [
|
||||
"ldp",
|
||||
"ldctl",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
[package]
|
||||
name = "ldctl"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
@@ -1,3 +0,0 @@
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
+6
-1
@@ -1,7 +1,12 @@
|
||||
[package]
|
||||
name = "ldp"
|
||||
version = "0.1.0"
|
||||
version = "0.0.0"
|
||||
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]
|
||||
async-trait.workspace = true
|
||||
|
||||
@@ -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
@@ -1,7 +1,9 @@
|
||||
use thiserror::Error;
|
||||
|
||||
/// The result type for the library.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// The error type for the library.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
@@ -13,15 +15,11 @@ pub enum Error {
|
||||
#[error(transparent)]
|
||||
ReqwestMiddleware(#[from] reqwest_middleware::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
InvalidHeaderValue(#[from] http::header::InvalidHeaderValue),
|
||||
|
||||
/// The server did not advertise LDP support.
|
||||
#[error("Server did not advertise LDP support")]
|
||||
LDPUnsupported,
|
||||
|
||||
/// The response from the server was not in a format we understand.
|
||||
#[error("Response was not in a supported RDF format")]
|
||||
UnsupportedFormat,
|
||||
|
||||
#[error("Document has been modified since last fetch, and overwrite was not enabled")]
|
||||
DocumentModified,
|
||||
}
|
||||
|
||||
+8
-7
@@ -1,3 +1,5 @@
|
||||
#![cfg_attr(doc, doc = include_str!("../README.md"))]
|
||||
|
||||
mod error;
|
||||
pub mod header;
|
||||
pub mod middleware;
|
||||
@@ -5,12 +7,11 @@ mod rdf_source;
|
||||
mod resource;
|
||||
pub mod vocab;
|
||||
|
||||
pub use http::{HeaderName, HeaderValue};
|
||||
pub use http;
|
||||
pub use oxigraph;
|
||||
pub use reqwest_middleware::ClientBuilder;
|
||||
pub use reqwest_middleware::reqwest::Client;
|
||||
pub use reqwest_middleware::reqwest::Url;
|
||||
pub use reqwest_middleware;
|
||||
pub use reqwest_middleware::reqwest;
|
||||
|
||||
pub use error::Result;
|
||||
pub use rdf_source::RdfSource;
|
||||
pub use resource::{Resource, ResourceRequestBuilder};
|
||||
pub use error::{Error, Result};
|
||||
pub use rdf_source::{RdfSource, RdfSourceUpdateRequest, RdfSourceUpdateResponse};
|
||||
pub use resource::{Resource, ResourceRequest, ResourceRequestBuilder, ResponseFormat};
|
||||
|
||||
@@ -4,6 +4,7 @@ use reqwest_middleware::reqwest::header::HeaderValue;
|
||||
use reqwest_middleware::reqwest::{Request, Response, header};
|
||||
use reqwest_middleware::{Middleware, Next};
|
||||
|
||||
/// HTTP Basic Authentication
|
||||
pub struct BasicAuthMiddleware {
|
||||
username: String,
|
||||
password: Option<String>,
|
||||
|
||||
+79
-24
@@ -1,11 +1,12 @@
|
||||
use crate::{Url, error};
|
||||
use bytes::BufMut;
|
||||
use http::{HeaderValue, StatusCode, header};
|
||||
use http::{StatusCode, header};
|
||||
use oxigraph::io::{RdfFormat, RdfSerializer};
|
||||
use oxigraph::model::Dataset;
|
||||
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(crate) origin: Url,
|
||||
pub(crate) described_by: Option<Url>,
|
||||
@@ -14,22 +15,31 @@ pub struct RdfSource {
|
||||
}
|
||||
|
||||
impl RdfSource {
|
||||
/// The original URL used to procure this RDF Source.
|
||||
pub fn origin(&self) -> &Url {
|
||||
&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 state token of the resource.
|
||||
///
|
||||
/// This is used for optimistic locking.
|
||||
pub fn state_token(&self) -> Option<&str> {
|
||||
self.state_token.as_deref()
|
||||
}
|
||||
|
||||
/// The underlying Dataset.
|
||||
pub fn dataset(&self) -> &Dataset {
|
||||
&self.dataset
|
||||
}
|
||||
|
||||
/// Serializes the Dataset in to the provided format.
|
||||
pub fn serialize(&self, format: RdfFormat) -> crate::Result<bytes::Bytes> {
|
||||
let writer = bytes::BytesMut::new().writer();
|
||||
let mut serializer = RdfSerializer::from_format(format).for_writer(writer);
|
||||
@@ -48,40 +58,85 @@ impl RdfSource {
|
||||
Ok(finished_writer.into_inner().freeze())
|
||||
}
|
||||
|
||||
pub fn to_request(
|
||||
&self,
|
||||
client: ClientWithMiddleware,
|
||||
format: RdfFormat,
|
||||
) -> crate::Result<Request> {
|
||||
/// Prepare an update request.
|
||||
pub fn to_update(&self, format: RdfFormat) -> crate::Result<RdfSourceUpdateRequest> {
|
||||
let url = self.described_by.clone().unwrap_or(self.origin.clone());
|
||||
let media_type = format.media_type().to_string();
|
||||
let body = self.serialize(format)?;
|
||||
Ok(client
|
||||
.put(url)
|
||||
.header(header::CONTENT_TYPE, format.media_type())
|
||||
.body(body)
|
||||
.build()?)
|
||||
|
||||
Ok(RdfSourceUpdateRequest {
|
||||
url,
|
||||
state_token: self.state_token.clone(),
|
||||
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(
|
||||
&self,
|
||||
self,
|
||||
client: ClientWithMiddleware,
|
||||
mut request: Request,
|
||||
overwrite: bool,
|
||||
) -> crate::Result<()> {
|
||||
if !overwrite && let Some(state_token) = &self.state_token {
|
||||
let value = HeaderValue::from_str(state_token.as_str())?;
|
||||
request
|
||||
.headers_mut()
|
||||
.insert(crate::header::X_IF_STATE_TOKEN, value);
|
||||
) -> crate::Result<RdfSourceUpdateResponse> {
|
||||
if overwrite {
|
||||
client
|
||||
.put(self.url)
|
||||
.header(header::CONTENT_TYPE, self.media_type)
|
||||
.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());
|
||||
|
||||
if let Some(state_token) = &self.state_token {
|
||||
builder = builder.header(crate::header::X_IF_STATE_TOKEN, state_token.as_str());
|
||||
}
|
||||
|
||||
let response = client.execute(request).await?;
|
||||
let response = builder.body(self.body.clone()).send().await?;
|
||||
|
||||
match response.status() {
|
||||
StatusCode::PRECONDITION_FAILED => Err(error::Error::DocumentModified),
|
||||
StatusCode::PRECONDITION_FAILED => {
|
||||
Ok(RdfSourceUpdateResponse::DocumentModified(self))
|
||||
}
|
||||
_ => {
|
||||
response.error_for_status()?;
|
||||
Ok(())
|
||||
Ok(RdfSourceUpdateResponse::Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+122
-37
@@ -1,5 +1,5 @@
|
||||
use crate::rdf_source::RdfSource;
|
||||
use crate::{error, vocab};
|
||||
use crate::vocab;
|
||||
use bytes::Bytes;
|
||||
use futures::Stream;
|
||||
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 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 {
|
||||
client: ClientWithMiddleware,
|
||||
url: Url,
|
||||
@@ -16,10 +33,14 @@ pub struct ResourceRequestBuilder {
|
||||
}
|
||||
|
||||
impl ResourceRequestBuilder {
|
||||
/// Creates a new request.
|
||||
///
|
||||
/// A reqwest client will be created and managed by this library.
|
||||
pub fn new(url: Url) -> Self {
|
||||
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 {
|
||||
Self {
|
||||
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 {
|
||||
self.follow_described_by = value;
|
||||
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
|
||||
}
|
||||
|
||||
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.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ResourceRequest {
|
||||
builder: ResourceRequestBuilder,
|
||||
}
|
||||
|
||||
impl Resource {
|
||||
impl ResourceRequest {
|
||||
/// Described by example:
|
||||
/// Link: <http://fedora.quill.lan/rest/E2/fcr:metadata>; rel="describedby"
|
||||
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(
|
||||
formats: Vec<RdfFormat>,
|
||||
mut request_builder: RequestBuilder,
|
||||
) -> RequestBuilder {
|
||||
let media_types = formats.iter().map(|f| f.media_type());
|
||||
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 {
|
||||
request_builder = request_builder.header(header::ACCEPT, media_type);
|
||||
}
|
||||
request_builder
|
||||
}
|
||||
|
||||
pub async fn from_builder(builder: ResourceRequestBuilder) -> crate::Result<Self> {
|
||||
let request_builder = builder.client.head(builder.url.clone());
|
||||
/// Send the request.
|
||||
///
|
||||
/// 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()?;
|
||||
Resource::ensure_ldp_support(&response)?;
|
||||
Self::ensure_ldp_support(&response)?;
|
||||
|
||||
let url_to_get;
|
||||
let described_by = Self::extract_described_by(&response);
|
||||
if let Some(new_url) = &described_by
|
||||
&& builder.follow_described_by
|
||||
&& self.builder.follow_described_by
|
||||
{
|
||||
url_to_get = new_url.clone();
|
||||
} else {
|
||||
url_to_get = builder.url.clone();
|
||||
url_to_get = self.builder.url.clone();
|
||||
}
|
||||
|
||||
let mut request_builder = builder.client.get(url_to_get);
|
||||
if !builder.formats.is_empty() {
|
||||
request_builder = Self::add_media_types(builder.formats, request_builder);
|
||||
}
|
||||
let mut request_builder = self.builder.client.get(url_to_get);
|
||||
request_builder = self.add_media_types(request_builder);
|
||||
|
||||
response = request_builder.send().await?.error_for_status()?;
|
||||
Resource::ensure_ldp_support(&response)?;
|
||||
Self::ensure_ldp_support(&response)?;
|
||||
|
||||
let state_token = response
|
||||
.headers()
|
||||
@@ -148,28 +179,82 @@ impl Resource {
|
||||
let format = response
|
||||
.headers()
|
||||
.get(header::CONTENT_TYPE)
|
||||
.map(|hv| hv.to_str().unwrap_or_default())
|
||||
.and_then(RdfFormat::from_media_type);
|
||||
.and_then(|hv| hv.to_str().ok())
|
||||
.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 {
|
||||
origin: builder.url,
|
||||
Ok(Resource {
|
||||
origin: self.builder.url.clone(),
|
||||
described_by,
|
||||
state_token,
|
||||
format,
|
||||
response,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn format(&self) -> Option<RdfFormat> {
|
||||
self.format
|
||||
}
|
||||
|
||||
/// The format of the response, as determined by the `Content-Type` HTTP header.
|
||||
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>> {
|
||||
self.response.bytes_stream()
|
||||
}
|
||||
|
||||
/// Parse the response.
|
||||
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 = GraphNameRef::NamedNode(NamedNodeRef::new_unchecked(graph_url.as_str()));
|
||||
let parser = RdfParser::from_format(format).with_default_graph(graph);
|
||||
@@ -183,7 +268,7 @@ impl Resource {
|
||||
dataset,
|
||||
})
|
||||
} else {
|
||||
Err(error::Error::UnsupportedFormat)
|
||||
Err(crate::Error::UnsupportedFormat)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user