use std::cmp::Ordering;
use std::fmt::Display;
use std::str::FromStr;
#[cfg(any(test, feature = "serde"))]
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::traits::Conditions;
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(any(test, feature = "serde"), derive(Deserialize, Serialize))]
pub enum AccessLevel {
Pull,
Read,
Write,
Manage,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(any(test, feature = "serde"), derive(Deserialize, Serialize))]
pub struct Access<C = ()> {
pub conditions: Option<C>,
pub level: AccessLevel,
}
impl<C> Display for Access<C> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self.level {
AccessLevel::Pull => "pull",
AccessLevel::Read => "read",
AccessLevel::Write => "write",
AccessLevel::Manage => "manage",
};
write!(f, "{s}")
}
}
impl<C> Access<C> {
pub fn pull() -> Self {
Self {
level: AccessLevel::Pull,
conditions: None,
}
}
pub fn read() -> Self {
Self {
level: AccessLevel::Read,
conditions: None,
}
}
pub fn write() -> Self {
Self {
level: AccessLevel::Write,
conditions: None,
}
}
pub fn manage() -> Self {
Self {
level: AccessLevel::Manage,
conditions: None,
}
}
pub fn with_conditions(mut self, conditions: C) -> Self {
self.conditions = Some(conditions);
self
}
pub fn is_pull(&self) -> bool {
matches!(self.level, AccessLevel::Pull)
}
pub fn is_read(&self) -> bool {
matches!(self.level, AccessLevel::Read)
}
pub fn is_write(&self) -> bool {
matches!(self.level, AccessLevel::Write)
}
pub fn is_manage(&self) -> bool {
matches!(self.level, AccessLevel::Manage)
}
}
impl<C: PartialOrd> PartialOrd for Access<C> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
match (self.conditions.as_ref(), other.conditions.as_ref()) {
(Some(self_cond), Some(other_cond)) => {
match self_cond.partial_cmp(other_cond) {
Some(Ordering::Greater | Ordering::Equal) => {
match self.level.cmp(&other.level) {
Ordering::Less => Some(Ordering::Less),
Ordering::Equal | Ordering::Greater => Some(Ordering::Greater),
}
}
Some(Ordering::Less) => Some(Ordering::Less),
None => None,
}
}
(None, Some(_)) => match self.level.cmp(&other.level) {
Ordering::Less => Some(Ordering::Less),
Ordering::Equal | Ordering::Greater => Some(Ordering::Greater),
},
_ => Some(self.level.cmp(&other.level)),
}
}
}
impl<C: PartialOrd + Eq> Ord for Access<C> {
fn cmp(&self, other: &Self) -> Ordering {
self.partial_cmp(other).unwrap_or(Ordering::Less)
}
}
impl Conditions for () {}
#[derive(Debug, Error)]
#[error("unknown access string: {0}")]
pub struct AccessError(String);
impl FromStr for Access {
type Err = AccessError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let access = match s {
"pull" => Access::pull(),
"read" => Access::read(),
"write" => Access::write(),
"manage" => Access::manage(),
_ => return Err(AccessError(s.to_string())),
};
Ok(access)
}
}
#[cfg(test)]
mod tests {
use std::cmp::Ordering;
use crate::Access;
#[derive(Debug, Clone, PartialEq, Eq)]
struct PathCondition(String);
impl PartialOrd for PathCondition {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
let self_parts: Vec<_> = self.0.split('/').filter(|s| !s.is_empty()).collect();
let other_parts: Vec<_> = other.0.split('/').filter(|s| !s.is_empty()).collect();
let min_len = self_parts.len().min(other_parts.len());
let is_prefix = self_parts[..min_len] == other_parts[..min_len];
if is_prefix {
match self_parts.len().cmp(&other_parts.len()) {
Ordering::Less => Some(Ordering::Greater),
Ordering::Equal => Some(Ordering::Equal),
Ordering::Greater => Some(Ordering::Less),
}
} else {
None
}
}
}
#[test]
fn path_condition_comparators() {
let root_access = Access::read().with_conditions(PathCondition("/root".to_string()));
let private_access =
Access::read().with_conditions(PathCondition("/root/private".to_string()));
let public_access =
Access::read().with_conditions(PathCondition("/root/public".to_string()));
assert!(root_access >= private_access);
assert!(root_access >= public_access);
assert!(!(private_access >= public_access));
assert!(!(private_access <= public_access));
let read_access_to_root =
Access::read().with_conditions(PathCondition("/root".to_string()));
let requested_write_access_to_sub_path =
Access::write().with_conditions(PathCondition("/root/private".to_string()));
assert!(requested_write_access_to_sub_path < read_access_to_root);
let unconditional_read = Access::<PathCondition>::read();
assert!(unconditional_read > public_access);
}
#[derive(Debug, Clone, PartialOrd, PartialEq, Eq)]
struct ExpiryTimestamp(u64);
#[test]
fn expiry_timestamp_access_ordering() {
let access_expires_soon = Access::read().with_conditions(ExpiryTimestamp(10));
let access_expires_later = Access::read().with_conditions(ExpiryTimestamp(100));
assert!(access_expires_later > access_expires_soon);
assert!(access_expires_soon < access_expires_later);
const NOW: ExpiryTimestamp = ExpiryTimestamp(50);
let requested_read_access = Access::read().with_conditions(NOW);
assert!(access_expires_soon < requested_read_access);
assert!(access_expires_later >= requested_read_access);
let requested_pull_access = Access::pull().with_conditions(NOW);
assert!(access_expires_soon < requested_pull_access);
let requested_write_access = Access::write().with_conditions(NOW);
assert!(access_expires_later < requested_write_access);
let requested_read_access = Access::read().with_conditions(NOW);
let access_no_expiry = Access::<ExpiryTimestamp>::read();
assert!(access_no_expiry > requested_read_access);
}
}