use crate::{
GlobalArgs, Subcommand,
commands::request::DisplayExchangeCommand,
completions::{
complete_profile, complete_recipe, complete_recipe_or_request_id,
},
util::print_table,
};
use anyhow::{anyhow, bail};
use chrono::{
DateTime, Local, Utc,
format::{DelayedFormat, StrftimeItems},
};
use clap::Parser;
use itertools::Itertools;
use slumber_core::{
collection::{ProfileId, RecipeId},
database::{Database, ProfileFilter},
http::RequestId,
};
use std::{process::ExitCode, str::FromStr};
#[derive(Clone, Debug, Parser)]
pub struct DbRequestCommand {
#[command(subcommand)]
subcommand: DbRequestSubcommand,
}
#[derive(Clone, Debug, clap::Subcommand)]
enum DbRequestSubcommand {
#[command(visible_alias = "ls")]
List {
#[clap(add = complete_recipe())]
recipe: Option<RecipeId>,
#[clap(long = "profile", short, add = complete_profile())]
#[expect(clippy::option_option)]
profile: Option<Option<ProfileId>>,
#[clap(short, long)]
all: bool,
#[clap(long)]
id_only: bool,
},
Get {
#[clap(add = complete_recipe_or_request_id())]
request: RecipeOrRequest,
#[clap(flatten)]
display: DisplayExchangeCommand,
},
#[command(visible_alias = "rm")]
Delete {
#[clap(num_args = 1..)]
request: Vec<RequestId>,
},
}
#[derive(Clone, Debug)]
enum RecipeOrRequest {
Recipe(RecipeId),
Request(RequestId),
}
impl Subcommand for DbRequestCommand {
async fn execute(self, global: GlobalArgs) -> anyhow::Result<ExitCode> {
match self.subcommand {
DbRequestSubcommand::List {
recipe,
profile,
all,
id_only,
} => {
let database = Database::load()?;
let exchanges = match (recipe, profile, all) {
(None, None, true) => database.get_all_requests()?,
(None, None, false) => database
.into_collection(&global.collection_file()?)?
.get_all_requests()?,
(Some(recipe_id), profile, false) => database
.into_collection(&global.collection_file()?)?
.get_recipe_requests(profile.into(), &recipe_id)?,
(Some(_), _, true) => {
bail!("Cannot specify `--all` with a recipe")
}
(None, Some(_), _) => {
bail!("Cannot specify `--profile` without a recipe")
}
};
if id_only {
for exchange in exchanges {
println!("{}", exchange.id);
}
} else {
print_table(
["Recipe", "Profile", "Time", "Status", "Request ID"],
&exchanges
.into_iter()
.map(|exchange| {
[
exchange.recipe_id.to_string(),
exchange
.profile_id
.map(ProfileId::into)
.unwrap_or_default(),
format_time_iso(&exchange.start_time)
.to_string(),
exchange.status.as_u16().to_string(),
exchange.id.to_string(),
]
})
.collect_vec(),
);
}
}
DbRequestSubcommand::Get { request, display } => {
let database = Database::load()?
.into_collection(&global.collection_file()?)?;
let exchange = match request {
RecipeOrRequest::Recipe(recipe_id) => database
.get_latest_request(ProfileFilter::All, &recipe_id)?
.ok_or_else(|| {
anyhow!("Recipe `{recipe_id}` has no history")
})?,
RecipeOrRequest::Request(request_id) => {
database.get_request(request_id)?.ok_or_else(|| {
anyhow!("Request `{request_id}` not found")
})?
}
};
display.write_request(&exchange.request);
display.write_response(&exchange.response)?;
}
DbRequestSubcommand::Delete { request } => {
let database = Database::load()?;
for id in &request {
database.delete_request(*id)?;
}
println!("Deleted {} request(s)", request.len());
}
}
Ok(ExitCode::SUCCESS)
}
}
impl FromStr for RecipeOrRequest {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(s.parse::<RequestId>()
.map(Self::Request)
.unwrap_or_else(|_| {
RecipeOrRequest::Recipe(RecipeId::from(s.to_owned()))
}))
}
}
fn format_time_iso(
time: &'_ DateTime<Utc>,
) -> DelayedFormat<StrftimeItems<'_>> {
time.with_timezone(&Local).format("%FT%TZ%Z")
}