password-as-service 0.1.2

a very poorly considered http/rest service
Documentation
use crate::Query;
use serde::{Deserialize, Serialize};

/// A unix-style user. Users have unique UIDs and are compared and sorted_by_id by them.
#[allow(missing_docs)]
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
pub struct User {
    pub name: String,
    pub uid: u64,
    pub gid: u64,
    pub comment: String,
    pub home: String,
    pub shell: String,
}

#[derive(Debug, Clone, PartialEq)]
pub enum ParseError {
    Format,
    Numeric(std::num::ParseIntError),
}

impl std::fmt::Display for User {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(
            f,
            "{name}::{uid}:{gid}::{home}:{shell}",
            name = self.name,
            uid = self.uid,
            gid = self.gid,
            home = self.home,
            shell = self.shell
        )
    }
}
#[derive(Clone, PartialEq, Debug)]
/// A [Query] for a [User] who matches all elements where the Query is Some. It is invalid for all values of a Query to be None. Build a Query using a [QueryBuilder].
struct UserQuery(UserQueryBuilder);
#[derive(Default, Clone, PartialEq, Debug)]
#[allow(missing_docs)]
pub struct UserQueryBuilder {
    pub name: Option<String>,
    pub uid: Option<u64>,
    pub gid: Option<u64>,
    pub comment: Option<String>,
    pub home: Option<String>,
    pub shell: Option<String>,
}

#[allow(dead_code)]
impl UserQueryBuilder {
    /// Create a new QueryBuilder. Note that

    pub const fn new() -> Self {
        UserQueryBuilder {
            name: None,
            uid: None,
            gid: None,
            comment: None,
            home: None,
            shell: None,
        }
    }

    /// Build a [`Query<User>`], returning None if all elements of the QueryBuilder are None.
    pub fn build(self) -> Option<impl Query<User>> {
        if (self.name.is_none() && self.uid.is_none() && self.gid.is_none())
            && (self.comment.is_none() && self.home.is_none() && self.shell.is_none())
        {
            None
        } else {
            Some(UserQuery(self))
        }
    }

    /// add a `name` to the query, replacing it if it already exists.
    pub fn with_name(self, name: &str) -> Self {
        Self {
            name: Some(name.to_owned()),
            ..self
        }
    }

    /// add a `uid` to the query, replacing it if it already exists.
    pub fn with_uid(self, uid: u64) -> Self {
        Self { uid: Some(uid), ..self }
    }

    /// add a `gid` to the query, replacing it if it already exists.
    pub fn with_gid(self, gid: u64) -> Self {
        Self { gid: Some(gid), ..self }
    }

    /// add a `comment` to the query, replacing it if it already exists.
    pub fn with_comment(self, comment: &str) -> Self {
        Self {
            comment: Some(comment.to_owned()),
            ..self
        }
    }

    /// add a `home` to the query, replacing it if it already exists
    pub fn with_home(self, home: &str) -> Self {
        Self {
            home: Some(home.to_owned()),
            ..self
        }
    }

    /// add a `shell` to the query, replacing it if it already exists
    pub fn with_shell(self, shell: &str) -> Self {
        Self {
            shell: Some(shell.to_owned()),
            ..self
        }
    }
}

fn none_or_eq<T: PartialEq<Q>, Q>(a: &Option<T>, b: &Q) -> bool {
    match a {
        Some(ref a) => a == b,
        None => true,
    }
}
impl Query<User> for UserQuery {
    fn is_match(&self, user: &User) -> bool {
        none_or_eq(&self.0.name, &user.name)
            && none_or_eq(&self.0.uid, &user.uid)
            && none_or_eq(&self.0.gid, &user.gid)
            && none_or_eq(&self.0.comment, &user.comment)
            && none_or_eq(&self.0.home, &user.home)
            && none_or_eq(&self.0.shell, &user.shell)
    }
}

impl std::fmt::Display for ParseError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            ParseError::Format => write!(f, "bad format; a passwd entry must be exactly seven colon-separted entries: \"$USERNAME:PASSWORD:UID:GID:GECOS:HOMESHELL\""),
            ParseError::Numeric(err) => write!(f, "bad format: could not parse GID or UID: {}", err),

        }
    }
}

impl From<std::num::ParseIntError> for ParseError {
    fn from(err: std::num::ParseIntError) -> Self {
        ParseError::Numeric(err)
    }
}
impl std::error::Error for ParseError {}

impl std::str::FromStr for User {
    type Err = ParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let mut it = s.split(':');
        match (
            it.next(),
            it.next(),
            it.next(),
            it.next(),
            it.next(),
            it.next(),
            it.next(),
            it.next(),
        ) {
            (Some(name), _, Some(uid), Some(gid), _, Some(home), Some(shell), None) => Ok(User {
                comment: "".to_string(),
                name: name.to_string(),
                uid: uid.parse()?,
                gid: gid.parse()?,
                home: home.to_string(),
                shell: shell.to_string(),
            }),
            _ => Err(ParseError::Format),
        }
    }
}

#[test]
fn test_parse_and_display() {
    let root = User {
        name: "root".to_string(),
        comment: "".to_string(),
        uid: 0,
        gid: 0,
        home: "/root".to_string(),
        shell: "bash".to_string(),
    };

    assert_eq!(root, format!("{}", root).parse::<User>().unwrap())
}