use anyhow::Context;
use chrono::{DateTime, Utc};
use trusty_mpm::client::DaemonClient;
use trusty_mpm::client::http_client::deliverables::CreateMilestoneArgs;
use trusty_mpm::deliverable::Milestone;
use crate::cli::MilestonesAction;
fn daemon(client: &reqwest::Client, url: &str) -> DaemonClient {
DaemonClient::with_client(client.clone(), url.to_string())
}
pub(crate) async fn dispatch(
client: &reqwest::Client,
url: &str,
action: MilestonesAction,
) -> anyhow::Result<()> {
match action {
MilestonesAction::List { project, json } => {
let milestones = daemon(client, url).list_milestones(&project).await?;
if json {
println!("{}", serde_json::to_string_pretty(&milestones)?);
} else if milestones.is_empty() {
println!("no milestones for project '{project}'");
} else {
for m in &milestones {
println!("{}", render_milestone_line(m));
}
}
Ok(())
}
MilestonesAction::Add {
project,
name,
target_date,
description,
} => {
let target_date = parse_target_date(&target_date)?;
let args = CreateMilestoneArgs {
name,
description,
target_date,
};
let created = daemon(client, url)
.create_milestone(&project, &args)
.await?;
println!("created milestone {}", created.id);
println!("{}", render_milestone_line(&created));
Ok(())
}
MilestonesAction::Show { project, id, json } => {
let m = daemon(client, url).get_milestone(&project, &id).await?;
if json {
println!("{}", serde_json::to_string_pretty(&m)?);
} else {
for line in render_milestone_detail(&m) {
println!("{line}");
}
}
Ok(())
}
}
}
fn parse_target_date(s: &str) -> anyhow::Result<DateTime<Utc>> {
Ok(DateTime::parse_from_rfc3339(s)
.with_context(|| {
format!("invalid --target-date {s:?}; expected RFC 3339 (e.g. 2026-09-01T00:00:00Z)")
})?
.with_timezone(&Utc))
}
fn render_milestone_line(m: &Milestone) -> String {
format!(
"{}\t{:?}\t{}\t{}",
m.id,
m.status,
m.target_date.to_rfc3339(),
m.name
)
}
fn render_milestone_detail(m: &Milestone) -> Vec<String> {
let mut lines = vec![
format!("{} — {}", m.id, m.name),
format!(" project: {}", m.project_name),
format!(" status: {:?}", m.status),
format!(" target: {}", m.target_date.to_rfc3339()),
format!(" deliverables: {}", m.deliverables.len()),
];
if !m.description.is_empty() {
lines.push(format!(" description: {}", m.description));
}
lines
}
#[cfg(test)]
mod tests {
use super::*;
fn milestone() -> Milestone {
Milestone {
id: trusty_mpm::deliverable::MilestoneId::new(),
project_name: "widget".into(),
name: "v1.0 Alpha".into(),
description: String::new(),
target_date: Utc::now(),
status: trusty_mpm::deliverable::MilestoneStatus::Proposed,
deliverables: vec![],
created_at: Utc::now(),
}
}
#[test]
fn render_milestone_line_has_name() {
let line = render_milestone_line(&milestone());
assert!(line.contains("v1.0 Alpha"), "{line}");
assert!(line.contains("Proposed"), "{line}");
}
#[test]
fn parse_target_date_ok() {
let dt = parse_target_date("2026-09-01T00:00:00Z").unwrap();
assert_eq!(dt.to_rfc3339(), "2026-09-01T00:00:00+00:00");
}
#[test]
fn parse_target_date_rejects_garbage() {
assert!(parse_target_date("not-a-date").is_err());
}
}