From 1000d5ea2dab0dd69600bd18312d80da442f5f67 Mon Sep 17 00:00:00 2001 From: felixm Date: Fri, 27 Dec 2024 23:06:09 -0500 Subject: [PATCH] Use default username 'me' and clean up query params --- README.md | 7 ++- examples/demo.rs | 11 ++-- src/lib.rs | 127 ++++++++++++++++++----------------------------- src/types.rs | 3 +- 4 files changed, 60 insertions(+), 88 deletions(-) diff --git a/README.md b/README.md index dc40780..894d8cd 100644 --- a/README.md +++ b/README.md @@ -24,17 +24,20 @@ use time::OffsetDateTime; #[tokio::main] async fn main() -> Result<(), Box> { let client = BeeminderClient::new(std::env::var("BEEMINDER_API_KEY")?); + + // username defaults to 'me'; use `with_username` to change it + // let client = BeeminderClient::new("api-key").with_username("foo"); // Create a datapoint let datapoint = CreateDatapoint::new(42.0) .with_timestamp(OffsetDateTime::now_utc()) .with_comment("Meditation session"); - client.create_datapoint("username", "meditation", &datapoint).await?; + client.create_datapoint("meditation", &datapoint).await?; // Fetch recent datapoints let datapoints = client - .get_datapoints("username", "meditation", Some("timestamp"), Some(10)) + .get_datapoints("meditation", Some("timestamp"), Some(10)) .await?; Ok(()) diff --git a/examples/demo.rs b/examples/demo.rs index bb5684a..d6290c1 100644 --- a/examples/demo.rs +++ b/examples/demo.rs @@ -9,21 +9,18 @@ async fn main() { env::var("BEEMINDER_API_KEY").expect("BEEMINDER_API_KEY environment variable not set"); let client = BeeminderClient::new(api_key); - match client.get_user("me").await { + match client.get_user().await { Ok(user) => println!("{user:#?}"), Err(e) => println!("{e:#?}"), } let since = datetime!(2024-12-13 20:00 UTC); - match client.get_user_diff("me", since).await { + match client.get_user_diff(since).await { Ok(user) => println!("{user:#?}"), Err(e) => println!("{e:#?}"), } - match client - .get_datapoints("me", "meditation", None, Some(10)) - .await - { + match client.get_datapoints("meditation", None, Some(2)).await { Ok(datapoints) => println!("{datapoints:#?}"), Err(e) => println!("{e:#?}"), } @@ -31,7 +28,7 @@ async fn main() { let d = CreateDatapoint::new(1.0) .with_comment("Test #hashtag datapoint") .with_requestid("unique-id-42"); - match client.create_datapoint("me", "meditation", &d).await { + match client.create_datapoint("meditation", &d).await { Ok(datapoint) => println!("Added: {datapoint:#?}"), Err(e) => println!("{e:#?}"), } diff --git a/src/lib.rs b/src/lib.rs index 8ed330b..72ad8e2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,25 +13,19 @@ pub struct BeeminderClient { client: Client, api_key: String, base_url: String, + username: String, } impl BeeminderClient { - async fn request( - &self, - endpoint: &str, - params: Option>, - ) -> Result + async fn get(&self, endpoint: &str, query: &U) -> Result where T: serde::de::DeserializeOwned, + U: serde::ser::Serialize, { - let mut query = vec![("auth_token", self.api_key.as_str())]; - if let Some(additional_params) = params { - query.extend(additional_params); - } - let response = self .client .get(format!("{}{}", self.base_url, endpoint)) + .query(&[("auth_token", self.api_key.as_str())]) .query(&query) .send() .await? @@ -39,57 +33,59 @@ impl BeeminderClient { response.json().await.map_err(Error::from) } - async fn post(&self, endpoint: &str, params: Option>) -> Result + async fn post(&self, endpoint: &str, query: &U) -> Result where T: serde::de::DeserializeOwned, + U: serde::ser::Serialize, { - let mut query = vec![("auth_token", self.api_key.as_str())]; - if let Some(additional_params) = params { - query.extend(additional_params); - } - let response = self .client .post(format!("{}{}", self.base_url, endpoint)) - .query(&query) + .query(&[("auth_token", self.api_key.as_str())]) + .query(query) .send() .await? .error_for_status()?; - response.json().await.map_err(Error::from) } /// Creates a new `BeeminderClient` with the given API key. + /// Default username is set to 'me'. #[must_use] pub fn new(api_key: String) -> Self { Self { client: Client::new(), api_key, base_url: "https://www.beeminder.com/api/v1/".to_string(), + username: "me".to_string(), } } - /// Retrieves information about a user. + /// Sets a username for this client. + #[must_use] + pub fn with_username(mut self, username: impl Into) -> Self { + self.username = username.into(); + self + } + + /// Retrieves user information for user associated with client. /// /// # Errors /// Returns an error if the HTTP request fails or response cannot be parsed. - pub async fn get_user(&self, username: &str) -> Result { - self.request(&format!("users/{username}.json"), None).await + pub async fn get_user(&self) -> Result { + let endpoint = format!("users/{}.json", self.username); + self.get(&endpoint, &()).await } /// Retrieves detailed user information with changes since the specified timestamp. /// /// # Errors /// Returns an error if the HTTP request fails or response cannot be parsed. - pub async fn get_user_diff( - &self, - username: &str, - diff_since: OffsetDateTime, - ) -> Result { + pub async fn get_user_diff(&self, diff_since: OffsetDateTime) -> Result { let diff_since = diff_since.unix_timestamp().to_string(); - let params = vec![("diff_since", diff_since.as_str())]; - self.request(&format!("users/{username}.json"), Some(params)) - .await + let query = [("diff_since", &diff_since)]; + let endpoint = format!("users/{}.json", self.username); + self.get(&endpoint, &query).await } /// Retrieves datapoints for a specific goal. @@ -98,22 +94,20 @@ impl BeeminderClient { /// Returns an error if the HTTP request fails or response cannot be parsed. pub async fn get_datapoints( &self, - username: &str, goal: &str, sort: Option<&str>, count: Option, ) -> Result, Error> { - let mut params = Vec::new(); - params.push(("sort", sort.unwrap_or("timestamp"))); + let query: Vec<(&str, String)> = vec![ + Some(("sort", sort.unwrap_or("timestamp").to_string())), + count.map(|c| ("count", c.to_string())), + ] + .into_iter() + .flatten() + .collect(); - let count_str; - if let Some(count) = count { - count_str = count.to_string(); - params.push(("count", &count_str)); - } - - let endpoint = format!("users/{username}/goals/{goal}/datapoints.json"); - self.request(&endpoint, Some(params)).await + let endpoint = format!("users/{}/goals/{goal}/datapoints.json", self.username); + self.get(&endpoint, &query).await } /// Creates a new datapoint for a goal. @@ -122,41 +116,16 @@ impl BeeminderClient { /// Returns an error if the HTTP request fails or response cannot be parsed. pub async fn create_datapoint( &self, - username: &str, goal: &str, datapoint: &CreateDatapoint, ) -> Result { - let mut params = Vec::new(); - - let value_str = datapoint.value.to_string(); - params.push(("value", value_str.as_str())); - - let timestamp_str; - if let Some(ts) = datapoint.timestamp { - timestamp_str = ts.unix_timestamp().to_string(); - params.push(("timestamp", timestamp_str.as_str())); - } - - if let Some(ds) = &datapoint.daystamp { - params.push(("daystamp", ds.as_str())); - } - - if let Some(c) = &datapoint.comment { - params.push(("comment", c.as_str())); - } - - if let Some(rid) = &datapoint.requestid { - params.push(("requestid", rid.as_str())); - } - - let endpoint = format!("users/{username}/goals/{goal}/datapoints.json"); - self.post(&endpoint, Some(params)).await + let endpoint = format!("users/{}/goals/{goal}/datapoints.json", self.username); + self.post(&endpoint, datapoint).await } - /// Deletes a specific datapoint for a user's goal. + /// Deletes a specific datapoint for the user's goal. /// /// # Arguments - /// * `username` - The username of the user. /// * `goal` - The name of the goal. /// * `datapoint_id` - The ID of the datapoint to delete. /// @@ -164,11 +133,13 @@ impl BeeminderClient { /// Returns an error if the HTTP request fails or if the response cannot be parsed. pub async fn delete_datapoint( &self, - username: &str, goal: &str, datapoint_id: &str, ) -> Result { - let endpoint = format!("users/{username}/goals/{goal}/datapoints/{datapoint_id}.json"); + let endpoint = format!( + "users/{}/goals/{goal}/datapoints/{datapoint_id}.json", + self.username + ); let query = vec![("auth_token", self.api_key.as_str())]; let response = self @@ -182,21 +153,21 @@ impl BeeminderClient { response.json().await.map_err(Error::from) } - /// Retrieves all goals for a user. + /// Retrieves all goals for the user. /// /// # Errors /// Returns an error if the HTTP request fails or response cannot be parsed. - pub async fn get_goals(&self, username: &str) -> Result, Error> { - self.request(&format!("users/{username}/goals.json"), None) - .await + pub async fn get_goals(&self) -> Result, Error> { + let endpoint = format!("users/{}/goals.json", self.username); + self.get(&endpoint, &()).await } - /// Retrieves archived goals for a user. + /// Retrieves archived goals for the user. /// /// # Errors /// Returns an error if the HTTP request fails or response cannot be parsed. - pub async fn get_archived_goals(&self, username: &str) -> Result, Error> { - self.request(&format!("users/{username}/goals/archived.json"), None) - .await + pub async fn get_archived_goals(&self) -> Result, Error> { + let endpoint = format!("users/{}/goals/archived.json", self.username); + self.get(&endpoint, &()).await } } diff --git a/src/types.rs b/src/types.rs index ef0126f..a2bcf67 100644 --- a/src/types.rs +++ b/src/types.rs @@ -119,11 +119,12 @@ pub struct Datapoint { /// Parameters for creating or updating a datapoint #[must_use] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct CreateDatapoint { /// The value to record pub value: f64, /// Timestamp for the datapoint, defaults to now if None + #[serde(with = "time::serde::timestamp::option")] pub timestamp: Option, /// Date string (e.g. "20150831"), alternative to timestamp pub daystamp: Option,