Use default username 'me' and clean up query params

This commit is contained in:
felixm 2024-12-27 23:06:09 -05:00
parent 2d60bf7c45
commit 1000d5ea2d
4 changed files with 60 additions and 88 deletions

View File

@ -24,17 +24,20 @@ use time::OffsetDateTime;
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> { async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = BeeminderClient::new(std::env::var("BEEMINDER_API_KEY")?); 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 // Create a datapoint
let datapoint = CreateDatapoint::new(42.0) let datapoint = CreateDatapoint::new(42.0)
.with_timestamp(OffsetDateTime::now_utc()) .with_timestamp(OffsetDateTime::now_utc())
.with_comment("Meditation session"); .with_comment("Meditation session");
client.create_datapoint("username", "meditation", &datapoint).await?; client.create_datapoint("meditation", &datapoint).await?;
// Fetch recent datapoints // Fetch recent datapoints
let datapoints = client let datapoints = client
.get_datapoints("username", "meditation", Some("timestamp"), Some(10)) .get_datapoints("meditation", Some("timestamp"), Some(10))
.await?; .await?;
Ok(()) Ok(())

View File

@ -9,21 +9,18 @@ async fn main() {
env::var("BEEMINDER_API_KEY").expect("BEEMINDER_API_KEY environment variable not set"); env::var("BEEMINDER_API_KEY").expect("BEEMINDER_API_KEY environment variable not set");
let client = BeeminderClient::new(api_key); let client = BeeminderClient::new(api_key);
match client.get_user("me").await { match client.get_user().await {
Ok(user) => println!("{user:#?}"), Ok(user) => println!("{user:#?}"),
Err(e) => println!("{e:#?}"), Err(e) => println!("{e:#?}"),
} }
let since = datetime!(2024-12-13 20:00 UTC); 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:#?}"), Ok(user) => println!("{user:#?}"),
Err(e) => println!("{e:#?}"), Err(e) => println!("{e:#?}"),
} }
match client match client.get_datapoints("meditation", None, Some(2)).await {
.get_datapoints("me", "meditation", None, Some(10))
.await
{
Ok(datapoints) => println!("{datapoints:#?}"), Ok(datapoints) => println!("{datapoints:#?}"),
Err(e) => println!("{e:#?}"), Err(e) => println!("{e:#?}"),
} }
@ -31,7 +28,7 @@ async fn main() {
let d = CreateDatapoint::new(1.0) let d = CreateDatapoint::new(1.0)
.with_comment("Test #hashtag datapoint") .with_comment("Test #hashtag datapoint")
.with_requestid("unique-id-42"); .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:#?}"), Ok(datapoint) => println!("Added: {datapoint:#?}"),
Err(e) => println!("{e:#?}"), Err(e) => println!("{e:#?}"),
} }

View File

@ -13,25 +13,19 @@ pub struct BeeminderClient {
client: Client, client: Client,
api_key: String, api_key: String,
base_url: String, base_url: String,
username: String,
} }
impl BeeminderClient { impl BeeminderClient {
async fn request<T>( async fn get<T, U>(&self, endpoint: &str, query: &U) -> Result<T, Error>
&self,
endpoint: &str,
params: Option<Vec<(&str, &str)>>,
) -> Result<T, Error>
where where
T: serde::de::DeserializeOwned, 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 let response = self
.client .client
.get(format!("{}{}", self.base_url, endpoint)) .get(format!("{}{}", self.base_url, endpoint))
.query(&[("auth_token", self.api_key.as_str())])
.query(&query) .query(&query)
.send() .send()
.await? .await?
@ -39,57 +33,59 @@ impl BeeminderClient {
response.json().await.map_err(Error::from) response.json().await.map_err(Error::from)
} }
async fn post<T>(&self, endpoint: &str, params: Option<Vec<(&str, &str)>>) -> Result<T, Error> async fn post<T, U>(&self, endpoint: &str, query: &U) -> Result<T, Error>
where where
T: serde::de::DeserializeOwned, 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 let response = self
.client .client
.post(format!("{}{}", self.base_url, endpoint)) .post(format!("{}{}", self.base_url, endpoint))
.query(&query) .query(&[("auth_token", self.api_key.as_str())])
.query(query)
.send() .send()
.await? .await?
.error_for_status()?; .error_for_status()?;
response.json().await.map_err(Error::from) response.json().await.map_err(Error::from)
} }
/// Creates a new `BeeminderClient` with the given API key. /// Creates a new `BeeminderClient` with the given API key.
/// Default username is set to 'me'.
#[must_use] #[must_use]
pub fn new(api_key: String) -> Self { pub fn new(api_key: String) -> Self {
Self { Self {
client: Client::new(), client: Client::new(),
api_key, api_key,
base_url: "https://www.beeminder.com/api/v1/".to_string(), 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<String>) -> Self {
self.username = username.into();
self
}
/// Retrieves user information for user associated with client.
/// ///
/// # Errors /// # Errors
/// Returns an error if the HTTP request fails or response cannot be parsed. /// Returns an error if the HTTP request fails or response cannot be parsed.
pub async fn get_user(&self, username: &str) -> Result<UserInfo, Error> { pub async fn get_user(&self) -> Result<UserInfo, Error> {
self.request(&format!("users/{username}.json"), None).await let endpoint = format!("users/{}.json", self.username);
self.get(&endpoint, &()).await
} }
/// Retrieves detailed user information with changes since the specified timestamp. /// Retrieves detailed user information with changes since the specified timestamp.
/// ///
/// # Errors /// # Errors
/// Returns an error if the HTTP request fails or response cannot be parsed. /// Returns an error if the HTTP request fails or response cannot be parsed.
pub async fn get_user_diff( pub async fn get_user_diff(&self, diff_since: OffsetDateTime) -> Result<UserInfoDiff, Error> {
&self,
username: &str,
diff_since: OffsetDateTime,
) -> Result<UserInfoDiff, Error> {
let diff_since = diff_since.unix_timestamp().to_string(); let diff_since = diff_since.unix_timestamp().to_string();
let params = vec![("diff_since", diff_since.as_str())]; let query = [("diff_since", &diff_since)];
self.request(&format!("users/{username}.json"), Some(params)) let endpoint = format!("users/{}.json", self.username);
.await self.get(&endpoint, &query).await
} }
/// Retrieves datapoints for a specific goal. /// 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. /// Returns an error if the HTTP request fails or response cannot be parsed.
pub async fn get_datapoints( pub async fn get_datapoints(
&self, &self,
username: &str,
goal: &str, goal: &str,
sort: Option<&str>, sort: Option<&str>,
count: Option<u64>, count: Option<u64>,
) -> Result<Vec<Datapoint>, Error> { ) -> Result<Vec<Datapoint>, Error> {
let mut params = Vec::new(); let query: Vec<(&str, String)> = vec![
params.push(("sort", sort.unwrap_or("timestamp"))); Some(("sort", sort.unwrap_or("timestamp").to_string())),
count.map(|c| ("count", c.to_string())),
]
.into_iter()
.flatten()
.collect();
let count_str; let endpoint = format!("users/{}/goals/{goal}/datapoints.json", self.username);
if let Some(count) = count { self.get(&endpoint, &query).await
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
} }
/// Creates a new datapoint for a goal. /// 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. /// Returns an error if the HTTP request fails or response cannot be parsed.
pub async fn create_datapoint( pub async fn create_datapoint(
&self, &self,
username: &str,
goal: &str, goal: &str,
datapoint: &CreateDatapoint, datapoint: &CreateDatapoint,
) -> Result<Datapoint, Error> { ) -> Result<Datapoint, Error> {
let mut params = Vec::new(); let endpoint = format!("users/{}/goals/{goal}/datapoints.json", self.username);
self.post(&endpoint, datapoint).await
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
} }
/// Deletes a specific datapoint for a user's goal. /// Deletes a specific datapoint for the user's goal.
/// ///
/// # Arguments /// # Arguments
/// * `username` - The username of the user.
/// * `goal` - The name of the goal. /// * `goal` - The name of the goal.
/// * `datapoint_id` - The ID of the datapoint to delete. /// * `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. /// Returns an error if the HTTP request fails or if the response cannot be parsed.
pub async fn delete_datapoint( pub async fn delete_datapoint(
&self, &self,
username: &str,
goal: &str, goal: &str,
datapoint_id: &str, datapoint_id: &str,
) -> Result<Datapoint, Error> { ) -> Result<Datapoint, Error> {
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 query = vec![("auth_token", self.api_key.as_str())];
let response = self let response = self
@ -182,21 +153,21 @@ impl BeeminderClient {
response.json().await.map_err(Error::from) response.json().await.map_err(Error::from)
} }
/// Retrieves all goals for a user. /// Retrieves all goals for the user.
/// ///
/// # Errors /// # Errors
/// Returns an error if the HTTP request fails or response cannot be parsed. /// Returns an error if the HTTP request fails or response cannot be parsed.
pub async fn get_goals(&self, username: &str) -> Result<Vec<GoalSummary>, Error> { pub async fn get_goals(&self) -> Result<Vec<GoalSummary>, Error> {
self.request(&format!("users/{username}/goals.json"), None) let endpoint = format!("users/{}/goals.json", self.username);
.await self.get(&endpoint, &()).await
} }
/// Retrieves archived goals for a user. /// Retrieves archived goals for the user.
/// ///
/// # Errors /// # Errors
/// Returns an error if the HTTP request fails or response cannot be parsed. /// Returns an error if the HTTP request fails or response cannot be parsed.
pub async fn get_archived_goals(&self, username: &str) -> Result<Vec<GoalSummary>, Error> { pub async fn get_archived_goals(&self) -> Result<Vec<GoalSummary>, Error> {
self.request(&format!("users/{username}/goals/archived.json"), None) let endpoint = format!("users/{}/goals/archived.json", self.username);
.await self.get(&endpoint, &()).await
} }
} }

View File

@ -119,11 +119,12 @@ pub struct Datapoint {
/// Parameters for creating or updating a datapoint /// Parameters for creating or updating a datapoint
#[must_use] #[must_use]
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize)]
pub struct CreateDatapoint { pub struct CreateDatapoint {
/// The value to record /// The value to record
pub value: f64, pub value: f64,
/// Timestamp for the datapoint, defaults to now if None /// Timestamp for the datapoint, defaults to now if None
#[serde(with = "time::serde::timestamp::option")]
pub timestamp: Option<OffsetDateTime>, pub timestamp: Option<OffsetDateTime>,
/// Date string (e.g. "20150831"), alternative to timestamp /// Date string (e.g. "20150831"), alternative to timestamp
pub daystamp: Option<String>, pub daystamp: Option<String>,