boreholeio 0.1.0

A library for interacting with borehole.io, a subsurface data management, delivery and visualisation platform
Documentation
mod absolute_uri;
mod authority;
mod fragment;
mod query;
mod relative_reference;
mod scheme;
mod uri;
mod uri_reference;

use crate::array_cache::ArrayCache;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::path::{Path, PathBuf};

pub use absolute_uri::AbsoluteUri;
pub use authority::Authority;
pub use fragment::Fragment;
pub use query::Query;
pub use relative_reference::RelativeReference;
pub use scheme::Scheme;
pub use uri::Uri;
pub use uri_reference::UriReference;

pub type Error = String;

/// A [URI Reference](https://datatracker.ietf.org/doc/html/rfc3986#section-4.1)
/// pointing to a multidimensional array.
///
/// The URI reference is represented as optional base and relative components.
/// - The optional base component must be an [Absolute URI](https://datatracker.ietf.org/doc/html/rfc3986#section-4.3).
/// - The optional relative component must be a [Relative Reference](https://datatracker.ietf.org/doc/html/rfc3986#section-4.2).
#[derive(Debug)]
pub struct ArrayUriReference {
    base: Option<AbsoluteUri>,
    relative: RelativeReference,
}

impl ArrayUriReference {
    pub fn new(base: Option<AbsoluteUri>, relative: RelativeReference) -> Self {
        let uri = Self { base, relative };
        ArrayCache::register(&uri);
        uri
    }

    /// Whether or not this URI is a relative URI.
    pub fn is_relative(&self) -> bool {
        self.base().is_none() && !self.relative().is_empty()
    }

    pub fn base(&self) -> Option<&AbsoluteUri> {
        self.base.as_ref()
    }

    /// Returns a URI Reference with the base URI replaced by the one provided.
    pub fn with_base(&self, base: &AbsoluteUri) -> Self {
        Self::new(Some(base.to_owned()), self.relative().clone())
    }

    /// If the current URI Reference is a Relative Reference, returns a URI
    /// Reference consisting of the provided base and the existing relative
    /// components. If the current URI Reference is not a Relative Reference,
    /// simply returns a copy of the current URI Reference.
    pub fn with_base_if_relative(&self, base: &AbsoluteUri) -> Self {
        if self.is_relative() {
            self.with_base(base)
        } else {
            self.clone()
        }
    }

    pub fn relative(&self) -> &RelativeReference {
        &self.relative
    }

    pub fn resolved(&self) -> Result<Uri, Error> {
        let base = self.base().ok_or("No base URI to resolve against")?;
        Ok(self.relative().resolve(base))
    }

    /// Returns the scheme of this URI Reference.
    pub fn scheme(&self) -> Option<Scheme> {
        self.base.as_ref().map(AbsoluteUri::scheme)
    }

    /// Returns the fragment identifier (the part of the URI following
    /// the # symbol), if present.
    pub fn fragment(&self) -> Option<Fragment> {
        self.relative().fragment()
    }

    /// Returns a new URI Reference with the specified fragment.
    pub fn with_fragment(&self, fragment: Option<&Fragment>) -> Self {
        let mut relative = self.relative().clone();
        relative.set_fragment(fragment);
        Self::new(self.base.clone(), relative)
    }

    /// Returns the query component, if present.
    pub fn query(&self) -> Option<Query> {
        self.relative().query()
    }

    /// Joins the provided URI Reference to this one.
    ///
    /// If the provided URI Reference is a Relative Reference, and this is a
    /// resolvable URI, the result has the base component set to
    /// `self.resolved()` with the fragment removed, and the relative component
    /// is the one provided.
    ///
    /// If the provided URI Reference is a Relative Reference, and this is also
    /// a Relative Reference, the call will panic as it has not been
    /// implemented.
    ///
    /// If the provided URI Reference is a URI, the response will have a base
    /// component that is the provided URI with the fragment removed, and the
    /// relative component will be the fragment.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use boreholeio::schema::array_uri_reference::{ArrayUriReference, UriReference};
    /// # use std::ops::Deref;
    ///
    /// // `self` is a resolvable URI, and a Relative Reference is provided.
    /// let array_uri = ArrayUriReference::try_from("file:///a/b/c.txt").unwrap();
    /// let relative = UriReference::try_from("d.png").unwrap();
    /// let result = array_uri.join(&relative).unwrap();
    /// let resolved = result.resolved().unwrap();
    /// assert_eq!(resolved.as_str(), "file:///a/b/d.png");
    /// assert_eq!(result.base().unwrap().as_str(), "file:///a/b/c.txt");
    /// assert_eq!(result.relative().as_str(), "d.png");
    ///
    /// // `self` is is a Relative Reference, and a Relative Reference is
    /// // provided.
    /// let array_uri = ArrayUriReference::try_from("a/b/c.txt").unwrap();
    /// let relative = UriReference::try_from("d.png").unwrap();
    /// assert!(array_uri.join(&relative).is_err());
    ///
    /// // A URI is provided.
    /// let array_uri = ArrayUriReference::try_from("a/b/c.txt").unwrap();
    /// let uri = UriReference::try_from("file:///a/b/d.png").unwrap();
    /// let result = array_uri.join(&uri).unwrap();
    /// assert_eq!(result.base().unwrap().as_str(), "file:///a/b/d.png");
    /// assert!(result.relative().is_empty());
    /// ```
    pub fn join(&self, uri_reference: &UriReference) -> Result<Self, Error> {
        match uri_reference.to_uri() {
            Ok(uri) => Ok(Self::from(&uri)),
            Err(relative) => {
                if self.is_relative() {
                    Err("Cannot join a Relative Reference to another Relative Reference".to_owned())
                } else {
                    let resolved = self.resolved()?;
                    let (absolute, _) = resolved.to_absolute_and_fragment();
                    Ok(Self::new(Some(absolute), relative))
                }
            }
        }
    }
}

impl Clone for ArrayUriReference {
    // This must be manually implemented to ensure correct registration with the array
    // cache.
    fn clone(&self) -> Self {
        Self::new(self.base().cloned(), self.relative().clone())
    }
}

impl Drop for ArrayUriReference {
    fn drop(&mut self) {
        ArrayCache::deregister(self);
    }
}

impl TryFrom<&str> for ArrayUriReference {
    type Error = String;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        let uri_reference = UriReference::try_from(value)?;
        Ok(Self::from(&uri_reference))
    }
}

impl From<&UriReference> for ArrayUriReference {
    fn from(value: &UriReference) -> Self {
        match value.to_uri() {
            Ok(uri) => Self::from(&uri),
            Err(relative) => Self::from(&relative),
        }
    }
}

impl From<&ArrayUriReference> for UriReference {
    fn from(value: &ArrayUriReference) -> Self {
        if let Some(base) = value.base() {
            UriReference::from(&value.relative().resolve(base))
        } else {
            UriReference::from(value.relative())
        }
    }
}

impl From<&Uri> for ArrayUriReference {
    fn from(value: &Uri) -> Self {
        let (absolute, fragment) = value.to_absolute_and_fragment();
        let base = absolute.to_owned();
        let mut relative = RelativeReference::empty();
        relative.set_fragment(fragment.as_ref());
        Self::new(Some(base), relative)
    }
}

impl From<&RelativeReference> for ArrayUriReference {
    fn from(value: &RelativeReference) -> Self {
        Self::new(None, value.to_owned())
    }
}

impl From<&AbsoluteUri> for ArrayUriReference {
    fn from(value: &AbsoluteUri) -> Self {
        Self::new(Some(value.to_owned()), RelativeReference::empty())
    }
}

/// Converts a filesystem path into a URI Reference.
impl From<&Path> for ArrayUriReference {
    fn from(value: &Path) -> Self {
        if value.is_relative() {
            // Note, the below will panic in a few situations:
            // - If the path string is not convertible to UTF-8.
            // - If the path string with "/" separators can't be converted to an IRI Reference.
            //
            // I can't actually think of situations where this will happen, so we YOLO it.
            let relative_reference: RelativeReference = value.try_into().unwrap();
            (&relative_reference).into()
        } else {
            let absolute_uri: AbsoluteUri = value.try_into().unwrap();
            (&absolute_uri).into()
        }
    }
}

/// Converts a URI Reference into a filesystem path. Currently only supports
/// Windows and the Unix family of operating systems.
///
/// The URI reference must:
///
/// - Have a `file` scheme if it is a URI
/// - Not have a query or fragment component
/// - Not have an authority component if it is a Relative Reference
impl TryFrom<&ArrayUriReference> for PathBuf {
    type Error = String;

    fn try_from(value: &ArrayUriReference) -> Result<Self, Self::Error> {
        if value.is_relative() {
            value.relative().try_into()
        } else {
            (&value.resolved()?).try_into()
        }
    }
}

impl Serialize for ArrayUriReference {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(self.relative().as_str())
    }
}

impl<'de> Deserialize<'de> for ArrayUriReference {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let relative = String::deserialize(deserializer)?;
        Self::try_from(relative.as_str()).map_err(serde::de::Error::custom)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::path::{Path, PathBuf};
    use std::str::FromStr;

    #[test]
    fn from_str() {
        assert!(ArrayUriReference::try_from("foo").is_ok());
        assert!(ArrayUriReference::try_from(
            "https://example.com/search?q=rust+language&page=2#section-comments"
        )
        .is_ok());
    }

    // File URIs with an authority (Which are used for UNC paths on Windows) can't be
    // converted back to a path.
    #[test]
    fn from_file_uri_with_authority_into_path() {
        let uri = ArrayUriReference::try_from("file://authority/foo").unwrap();
        assert!(PathBuf::try_from(&uri).is_err());
    }

    #[cfg(unix)]
    #[test]
    fn from_path() {
        let absolute = ArrayUriReference::try_from(Path::new("/root/.ssh/id_rsa")).unwrap();
        assert_eq!(
            absolute.base().unwrap().as_str(),
            "file:///root/.ssh/id_rsa"
        );
        let relative = ArrayUriReference::try_from(Path::new(".ssh/id_rsa")).unwrap();
        assert_eq!(relative.relative().as_str(), ".ssh/id_rsa");
    }

    // On Windows we verify that both "/" and "\" work as separators.
    #[cfg(windows)]
    #[test]
    fn from_path() {
        // Verify absolute URIs first.
        for str in ["C:\\a\\b\\c", "C:/a/b/c"] {
            let path = Path::new(str);
            let uri = ArrayUriReference::from(path);
            assert_eq!(uri.base().unwrap().as_str(), "file:///C:/a/b/c");
        }
        // Verify relative URIs.
        for str in [".ssh\\id_rsa", ".ssh/id_rsa"] {
            let uri = ArrayUriReference::from(Path::new(str));
            assert_eq!(uri.relative().as_str(), ".ssh/id_rsa");
        }
    }

    // Windows UNC paths are currently not supported.
    #[cfg(windows)]
    #[test]
    #[should_panic]
    fn from_unc_path() {
        let _uri = ArrayUriReference::from(Path::new("\\\\authority\\foo"));
    }

    #[test]
    fn scheme_relative() {
        let result = ArrayUriReference::try_from("//www.alt.lu/foo");
        assert!(result.is_ok());
        let uri = result.unwrap();
        assert!(uri.base().is_none());
        assert!(uri.is_relative());
        assert_eq!(uri.relative().as_str(), "//www.alt.lu/foo");
        let uri = uri.with_base(&AbsoluteUri::try_from("http://example.com/bar/baz").unwrap());
        let result = uri.resolved();
        assert!(result.is_ok());
        let uri = result.unwrap();
        assert_eq!(uri.as_str(), "http://www.alt.lu/foo");
    }

    #[cfg(unix)]
    #[test]
    fn into_path() {
        // Both absolute and relative file URIs are verified in the same way.
        let ground_truth = [
            ("file:///root/foo#42", "/root/foo"),
            ("foo/bar#42", "foo/bar"),
        ];
        for (ground_truth_uri, ground_truth_path) in ground_truth {
            // URI with fragment cannot be converted to PathBuf.
            let uri = ArrayUriReference::try_from(ground_truth_uri).unwrap();
            let path: Result<PathBuf, Error> = (&uri).try_into();
            assert!(path.is_err());
            // The fragment must be explicitly stripped away.
            let path: Result<PathBuf, Error> = (&uri.with_fragment(None)).try_into();
            assert_eq!(path, Ok(PathBuf::from_str(ground_truth_path).unwrap()));
        }
    }

    #[cfg(windows)]
    #[test]
    fn into_path() {
        let ground_truth = [("file:///C:/foo#42", "C:/foo"), ("foo/bar#42", "foo/bar")];
        for (ground_truth_uri, ground_truth_path) in ground_truth {
            // URI with fragment cannot be converted to PathBuf.
            let uri = ArrayUriReference::try_from(ground_truth_uri).unwrap();
            let path: Result<PathBuf, Error> = (&uri).try_into();
            assert!(path.is_err());
            // The fragment must be explicitly stripped away.
            let path: Result<PathBuf, Error> = (&uri.with_fragment(None)).try_into();
            assert_eq!(path, Ok(PathBuf::from_str(ground_truth_path).unwrap()));
        }
    }

    #[test]
    fn unicode_characters() {
        let result = ArrayUriReference::try_from("🦀");
        assert!(result.is_ok());
        let uri = result.unwrap();
        assert_eq!(uri.relative().as_str(), "%F0%9F%A6%80");

        let percent_encoded = ArrayUriReference::from(uri.relative());
        assert_eq!(uri.relative(), percent_encoded.relative());
    }
}