ldp 0.1.0

A library to assist with the creation and maintenance of remote RDF data via LDP
use crate::resource::ResourceType;
use bytes::BufMut;
use http::{StatusCode, header};
use oxigraph::io::{RdfFormat, RdfSerializer};
use oxigraph::model::{
    GraphName, Literal, NamedNode, NamedOrBlankNode, Quad, QuadRef, Term, Triple, TripleRef, vocab,
};
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<D> {
    pub(crate) origin: Url,
    pub(crate) origin_type: Option<ResourceType>,
    pub(crate) state_token: Option<String>,
    pub(crate) described_by: Option<Url>,
    pub(crate) file_name: Option<String>,
    pub(crate) size: Option<usize>,
    pub(crate) dataset: D,
}

impl<D: Default> RdfSource<D> {
    /// Create a new, empty, RdfSource with the given URL.
    ///
    /// The subject of new quads will use this URL.
    pub fn new(origin: Url) -> Self {
        Self {
            origin,
            origin_type: None,
            state_token: None,
            described_by: None,
            file_name: None,
            size: None,
            dataset: Default::default(),
        }
    }
}

impl<D> RdfSource<D> {
    /// The original URL used to procure this RDF Source.
    pub fn origin(&self) -> &Url {
        &self.origin
    }

    /// The type of Resource that was originally fetched.
    pub fn origin_type(&self) -> Option<&ResourceType> {
        self.origin_type.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 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 name of the file that was originally fetched.
    pub fn origin_file_name(&self) -> Option<&str> {
        self.file_name.as_deref()
    }

    /// The size of the file that was originally fetched.
    pub fn origin_size(&self) -> Option<usize> {
        self.size
    }

    /// The underlying dataset.
    pub fn dataset(&self) -> &D {
        &self.dataset
    }

    /// A mutable reference to the underlying dataset.
    pub fn dataset_mut(&mut self) -> &mut D {
        &mut self.dataset
    }

    /// Create a new quad, using the origin as the subject.
    ///
    /// The graph name is the `describedby` value, if present. If not present, the graph name is the
    /// origin.
    pub fn new_quad(&self) -> Quad {
        let graph_name = self
            .described_by
            .as_ref()
            .map(|db| db.as_str())
            .unwrap_or(self.origin().as_str());
        Quad::new(
            NamedOrBlankNode::NamedNode(NamedNode::new_unchecked(self.origin.clone())),
            vocab::rdf::TYPE,
            Term::Literal(Literal::new_simple_literal("")),
            GraphName::NamedNode(NamedNode::new_unchecked(graph_name)),
        )
    }

    /// Create a new quad, using the given triple as a template.
    ///
    /// The graph name is the `describedby` value, if present. If not present, the graph name is the
    /// origin.
    pub fn quad_from_triple(&self, triple: Triple) -> Quad {
        let graph_name = GraphName::NamedNode(NamedNode::new_unchecked(
            self.described_by
                .as_ref()
                .map(|db| db.as_str())
                .unwrap_or(self.origin().as_str()),
        ));

        Quad::new(
            triple.subject,
            triple.predicate,
            triple.object,
            graph_name.clone(),
        )
    }
}

/// Holds options related to serialization.
pub struct SerializationOptions<'a> {
    format: RdfFormat,
    filter: Box<dyn Fn(TripleRef<'a>) -> bool>,
}

impl<'a> SerializationOptions<'a> {
    /// Create a new set of serialization options with the provided format.
    pub fn from_format(format: RdfFormat) -> Self {
        Self {
            format,
            filter: Box::new(|_| true),
        }
    }

    /// Filter triples that match the predicate.
    ///
    /// A return value of `true` means that it the triple ought to be included in the serialization.
    #[must_use]
    pub fn with_filter<F>(self, filter: F) -> Self
    where
        F: Fn(TripleRef<'a>) -> bool + 'static,
    {
        Self {
            format: self.format,
            filter: Box::new(filter),
        }
    }
}

impl<'a, D: 'a> RdfSource<D>
where
    &'a D: IntoIterator<Item = QuadRef<'a>>,
{
    /// Serializes the dataset in to the provided format.
    pub fn serialize(&'a self, options: SerializationOptions<'a>) -> crate::Result<bytes::Bytes> {
        let writer = bytes::BytesMut::new().writer();
        let mut serializer = RdfSerializer::from_format(options.format).for_writer(writer);

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

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

    /// Prepare an update request.
    pub fn to_update(
        &'a self,
        options: SerializationOptions<'a>,
    ) -> crate::Result<RdfSourceUpdateRequest> {
        let url = self.described_by.clone().unwrap_or(self.origin.clone());
        let media_type = options.format.media_type().to_string();
        let body = self.serialize(options)?;

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