Split toggl module into multiple files
This commit is contained in:
parent
29b2dbd69b
commit
02a5593411
380
src/toggl/mod.rs
380
src/toggl/mod.rs
@ -1,40 +1,17 @@
|
|||||||
use axum::async_trait;
|
|
||||||
use base64::engine::general_purpose::STANDARD;
|
use base64::engine::general_purpose::STANDARD;
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use chrono::{DateTime, SecondsFormat, Utc};
|
use chrono::{DateTime, SecondsFormat, Utc};
|
||||||
use governor::clock::DefaultClock;
|
|
||||||
use governor::state::{InMemoryState, NotKeyed};
|
|
||||||
use reqwest::header::{HeaderMap, HeaderValue};
|
use reqwest::header::{HeaderMap, HeaderValue};
|
||||||
use reqwest::Response;
|
use reqwest::Response;
|
||||||
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
|
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
|
||||||
use reqwest_retry::policies::ExponentialBackoff;
|
use reqwest_retry::policies::ExponentialBackoff;
|
||||||
use reqwest_retry::{Jitter, RetryTransientMiddleware};
|
use reqwest_retry::{Jitter, RetryTransientMiddleware};
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use std::num::NonZero;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use support::ReqwestRateLimiter;
|
||||||
|
|
||||||
struct ReqwestRateLimiter {
|
pub mod types;
|
||||||
rate_limiter: governor::RateLimiter<NotKeyed, InMemoryState, DefaultClock>,
|
mod support;
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct TogglApi {
|
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<u64>,
|
|
||||||
pub task_id: Option<u64>,
|
|
||||||
|
|
||||||
pub start: DateTime<Utc>,
|
|
||||||
pub stop: Option<DateTime<Utc>>,
|
|
||||||
|
|
||||||
#[serde(with = "duration_field")]
|
|
||||||
pub duration: Option<u32>,
|
|
||||||
|
|
||||||
#[serde(rename = "at")]
|
|
||||||
pub updated_at: DateTime<Utc>,
|
|
||||||
|
|
||||||
pub description: Option<String>,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub tags: Vec<String>,
|
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub tag_ids: Vec<u64>,
|
|
||||||
|
|
||||||
pub billable: bool,
|
|
||||||
pub server_deleted_at: Option<DateTime<Utc>>,
|
|
||||||
pub permissions: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
mod duration_field {
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
pub fn serialize<S>(duration: &Option<u32>, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
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<Option<u32>, 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<i64>,
|
|
||||||
|
|
||||||
pub name: String,
|
|
||||||
pub color: String,
|
|
||||||
pub status: ProjectStatus,
|
|
||||||
pub active: bool,
|
|
||||||
|
|
||||||
#[serde(rename = "at")]
|
|
||||||
pub updated_at: DateTime<Utc>,
|
|
||||||
pub start_date: NaiveDate,
|
|
||||||
pub created_at: DateTime<Utc>,
|
|
||||||
pub server_deleted_at: Option<DateTime<Utc>>,
|
|
||||||
|
|
||||||
pub actual_hours: Option<i64>,
|
|
||||||
pub actual_seconds: Option<i64>,
|
|
||||||
pub can_track_time: bool,
|
|
||||||
pub permissions: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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<Utc>,
|
|
||||||
|
|
||||||
/// Indicates whether the client is archived or not.
|
|
||||||
pub archived: bool,
|
|
||||||
|
|
||||||
pub creator_id: i64,
|
|
||||||
pub integration_provider: Option<String>,
|
|
||||||
pub notes: Option<String>,
|
|
||||||
|
|
||||||
/// 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<DateTime<Utc>>,
|
|
||||||
|
|
||||||
/// The Workspace ID associated with the client.
|
|
||||||
#[serde(rename = "wid")]
|
|
||||||
pub workspace_id: i64,
|
|
||||||
|
|
||||||
pub permissions: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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<Utc>,
|
|
||||||
pub deleted_at: Option<DateTime<Utc>>,
|
|
||||||
pub permissions: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
||||||
pub struct ReportEntry {
|
|
||||||
pub user_id: u64,
|
|
||||||
pub username: String,
|
|
||||||
pub project_id: Option<u64>,
|
|
||||||
pub task_id: Option<u64>,
|
|
||||||
pub billable: bool,
|
|
||||||
pub description: Option<String>,
|
|
||||||
pub tag_ids: Vec<u64>,
|
|
||||||
pub billable_amount_in_cents: Option<u64>,
|
|
||||||
pub hourly_rate_in_cents: Option<u64>,
|
|
||||||
pub currency: String,
|
|
||||||
pub time_entries: Vec<ReportEntryTimeDetails>,
|
|
||||||
pub row_number: u32,
|
|
||||||
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub enriched_information: Option<ReportEntryEnrichedInfo>,
|
|
||||||
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub rest: HashMap<String, serde_json::Value>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
||||||
pub struct ReportEntryEnrichedInfo {
|
|
||||||
pub project_id: Option<u64>,
|
|
||||||
pub project_name: Option<String>,
|
|
||||||
pub project_hex: Option<String>,
|
|
||||||
|
|
||||||
pub tag_names: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
|
||||||
pub struct ReportEntryTimeDetails {
|
|
||||||
pub id: u64,
|
|
||||||
pub seconds: u32,
|
|
||||||
pub start: DateTime<Utc>,
|
|
||||||
pub stop: DateTime<Utc>,
|
|
||||||
#[serde(rename = "at")]
|
|
||||||
pub updated_at: DateTime<Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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<bool>,
|
|
||||||
pub client_ids: Option<Vec<u64>>,
|
|
||||||
pub description: Option<String>,
|
|
||||||
pub end_date: Option<NaiveDate>,
|
|
||||||
pub enrich_response: Option<bool>,
|
|
||||||
pub first_id: Option<u64>,
|
|
||||||
pub first_row_number: Option<u64>,
|
|
||||||
pub first_timestamp: Option<u64>,
|
|
||||||
pub group_ids: Option<Vec<u64>>,
|
|
||||||
pub grouped: Option<bool>,
|
|
||||||
pub hide_amounts: Option<bool>,
|
|
||||||
pub max_duration_seconds: Option<u64>,
|
|
||||||
pub min_duration_seconds: Option<u64>,
|
|
||||||
pub order_by: Option<String>,
|
|
||||||
pub order_dir: Option<String>,
|
|
||||||
pub page_size: Option<u64>,
|
|
||||||
#[serde(rename = "postedFields")]
|
|
||||||
pub posted_fields: Option<Vec<String>>,
|
|
||||||
pub project_ids: Option<Vec<u64>>,
|
|
||||||
pub rounding: Option<u64>,
|
|
||||||
pub rounding_minutes: Option<u64>,
|
|
||||||
#[serde(rename = "startTime")]
|
|
||||||
pub start_time: Option<String>,
|
|
||||||
pub start_date: Option<NaiveDate>,
|
|
||||||
pub tag_ids: Option<Vec<u64>>,
|
|
||||||
pub task_ids: Option<Vec<u64>>,
|
|
||||||
pub time_entry_ids: Option<Vec<u64>>,
|
|
||||||
pub user_ids: Option<Vec<u64>>,
|
|
||||||
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub rest: HashMap<String, serde_json::Value>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum TogglError {
|
pub enum TogglError {
|
||||||
#[error("Toggl returned error: {0}")]
|
#[error("Toggl returned error: {0}")]
|
||||||
|
|||||||
27
src/toggl/support.rs
Normal file
27
src/toggl/support.rs
Normal file
@ -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<NotKeyed, InMemoryState, DefaultClock>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
348
src/toggl/types.rs
Normal file
348
src/toggl/types.rs
Normal file
@ -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<u64>,
|
||||||
|
pub task_id: Option<u64>,
|
||||||
|
|
||||||
|
pub start: DateTime<Utc>,
|
||||||
|
pub stop: Option<DateTime<Utc>>,
|
||||||
|
|
||||||
|
#[serde(with = "duration_field")]
|
||||||
|
pub duration: Option<u32>,
|
||||||
|
|
||||||
|
#[serde(rename = "at")]
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
|
||||||
|
pub description: Option<String>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub tags: Vec<String>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub tag_ids: Vec<u64>,
|
||||||
|
|
||||||
|
pub billable: bool,
|
||||||
|
pub server_deleted_at: Option<DateTime<Utc>>,
|
||||||
|
pub permissions: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
mod duration_field {
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub fn serialize<S>(duration: &Option<u32>, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
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<Option<u32>, 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<i64>,
|
||||||
|
|
||||||
|
pub name: String,
|
||||||
|
pub color: String,
|
||||||
|
pub status: ProjectStatus,
|
||||||
|
pub active: bool,
|
||||||
|
|
||||||
|
#[serde(rename = "at")]
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
pub start_date: NaiveDate,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub server_deleted_at: Option<DateTime<Utc>>,
|
||||||
|
|
||||||
|
pub actual_hours: Option<i64>,
|
||||||
|
pub actual_seconds: Option<i64>,
|
||||||
|
pub can_track_time: bool,
|
||||||
|
pub permissions: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<Utc>,
|
||||||
|
|
||||||
|
/// Indicates whether the client is archived or not.
|
||||||
|
pub archived: bool,
|
||||||
|
|
||||||
|
pub creator_id: i64,
|
||||||
|
pub integration_provider: Option<String>,
|
||||||
|
pub notes: Option<String>,
|
||||||
|
|
||||||
|
/// 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<DateTime<Utc>>,
|
||||||
|
|
||||||
|
/// The Workspace ID associated with the client.
|
||||||
|
#[serde(rename = "wid")]
|
||||||
|
pub workspace_id: i64,
|
||||||
|
|
||||||
|
pub permissions: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<Utc>,
|
||||||
|
pub deleted_at: Option<DateTime<Utc>>,
|
||||||
|
pub permissions: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct ReportEntry {
|
||||||
|
pub user_id: u64,
|
||||||
|
pub username: String,
|
||||||
|
pub project_id: Option<u64>,
|
||||||
|
pub task_id: Option<u64>,
|
||||||
|
pub billable: bool,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub tag_ids: Vec<u64>,
|
||||||
|
pub billable_amount_in_cents: Option<u64>,
|
||||||
|
pub hourly_rate_in_cents: Option<u64>,
|
||||||
|
pub currency: String,
|
||||||
|
pub time_entries: Vec<ReportEntryTimeDetails>,
|
||||||
|
pub row_number: u32,
|
||||||
|
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub enriched_information: Option<ReportEntryEnrichedInfo>,
|
||||||
|
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub rest: HashMap<String, serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct ReportEntryEnrichedInfo {
|
||||||
|
pub project_id: Option<u64>,
|
||||||
|
pub project_name: Option<String>,
|
||||||
|
pub project_hex: Option<String>,
|
||||||
|
|
||||||
|
pub tag_names: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct ReportEntryTimeDetails {
|
||||||
|
pub id: u64,
|
||||||
|
pub seconds: u32,
|
||||||
|
pub start: DateTime<Utc>,
|
||||||
|
pub stop: DateTime<Utc>,
|
||||||
|
#[serde(rename = "at")]
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<bool>,
|
||||||
|
pub client_ids: Option<Vec<u64>>,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub end_date: Option<NaiveDate>,
|
||||||
|
pub enrich_response: Option<bool>,
|
||||||
|
pub first_id: Option<u64>,
|
||||||
|
pub first_row_number: Option<u64>,
|
||||||
|
pub first_timestamp: Option<u64>,
|
||||||
|
pub group_ids: Option<Vec<u64>>,
|
||||||
|
pub grouped: Option<bool>,
|
||||||
|
pub hide_amounts: Option<bool>,
|
||||||
|
pub max_duration_seconds: Option<u64>,
|
||||||
|
pub min_duration_seconds: Option<u64>,
|
||||||
|
pub order_by: Option<String>,
|
||||||
|
pub order_dir: Option<String>,
|
||||||
|
pub page_size: Option<u64>,
|
||||||
|
#[serde(rename = "postedFields")]
|
||||||
|
pub posted_fields: Option<Vec<String>>,
|
||||||
|
pub project_ids: Option<Vec<u64>>,
|
||||||
|
pub rounding: Option<u64>,
|
||||||
|
pub rounding_minutes: Option<u64>,
|
||||||
|
#[serde(rename = "startTime")]
|
||||||
|
pub start_time: Option<String>,
|
||||||
|
pub start_date: Option<NaiveDate>,
|
||||||
|
pub tag_ids: Option<Vec<u64>>,
|
||||||
|
pub task_ids: Option<Vec<u64>>,
|
||||||
|
pub time_entry_ids: Option<Vec<u64>>,
|
||||||
|
pub user_ids: Option<Vec<u64>>,
|
||||||
|
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub rest: HashMap<String, serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user