use std::fmt;
use std::str::FromStr;
pub const ROLE_ENV: &str = "LEPTOSKIT_ROLE";
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Role {
Web,
Live,
Worker,
Capture,
}
impl Role {
pub const ALL: [Self; 4] = [Self::Web, Self::Live, Self::Worker, Self::Capture];
pub fn as_str(self) -> &'static str {
match self {
Self::Web => "web",
Self::Live => "live",
Self::Worker => "worker",
Self::Capture => "capture",
}
}
fn bit(self) -> u8 {
match self {
Self::Web => 1,
Self::Live => 2,
Self::Worker => 4,
Self::Capture => 8,
}
}
}
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 = RolesError;
fn from_str(name: &str) -> Result<Self, Self::Err> {
Self::ALL
.into_iter()
.find(|role| role.as_str().eq_ignore_ascii_case(name.trim()))
.ok_or_else(|| RolesError::Unknown(name.trim().to_owned()))
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct Roles(u8);
impl Roles {
pub const ALL: Self = Self(15);
pub fn only(roles: impl IntoIterator<Item = Role>) -> Self {
Self(roles.into_iter().fold(0, |bits, role| bits | role.bit()))
}
pub fn has(self, role: Role) -> bool {
self.0 & role.bit() != 0
}
pub fn web(self) -> bool {
self.has(Role::Web)
}
pub fn live(self) -> bool {
self.has(Role::Live)
}
pub fn worker(self) -> bool {
self.has(Role::Worker)
}
pub fn capture(self) -> bool {
self.has(Role::Capture)
}
pub fn is_all(self) -> bool {
self == Self::ALL
}
pub fn iter(self) -> impl Iterator<Item = Role> {
Role::ALL.into_iter().filter(move |role| self.has(*role))
}
pub fn parse(list: &str) -> Result<Self, RolesError> {
let mut roles = Self(0);
for name in list
.split(',')
.map(str::trim)
.filter(|name| !name.is_empty())
{
if name.eq_ignore_ascii_case("all") {
roles = Self::ALL;
} else {
roles.0 |= name.parse::<Role>()?.bit();
}
}
if roles.0 == 0 {
return Err(RolesError::Empty);
}
Ok(roles)
}
pub fn from_env() -> Result<Self, RolesError> {
match std::env::var(ROLE_ENV) {
Ok(list) if !list.trim().is_empty() => Self::parse(&list),
_ => Ok(Self::ALL),
}
}
pub fn from_args<I, S>(args: I) -> Result<Self, RolesError>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let mut named: Option<Self> = None;
let mut args = args.into_iter();
while let Some(arg) = args.next() {
let arg = arg.as_ref();
let list = if arg == "--role" {
args.next()
.map(|value| value.as_ref().to_owned())
.ok_or(RolesError::Missing)?
} else if let Some(list) = arg.strip_prefix("--role=") {
list.to_owned()
} else {
continue;
};
let roles = Self::parse(&list)?;
named = Some(named.map_or(roles, |named| Self(named.0 | roles.0)));
}
match named {
Some(roles) => Ok(roles),
None => Self::from_env(),
}
}
pub fn from_env_and_args() -> Result<Self, RolesError> {
Self::from_args(std::env::args().skip(1))
}
}
impl Default for Roles {
fn default() -> Self {
Self::ALL
}
}
impl fmt::Display for Roles {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let names: Vec<&str> = self.iter().map(Role::as_str).collect();
f.write_str(&names.join(","))
}
}
impl fmt::Debug for Roles {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Roles({self})")
}
}
impl FromStr for Roles {
type Err = RolesError;
fn from_str(list: &str) -> Result<Self, Self::Err> {
Self::parse(list)
}
}
impl FromIterator<Role> for Roles {
fn from_iter<I: IntoIterator<Item = Role>>(roles: I) -> Self {
Self::only(roles)
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum RolesError {
#[error("`{0}` isn't a role: use web, live, worker, capture, or all")]
Unknown(String),
#[error("no roles given: use web, live, worker, capture, or all")]
Empty,
#[error("`--role` needs a value, such as `--role web,live`")]
Missing,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_role_by_default() {
assert_eq!(Roles::default(), Roles::ALL);
assert!(
Roles::ALL.web() && Roles::ALL.live() && Roles::ALL.worker() && Roles::ALL.capture()
);
assert_eq!(Roles::ALL.to_string(), "web,live,worker,capture");
}
#[test]
fn lists_parse_in_any_order_and_case() {
let roles = Roles::parse(" Live, web ").unwrap();
assert!(roles.web() && roles.live() && !roles.worker() && !roles.capture());
assert_eq!(roles.to_string(), "web,live");
assert_eq!(Roles::parse("all").unwrap(), Roles::ALL);
assert_eq!(
Roles::parse("bogus"),
Err(RolesError::Unknown("bogus".into()))
);
assert_eq!(Roles::parse(" , "), Err(RolesError::Empty));
}
#[test]
fn the_flag_takes_every_form_and_adds_up() {
let roles =
Roles::from_args(["--port", "3000", "--role", "worker", "--role=capture"]).unwrap();
assert_eq!(roles, Roles::only([Role::Worker, Role::Capture]));
assert_eq!(Roles::from_args(["--role"]), Err(RolesError::Missing));
assert_eq!(
Roles::from_args(["--role", "web,live"]).unwrap(),
[Role::Web, Role::Live].into_iter().collect()
);
}
#[test]
fn without_the_flag_the_environment_decides() {
assert!(std::env::var_os(ROLE_ENV).is_none());
assert!(Roles::from_args(["--port", "3000"]).unwrap().is_all());
assert!(Roles::from_env_and_args().unwrap().is_all());
assert!(!Roles::only([Role::Live]).is_all());
}
#[test]
fn roles_read_and_print_by_name() {
for role in Role::ALL {
assert_eq!(role.to_string().parse::<Role>(), Ok(role));
}
assert_eq!(" Worker ".parse::<Role>(), Ok(Role::Worker));
assert_eq!(
"cron".parse::<Role>().unwrap_err().to_string(),
"`cron` isn't a role: use web, live, worker, capture, or all"
);
let roles: Roles = "capture,web".parse().unwrap();
assert_eq!(format!("{roles:?}"), "Roles(web,capture)");
assert_eq!(
Roles::from_args(["--role="]).unwrap_err().to_string(),
"no roles given: use web, live, worker, capture, or all"
);
}
}