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]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
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(())

View File

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

View File

@ -13,25 +13,19 @@ pub struct BeeminderClient {
client: Client,
api_key: String,
base_url: String,
username: String,
}
impl BeeminderClient {
async fn request<T>(
&self,
endpoint: &str,
params: Option<Vec<(&str, &str)>>,
) -> Result<T, Error>
async fn get<T, U>(&self, endpoint: &str, query: &U) -> Result<T, Error>
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<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
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<String>) -> 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<UserInfo, Error> {
self.request(&format!("users/{username}.json"), None).await
pub async fn get_user(&self) -> Result<UserInfo, Error> {
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<UserInfoDiff, Error> {
pub async fn get_user_diff(&self, diff_since: OffsetDateTime) -> Result<UserInfoDiff, Error> {
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<u64>,
) -> Result<Vec<Datapoint>, 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<Datapoint, Error> {
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<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 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<Vec<GoalSummary>, Error> {
self.request(&format!("users/{username}/goals.json"), None)
.await
pub async fn get_goals(&self) -> Result<Vec<GoalSummary>, 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<Vec<GoalSummary>, Error> {
self.request(&format!("users/{username}/goals/archived.json"), None)
.await
pub async fn get_archived_goals(&self) -> Result<Vec<GoalSummary>, Error> {
let endpoint = format!("users/{}/goals/archived.json", self.username);
self.get(&endpoint, &()).await
}
}

View File

@ -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<OffsetDateTime>,
/// Date string (e.g. "20150831"), alternative to timestamp
pub daystamp: Option<String>,