cream 0.2.0

An implementation of SCIM (System for Cross-domain Identity Management).
Documentation
use std::str::FromStr;

use axum::http::StatusCode;
use bumpalo::Bump;
use ijson::INumber;
use nom::Finish;

use crate::{
    error::{Error, ErrorType},
    META_RESOURCE_TYPE,
};

mod parse;

#[cfg(test)]
mod tests;

#[derive(Debug, PartialEq)]
pub(crate) enum Filter {
    Present(AttrPath),
    Compare(AttrPath, CompareOp, CompValue),
    Has(AttrPath, Box<Self>),
    And(Vec<Self>),
    Or(Vec<Self>),
    Not(Box<Self>),
}

pub(crate) fn default_visit_filter(visitor: &mut impl Visitor, filter: &mut Filter) {
    match filter {
        Filter::Present(attr_path) => visitor.visit_attr_path(attr_path),
        Filter::Compare(attr_path, _, value) => {
            visitor.visit_attr_path(attr_path);
            visitor.visit_comp_value(value);
        }
        Filter::Has(attr_path, filter) => {
            visitor.visit_attr_path(attr_path);
            visitor.visit_filter(filter);
        }
        Filter::And(filters) | Filter::Or(filters) => {
            for filter in filters {
                visitor.visit_filter(filter);
            }
        }
        Filter::Not(filter) => visitor.visit_filter(filter),
    }
}

pub(crate) fn default_visit_value_path(visitor: &mut impl Visitor, value_path: &mut ValuePath) {
    match value_path {
        ValuePath::Attr(attr_path) => visitor.visit_attr_path(attr_path),
        ValuePath::Filtered(attr_path, filter) => {
            visitor.visit_attr_path(attr_path);
            visitor.visit_filter(filter);
        }
    }
}

pub(crate) fn default_visit_attr_path(_visitor: &mut impl Visitor, _attr_path: &mut AttrPath) {}
pub(crate) fn default_visit_comp_value(_visitor: &mut impl Visitor, _comp_value: &mut CompValue) {}

pub(crate) trait Visitor: Sized {
    fn visit_filter(&mut self, filter: &mut Filter) {
        default_visit_filter(self, filter);
    }
    fn visit_attr_path(&mut self, attr_path: &mut AttrPath) {
        default_visit_attr_path(self, attr_path);
    }
    fn visit_comp_value(&mut self, comp_value: &mut CompValue) {
        default_visit_comp_value(self, comp_value);
    }
    fn visit_value_path(&mut self, value_path: &mut ValuePath) {
        default_visit_value_path(self, value_path);
    }
}

/// Re-exports all names which are used for matching on filters.
pub mod prelude {
    pub use super::{CompValueRef::*, CompareOp::*, FilterRef::*, ValuePathRef::*};
}

impl Filter {
    pub(crate) fn as_ref<'a>(&'a self, scope: &'a Bump) -> FilterRef<'a> {
        match self {
            Self::Present(attr_path) => FilterRef::Present(attr_path.as_ref()),
            Self::Compare(attr_path, op, value) => {
                FilterRef::Compare(attr_path.as_ref(), *op, value.as_ref())
            }
            Self::Has(attr_path, filter) => {
                FilterRef::Has(attr_path.as_ref(), scope.alloc((**filter).as_ref(scope)))
            }
            Self::And(filters) => FilterRef::And(
                scope.alloc(
                    filters
                        .iter()
                        .map(|filter| filter.as_ref(scope))
                        .collect::<Vec<_>>(),
                ),
            ),
            Self::Or(filters) => FilterRef::Or(
                scope.alloc(
                    filters
                        .iter()
                        .map(|filter| filter.as_ref(scope))
                        .collect::<Vec<_>>(),
                ),
            ),
            Self::Not(filter) => FilterRef::Not(scope.alloc((**filter).as_ref(scope))),
        }
    }
    pub(crate) fn take_resource_type_filter(self) -> Result<(Option<Self>, String), Self> {
        match self {
            Self::And(filters) => {
                let mut remaining = Vec::new();
                let mut result = None;
                for filter in filters {
                    if result.is_none() {
                        match filter.take_resource_type_filter() {
                            Ok(found) => {
                                result = Some(found);
                            }
                            Err(filter) => {
                                remaining.push(filter);
                            }
                        }
                    } else {
                        remaining.push(filter);
                    }
                }
                if let Some((other, result)) = result {
                    if let Some(other) = other {
                        remaining.push(other);
                    }
                    match remaining.len() {
                        0 => Ok((None, result)),
                        1 => Ok((
                            Some(remaining.pop().expect("Already checked length")),
                            result,
                        )),
                        _ => Ok((Some(Self::And(remaining)), result)),
                    }
                } else {
                    Err(Self::And(remaining))
                }
            }
            Self::Compare(ref attr_path, CompareOp::Equal, CompValue::Str(resource_type))
                if attr_path.urn.is_none()
                    && attr_path.name.eq_ignore_ascii_case(META_RESOURCE_TYPE.name)
                    && attr_path.sub_attr.as_ref().is_some_and(|sub_attr| {
                        sub_attr.eq_ignore_ascii_case(
                            META_RESOURCE_TYPE
                                .sub_attr
                                .expect("Meta resource type has sub attribute"),
                        )
                    }) =>
            {
                Ok((None, resource_type))
            }
            _ => Err(self),
        }
    }
}

/// A reference to a filter expression.
#[derive(Debug, PartialEq, Copy, Clone)]
pub enum FilterRef<'a> {
    /// Whether an attribute is present.
    Present(AttrPathRef<'a>),
    /// A comparison between an attribute and a value.
    Compare(AttrPathRef<'a>, CompareOp, CompValueRef<'a>),
    /// Whether a value in a multi-valued attribute matches a filter.
    Has(AttrPathRef<'a>, &'a Self),
    /// A logical AND operation between filters.
    And(&'a [Self]),
    /// A logical OR operation between filters.
    Or(&'a [Self]),
    /// A logical NOT operation on a filter.
    Not(&'a Self),
}

impl FilterRef<'_> {
    /// Iterates over individual filter expressions which are combined with logical AND.
    pub fn iter_cnf(&self) -> impl Iterator<Item = Self> {
        let items: Vec<_> = match self {
            Self::And(filters) => filters.iter().flat_map(Self::iter_cnf).collect(),
            _ => vec![*self],
        };
        items.into_iter()
    }
}

#[derive(Debug, PartialEq)]
pub(crate) enum ValuePath {
    Attr(AttrPath),
    Filtered(AttrPath, Filter),
}

impl ValuePath {
    pub(crate) fn as_ref<'a>(&'a self, scope: &'a Bump) -> ValuePathRef<'a> {
        match self {
            Self::Attr(attr_path) => ValuePathRef::Attr(attr_path.as_ref()),
            Self::Filtered(attr_path, filter) => {
                ValuePathRef::Filtered(attr_path.as_ref(), filter.as_ref(scope))
            }
        }
    }
}

/// A attribute or filter expression which can be the target of an update.
#[derive(Debug, PartialEq, Copy, Clone)]
pub enum ValuePathRef<'a> {
    /// A simple attribute path.
    Attr(AttrPathRef<'a>),
    /// An attribute path with a filter applied.
    Filtered(AttrPathRef<'a>, FilterRef<'a>),
}

#[derive(Debug, PartialEq, Clone)]
pub(crate) struct AttrPath {
    pub urn: Option<String>,
    pub name: String,
    pub sub_attr: Option<String>,
}

impl AttrPath {
    pub(crate) fn as_ref(&self) -> AttrPathRef {
        AttrPathRef {
            urn: self.urn.as_deref(),
            name: self.name.as_str(),
            sub_attr: self.sub_attr.as_deref(),
        }
    }
}

/// A single attribute reference.
#[derive(Debug, PartialEq, Copy, Clone)]
pub struct AttrPathRef<'a> {
    /// The schema ID to which this attribute belongs. Omitted for core schema attributes.
    pub urn: Option<&'a str>,
    /// The name of the attribute.
    pub name: &'a str,
    /// The name of a sub-attribute, if any.
    pub sub_attr: Option<&'a str>,
}

/// Supported comparison operators.
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum CompareOp {
    /// Equal to.
    Equal,
    /// Not equal to.
    NotEqual,
    /// Contains.
    Contains,
    /// Starts with.
    StartsWith,
    /// Ends with.
    EndsWith,
    /// Greater than.
    GreaterThan,
    /// Greater than or equal to.
    GreaterThanOrEqual,
    /// Less than.
    LessThan,
    /// Less than or equal to.
    LessThanOrEqual,
}

impl FromStr for CompareOp {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "eq" => Ok(Self::Equal),
            "ne" => Ok(Self::NotEqual),
            "co" => Ok(Self::Contains),
            "sw" => Ok(Self::StartsWith),
            "ew" => Ok(Self::EndsWith),
            "gt" => Ok(Self::GreaterThan),
            "ge" => Ok(Self::GreaterThanOrEqual),
            "lt" => Ok(Self::LessThan),
            "le" => Ok(Self::LessThanOrEqual),
            _ => Err(format!("{} is not a valid operator", s)),
        }
    }
}

// https://datatracker.ietf.org/doc/html/rfc7159
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum CompValue {
    Null,
    Bool(bool),
    Num(INumber),
    Str(String),
}

impl CompValue {
    pub(crate) fn as_ref(&self) -> CompValueRef {
        match self {
            Self::Null => CompValueRef::Null,
            Self::Bool(b) => CompValueRef::Bool(*b),
            Self::Num(n) => CompValueRef::Num(n),
            Self::Str(s) => CompValueRef::Str(s.as_str()),
        }
    }
}

/// A literal value in a filter.
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum CompValueRef<'a> {
    /// The null value.
    Null,
    /// A boolean value.
    Bool(bool),
    /// A numeric value.
    Num(&'a INumber),
    /// A string value.
    Str(&'a str),
}

pub(crate) fn parse_filter(input: &str) -> Result<Filter, Error> {
    let (remain, expression) = parse::filter(input)
        .map_err(|e| e.to_owned())
        .finish()
        .map_err(|e| Error {
            status: StatusCode::BAD_REQUEST,
            schemas: Default::default(),
            scim_type: Some(ErrorType::InvalidFilter),
            detail: format!("Invalid filter: {:?}", e.input),
        })?;
    if !remain.is_empty() {
        return Err(Error {
            status: StatusCode::BAD_REQUEST,
            schemas: Default::default(),
            scim_type: Some(ErrorType::InvalidFilter),
            detail: format!("Invalid filter: unexpected {:?}", remain),
        });
    }
    Ok(expression)
}

pub(crate) fn parse_value_path(input: &str) -> Result<ValuePath, Error> {
    let (remain, expression) = parse::value_path(input)
        .map_err(|e| e.to_owned())
        .finish()
        .map_err(|e| Error {
            status: StatusCode::BAD_REQUEST,
            schemas: Default::default(),
            scim_type: Some(ErrorType::InvalidPath),
            detail: format!("Invalid path: {:?}", e.input),
        })?;
    if !remain.is_empty() {
        return Err(Error {
            status: StatusCode::BAD_REQUEST,
            schemas: Default::default(),
            scim_type: Some(ErrorType::InvalidPath),
            detail: format!("Invalid path: unexpected {:?}", remain),
        });
    }
    Ok(expression)
}
pub(crate) fn parse_attr_path(input: &str) -> Result<AttrPath, Error> {
    let (remain, expression) = parse::attr_path(input)
        .map_err(|e| e.to_owned())
        .finish()
        .map_err(|e| Error {
            status: StatusCode::BAD_REQUEST,
            schemas: Default::default(),
            scim_type: Some(ErrorType::InvalidPath),
            detail: format!("Invalid attribute path: {:?}", e.input),
        })?;
    if !remain.is_empty() {
        return Err(Error {
            status: StatusCode::BAD_REQUEST,
            schemas: Default::default(),
            scim_type: Some(ErrorType::InvalidPath),
            detail: format!("Invalid attribute path: unexpected {:?}", remain),
        });
    }
    Ok(expression)
}