3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Rust で Toggl API を利用する

Posted at

タイムトラッキングアプリである Toggl の API を Rust で叩いてみたので、実装例のメモです。
HTTP リクエストを Rust の HTTP クライアントである reqwest を利用して送って利用します。
Rust 初心者なので、 Toggl API を通して、 HTTP リクエストの送信方法等を勉強することも兼ねました。

API を叩く際に用意するもの

  • API Token

    • API Token はユーザーごとに発行される。
    • 左下のアカウント名をクリック → 「Profile settings」 からプロファイルを表示。下方に「API Token」が表示されている。
      Untitled.png
  • workspace_id

    • 「projects」等の画面を開いた際の URL にある7桁の数字
      • 例: https://track.toggl.com/projects/1234567/list の「1234567」
  • project_id

    • 「projects」→特定のプロジェクトを開いた際に、URL にある9桁の数字
      • 例: https://track.toggl.com/1234567/projects/123456789/team の「123456789」

ID を取得する別の方法

  • 「Settings」→「Data export」を選択。「Projects」にチェックを入れた状態で「Compile file and send to email」をクリック。

    • 登録メールに json が送られてくる
      Untitled 1.png
  • 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 の利用方法も学びました。

    • 非同期処理の方法もよくわかってなかったので、調査に時間がかかってしまいました。。。

参考資料

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?