dotperms 0.1.0

A simple library for LuckPerms-like authorization using permission nodes.
Documentation
#![doc = include_str!("../README.md")]
#![forbid(unsafe_code)]
#![forbid(missing_docs)]
#![allow(clippy::needless_return)]

use std::collections::HashMap;

#[cfg(feature = "dot-separator")]
const DEFAULT_SEPARATOR: &str = ".";
#[cfg(feature = "colon-separator")]
const DEFAULT_SEPARATOR: &str = ":";

#[cfg(not(any(feature = "dot-separator", feature = "colon-separator")))]
compile_error!(
    "No default separator chosen! Please add a coresponding feature to the dotperms crate."
);

#[cfg(test)]
mod tests;

mod std_trait_impl;
#[allow(unused_imports)]
pub use crate::std_trait_impl::*;

/// A string that represents a certain permission.
///
/// Usually looks something like "minecraft.command.ban", but it can also use other separators beside dots.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct PermNode<'a> {
    inter: Vec<&'a str>,
    divider: &'a str,
    context: HashMap<String, String>,
}

impl<'a> PermNode<'a> {
    /// Creates an empty PermNode.
    pub fn new() -> Self {
        return PermNode {
            inter: Vec::new(),
            context: HashMap::new(),
            divider: DEFAULT_SEPARATOR,
        };
    }

    /// Creates an empty PermNode, with a given divider
    pub fn new_with_divider(divider: &'a str) -> Self {
        return PermNode {
            inter: Vec::new(),
            context: HashMap::new(),
            divider,
        };
    }

    /// Push a permission node part.
    pub fn push(&mut self, part: &'a str) {
        if part == "*" && !self.inter.is_empty() {
            return;
        }

        if part.is_empty() {
            return;
        }

        self.inter.push(part);
    }

    /// Pop a permission node, if one remains, otherwise - [`None`].
    pub fn pop(&mut self) -> Option<&'a str> {
        self.inter.pop()
    }

    /// Set the permission node according to a given string and separator.
    pub fn set(&mut self, perm_node_str: &'a str, divider: &str) {
        self.inter.extend(PermNode::after_parse_logic(
            perm_node_str.split(divider).collect(),
        ));
    }

    /// Convert a string like value into.
    pub fn from_str(value: &'a str, divider: &'a str) -> Self {
        PermNode {
            inter: PermNode::after_parse_logic(value.split(divider).collect()),
            context: HashMap::new(),
            divider,
        }
    }

    fn after_parse_logic(input: Vec<&'a str>) -> Vec<&'a str> {
        // Invalidate any string that contain parts that contain "*" but are not exactly "*"
        if input
            .iter()
            .any(|el| el.chars().count() > 1 && el.contains("*"))
        {
            return Vec::new();
        }

        for i in 0..input.len() {
            if i == 0 && input[i] == "*" {
                return vec!["*"];
            }
            if input[i] == "*" || input[i].is_empty() {
                let mut new = Vec::with_capacity(i);
                new.extend_from_slice(&input[..i]);
                return new;
            }
        }

        return input;
    }

    /// Check if the `requirement` allowed under this permission.
    ///
    /// Consider ourselves to be a rule that a user has. `requirement` is the permission user must have.
    /// Context is also taken into consideration.
    ///
    /// ## Example
    ///
    /// ```rust
    /// use dotperms::PermNode;
    ///
    /// let a: PermNode = "minecraft.cmd.ban".into();
    /// let b: PermNode = "minecraft".into();
    ///
    /// assert!(b.allows(&a));
    /// assert!(!a.allows(&b));
    /// ```
    pub fn allows(&self, requirement: &PermNode) -> bool {
        if self.inter.is_empty() {
            return false;
        }

        let context_allowed: bool = 'c: {
            for (context_item_key, context_item_value) in self.context.iter() {
                let val = requirement.context.get(context_item_key);
                if val.is_none() || val.is_some_and(|it| it != context_item_value) {
                    break 'c false;
                }
            }

            break 'c true;
        };

        let perm_allowed: bool = 'c: {
            let our_part = &self.inter;
            let their_part = &requirement.inter;

            if their_part.is_empty() {
                return false;
            }

            if our_part.len() == 1 && our_part[0] == "*" {
                return true;
            }

            // dbg!(&our_part, &their_part);

            if their_part.starts_with(our_part) {
                break 'c true;
            }

            break 'c false;
        };

        // dbg!(perm_allowed, context_allowed);

        return perm_allowed && context_allowed;
    }

    /// Set a certain key to a value in the permission node's context.
    pub fn set_ctx(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.context.insert(key.into(), value.into());

        self
    }

    /// Get the value from the context by key.
    pub fn get_ctx(&self, key: &str) -> Option<impl AsRef<str>> {
        return self.context.get(key);
    }

    /// Get the iterator over keys and values of the Context.
    pub fn ctx_iter(&self) -> impl Iterator<Item = (impl AsRef<str>, impl AsRef<str>)> {
        return self.context.iter();
    }

    /// Get an iterator over parts of the permission node, starting from the very first all the way to the last one.
    pub fn node_iter(&self) -> impl ExactSizeIterator<Item = impl AsRef<str>> {
        return self.inter.iter();
    }

    /// Get the amount of parts in a permission node.
    pub fn node_len(&self) -> usize {
        return self.inter.len();
    }
}

/// Checks if all requirements meet given permissions.
pub fn check_requirement<'a>(
    requirements: impl Iterator<Item = &'a PermNode<'a>>,
    permissions: impl Iterator<Item = &'a PermNode<'a>> + Clone,
) -> bool {
    for requirement in requirements {
        let mut at_least_one_allow = false;
        for perm in permissions.clone() {
            if perm.allows(requirement) {
                at_least_one_allow = true;
            }
        }
        if !at_least_one_allow {
            return false;
        }
    }

    return true;
}

/// Just a simple macro to create a [`PermNode`] from a string.
#[macro_export]
macro_rules! pn {
    ($val:expr) => {
        PermNode::from($val)
    };
}