タイムトラッキングアプリである Toggl の API を Rust で叩いてみたので、実装例のメモです。
HTTP リクエストを Rust の HTTP クライアントである reqwest を利用して送って利用します。
Rust 初心者なので、 Toggl API を通して、 HTTP リクエストの送信方法等を勉強することも兼ねました。
API を叩く際に用意するもの
-
API Token
-
workspace_id
- 「projects」等の画面を開いた際の URL にある7桁の数字
- 例:
https://track.toggl.com/projects/1234567/list
の「1234567」
- 例:
- 「projects」等の画面を開いた際の URL にある7桁の数字
-
project_id
- 「projects」→特定のプロジェクトを開いた際に、URL にある9桁の数字
- 例:
https://track.toggl.com/1234567/projects/123456789/team
の「123456789」
- 例:
- 「projects」→特定のプロジェクトを開いた際に、URL にある9桁の数字
ID を取得する別の方法
-
「Settings」→「Data export」を選択。「Projects」にチェックを入れた状態で「Compile file and send to email」をクリック。
-
json の中身はこんな感じ:
[ { "active":true, "actual_hours":0, "auto_estimates":null, "billable":null, "cid":null, "client_id":null, "color":"#e36a00", "currency":null, "estimated_hours":null, "id":123456789, "is_private":true, "name":"Example", "rate":null, "template":null, "wid":0, "workspace_id":1234567 } ]
-
id
が project_id,workspace_id
が workspace_id になる
-
環境
-
Rust 関連は以下の通り:
$ rustc -V rustc 1.45.0 (5c1f21c3b 2020-07-13) $ cargo --version cargo 1.45.0 (744bd1fbb 2020-06-15)
-
Cargo.toml
には以下の内容を記載[dep[dependencies] reqwest = { version = "0.10", features = ["json"] } tokio = { version = "0.2", features = ["full"] } serde = "1.0.117" serde_derive = "1.0.117" serde_json = "1.0.60" dotenv = "0.15.0" config = "0.10.1" lazy_static = "1.4.0"endencies]
Toggl API を利用する
代表的なものとして以下のものを記載:
- Workspace 一覧を取得する
- Workspace 内の Project ID 一覧を取得する
- Time Entry を操作する
- Time Entry を作成する
- Time Entry をスタートさせる
- 進行中の Time Entry の情報を取得する
- Time Entry を Stop させる
下準備
-
API Token を環境変数に入れておく場合、起動時に設定内容を読み込むように処理を記述しておく
-
dotenv を利用して、プロジェクトルートに作成した
.env
に定義された環境変数を読み込むようにする -
.env
は以下のように作成する:API_TOKEN=XXX
-
-
予め、 Toggl API から返却される情報を入れるための構造体を用意しておく
- API のドキュメントを参考に、構造体のメンバ変数を増減させれば、取得できる情報を増減できる
#[macro_use] extern crate serde_derive; extern crate lazy_static; extern crate serde_json; use config::ConfigError; use dotenv::dotenv; use lazy_static::lazy_static; /** * 環境変数を読み込むための設定 */ /// 環境変数の内容を格納する構造体 #[derive(Deserialize, Debug)] struct Config { api_token: String, } /// 環境変数からデータを読み込む処理を定義 impl Config { pub fn from_env() -> Result<Self, ConfigError> { let mut cfg = config::Config::new(); cfg.merge(config::Environment::new())?; cfg.try_into() } } /// 起動時に環境変数を読み込む lazy_static! { static ref CONFIG: Config = { dotenv().ok(); Config::from_env().unwrap() }; } /** * 取得した Toggl に関する情報を格納する構造体を定義する */ /// ワークスペース情報 #[derive(Deserialize, Debug)] struct Workspace { id: u64, name: String, } /// プロジェクト情報 #[derive(Deserialize, Debug)] struct Project { id: u64, name: String, } /// Time Entry 情報の中身 #[derive(Deserialize, Debug)] struct TimeEntryData { id: u64, pid: u64, wid: u64, start: String, } /// Time Entry 情報を取得した際に返却される情報の JSON に対応する構造体 #[derive(Deserialize, Debug)] struct TimeEntryOutput { data: TimeEntryData, } /// Time Entry 情報を取得する際に送信する情報 #[derive(Serialize, Deserialize, Debug)] struct TimeEntryInfo { description: String, tags: Vec<String>, pid: u64, created_with: String, } /// Time Entry 情報を取得する際に送信する情報の JSON に対応する構造体 #[derive(Serialize, Deserialize, Debug)] struct TimeEntryInput { time_entry: TimeEntryInfo, }
Workspace 一覧を取得する
-
API の
curl
での利用方法は以下の通り:curl -v -u XXX:api_token \ -X GET https://api.track.toggl.com/api/v8/workspaces
-
-u USER[:PASS]
は BASIC 認証で、ここでは API Token をXXX
に代入して実行する
-
-
Rust で実装すると、以下の通り:
/// 指定した API Token で利用可能なワークスペース一覧を取得する fn get_workspaces(api_token: &str) -> Vec<Workspace> { let mut rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { let client = reqwest::Client::new(); let res: Vec<Workspace> = client .get("https://www.toggl.com/api/v8/workspaces") .basic_auth(api_token, Some("api_token")) .send() .await .unwrap() .json() .await .unwrap(); return res; }) } fn main() -> Result<(), Box<dyn std::error::Error>> { let workspaces = get_workspaces(&CONFIG.api_token); for w in workspaces { println!("{}", w.id); } Ok(()) }
Workspace 内の Project ID 一覧を取得する
-
先程同様、
curl
で Project 一覧を取得する方法は以下の通り:curl -v -u XXX:api_token \ -X GET https://api.track.toggl.com/api/v8/workspaces/[workspace_id]/projects
-
[workspace_id]
に Workspace ID を代入する
-
-
Rust で実装すると、以下の通り:
fn get_projects(api_token: &str, workspace_id: &str) -> Vec<Project> { let client = reqwest::Client::new(); let mut rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { let res: Vec<Project> = client .get(&format!( "https://api.track.toggl.com/api/v8/workspaces/{}/projects", workspace_id )) .basic_auth(api_token, Some("api_token")) .send() .await .unwrap() .json() .await .unwrap(); return res }) } fn main() -> Result<(), Box<dyn std::error::Error>> { let projects = get_projects(&CONFIG.api_token, "[workspace_id]"); for p in projects { println!("{}", p.id); } Ok(()) }
Time Entry を操作する
Time Entry を作成&スタートさせる
-
curl
では以下のようにコマンドを実行する:curl -v -u XXX:api_token \ -H "Content-Type: application/json" \ -d '{"time_entry":{"description":"[タスク名を指定]","tags":["[適当なタグを指定]"],"pid":[project_id],"created_with":"[適当な名前を指定]"}}' \ -X POST https://api.track.toggl.com/api/v8/time_entries/start
- JSON で Time Entry の情報を送信する
-
[project_id]
には Time Entry を作成したい Project の ID を指定する
-
Rust での実装は以下の通り:
fn start_time_entry(api_token: &str, project_id: u64) -> TimeEntryOutput { let input = TimeEntryInfo { description: "[タスク名を指定]".to_string(), tags: vec!["[適当なタグを指定]".to_string()], pid: project_id, created_with: "[適当な名前を指定]".to_string(), }; let time_entry = TimeEntryInput { time_entry: input }; let client = reqwest::Client::new(); let mut rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { let res: TimeEntryOutput = client .post("https://api.track.toggl.com/api/v8/time_entries/start") .basic_auth(api_token, Some("api_token")) .json(&time_entry) .send() .await .unwrap() .json() .await .unwrap(); return res; }) } fn main() -> Result<(), Box<dyn std::error::Error>> { let time_entry = start_time_entry(&CONFIG.api_token, [project_id]); print!("{}", time_entry.data.start); Ok(()) }
進行中の Time Entry の情報を取得する
-
curl
でのコマンド内容は以下の通り:curl -v -u XXX:api_token \ -X GET https://api.track.toggl.com/api/v8/time_entries/current
-
Rust での実装は以下の通り:
fn get_running_time_entry(api_token: &str) -> TimeEntryOutput { let mut rt = tokio::runtime::Runtime::new().unwrap(); let client = reqwest::Client::new(); rt.block_on(async { let res: TimeEntryOutput = client .get("https://api.track.toggl.com/api/v8/time_entries/current") .basic_auth(api_token, Some("api_token")) .send() .await .unwrap() .json() .await .unwrap(); return res; }) } fn main() -> Result<(), Box<dyn std::error::Error>> { let active_time_entry = get_running_time_entry(&CONFIG.api_token); print!("{}", active_time_entry.data.id); Ok(()) }
Time Entry を Stop させる
-
curl でのコマンド内容は以下の通り:
curl -v -u XXX:api_token \ -H "Content-Type: application/json" \ -X PUT https://api.track.toggl.com/api/v8/time_entries/[time_entry_id]/stop
-
[time_entry_id]
に Stop させる Time Entry の ID を指定する - HTTP メソッドとして PUT を使用している点に注意
-
-
Rust での実装は以下の通り:
fn stop_time_entry(api_token: &str, time_entry_id: u64) -> TimeEntryOutput { let client = reqwest::Client::new(); let mut rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { let res: TimeEntryOutput = client .put(&format!( "https://api.track.toggl.com/api/v8/time_entries/{}/stop", time_entry_id )) .basic_auth(api_token, Some("api_token")) .header(reqwest::header::CONTENT_TYPE, "application/json") .json("") .send() .await .unwrap() .json() .await .unwrap(); return res; }) } fn main() -> Result<(), Box<dyn std::error::Error>> { let stopped_time_entry = stop_time_entry(&CONFIG.api_token, [time_entry_id]); print!("{}", stopped_time_entry.data.id); Ok(()) }
まとめ
-
ここまで実装した内容を
main.rs
にまとめると以下の通り:#[macro_use] extern crate serde_derive; extern crate lazy_static; extern crate serde_json; use config::ConfigError; use dotenv::dotenv; use lazy_static::lazy_static; /** * 環境変数を読み込むための設定 */ /// 環境変数の内容を格納する構造体 #[derive(Deserialize, Debug)] struct Config { api_token: String, } /// 環境変数からデータを読み込む処理を定義 impl Config { pub fn from_env() -> Result<Self, ConfigError> { let mut cfg = config::Config::new(); cfg.merge(config::Environment::new())?; cfg.try_into() } } /// 起動時に環境変数を読み込む lazy_static! { static ref CONFIG: Config = { dotenv().ok(); Config::from_env().unwrap() }; } /** * 取得した Toggl に関する情報を格納する構造体を定義する */ /// ワークスペース情報 #[derive(Deserialize, Debug)] struct Workspace { id: u64, name: String, } /// プロジェクト情報 #[derive(Deserialize, Debug)] struct Project { id: u64, name: String, } /// Time Entry 情報の中身 #[derive(Deserialize, Debug)] struct TimeEntryData { id: u64, pid: u64, wid: u64, start: String, } /// Time Entry 情報を取得した際に返却される情報の JSON に対応する構造体 #[derive(Deserialize, Debug)] struct TimeEntryOutput { data: TimeEntryData, } /// Time Entry 情報を取得する際に送信する情報 #[derive(Serialize, Deserialize, Debug)] struct TimeEntryInfo { description: String, tags: Vec<String>, pid: u64, created_with: String, } /// Time Entry 情報を取得する際に送信する情報の JSON に対応する構造体 #[derive(Serialize, Deserialize, Debug)] struct TimeEntryInput { time_entry: TimeEntryInfo, } /// ワークスペース一覧を取得する fn get_workspaces(api_token: &str) -> Vec<Workspace> { let mut rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { let client = reqwest::Client::new(); let res: Vec<Workspace> = client .get("https://www.toggl.com/api/v8/workspaces") .basic_auth(api_token, Some("api_token")) .send() .await .unwrap() .json() .await .unwrap(); return res; }) } /// プロジェクト一覧を取得する fn get_projects(api_token: &str, workspace_id: u64) -> Vec<Project> { let client = reqwest::Client::new(); let mut rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { let res: Vec<Project> = client .get(&format!( "https://api.track.toggl.com/api/v8/workspaces/{}/projects", workspace_id )) .basic_auth(api_token, Some("api_token")) .send() .await .unwrap() .json() .await .unwrap(); return res }) } /// Timne Entry を作成し、スタートさせる fn start_time_entry(api_token: &str, project_id: u64) -> TimeEntryOutput { let input = TimeEntryInfo { description: "[タスク名を指定]".to_string(), tags: vec!["[適当なタグを指定]".to_string()], pid: project_id, created_with: "[適当な名前を指定]".to_string(), }; let time_entry = TimeEntryInput { time_entry: input }; let client = reqwest::Client::new(); let mut rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { let res: TimeEntryOutput = client .post("https://api.track.toggl.com/api/v8/time_entries/start") .basic_auth(api_token, Some("api_token")) .json(&time_entry) .send() .await .unwrap() .json() .await .unwrap(); return res; }) } /// 進行中の Time Entry を取得する fn get_running_time_entry(api_token: &str) -> TimeEntryOutput { let client = reqwest::Client::new(); let mut rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { let res: TimeEntryOutput = client .get("https://api.track.toggl.com/api/v8/time_entries/current") .basic_auth(api_token, Some("api_token")) .send() .await .unwrap() .json() .await .unwrap(); return res; }) } /// 指定した ID の Time Entry を停止する fn stop_time_entry(api_token: &str, time_entry_id: u64) -> TimeEntryOutput { let client = reqwest::Client::new(); let mut rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { let res: TimeEntryOutput = client .put(&format!( "https://api.track.toggl.com/api/v8/time_entries/{}/stop", time_entry_id )) .basic_auth(api_token, Some("api_token")) .header(reqwest::header::CONTENT_TYPE, "application/json") .json("") .send() .await .unwrap() .json() .await .unwrap(); return res; }) } fn main() -> Result<(), Box<dyn std::error::Error>> { let workspaces = &get_workspaces(&CONFIG.api_token); for w in workspaces { println!("{}", w.id); } let projects = &get_projects(&CONFIG.api_token, workspaces[0].id); for p in projects { println!("{}", p.id); } let started_time_entry = start_time_entry(&CONFIG.api_token, projects[0].id); print!("{}", started_time_entry.data.start); let active_time_entry = get_running_time_entry(&CONFIG.api_token); print!("{}", active_time_entry.data.id); let stopped_time_entry = stop_time_entry(&CONFIG.api_token, active_time_entry.data.id); print!("{}", stopped_time_entry.data.id); Ok(()) }
-
Toggl API を例として Web API の利用方法も学びました。
- 非同期処理の方法もよくわかってなかったので、調査に時間がかかってしまいました。。。
参考資料
-
Toggl API Documentation
- API の内容全般について参考にしました。
-
PythonでTogglのAPIをたたく - Qiita
- API を利用する際のコーディングを参考にしました。
-
Rustで環境変数を扱う
- Rust で環境変数を扱う方法を参考にしました
-
Rustで設定ファイルの内容をグローバル変数に保存する - Qiita
- 起動時に環境変数を読み込む方法を参考にしました。