use crate::data::{find_person, MotdRemindEnum, PplOps, TierDefaultOps, TierOps};
use crate::entities::{ppl, tier, tier_defaults};
use crate::PplError;
use clap::{Args, Subcommand};
use sea_orm::DatabaseConnection;
use std::io;
#[derive(Args, Debug)]
pub struct TierArgs {
#[command(subcommand)]
pub action: Option<TierAction>,
/// Change one person's settings (name, nick or id) instead of the tier defaults
#[arg(long, short, global = true)]
pub person: Option<String>,
}
#[derive(Subcommand, Debug)]
pub enum TierAction {
/// List tiers and their motd reminder settings
List,
/// Remind DAYS days ahead of dates for ppl in this tier
Set { tier: String, days: u32 },
/// How often the motd shows ppl in this tier: daily, hourly, random:N, always, never
Remind { tier: String, mode: MotdRemindEnum },
/// Remove the reminder window and remind mode for this tier
Clear { tier: String },
/// Create a new tier, or with --person put that person in an existing one
Add {
tier: String,
#[arg(long)]
symbol: Option<String>,
/// One of: red, gold, amber, green, teal, cyan, blue, violet, pink, slate
#[arg(long)]
color: Option<String>,
/// Remind this many days ahead of dates for ppl in this tier
#[arg(long)]
days: Option<u32>,
},
}
/// Which row gets the change: a tier default, or one person's tier
#[derive(Clone, Copy)]
enum Target {
Default(i32),
Person(i32),
}
fn not_found(msg: String) -> PplError {
io::Error::new(io::ErrorKind::NotFound, msg).into()
}
fn describe(delta: Option<u32>, remind: Option<&str>) -> (String, String) {
let window = match delta {
Some(d) => format!("{} days", d),
None => "-".to_string(),
};
let mode = remind
.and_then(|r| r.parse::<MotdRemindEnum>().ok())
.map(|r| r.label())
.unwrap_or_else(|| "-".to_string());
(window, mode)
}
fn find_default<'a>(
tiers: &'a [tier_defaults::Model],
name: &str,
) -> Result<&'a tier_defaults::Model, PplError> {
tiers
.iter()
.find(|t| t.key.eq_ignore_ascii_case(name))
.ok_or_else(|| {
let keys = tiers.iter().map(|t| t.key.as_str()).collect::<Vec<_>>();
not_found(format!(
"no tier '{}', expected one of: {}",
name,
keys.join(", ")
))
})
}
fn find_person_tier<'a>(
tiers: &'a [tier::Model],
person: &ppl::Model,
name: &str,
) -> Result<&'a tier::Model, PplError> {
let theirs = tiers
.iter()
.filter(|t| t.ppl_id == person.id)
.collect::<Vec<_>>();
theirs
.iter()
.find(|t| t.name.eq_ignore_ascii_case(name))
.copied()
.ok_or_else(|| {
let keys = theirs.iter().map(|t| t.name.as_str()).collect::<Vec<_>>();
not_found(format!(
"{} isn't in tier '{}' (their tiers: {})",
person.name.trim(),
name,
if keys.is_empty() {
"none".to_string()
} else {
keys.join(", ")
}
))
})
}
pub async fn do_tiers(args: &TierArgs, db: &DatabaseConnection) -> Result<(), PplError> {
let defaults = TierDefaultOps::list(db).await?;
let person = match &args.person {
Some(q) => {
let ppl = PplOps::list(db).await?;
Some(find_person(&ppl, q)?.clone())
}
None => None,
};
let person_tiers = TierOps::list(db).await?;
let action = match &args.action {
None | Some(TierAction::List) => {
list(&defaults, &person_tiers, person.as_ref());
return Ok(());
}
Some(TierAction::Add {
tier,
symbol,
color,
days,
}) => {
return add(
db,
&defaults,
&person_tiers,
person.as_ref(),
tier,
symbol,
color,
*days,
)
.await;
}
Some(a) => a,
};
let tier_name = match action {
TierAction::Set { tier, .. }
| TierAction::Remind { tier, .. }
| TierAction::Clear { tier } => tier,
TierAction::List | TierAction::Add { .. } => unreachable!(),
};
// Who we're changing, for the confirmation message
let (target, label) = match &person {
Some(p) => {
let t = find_person_tier(&person_tiers, p, tier_name)?;
(
Target::Person(t.id),
format!("{} ({})", t.name, p.name.trim()),
)
}
None => {
let t = find_default(&defaults, tier_name)?;
(Target::Default(t.id), t.key.clone())
}
};
let set_delta = |delta| async move {
match target {
Target::Default(id) => TierDefaultOps::set_sig_date_delta(db, id, delta).await,
Target::Person(id) => TierOps::set_sig_date_delta(db, id, delta).await,
}
};
let set_remind = |remind| async move {
match target {
Target::Default(id) => TierDefaultOps::set_sig_remind_enum(db, id, remind).await,
Target::Person(id) => TierOps::set_sig_remind_enum(db, id, remind).await,
}
};
match action {
TierAction::Set { days, .. } => {
set_delta(Some(*days)).await?;
println!("{}: remind {} days ahead", label, days);
}
TierAction::Remind { mode, .. } => {
set_remind(Some(*mode)).await?;
println!("{}: remind {}", label, mode.label());
}
TierAction::Clear { .. } => {
set_delta(None).await?;
set_remind(None).await?;
match person {
Some(_) => println!("{}: using the tier defaults", label),
None => println!("{}: reminder settings cleared", label),
}
}
TierAction::List | TierAction::Add { .. } => unreachable!(),
}
Ok(())
}
async fn add(
db: &DatabaseConnection,
defaults: &[tier_defaults::Model],
person_tiers: &[tier::Model],
person: Option<&ppl::Model>,
tier: &str,
symbol: &Option<String>,
color: &Option<String>,
days: Option<u32>,
) -> Result<(), PplError> {
let Some(p) = person else {
let key = TierDefaultOps::add(
db,
tier,
symbol.as_deref().unwrap_or(""),
color.as_deref().unwrap_or(""),
days,
)
.await?;
println!("added tier {}", key);
return Ok(());
};
if symbol.is_some() || color.is_some() || days.is_some() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"--symbol, --color and --days describe a new tier; leave out --person to create one",
)
.into());
}
let d = find_default(defaults, tier)?;
if person_tiers
.iter()
.any(|t| t.ppl_id == p.id && t.name == d.key)
{
println!("{} is already in {}", p.name.trim(), d.key);
return Ok(());
}
TierOps::create(db, p.id, d.key.clone(), None).await?;
println!("{} added to {}", p.name.trim(), d.key);
Ok(())
}
fn list(
defaults: &[tier_defaults::Model],
person_tiers: &[tier::Model],
person: Option<&ppl::Model>,
) {
match person {
None => {
for t in defaults {
let (window, mode) = describe(t.sig_date_delta, t.sig_remind_enum.as_deref());
println!(
"{} {:<16} {:<10} {:<12} {}",
t.symbol.as_deref().unwrap_or(" "),
t.key,
window,
mode,
if t.enabled { "" } else { "(disabled)" }
);
}
}
Some(p) => {
let theirs = person_tiers
.iter()
.filter(|t| t.ppl_id == p.id)
.collect::<Vec<_>>();
if theirs.is_empty() {
println!("{} isn't in any tiers", p.name.trim());
}
// Show the person's own value, or the tier default it falls back to
for t in theirs {
let d = defaults.iter().find(|d| d.key == t.name);
let (window, mode) = describe(
t.sig_date_delta.or(d.and_then(|d| d.sig_date_delta)),
t.sig_remind_enum
.as_deref()
.or(d.and_then(|d| d.sig_remind_enum.as_deref())),
);
let mark = |own: bool| if own { "" } else { " (default)" };
let window = format!("{}{}", window, mark(t.sig_date_delta.is_some()));
println!(
"{} {:<16} {:<20} {}{}",
t.symbol
.as_deref()
.or(d.and_then(|d| d.symbol.as_deref()))
.unwrap_or(" "),
t.name,
window,
mode,
mark(t.sig_remind_enum.is_some()),
);
}
}
}
}
⭐
Log in to mark this repository.
9.1 KiB · text
History
aea77cc