tgl_cli/
api.rs

1//! Low-level client for interacting with the [Toggl API](https://developers.track.toggl.com/docs/).
2
3use chrono::NaiveDate;
4use reqwest::header;
5use serde::{Deserialize, Serialize};
6use serde_json::Number;
7
8static BASE_API_URL: &str = "https://api.track.toggl.com/api/v9";
9
10/// Low-level client for interacting with the [Toggl API](https://developers.track.toggl.com/docs/).
11pub struct Client {
12    c: reqwest::blocking::Client,
13    token: String,
14}
15
16impl Client {
17    /// Creates a new client with the given API token.
18    pub fn new(token: String) -> Result<Self, reqwest::Error> {
19        let mut headers = header::HeaderMap::new();
20
21        // Toggl API docs indicate that we should always include the JSON
22        // content type header.
23        headers.insert(
24            header::CONTENT_TYPE,
25            header::HeaderValue::from_static("application/json"),
26        );
27
28        Ok(Client {
29            c: reqwest::blocking::Client::builder()
30                .default_headers(headers)
31                .build()?,
32            token,
33        })
34    }
35
36    pub fn get_time_entries(
37        &self,
38        start_end_dates: Option<(NaiveDate, NaiveDate)>,
39    ) -> Result<Vec<TimeEntry>, reqwest::Error> {
40        let url = match start_end_dates {
41            Some((start_date, end_date)) => {
42                format!(
43                    "{BASE_API_URL}/me/time_entries?start_date={start_date}&end_date={end_date}"
44                )
45            }
46            None => format!("{BASE_API_URL}/me/time_entries"),
47        };
48
49        self.c
50            .get(url)
51            .basic_auth(&self.token, Some("api_token"))
52            .send()?
53            .error_for_status()?
54            .json::<Vec<TimeEntry>>()
55    }
56
57    pub fn get_current_entry(&self) -> Result<Option<TimeEntry>, reqwest::Error> {
58        self.c
59            .get(format!("{BASE_API_URL}/me/time_entries/current"))
60            .basic_auth(&self.token, Some("api_token"))
61            .send()?
62            .error_for_status()?
63            .json()
64    }
65
66    pub fn create_time_entry(&self, entry: NewTimeEntry) -> Result<TimeEntry, reqwest::Error> {
67        let url = format!(
68            "{BASE_API_URL}/workspaces/{}/time_entries",
69            entry.workspace_id
70        );
71
72        self.c
73            .post(url)
74            .json(&entry)
75            .basic_auth(&self.token, Some("api_token"))
76            .send()?
77            .error_for_status()?
78            .json()
79    }
80
81    pub fn stop_time_entry(
82        &self,
83        workspace_id: &Number,
84        time_entry_id: &Number,
85    ) -> Result<TimeEntry, reqwest::Error> {
86        let url =
87            format!("{BASE_API_URL}/workspaces/{workspace_id}/time_entries/{time_entry_id}/stop");
88
89        self.c
90            .patch(url)
91            .basic_auth(&self.token, Some("api_token"))
92            .send()?
93            .error_for_status()?
94            .json()
95    }
96
97    pub fn get_projects(&self, workspace_id: &Number) -> Result<Vec<Project>, reqwest::Error> {
98        self.c
99            .get(format!("{BASE_API_URL}/workspaces/{workspace_id}/projects"))
100            .basic_auth(&self.token, Some("api_token"))
101            .send()?
102            .error_for_status()?
103            .json()
104    }
105
106    pub fn get_workspaces(&self) -> Result<Vec<Workspace>, reqwest::Error> {
107        self.c
108            .get(format!("{BASE_API_URL}/workspaces"))
109            .basic_auth(&self.token, Some("api_token"))
110            .send()?
111            .error_for_status()?
112            .json()
113    }
114}
115
116#[derive(Deserialize, Debug)]
117pub struct TimeEntry {
118    pub description: Option<String>,
119    pub duration: Number,
120    pub id: Number,
121    pub project_id: Option<Number>,
122    pub start: Option<String>,
123    pub stop: Option<String>,
124    pub task_id: Option<Number>,
125    pub workspace_id: Number,
126}
127
128#[derive(Serialize, Debug)]
129pub struct NewTimeEntry {
130    pub created_with: String,
131    pub description: Option<String>,
132    pub duration: Number,
133    pub project_id: Option<Number>,
134    pub start: String,
135    pub stop: Option<String>,
136    pub task_id: Option<Number>,
137    pub workspace_id: Number,
138}
139
140#[derive(Deserialize, Debug)]
141pub struct Project {
142    pub active: bool,
143    pub client_id: Option<Number>,
144    pub id: Number,
145    pub name: String,
146    pub workspace_id: Number,
147}
148
149#[derive(Deserialize, Debug)]
150pub struct Workspace {
151    pub id: Number,
152    pub name: String,
153}