ldp 0.0.0

A library to assist with the creation and maintenance of remote RDF data via LDP
Documentation
use bytes::BufMut;
use http::{StatusCode, header};
use oxigraph::io::{RdfFormat, RdfSerializer};
use oxigraph::model::Dataset;
use reqwest_middleware::ClientWithMiddleware;
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>,
    pub(crate) state_token: Option<String>,
    pub(crate) dataset: Dataset,
}

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);

        if format.supports_datasets() {
            for quad in &self.dataset {
                serializer.serialize_quad(quad)?;
            }
        } else {
            for quad in &self.dataset {
                serializer.serialize_triple(quad)?;
            }
        }

        let finished_writer = serializer.finish()?;
        Ok(finished_writer.into_inner().freeze())
    }

    /// 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(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,
        client: ClientWithMiddleware,
        overwrite: bool,
    ) -> 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 = builder.body(self.body.clone()).send().await?;

            match response.status() {
                StatusCode::PRECONDITION_FAILED => {
                    Ok(RdfSourceUpdateResponse::DocumentModified(self))
                }
                _ => {
                    response.error_for_status()?;
                    Ok(RdfSourceUpdateResponse::Success)
                }
            }
        }
    }
}