diff --git a/Cargo.toml b/Cargo.toml index 602206a..e79cbdb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,4 +12,4 @@ reqwest = { version = "^0.12", features = ["json"] } serde = { version = "^1.0", features = ["derive"] } thiserror = "^2.0" tokio = { version = "^1.42", features = ["full"] } -time = { version = "^0.3", features = ["serde", "parsing", "formatting", "macros"] } +time = { version = "^0.3", features = ["serde", "parsing", "formatting"]} diff --git a/examples/demo.rs b/examples/demo.rs index d6290c1..5a815ac 100644 --- a/examples/demo.rs +++ b/examples/demo.rs @@ -1,7 +1,7 @@ -use beeminder::types::CreateDatapoint; +use beeminder::types::{CreateDatapoint, UpdateDatapoint}; use beeminder::BeeminderClient; use std::env; -use time::macros::datetime; +use time::{Duration, OffsetDateTime}; #[tokio::main] async fn main() { @@ -14,22 +14,44 @@ async fn main() { Err(e) => println!("{e:#?}"), } - let since = datetime!(2024-12-13 20:00 UTC); + let since = OffsetDateTime::now_utc() - Duration::days(2); match client.get_user_diff(since).await { Ok(user) => println!("{user:#?}"), Err(e) => println!("{e:#?}"), } - match client.get_datapoints("meditation", None, Some(2)).await { - Ok(datapoints) => println!("{datapoints:#?}"), - Err(e) => println!("{e:#?}"), - } - - let d = CreateDatapoint::new(1.0) - .with_comment("Test #hashtag datapoint") - .with_requestid("unique-id-42"); - match client.create_datapoint("meditation", &d).await { + let new_datapoint = CreateDatapoint::new(20.0) + .with_comment("I did some pushups!") + .with_requestid("unique-pushup-id-42"); + match client.create_datapoint("pushups", &new_datapoint).await { Ok(datapoint) => println!("Added: {datapoint:#?}"), Err(e) => println!("{e:#?}"), } + + let goal_name = "pushups"; + match client.get_datapoints(&goal_name, None, Some(3)).await { + Ok(datapoints) => { + if let Some(first_datapoint) = datapoints.first() { + let update_datapoint = UpdateDatapoint::from(first_datapoint) + .with_value(40.0) + .with_comment("Much better."); + + match client.update_datapoint(&goal_name, &update_datapoint).await { + Ok(datapoint) => println!("Updated: {datapoint:#?}"), + Err(e) => println!("Update error: {e:#?}"), + } + + match client + .delete_datapoint(&goal_name, &update_datapoint.id) + .await + { + Ok(datapoint) => println!("Deleted: {datapoint:#?}"), + Err(e) => println!("Delete error: {e:#?}"), + } + } else { + println!("No datapoints found"); + } + } + Err(e) => println!("Get datapoints error: {e:#?}"), + } } diff --git a/src/lib.rs b/src/lib.rs index 72ad8e2..6379842 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,7 @@ pub mod types; -use crate::types::{CreateDatapoint, Datapoint, GoalSummary, UserInfo, UserInfoDiff}; +use crate::types::{ + CreateDatapoint, Datapoint, GoalSummary, UpdateDatapoint, UserInfo, UserInfoDiff, +}; use reqwest::Client; use time::OffsetDateTime; @@ -123,7 +125,37 @@ impl BeeminderClient { self.post(&endpoint, datapoint).await } - /// Deletes a specific datapoint for the user's goal. + /// Updates an existing datapoint for a goal. + /// + /// # Arguments + /// * `goal` - The slug/name of the goal to update + /// * `update` - The datapoint update containing the ID and fields to update + /// + /// # Errors + /// Returns an error if the HTTP request fails or if the response cannot be parsed + pub async fn update_datapoint( + &self, + goal: &str, + update: &UpdateDatapoint, + ) -> Result { + let endpoint = format!( + "users/{}/goals/{}/datapoints/{}.json", + self.username, goal, update.id + ); + + let response = self + .client + .put(format!("{}{}", self.base_url, endpoint)) + .query(&[("auth_token", self.api_key.as_str())]) + .query(update) + .send() + .await? + .error_for_status()?; + + response.json().await.map_err(Error::from) + } + + /// Deletes a specific datapoint for a goal. /// /// # Arguments /// * `goal` - The name of the goal. diff --git a/src/types.rs b/src/types.rs index a2bcf67..3aefada 100644 --- a/src/types.rs +++ b/src/types.rs @@ -171,6 +171,82 @@ impl CreateDatapoint { } } +/// Parameters for updating an existing datapoint +#[derive(Debug, Clone, Serialize)] +pub struct UpdateDatapoint { + /// ID of the datapoint to update + #[serde(skip_serializing)] + pub id: String, + /// Optional new timestamp for the datapoint + #[serde( + with = "time::serde::timestamp::option", + skip_serializing_if = "Option::is_none" + )] + pub timestamp: Option, + /// Optional new value + #[serde(skip_serializing_if = "Option::is_none")] + pub value: Option, + /// Optional new comment + #[serde(skip_serializing_if = "Option::is_none")] + pub comment: Option, +} + +impl From<&Datapoint> for UpdateDatapoint { + fn from(datapoint: &Datapoint) -> Self { + Self { + id: datapoint.id.clone(), + timestamp: Some(datapoint.timestamp), + value: Some(datapoint.value), + comment: datapoint.comment.clone(), + } + } +} + +impl UpdateDatapoint { + /// Creates an empty update for the given datapoint ID + #[must_use] + pub fn new(id: impl Into) -> Self { + Self { + id: id.into(), + timestamp: None, + value: None, + comment: None, + } + } + + /// Creates an update from an existing datapoint with no changes + #[must_use] + pub fn from_datapoint(datapoint: &Datapoint) -> Self { + Self { + id: datapoint.id.clone(), + timestamp: Some(datapoint.timestamp), + value: Some(datapoint.value), + comment: datapoint.comment.clone(), + } + } + + /// Sets a new timestamp + #[must_use] + pub fn with_timestamp(mut self, timestamp: OffsetDateTime) -> Self { + self.timestamp = Some(timestamp); + self + } + + /// Sets a new value + #[must_use] + pub fn with_value(mut self, value: f64) -> Self { + self.value = Some(value); + self + } + + /// Sets a new comment + #[must_use] + pub fn with_comment(mut self, comment: &str) -> Self { + self.comment = Some(comment.to_string()); + self + } +} + #[derive(Debug, Serialize, Deserialize)] pub struct UserInfo { /// Username of the Beeminder account