use std::fmt;
use std::str::FromStr;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Role {
Reader,
Shopper,
Editor,
Admin,
}
impl Role {
pub const ALL: [Role; 4] = [Role::Reader, Role::Shopper, Role::Editor, Role::Admin];
pub fn as_str(self) -> &'static str {
match self {
Role::Reader => "reader",
Role::Shopper => "shopper",
Role::Editor => "editor",
Role::Admin => "admin",
}
}
pub fn allows(self, capability: Capability) -> bool {
self >= capability.minimum_role()
}
}
impl fmt::Display for Role {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for Role {
type Err = String;
fn from_str(name: &str) -> Result<Self, Self::Err> {
Role::ALL
.into_iter()
.find(|role| role.as_str() == name)
.ok_or_else(|| {
let names: Vec<_> = Role::ALL.iter().map(|role| role.as_str()).collect();
format!("unknown role {name:?}: use {}", names.join(", "))
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Capability {
EditLists,
EditRecipes,
Administer,
}
impl Capability {
pub fn minimum_role(self) -> Role {
match self {
Capability::EditLists => Role::Shopper,
Capability::EditRecipes => Role::Editor,
Capability::Administer => Role::Admin,
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Viewer {
auth_enabled: bool,
user: Option<(String, Role)>,
recipes_only: bool,
}
impl Viewer {
#[cfg(feature = "server")]
pub fn open() -> Self {
Self::default()
}
#[cfg(feature = "server")]
pub fn guest() -> Self {
Self {
auth_enabled: true,
user: None,
recipes_only: false,
}
}
#[cfg(feature = "server")]
pub fn signed_in(user: impl Into<String>, role: Role) -> Self {
Self {
auth_enabled: true,
user: Some((user.into(), role)),
recipes_only: false,
}
}
#[cfg(feature = "server")]
pub fn limited_to_recipes(self) -> Self {
Self {
recipes_only: !self.is_signed_in(),
..self
}
}
pub fn can(&self, capability: Capability) -> bool {
if self.recipes_only {
return false;
}
match &self.user {
Some((_, role)) => role.allows(capability),
None => !self.auth_enabled,
}
}
pub fn can_edit_lists(&self) -> bool {
self.can(Capability::EditLists)
}
pub fn can_edit_recipes(&self) -> bool {
self.can(Capability::EditRecipes)
}
pub fn can_admin(&self) -> bool {
self.can(Capability::Administer)
}
pub fn auth_enabled(&self) -> bool {
self.auth_enabled
}
pub fn recipes_only(&self) -> bool {
self.recipes_only
}
pub fn is_signed_in(&self) -> bool {
self.user.is_some()
}
pub fn username(&self) -> &str {
self.user.as_ref().map_or("", |(name, _)| name.as_str())
}
pub fn role(&self) -> Option<Role> {
self.user.as_ref().map(|(_, role)| *role)
}
}
#[cfg(all(test, feature = "server"))]
mod tests {
use super::*;
const EVERYTHING: [Capability; 3] = [
Capability::EditLists,
Capability::EditRecipes,
Capability::Administer,
];
#[test]
fn open_server_lets_everyone_do_everything() {
let viewer = Viewer::default();
assert_eq!(viewer, Viewer::open());
for capability in EVERYTHING {
assert!(viewer.can(capability), "{capability:?}");
}
assert!(!viewer.auth_enabled());
assert!(!viewer.is_signed_in());
assert_eq!(viewer.role(), None);
}
#[test]
fn guest_can_do_nothing() {
let viewer = Viewer::guest();
for capability in EVERYTHING {
assert!(!viewer.can(capability), "{capability:?}");
}
assert!(viewer.auth_enabled());
assert_eq!(viewer.username(), "");
}
#[test]
fn each_role_adds_to_the_one_before() {
let can = |role| {
let viewer = Viewer::signed_in("alice", role);
[
viewer.can_edit_lists(),
viewer.can_edit_recipes(),
viewer.can_admin(),
]
};
assert_eq!(can(Role::Reader), [false, false, false]);
assert_eq!(can(Role::Shopper), [true, false, false]);
assert_eq!(can(Role::Editor), [true, true, false]);
assert_eq!(can(Role::Admin), [true, true, true]);
}
#[test]
fn recipes_only_limits_whoever_is_not_signed_in() {
for viewer in [Viewer::open(), Viewer::guest()] {
let viewer = viewer.limited_to_recipes();
assert!(viewer.recipes_only());
for capability in EVERYTHING {
assert!(!viewer.can(capability), "{capability:?}");
}
}
let viewer = Viewer::signed_in("alice", Role::Shopper).limited_to_recipes();
assert!(!viewer.recipes_only());
assert!(viewer.can_edit_lists());
}
#[test]
fn signed_in_user_is_named() {
let viewer = Viewer::signed_in("alice", Role::Editor);
assert!(viewer.is_signed_in());
assert_eq!(viewer.username(), "alice");
assert_eq!(viewer.role(), Some(Role::Editor));
}
#[test]
fn role_names_round_trip() {
for role in Role::ALL {
assert_eq!(role.as_str().parse::<Role>(), Ok(role));
}
let err = "Editor".parse::<Role>().unwrap_err();
assert!(err.contains("reader, shopper, editor, admin"), "{err}");
}
}