password-as-service 0.1.4

a very poorly considered http/rest service
Documentation
//! coding challenge

#![feature(proc_macro_hygiene, decl_macro)]
#![deny(missing_docs)]
#![allow(clippy::pedantic)]

extern crate rocket;
/// the outside-facing API
pub mod api;
#[allow(missing_docs)]
pub mod model;
mod parse;

use std::str::FromStr;

use std::{
    collections::HashMap,
    fs::{metadata, File},
    io::{BufRead, BufReader, Result},
    path::{Path, PathBuf},
    sync::{Arc, RwLock},
    time::SystemTime,
};

/// An thread-safe reference-counted allocation of a hashmap.
pub type ArcMap<T> = Arc<HashMap<u64, T>>;

/// A ThreadSafeError has a static lifetime and is safe to be shared across threads.
pub trait ThreadSafeErr: std::error::Error + Sync + Send + 'static {}

/// A resouce is an abstraction around some outside resource like a File or Network connection. It may or may not involve levels of caching.
pub trait Resource<T: UniqueID> {
    /// Load the resource, returning a reference-counted view to it's most recent contents
    /// Once loaded, the caller will continue to have a view into the contents _at the time_ load was called; this means it may get out of sync with later callers.
    fn load(&self) -> Result<Arc<HashMap<u64, T>>>;
}

/// An element with a unique identifier.
pub trait UniqueID {
    /// return the unique id
    fn id(&self) -> u64;
}

/// A Query matches against elements of some kind,
pub trait Query<T> {
    /// returns true if query is a match.
    fn is_match(&self, t: &T) -> bool;

    /// collects a vector of all elements that match the query.
    fn matching_elements<'a>(&self, a: impl IntoIterator<Item = &'a T>) -> Vec<&'a T> {
        a.into_iter().filter(|t| self.is_match(t)).collect()
    }
}
impl<F: Fn(&T) -> bool, T> Query<T> for F {
    fn is_match(&self, t: &T) -> bool {
        (self)(t)
    }
}

#[derive(Debug)]
/// A resource which mirrors a flat file. holding an internal cache of it's contents and reloading them on demand if they have changed.
/// calling [Resource::load] on a fileresouce returns a view into the contents of the file _at the time_ `load()` was called
pub struct FileResource<T: std::str::FromStr + Clone> {
    path: PathBuf,
    cached_at: RwLock<std::time::SystemTime>,
    cache: RwLock<Arc<HashMap<u64, T>>>,
}

impl<T> FileResource<T>
where
    T: FromStr + Clone + UniqueID,
    T::Err: ThreadSafeErr,
{
    /// create a new FileResource, building the cache eagerly.
    pub fn new(path: impl AsRef<Path>) -> Result<Self> {
        let path = path.as_ref().to_owned();
        let cached_at = RwLock::new(metadata(&path)?.modified()?);
        let cache = RwLock::new(Arc::new(build_cache(&path)?));
        Ok(Self { path, cached_at, cache })
    }
}

/// build the initial cache for a [FileResource]
fn build_cache<T>(p: impl AsRef<Path>) -> Result<HashMap<u64, T>>
where
    T: FromStr + UniqueID,
    T::Err: std::error::Error + Sync + Send + 'static,
{
    let mut cache = HashMap::new();
    for line_res in BufReader::new(File::open(p)?).lines() {
        let line = line_res?;
        match line.trim() {
            line if line.is_empty() => continue,
            line => match line.parse::<T>() {
                Ok(t) => cache.insert(t.id(), t),
                Err(err) => return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, Box::new(err))),
            },
        };
    }
    Ok(cache)
}

impl<T> Resource<T> for FileResource<T>
where
    T: FromStr + Clone + UniqueID + Sync + Send,
    T::Err: ThreadSafeErr,
{
    /// load the resource. this will return a cache of the file _at the time it was loaded_
    fn load(&self) -> Result<Arc<HashMap<u64, T>>> {
        let last_modified: SystemTime = metadata(&self.path)?.modified()?;

        // this needs to be on it's own line so that the read guard is dropped before we attempt to write to the cache. otherwise we will deadlock.
        let need_cache_reload = *self.cached_at.read().expect("thread failure") < last_modified;

        if need_cache_reload {
            *Arc::make_mut(&mut self.cache.write().unwrap()) = build_cache(&self.path)?;
            *self.cached_at.write().unwrap() = last_modified;
        };
        Ok(self.cache.read().unwrap().clone())
    }
}

impl<E: std::error::Error + Sync + Send + 'static> ThreadSafeErr for E {}

impl<T> Resource<T> for &FileResource<T>
where
    T: FromStr + Clone + UniqueID + Sync + Send,
    T::Err: ThreadSafeErr,
{
    /// load the resource. this will return a cache of the file _at the time it was loaded_
    fn load(&self) -> Result<Arc<HashMap<u64, T>>> {
        let last_modified: SystemTime = metadata(&self.path)?.modified()?;

        // this needs to be on it's own line so that the read guard is dropped before we attempt to write to the cache. otherwise we will deadlock.
        let need_cache_reload = *self.cached_at.read().expect("thread failure") < last_modified;

        if need_cache_reload {
            *Arc::make_mut(&mut self.cache.write().unwrap()) = build_cache(&self.path)?;
            *self.cached_at.write().unwrap() = last_modified;
        };
        Ok(self.cache.read().unwrap().clone())
    }
}

impl<'r, T> Resource<T> for rocket::State<'r, FileResource<T>>
where
    T: Clone + UniqueID + Send + Sync + FromStr,
    T::Err: ThreadSafeErr,
{
    fn load(&self) -> Result<Arc<HashMap<u64, T>>> {
        (*self.inner()).load()
    }
}

impl UniqueID for model::User {
    fn id(&self) -> u64 {
        self.uid
    }
}
impl UniqueID for model::Group {
    fn id(&self) -> u64 {
        self.gid
    }
}