just-fetch 0.1.2

Just fetch the file, unzipping it if needed
Documentation
#![warn(
    // Harden built-in lints
    missing_copy_implementations,
    missing_debug_implementations,
    missing_docs,
    unreachable_pub,

    // Harden clippy lints
    clippy::clone_on_ref_ptr,
    clippy::dbg_macro,
    clippy::decimal_literal_representation,
    clippy::float_cmp_const,
    clippy::get_unwrap,
    clippy::integer_arithmetic,
    clippy::integer_division,
    clippy::pedantic,
    clippy::print_stdout,
)]

//! A library to simply fetch stuff, regardless of whether it's from
//! the internet, from the filesystem, inside a gzipped archive,
//! whatever. Used by `scaff` so it can focus on what's important: The
//! contents of the specified archive.

use std::{
    ffi::OsStr,
    fmt,
    fs::File,
    io::Read,
    path::{Path, PathBuf},
    time::Duration,
};

use anyhow::{anyhow, Error};
use flate2::read::GzDecoder;
use ureq::{Agent, Response};
use url::Url;

/// A wrapper around `ureq::Agent` that supports opening any kind
/// of `Fetchable` resource
#[derive(Debug)]
pub struct Fetcher {
    /// The inner client
    pub client: Agent,
}
impl Default for Fetcher {
    fn default() -> Self {
        Self {
            client: ureq::agent().build(),
        }
    }
}
impl Fetcher {
    /// Create a new instance
    pub fn new() -> Self {
        Self::default()
    }
    /// Open any kind of `Fetchable` resource
    pub fn open<F: Fetchable>(&mut self, resource: F) -> Result<F::Reader, F::Error> {
        resource.reader_for(self)
    }
}

/// Something that can be turned into a reader given a
/// fetcher. Example is a URL which can be turned into a reader by
/// sending a GET request, or a file path which can be turned into a
/// reader by just opening the file.
pub trait Fetchable {
    /// The output reader type
    type Reader: Read;
    /// Any potential error that could occur
    type Error;

    /// Return a reader given a fetcher
    fn reader_for(self, f: &mut Fetcher) -> Result<Self::Reader, Self::Error>;
}

impl Fetchable for &str {
    type Reader = Box<dyn Read>;
    type Error = Error;

    fn reader_for(self, f: &mut Fetcher) -> Result<Self::Reader, Self::Error> {
        match Url::parse(self) {
            Ok(path) => path.reader_for(f),
            Err(_) => Path::new(self).reader_for(f),
        }
    }
}

impl Fetchable for Url {
    type Reader = Box<dyn Read>;
    type Error = Error;

    fn reader_for(self, f: &mut Fetcher) -> Result<Self::Reader, Self::Error> {
        match self.to_file_path() {
            Ok(path) => path.reader_for(f),
            Err(()) => f
                .client
                .get(self.as_str())
                .timeout(Duration::from_secs(3))
                .call()
                .reader_for(f),
        }
    }
}

impl Fetchable for Response {
    type Reader = Box<dyn Read>;
    type Error = Error;

    fn reader_for(self, _f: &mut Fetcher) -> Result<Self::Reader, Self::Error> {
        if self.header("Content-Type").map_or(false, |v| v == "application/x-gzip")
            || self
                .header("Content-Disposition")
                .map_or(false, |v| v.contains(".gz"))
        {
            Ok(Box::new(GzDecoder::new(self.into_reader())))
        } else {
            Ok(Box::new(self.into_reader()))
        }
    }
}

impl Fetchable for &Path {
    type Reader = Box<dyn Read>;
    type Error = Error;

    fn reader_for(self, _f: &mut Fetcher) -> Result<Self::Reader, Self::Error> {
        let file = File::open(self)?;

        if self.extension().map_or(false, |ext| ext == "gz") {
            Ok(Box::new(GzDecoder::new(file)))
        } else {
            Ok(Box::new(file))
        }
    }
}
impl Fetchable for PathBuf {
    type Reader = Box<dyn Read>;
    type Error = Error;

    fn reader_for(self, f: &mut Fetcher) -> Result<Self::Reader, Self::Error> {
        (&*self).reader_for(f)
    }
}

/// An enum wrapping the two common fetchers `PathBuf` and `Url`. This
/// is more efficient than using `Box<dyn Fetcher>`.
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Resource {
    /// Fetcher for a URL, either for an online `http(s)://` resource
    /// or for a `file://`
    Url(Url),
    /// Fetcher for a local file path
    PathBuf(PathBuf),
}
impl Fetchable for Resource {
    type Reader = Box<dyn Read>;
    type Error = Error;

    fn reader_for(self, f: &mut Fetcher) -> Result<Self::Reader, Self::Error> {
        match self {
            Self::Url(url) => url.reader_for(f),
            Self::PathBuf(path) => path.reader_for(f),
        }
    }
}
impl Resource {
    /// Return true if a resource is absolute. This is used in `join` to
    /// disgard the first parent part.
    pub fn is_absolute(&self) -> bool {
        match self {
            Self::Url(_) => true,
            Self::PathBuf(path) => path.is_absolute(),
        }
    }

    /// Join a resource (relative or absolute) with `self`. Useful for
    /// resolving relative paths.
    pub fn join(mut self, other: Self) -> Result<Self, Error> {
        if other.is_absolute() {
            return Ok(other);
        }

        let other_str = other.as_ref().to_str().ok_or(anyhow!("UTF-8 error"))?;

        match self {
            Self::Url(parent) => parent.join(other_str).map(Self::Url).map_err(Error::from),
            Self::PathBuf(ref mut parent) => {
                parent.set_file_name(other_str);
                Ok(self)
            }
        }
    }
}
impl From<PathBuf> for Resource {
    fn from(path: PathBuf) -> Self {
        Self::PathBuf(path)
    }
}
impl From<Url> for Resource {
    fn from(url: Url) -> Self {
        Self::Url(url)
    }
}
impl From<String> for Resource {
    fn from(s: String) -> Self {
        match Url::parse(&s) {
            Ok(path) => Self::Url(path),
            Err(_) => Self::PathBuf(PathBuf::from(s)),
        }
    }
}
impl From<&str> for Resource {
    fn from(s: &str) -> Self {
        match Url::parse(s) {
            Ok(path) => Self::Url(path),
            Err(_) => Self::PathBuf(PathBuf::from(s)),
        }
    }
}
impl AsRef<OsStr> for Resource {
    fn as_ref(&self) -> &OsStr {
        match self {
            Self::Url(url) => OsStr::new(url.as_str()),
            Self::PathBuf(path) => OsStr::new(path),
        }
    }
}
impl fmt::Display for Resource {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Self::Url(url) => url.fmt(f),
            Self::PathBuf(path) => path.display().fmt(f),
        }
    }
}

#[cfg(feature = "serde")]
impl serde::Serialize for Resource {
    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        match self {
            Self::Url(url) => serializer.serialize_str(url.as_str()),
            Self::PathBuf(path) => path.serialize(serializer),
        }
    }
}

#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for Resource {
    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        <&str>::deserialize(deserializer).map(Self::from)
    }
}