use std::fmt::{Debug, Display, Formatter};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum AuthorizationDecision<AllowedReason = (), ForbiddenReason = ()> {
Allowed { reason: AllowedReason },
Forbidden { reason: ForbiddenReason },
}
impl<A, F> Display for AuthorizationDecision<A, F>
where
A: Display,
F: Display,
{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
AuthorizationDecision::Allowed { reason } => write!(f, "Allowed({})", reason),
AuthorizationDecision::Forbidden { reason } => write!(f, "Forbidden({})", reason),
}
}
}
impl<F> AuthorizationDecision<(), F> {
pub fn allowed() -> Self {
Self::Allowed { reason: () }
}
}
impl<A> AuthorizationDecision<A, ()> {
pub fn forbidden() -> Self {
Self::Forbidden { reason: () }
}
}
impl<A, F> AuthorizationDecision<A, F> {
pub fn allowed_with(reason: A) -> Self {
AuthorizationDecision::Allowed { reason }
}
pub fn forbidden_with(reason: F) -> Self {
AuthorizationDecision::Forbidden { reason }
}
pub fn is_allowed(&self) -> bool {
matches!(self, AuthorizationDecision::Allowed { .. })
}
pub fn is_forbidden(&self) -> bool {
matches!(self, AuthorizationDecision::Forbidden { .. })
}
pub fn ok_or<E>(self, err: E) -> Result<(), E> {
match self {
AuthorizationDecision::Allowed { reason: _ } => Ok(()),
AuthorizationDecision::Forbidden { reason: _ } => Err(err),
}
}
pub fn ok_or_else<E, G>(self, err_fn: G) -> Result<(), E>
where
G: FnOnce(F) -> E,
{
match self {
AuthorizationDecision::Allowed { reason: _ } => Ok(()),
AuthorizationDecision::Forbidden { reason } => Err(err_fn(reason)),
}
}
pub fn into_result(self) -> Result<A, F> {
match self {
AuthorizationDecision::Allowed { reason } => Ok(reason),
AuthorizationDecision::Forbidden { reason } => Err(reason),
}
}
pub fn expect_allowed(self, msg: &str) -> A {
match self {
AuthorizationDecision::Allowed { reason } => reason,
AuthorizationDecision::Forbidden { reason: _ } => {
panic!("{}", msg)
}
}
}
pub fn expect_forbidden(self, msg: &str) -> F {
match self {
AuthorizationDecision::Forbidden { reason } => reason,
AuthorizationDecision::Allowed { reason: _ } => {
panic!("{}", msg)
}
}
}
pub fn unwrap_forbidden(self) -> F {
match self {
AuthorizationDecision::Allowed { reason: _ } => {
panic!("tried to unwrap authorization decision as forbidden but it was allowed")
}
AuthorizationDecision::Forbidden { reason } => reason,
}
}
pub fn unwrap_allowed(self) -> A {
match self {
AuthorizationDecision::Allowed { reason } => reason,
AuthorizationDecision::Forbidden { reason: _ } => {
panic!("tried to unwrap authorization decision as allowed but it was forbidden")
}
}
}
pub fn inspect_forbidden<G: FnOnce(&F)>(self, inspect_fn: G) -> Self {
if let AuthorizationDecision::Forbidden { ref reason } = self {
inspect_fn(reason);
}
self
}
pub fn inspect_allowed<G: FnOnce(&A)>(self, inspect_fn: G) -> Self {
if let AuthorizationDecision::Allowed { ref reason } = self {
inspect_fn(reason);
}
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
#[allow(clippy::unit_cmp)]
#[test]
fn basic_allowed_and_forbidden() {
let ok: AuthorizationDecision<(), ()> = AuthorizationDecision::allowed();
assert!(ok.is_allowed());
assert!(!ok.is_forbidden());
assert_eq!(ok.into_result(), Ok(()));
assert_eq!(ok.ok_or("err"), Ok(()));
assert_eq!(ok.ok_or_else(|_| "err"), Ok(()));
assert_eq!(ok.unwrap_allowed(), ());
let no: AuthorizationDecision<(), ()> = AuthorizationDecision::forbidden();
assert!(!no.is_allowed());
assert!(no.is_forbidden());
assert_eq!(no.into_result(), Err(()));
assert_eq!(no.ok_or("err"), Err("err"));
assert_eq!(no.ok_or_else(|_| "err"), Err("err"));
assert_eq!(no.unwrap_forbidden(), ());
}
#[test]
fn custom_reason_constructors_and_unwraps() {
let allow_r: AuthorizationDecision<i32, ()> = AuthorizationDecision::allowed_with(42);
assert!(allow_r.is_allowed());
assert_eq!(allow_r.into_result(), Ok(42));
assert_eq!(allow_r.unwrap_allowed(), 42);
let forbid_r: AuthorizationDecision<(), &str> =
AuthorizationDecision::forbidden_with("nope");
assert!(forbid_r.is_forbidden());
assert_eq!(forbid_r.into_result(), Err("nope"));
assert_eq!(forbid_r.unwrap_forbidden(), "nope");
}
#[test]
fn expect_methods_return_reason() {
let a: AuthorizationDecision<&str, &str> = AuthorizationDecision::allowed_with("yes");
assert_eq!(a.expect_allowed("fail"), "yes");
let f: AuthorizationDecision<&str, &str> = AuthorizationDecision::forbidden_with("denied");
assert_eq!(f.expect_forbidden("fail"), "denied");
}
#[test]
#[should_panic(expected = "must be allowed")]
fn expect_allowed_panics_when_forbidden() {
let dec: AuthorizationDecision = AuthorizationDecision::forbidden();
dec.expect_allowed("must be allowed");
}
#[test]
#[should_panic(expected = "must be forbidden")]
fn expect_forbidden_panics_when_allowed() {
let dec: AuthorizationDecision<&str> = AuthorizationDecision::allowed_with("x");
dec.expect_forbidden("must be forbidden");
}
#[test]
fn inspect_allowed_and_forbidden_call_closure() {
let flag_allowed = Cell::new(false);
let dec_allowed: AuthorizationDecision<&str, ()> =
AuthorizationDecision::allowed_with("admin").inspect_allowed(|reason| {
assert_eq!(reason, &"admin");
flag_allowed.set(true);
});
assert!(flag_allowed.get());
assert!(dec_allowed.is_allowed());
let flag_forbidden = Cell::new(false);
let dec_forbidden: AuthorizationDecision<(), &str> =
AuthorizationDecision::forbidden_with("denied").inspect_forbidden(|reason| {
assert_eq!(reason, &"denied");
flag_forbidden.set(true);
});
assert!(flag_forbidden.get());
assert!(dec_forbidden.is_forbidden());
}
}