Implement datapoint update endpoint

This commit is contained in:
2025-01-27 20:58:41 -05:00
parent 1000d5ea2d
commit d368d045ce
4 changed files with 145 additions and 15 deletions

View File

@@ -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"]}

View File

@@ -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:#?}"),
}
}

View File

@@ -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<Datapoint, Error> {
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.

View File

@@ -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<OffsetDateTime>,
/// Optional new value
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<f64>,
/// Optional new comment
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
}
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<String>) -> 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