From 02a55934115f949fa597de92d72eb8590e41a63f Mon Sep 17 00:00:00 2001 From: Joshua Coles Date: Sat, 27 Jul 2024 21:02:38 +0100 Subject: [PATCH] Split toggl module into multiple files --- src/toggl/mod.rs | 380 +------------------------------------------ src/toggl/support.rs | 27 +++ src/toggl/types.rs | 348 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 378 insertions(+), 377 deletions(-) create mode 100644 src/toggl/support.rs create mode 100644 src/toggl/types.rs diff --git a/src/toggl/mod.rs b/src/toggl/mod.rs index fbafa6e..8dd5f4c 100644 --- a/src/toggl/mod.rs +++ b/src/toggl/mod.rs @@ -1,40 +1,17 @@ -use axum::async_trait; use base64::engine::general_purpose::STANDARD; use base64::Engine; use chrono::{DateTime, SecondsFormat, Utc}; -use governor::clock::DefaultClock; -use governor::state::{InMemoryState, NotKeyed}; use reqwest::header::{HeaderMap, HeaderValue}; use reqwest::Response; use reqwest_middleware::{ClientBuilder, ClientWithMiddleware}; use reqwest_retry::policies::ExponentialBackoff; use reqwest_retry::{Jitter, RetryTransientMiddleware}; use serde::de::DeserializeOwned; -use std::num::NonZero; use std::time::Duration; +use support::ReqwestRateLimiter; -struct ReqwestRateLimiter { - rate_limiter: governor::RateLimiter, -} - -impl ReqwestRateLimiter { - fn new() -> Self { - Self { - rate_limiter: governor::RateLimiter::direct(governor::Quota::per_second( - NonZero::new(1u32).unwrap(), - )), - } - } -} - -#[async_trait] -impl reqwest_ratelimit::RateLimiter for ReqwestRateLimiter { - async fn acquire_permit(&self) { - // We don't need to introduce jitter here as that is handled by the retry_request - // middleware. - self.rate_limiter.until_ready().await; - } -} +pub mod types; +mod support; #[derive(Clone)] pub struct TogglApi { @@ -288,357 +265,6 @@ impl TogglApi { } } -pub mod types { - use chrono::{DateTime, NaiveDate, Utc}; - use serde::{Deserialize, Serialize}; - use serde_with::skip_serializing_none; - use std::collections::HashMap; - - #[derive(Serialize, Deserialize, Debug, Clone, Soars)] - pub struct TimeEntry { - pub id: u64, - pub workspace_id: u64, - pub user_id: u64, - pub project_id: Option, - pub task_id: Option, - - pub start: DateTime, - pub stop: Option>, - - #[serde(with = "duration_field")] - pub duration: Option, - - #[serde(rename = "at")] - pub updated_at: DateTime, - - pub description: Option, - - #[serde(default)] - pub tags: Vec, - - #[serde(default)] - pub tag_ids: Vec, - - pub billable: bool, - pub server_deleted_at: Option>, - pub permissions: Option, - } - - mod duration_field { - use serde::{Deserialize, Serialize}; - - pub fn serialize(duration: &Option, serializer: S) -> Result - where - S: serde::Serializer, - { - match duration { - None => i32::serialize(&-1, serializer), - Some(duration) => i32::serialize(&(*duration as i32), serializer), - } - } - - pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> - where - D: serde::Deserializer<'de>, - { - let duration = i32::deserialize(deserializer)?; - if duration < 0 { - Ok(None) - } else { - Ok(Some(duration as u32)) - } - } - } - - #[derive(Serialize, Deserialize, Debug, Clone, Soars)] - pub struct Project { - pub id: i64, - pub workspace_id: i64, - pub client_id: Option, - - pub name: String, - pub color: String, - pub status: ProjectStatus, - pub active: bool, - - #[serde(rename = "at")] - pub updated_at: DateTime, - pub start_date: NaiveDate, - pub created_at: DateTime, - pub server_deleted_at: Option>, - - pub actual_hours: Option, - pub actual_seconds: Option, - pub can_track_time: bool, - pub permissions: Option, - } - - #[derive(Serialize, Deserialize, Debug, Clone)] - #[serde(rename_all = "lowercase")] - pub enum ProjectStatus { - Upcoming, - Active, - Archived, - Ended, - Deleted, - } - - impl fmt::Display for ProjectStatus { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - match self { - ProjectStatus::Upcoming => "upcoming", - ProjectStatus::Active => "active", - ProjectStatus::Archived => "archived", - ProjectStatus::Ended => "ended", - ProjectStatus::Deleted => "deleted", - } - ) - } - } - - #[derive(Serialize, Deserialize, Debug, Clone, Soars)] - pub struct TrackingClient { - /// The unique identifier for the client. - pub id: i64, - - /// Represents the timestamp of the last update made to the client. - #[serde(rename = "at")] - pub updated_at: DateTime, - - /// Indicates whether the client is archived or not. - pub archived: bool, - - pub creator_id: i64, - pub integration_provider: Option, - pub notes: Option, - - /// The name of the client. - pub name: String, - - /// Indicates the timestamp when the client was deleted. If the client is not deleted, this property will be null. - pub server_deleted_at: Option>, - - /// The Workspace ID associated with the client. - #[serde(rename = "wid")] - pub workspace_id: i64, - - pub permissions: Option, - } - - #[derive(Soars, Serialize, Deserialize, Debug, Clone)] - pub struct Tag { - pub id: u64, - pub name: String, - pub workspace_id: u64, - pub creator_id: u64, - #[serde(rename = "at")] - pub updated_at: DateTime, - pub deleted_at: Option>, - pub permissions: Option, - } - - #[derive(Serialize, Deserialize, Debug, Clone)] - pub struct ReportEntry { - pub user_id: u64, - pub username: String, - pub project_id: Option, - pub task_id: Option, - pub billable: bool, - pub description: Option, - pub tag_ids: Vec, - pub billable_amount_in_cents: Option, - pub hourly_rate_in_cents: Option, - pub currency: String, - pub time_entries: Vec, - pub row_number: u32, - - #[serde(flatten)] - pub enriched_information: Option, - - #[serde(flatten)] - pub rest: HashMap, - } - - #[derive(Serialize, Deserialize, Debug, Clone)] - pub struct ReportEntryEnrichedInfo { - pub project_id: Option, - pub project_name: Option, - pub project_hex: Option, - - pub tag_names: Vec, - } - - #[derive(Clone, Serialize, Deserialize, Debug)] - pub struct ReportEntryTimeDetails { - pub id: u64, - pub seconds: u32, - pub start: DateTime, - pub stop: DateTime, - #[serde(rename = "at")] - pub updated_at: DateTime, - } - - impl ReportEntry { - pub fn into_time_entry(self, workspace_id: u64) -> TimeEntry { - TimeEntry { - id: self.time_entries[0].id, - workspace_id, - user_id: self.user_id, - project_id: self.project_id, - task_id: self.task_id, - start: self.time_entries[0].start, - stop: Some(self.time_entries[0].stop), - duration: Some(self.time_entries[0].seconds), - updated_at: self.time_entries[0].updated_at, - description: self.description, - tags: self - .enriched_information - .map(|e| e.tag_names.clone()) - .unwrap_or_default(), - - tag_ids: self.tag_ids, - billable: self.billable, - server_deleted_at: None, - permissions: None, - } - } - } - - #[skip_serializing_none] - #[derive(Serialize, Deserialize, Clone, Default)] - pub struct TogglReportFilters { - pub billable: Option, - pub client_ids: Option>, - pub description: Option, - pub end_date: Option, - pub enrich_response: Option, - pub first_id: Option, - pub first_row_number: Option, - pub first_timestamp: Option, - pub group_ids: Option>, - pub grouped: Option, - pub hide_amounts: Option, - pub max_duration_seconds: Option, - pub min_duration_seconds: Option, - pub order_by: Option, - pub order_dir: Option, - pub page_size: Option, - #[serde(rename = "postedFields")] - pub posted_fields: Option>, - pub project_ids: Option>, - pub rounding: Option, - pub rounding_minutes: Option, - #[serde(rename = "startTime")] - pub start_time: Option, - pub start_date: Option, - pub tag_ids: Option>, - pub task_ids: Option>, - pub time_entry_ids: Option>, - pub user_ids: Option>, - - #[serde(flatten)] - pub rest: HashMap, - } - - use soa_rs::Soars; - use std::fmt; - - impl fmt::Debug for TogglReportFilters { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut ds = f.debug_struct("TogglQuery"); - - if let Some(billable) = &self.billable { - ds.field("billable", billable); - } - if let Some(client_ids) = &self.client_ids { - ds.field("client_ids", client_ids); - } - if let Some(description) = &self.description { - ds.field("description", description); - } - if let Some(end_date) = &self.end_date { - ds.field("end_date", end_date); - } - if let Some(first_id) = &self.first_id { - ds.field("first_id", first_id); - } - if let Some(first_row_number) = &self.first_row_number { - ds.field("first_row_number", first_row_number); - } - if let Some(first_timestamp) = &self.first_timestamp { - ds.field("first_timestamp", first_timestamp); - } - if let Some(group_ids) = &self.group_ids { - ds.field("group_ids", group_ids); - } - if let Some(grouped) = &self.grouped { - ds.field("grouped", grouped); - } - if let Some(hide_amounts) = &self.hide_amounts { - ds.field("hide_amounts", hide_amounts); - } - if let Some(max_duration_seconds) = &self.max_duration_seconds { - ds.field("max_duration_seconds", max_duration_seconds); - } - if let Some(min_duration_seconds) = &self.min_duration_seconds { - ds.field("min_duration_seconds", min_duration_seconds); - } - if let Some(order_by) = &self.order_by { - ds.field("order_by", order_by); - } - if let Some(order_dir) = &self.order_dir { - ds.field("order_dir", order_dir); - } - if let Some(posted_fields) = &self.posted_fields { - ds.field("postedFields", posted_fields); - } - if let Some(project_ids) = &self.project_ids { - ds.field("project_ids", project_ids); - } - if let Some(rounding) = &self.rounding { - ds.field("rounding", rounding); - } - if let Some(rounding_minutes) = &self.rounding_minutes { - ds.field("rounding_minutes", rounding_minutes); - } - if let Some(start_time) = &self.start_time { - ds.field("startTime", start_time); - } - if let Some(start_date) = &self.start_date { - ds.field("start_date", start_date); - } - if let Some(tag_ids) = &self.tag_ids { - ds.field("tag_ids", tag_ids); - } - if let Some(task_ids) = &self.task_ids { - ds.field("task_ids", task_ids); - } - if let Some(time_entry_ids) = &self.time_entry_ids { - ds.field("time_entry_ids", time_entry_ids); - } - if let Some(user_ids) = &self.user_ids { - ds.field("user_ids", user_ids); - } - - if !self.rest.is_empty() { - ds.field("rest", &self.rest); - } - - ds.finish() - } - } - - #[derive(Serialize, Deserialize, Debug, Clone, Soars)] - pub struct Workspace { - pub id: u64, - pub organization_id: u64, - pub name: String, - } -} - #[derive(Debug, thiserror::Error)] pub enum TogglError { #[error("Toggl returned error: {0}")] diff --git a/src/toggl/support.rs b/src/toggl/support.rs new file mode 100644 index 0000000..d3b0c8f --- /dev/null +++ b/src/toggl/support.rs @@ -0,0 +1,27 @@ +use axum::async_trait; +use std::num::NonZero; +use governor::state::{InMemoryState, NotKeyed}; +use governor::clock::DefaultClock; + +pub struct ReqwestRateLimiter { + rate_limiter: governor::RateLimiter, +} + +impl ReqwestRateLimiter { + pub fn new() -> Self { + Self { + rate_limiter: governor::RateLimiter::direct(governor::Quota::per_second( + NonZero::new(1u32).unwrap(), + )), + } + } +} + +#[async_trait] +impl reqwest_ratelimit::RateLimiter for ReqwestRateLimiter { + async fn acquire_permit(&self) { + // We don't need to introduce jitter here as that is handled by the retry_request + // middleware. + self.rate_limiter.until_ready().await; + } +} diff --git a/src/toggl/types.rs b/src/toggl/types.rs new file mode 100644 index 0000000..b95d83d --- /dev/null +++ b/src/toggl/types.rs @@ -0,0 +1,348 @@ +use chrono::{DateTime, NaiveDate, Utc}; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +use std::collections::HashMap; + +#[derive(Serialize, Deserialize, Debug, Clone, Soars)] +pub struct TimeEntry { + pub id: u64, + pub workspace_id: u64, + pub user_id: u64, + pub project_id: Option, + pub task_id: Option, + + pub start: DateTime, + pub stop: Option>, + + #[serde(with = "duration_field")] + pub duration: Option, + + #[serde(rename = "at")] + pub updated_at: DateTime, + + pub description: Option, + + #[serde(default)] + pub tags: Vec, + + #[serde(default)] + pub tag_ids: Vec, + + pub billable: bool, + pub server_deleted_at: Option>, + pub permissions: Option, +} + +mod duration_field { + use serde::{Deserialize, Serialize}; + + pub fn serialize(duration: &Option, serializer: S) -> Result + where + S: serde::Serializer, + { + match duration { + None => i32::serialize(&-1, serializer), + Some(duration) => i32::serialize(&(*duration as i32), serializer), + } + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: serde::Deserializer<'de>, + { + let duration = i32::deserialize(deserializer)?; + if duration < 0 { + Ok(None) + } else { + Ok(Some(duration as u32)) + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Soars)] +pub struct Project { + pub id: i64, + pub workspace_id: i64, + pub client_id: Option, + + pub name: String, + pub color: String, + pub status: ProjectStatus, + pub active: bool, + + #[serde(rename = "at")] + pub updated_at: DateTime, + pub start_date: NaiveDate, + pub created_at: DateTime, + pub server_deleted_at: Option>, + + pub actual_hours: Option, + pub actual_seconds: Option, + pub can_track_time: bool, + pub permissions: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "lowercase")] +pub enum ProjectStatus { + Upcoming, + Active, + Archived, + Ended, + Deleted, +} + +impl fmt::Display for ProjectStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + ProjectStatus::Upcoming => "upcoming", + ProjectStatus::Active => "active", + ProjectStatus::Archived => "archived", + ProjectStatus::Ended => "ended", + ProjectStatus::Deleted => "deleted", + } + ) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Soars)] +pub struct TrackingClient { + /// The unique identifier for the client. + pub id: i64, + + /// Represents the timestamp of the last update made to the client. + #[serde(rename = "at")] + pub updated_at: DateTime, + + /// Indicates whether the client is archived or not. + pub archived: bool, + + pub creator_id: i64, + pub integration_provider: Option, + pub notes: Option, + + /// The name of the client. + pub name: String, + + /// Indicates the timestamp when the client was deleted. If the client is not deleted, this property will be null. + pub server_deleted_at: Option>, + + /// The Workspace ID associated with the client. + #[serde(rename = "wid")] + pub workspace_id: i64, + + pub permissions: Option, +} + +#[derive(Soars, Serialize, Deserialize, Debug, Clone)] +pub struct Tag { + pub id: u64, + pub name: String, + pub workspace_id: u64, + pub creator_id: u64, + #[serde(rename = "at")] + pub updated_at: DateTime, + pub deleted_at: Option>, + pub permissions: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ReportEntry { + pub user_id: u64, + pub username: String, + pub project_id: Option, + pub task_id: Option, + pub billable: bool, + pub description: Option, + pub tag_ids: Vec, + pub billable_amount_in_cents: Option, + pub hourly_rate_in_cents: Option, + pub currency: String, + pub time_entries: Vec, + pub row_number: u32, + + #[serde(flatten)] + pub enriched_information: Option, + + #[serde(flatten)] + pub rest: HashMap, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ReportEntryEnrichedInfo { + pub project_id: Option, + pub project_name: Option, + pub project_hex: Option, + + pub tag_names: Vec, +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct ReportEntryTimeDetails { + pub id: u64, + pub seconds: u32, + pub start: DateTime, + pub stop: DateTime, + #[serde(rename = "at")] + pub updated_at: DateTime, +} + +impl ReportEntry { + pub fn into_time_entry(self, workspace_id: u64) -> TimeEntry { + TimeEntry { + id: self.time_entries[0].id, + workspace_id, + user_id: self.user_id, + project_id: self.project_id, + task_id: self.task_id, + start: self.time_entries[0].start, + stop: Some(self.time_entries[0].stop), + duration: Some(self.time_entries[0].seconds), + updated_at: self.time_entries[0].updated_at, + description: self.description, + tags: self + .enriched_information + .map(|e| e.tag_names.clone()) + .unwrap_or_default(), + + tag_ids: self.tag_ids, + billable: self.billable, + server_deleted_at: None, + permissions: None, + } + } +} + +#[skip_serializing_none] +#[derive(Serialize, Deserialize, Clone, Default)] +pub struct TogglReportFilters { + pub billable: Option, + pub client_ids: Option>, + pub description: Option, + pub end_date: Option, + pub enrich_response: Option, + pub first_id: Option, + pub first_row_number: Option, + pub first_timestamp: Option, + pub group_ids: Option>, + pub grouped: Option, + pub hide_amounts: Option, + pub max_duration_seconds: Option, + pub min_duration_seconds: Option, + pub order_by: Option, + pub order_dir: Option, + pub page_size: Option, + #[serde(rename = "postedFields")] + pub posted_fields: Option>, + pub project_ids: Option>, + pub rounding: Option, + pub rounding_minutes: Option, + #[serde(rename = "startTime")] + pub start_time: Option, + pub start_date: Option, + pub tag_ids: Option>, + pub task_ids: Option>, + pub time_entry_ids: Option>, + pub user_ids: Option>, + + #[serde(flatten)] + pub rest: HashMap, +} + +use soa_rs::Soars; +use std::fmt; + +impl fmt::Debug for TogglReportFilters { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut ds = f.debug_struct("TogglQuery"); + + if let Some(billable) = &self.billable { + ds.field("billable", billable); + } + if let Some(client_ids) = &self.client_ids { + ds.field("client_ids", client_ids); + } + if let Some(description) = &self.description { + ds.field("description", description); + } + if let Some(end_date) = &self.end_date { + ds.field("end_date", end_date); + } + if let Some(first_id) = &self.first_id { + ds.field("first_id", first_id); + } + if let Some(first_row_number) = &self.first_row_number { + ds.field("first_row_number", first_row_number); + } + if let Some(first_timestamp) = &self.first_timestamp { + ds.field("first_timestamp", first_timestamp); + } + if let Some(group_ids) = &self.group_ids { + ds.field("group_ids", group_ids); + } + if let Some(grouped) = &self.grouped { + ds.field("grouped", grouped); + } + if let Some(hide_amounts) = &self.hide_amounts { + ds.field("hide_amounts", hide_amounts); + } + if let Some(max_duration_seconds) = &self.max_duration_seconds { + ds.field("max_duration_seconds", max_duration_seconds); + } + if let Some(min_duration_seconds) = &self.min_duration_seconds { + ds.field("min_duration_seconds", min_duration_seconds); + } + if let Some(order_by) = &self.order_by { + ds.field("order_by", order_by); + } + if let Some(order_dir) = &self.order_dir { + ds.field("order_dir", order_dir); + } + if let Some(posted_fields) = &self.posted_fields { + ds.field("postedFields", posted_fields); + } + if let Some(project_ids) = &self.project_ids { + ds.field("project_ids", project_ids); + } + if let Some(rounding) = &self.rounding { + ds.field("rounding", rounding); + } + if let Some(rounding_minutes) = &self.rounding_minutes { + ds.field("rounding_minutes", rounding_minutes); + } + if let Some(start_time) = &self.start_time { + ds.field("startTime", start_time); + } + if let Some(start_date) = &self.start_date { + ds.field("start_date", start_date); + } + if let Some(tag_ids) = &self.tag_ids { + ds.field("tag_ids", tag_ids); + } + if let Some(task_ids) = &self.task_ids { + ds.field("task_ids", task_ids); + } + if let Some(time_entry_ids) = &self.time_entry_ids { + ds.field("time_entry_ids", time_entry_ids); + } + if let Some(user_ids) = &self.user_ids { + ds.field("user_ids", user_ids); + } + + if !self.rest.is_empty() { + ds.field("rest", &self.rest); + } + + ds.finish() + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Soars)] +pub struct Workspace { + pub id: u64, + pub organization_id: u64, + pub name: String, +}