From 5f99ca0953e68b99b5dab84b088034b0f42e8e04 Mon Sep 17 00:00:00 2001 From: Jon Seager Date: Tue, 10 Feb 2026 18:40:19 +0000 Subject: [PATCH] feat: add CLI update commands for brews and cups Add UpdateBrewCommand and UpdateCupCommand to the CLI with all updatable fields. Add corresponding update() methods to BrewsClient and CupsClient. --- src/infrastructure/client/brews.rs | 15 +++++- src/infrastructure/client/cups.rs | 15 +++++- src/presentation/cli/brews.rs | 87 +++++++++++++++++++++++++++++- src/presentation/cli/cups.rs | 41 +++++++++++++- 4 files changed, 154 insertions(+), 4 deletions(-) diff --git a/src/infrastructure/client/brews.rs b/src/infrastructure/client/brews.rs index 749ab20..46039ac 100644 --- a/src/infrastructure/client/brews.rs +++ b/src/infrastructure/client/brews.rs @@ -1,7 +1,7 @@ use anyhow::{Context, Result}; use chrono::{DateTime, Utc}; -use crate::domain::brews::{BrewWithDetails, QuickNote}; +use crate::domain::brews::{BrewWithDetails, QuickNote, UpdateBrew}; use crate::domain::ids::{BagId, BrewId, GearId}; use super::BrewlogClient; @@ -94,6 +94,19 @@ impl<'a> BrewsClient<'a> { self.inner.handle_response(response).await } + pub async fn update(&self, id: BrewId, payload: &UpdateBrew) -> Result { + let url = self.inner.endpoint(&format!("api/v1/brews/{id}"))?; + let response = self + .inner + .request(reqwest::Method::PUT, url) + .json(payload) + .send() + .await + .context("failed to issue update brew request")?; + + self.inner.handle_response(response).await + } + pub async fn delete(&self, id: BrewId) -> Result<()> { let url = self.inner.endpoint(&format!("api/v1/brews/{id}"))?; let response = self diff --git a/src/infrastructure/client/cups.rs b/src/infrastructure/client/cups.rs index 1a4b2ea..d09ab78 100644 --- a/src/infrastructure/client/cups.rs +++ b/src/infrastructure/client/cups.rs @@ -1,7 +1,7 @@ use anyhow::{Context, Result}; use reqwest::StatusCode; -use crate::domain::cups::{Cup, CupWithDetails, NewCup}; +use crate::domain::cups::{Cup, CupWithDetails, NewCup, UpdateCup}; use crate::domain::ids::CupId; use super::BrewlogClient; @@ -52,6 +52,19 @@ impl<'a> CupsClient<'a> { self.inner.handle_response(response).await } + pub async fn update(&self, id: CupId, payload: &UpdateCup) -> Result { + let url = self.inner.endpoint(&format!("api/v1/cups/{id}"))?; + let response = self + .inner + .request(reqwest::Method::PUT, url) + .json(payload) + .send() + .await + .context("failed to issue update cup request")?; + + self.inner.handle_response(response).await + } + pub async fn delete(&self, id: CupId) -> Result<()> { let url = self.inner.endpoint(&format!("api/v1/cups/{id}"))?; let response = self diff --git a/src/presentation/cli/brews.rs b/src/presentation/cli/brews.rs index 154ae83..c91ee92 100644 --- a/src/presentation/cli/brews.rs +++ b/src/presentation/cli/brews.rs @@ -4,7 +4,7 @@ use clap::{Args, Subcommand}; use super::macros::{define_delete_command, define_get_command}; use super::parse_created_at; use super::print_json; -use crate::domain::brews::QuickNote; +use crate::domain::brews::{QuickNote, UpdateBrew}; use crate::domain::ids::{BagId, BrewId, GearId}; use crate::infrastructure::client::BrewlogClient; @@ -16,6 +16,8 @@ pub enum BrewCommands { List(ListBrewsCommand), /// Get a brew by ID Get(GetBrewCommand), + /// Update a brew + Update(UpdateBrewCommand), /// Delete a brew Delete(DeleteBrewCommand), } @@ -25,6 +27,7 @@ pub async fn run(client: &BrewlogClient, cmd: BrewCommands) -> Result<()> { BrewCommands::Add(c) => add_brew(client, c).await, BrewCommands::List(c) => list_brews(client, c).await, BrewCommands::Get(c) => get_brew(client, c).await, + BrewCommands::Update(c) => update_brew(client, c).await, BrewCommands::Delete(c) => delete_brew(client, c).await, } } @@ -118,5 +121,87 @@ pub async fn list_brews(client: &BrewlogClient, command: ListBrewsCommand) -> Re print_json(&brews) } +#[derive(Debug, Args)] +pub struct UpdateBrewCommand { + #[arg(long)] + pub id: i64, + + /// ID of the bag to brew from + #[arg(long)] + pub bag_id: Option, + + /// Amount of coffee in grams + #[arg(long)] + pub coffee_weight: Option, + + /// ID of the grinder to use + #[arg(long)] + pub grinder_id: Option, + + /// Grind setting (e.g., 6.0 or 7.5) + #[arg(long)] + pub grind_setting: Option, + + /// ID of the brewer to use + #[arg(long)] + pub brewer_id: Option, + + /// ID of the filter paper to use + #[arg(long)] + pub filter_paper_id: Option, + + /// Volume of water in ml + #[arg(long)] + pub water_volume: Option, + + /// Water temperature in Celsius + #[arg(long)] + pub water_temp: Option, + + /// Quick notes (comma-separated: good,too-fast,too-slow,too-hot,under-extracted,over-extracted) + #[arg(long, value_delimiter = ',')] + pub quick_notes: Option>, + + /// Brew time in seconds (e.g., 150 for 2:30) + #[arg(long)] + pub brew_time: Option, + + /// Override creation timestamp (e.g. 2025-08-05T10:00:00Z or 2025-08-05) + #[arg(long)] + pub created_at: Option, +} + +pub async fn update_brew(client: &BrewlogClient, command: UpdateBrewCommand) -> Result<()> { + let created_at = command + .created_at + .map(|s| parse_created_at(&s)) + .transpose()?; + let quick_notes = command.quick_notes.map(|notes| { + notes + .iter() + .filter_map(|s| QuickNote::from_str_value(s)) + .collect() + }); + let payload = UpdateBrew { + bag_id: command.bag_id.map(BagId::new), + coffee_weight: command.coffee_weight, + grinder_id: command.grinder_id.map(GearId::new), + grind_setting: command.grind_setting, + brewer_id: command.brewer_id.map(GearId::new), + filter_paper_id: command.filter_paper_id.map(GearId::new), + water_volume: command.water_volume, + water_temp: command.water_temp, + quick_notes, + brew_time: command.brew_time, + created_at, + }; + + let brew = client + .brews() + .update(BrewId::new(command.id), &payload) + .await?; + print_json(&brew) +} + define_get_command!(GetBrewCommand, get_brew, BrewId, brews); define_delete_command!(DeleteBrewCommand, delete_brew, BrewId, brews, "brew"); diff --git a/src/presentation/cli/cups.rs b/src/presentation/cli/cups.rs index 29f2a33..1ec7dbf 100644 --- a/src/presentation/cli/cups.rs +++ b/src/presentation/cli/cups.rs @@ -4,7 +4,7 @@ use clap::{Args, Subcommand}; use super::macros::{define_delete_command, define_get_command}; use super::parse_created_at; use super::print_json; -use crate::domain::cups::NewCup; +use crate::domain::cups::{NewCup, UpdateCup}; use crate::domain::ids::{CafeId, CupId, RoastId}; use crate::infrastructure::client::BrewlogClient; @@ -16,6 +16,8 @@ pub enum CupCommands { List, /// Get a cup by ID Get(GetCupCommand), + /// Update a cup + Update(UpdateCupCommand), /// Delete a cup Delete(DeleteCupCommand), } @@ -25,6 +27,7 @@ pub async fn run(client: &BrewlogClient, cmd: CupCommands) -> Result<()> { CupCommands::Add(c) => add_cup(client, c).await, CupCommands::List => list_cups(client).await, CupCommands::Get(c) => get_cup(client, c).await, + CupCommands::Update(c) => update_cup(client, c).await, CupCommands::Delete(c) => delete_cup(client, c).await, } } @@ -60,5 +63,41 @@ pub async fn list_cups(client: &BrewlogClient) -> Result<()> { print_json(&cups) } +#[derive(Debug, Args)] +pub struct UpdateCupCommand { + #[arg(long)] + pub id: i64, + + /// ID of the roast + #[arg(long)] + pub roast_id: Option, + + /// ID of the cafe + #[arg(long)] + pub cafe_id: Option, + + /// Override creation timestamp (e.g. 2025-08-05T10:00:00Z or 2025-08-05) + #[arg(long)] + pub created_at: Option, +} + +pub async fn update_cup(client: &BrewlogClient, command: UpdateCupCommand) -> Result<()> { + let created_at = command + .created_at + .map(|s| parse_created_at(&s)) + .transpose()?; + let payload = UpdateCup { + roast_id: command.roast_id.map(RoastId::new), + cafe_id: command.cafe_id.map(CafeId::new), + created_at, + }; + + let cup = client + .cups() + .update(CupId::new(command.id), &payload) + .await?; + print_json(&cup) +} + define_get_command!(GetCupCommand, get_cup, CupId, cups); define_delete_command!(DeleteCupCommand, delete_cup, CupId, cups, "cup");