flipfrogの技術ブログ

主に技術的な記事を書きます

Rustでfreee会計とAPI連携するためのSDKとサンプル実装の作成

今回の記事

Rustを使って、freee会計のパブリックAPIとの連携用SDKを作ったので記事を書きます。

https://github.com/flipfrog/freee-accounting-sdk-rust

このSDKは、昨年、最初のバージョンを作成し、その後、サンプルの拡充やスキーマをもとにした最新化を続けています。

この記事では、SDKでできることや、サンプルプログラムの動かし方、SDKを実装するときにやったことなどを書きます。

このSDKでできること

Rust言語でfreeeと連携するアプリを作成することができます。

具体的には、freeeアプリストアで作成したアプリの接続情報を使って、API経由でアプリのアクセストークン取得や会計システムと連携することができます。

連携できる内容は、freeeのOAuth2認証と個々のAPI呼び出しです。詳細は、freeeのパブリックAPIの説明を参照して下さい。Web画面でできることの多くをAPIを使って実行することができます。

記事のタイトルの通り、このSDKはRustで実装されており、Rustで書かれたアプリケーションから呼び出されることを想定してます。

このSDKでできないこと

freeeでは、会計、人事労務など、いくつかのプロダクトのパブリックAPIが用意されています。 このSDKは、会計APISDKなので、会計API以外にアクセスする機能は提供していません。

サンプルプログラムの動かし方

このリポジトリに含まれるサンプルは、全てmacOS環境で動作確認をしています。 Windowsでは確認していないので、必要に応じてコマンドの使い方などを置き換えて読んで下さい。

最初にやること

このSDKに付属するサンプルプログラムを動かすには、GitHubからSDKリポジトリをcloneすることから始めます。

% git clone git@github.com:flipfrog/freee-accounting-sdk-rust.git

PCにRustのコンパイル環境がまだ無い場合は、コンパイラをインストールしましょう。 rustupを使えば、コンパイラのバージョン管理も楽になります。

https://www.rust-lang.org/ja/tools/install

次に、APIへのアクセスを試すための開発用テスト事業所を作成します。 freeeアプリストアの開発者ページ上部にある「開発用テスト事業所の作成」リンクから作成画面に入ります。 画面の指示通りに進めていけば、簡単にテスト事業所を作成できます。 開発用テスト事業所の作成ボタンの画像

つぎは、サンプルプログラムの動かし方を説明します。

コンソールで動くサンプルプログラムの動かし方

最初にコンソールで動作するサンプルの実行方法を説明します。

開発用テスト事業所で、テスト用のアプリを作成します

このサンプルでは、必須入力項目だけ入力してアプリを作成し下書き保存します。公開する必要はありません。

環境変数の設定

作成したアプリの基本情報にある、モバイル・JSアプリ認証用URLをクリップボードにコピーして、ブラウザの別タブなどで開きます。

モバイル・JSアプリ認証用URLの表示

アプリ連携の開始画面が表示されるので、許可するボタンをクリックします。

すると、アクセストークンが表示されるので、それをクリップボードにコピーして、以下のように環境変数にセットします。

% export RUST_API_EXAMPLE_OAUTH_ACCESS_TOKEN=<コピーしたアクセストークン>

プログラムの実行(事業所IDを一覧させるところまで)

先にcloneしたSDKにあるサンプルコードのディレクトリに入ります。

% cd freee-accounting-sdk-rust/examples/console

cargo run と打ちます。

% cargo run

すると、プログラムが使うライブラリのダウンロードとコンパイルが行われた後に、プログラムが実行されます。

コンパイル時にワーニングが表示されますが、気にしないで下さい。

% cargo run
   Compiling hashbrown v0.11.2
   Compiling log v0.4.14
   Compiling futures-task v0.3.19
   Compiling tracing v0.1.32
   :

最後に、事業所情報の一覧に続けて、エラーが表示されます。 このエラーは、テストする事業所コードが設定されていないために発生します。

    Finished dev [unoptimized + debuginfo] target(s) in 21.00s
     Running `/Users/flipfrog/github/freee-accounting-sdk-rust/target/debug/openapi-examples`
- company.id: XXXXXXX, company.display_name: YYYYYYYYYYYYYY
- company.id: ZZZZZZZ, company.display_name: 開発用テスト事業所
thread 'main' panicked at '事業所IDの取得に失敗しました: NotPresent', examples/console/src/main.rs:31:70
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

プログラムの実行(環境変数に事業所IDを設定して実行)

事業所一覧の中から、開発用テスト事業所のcompany.id(事業所ID)をコピーし、次の環境変数にセットします。

% export RUST_API_EXAMPLE_COMPANY_ID=ZZZZZZZ(実際は数値)

そして、cargo run を再度実行します。

% cargo run                                  
warning: unused variable: `receipt`
    :
    Finished dev [unoptimized + debuginfo] target(s) in 0.05s
     Running `/Users/flipfrog/github/freee-accounting-sdk-rust/target/debug/openapi-examples`
- company.id: XXXXXXX, company.display_name: YYYYYYYYYYYYYY
- company.id: ZZZZZZZ, company.display_name: 開発用テスト事業所
created: partner.id=99999999, partner.name=Rust API SDKテスト
got: partner.id=99999999, partner.name=Rust API SDKテスト
destroy: deleted the partner.

すると、指定した事業所に取引先を登録したすぐ後に、それを削除する処理が動いたことが分かります。

プログラムの実装内容

このサンプルの処理は、 examples/console/src/main.rs に書かれています。

このソースで参照する、Rust版SDKexamples/console/Cargo.toml で参照するように設定を書いています。

:
[dependencies]
openapi-sdk = { path = "../../sdk" }
:

リポジトリのトップ階層にある、sdkディレクトリをopenapi-sdkとして定義してあり、先のmain.rsからは、下記のようにuseディレクティブを使って参照しています。

use openapi_sdk::apis::partners_api;
use openapi_sdk::apis::companies_api;
use openapi_sdk::apis::configuration::Configuration;
use openapi_sdk::models::PartnerCreateParams;

sdkディレクトリ以下の内容は、openapi-generatorによって自動生成されています。 また、生成されるソースコードの構成は、openapi-generatorが持つRust言語用テンプレートや、APIエンドポイントのスキーマファイルの内容に依存するので、各々のバージョンの組み合わせによって関数や構造体の構成が変更される可能性があります。

このサンプルでは、予め取得したアクセストークンを使いAPIにアクセスします。

後述する「Webサーバーとして動くサンプル」では、ユーザーに、freeeにログインしてもらい、アプリのアクセス範囲の確認後(アプリの認可後)、認証コードを得て、アクセストークンを取得する処理を行います。

main.rsでは、以下の流れで処理を行います。

  • Configuration構造体のオブジェクトを作成する

    この構造体のメンバーに、環境変数から取得したアクセストークンを設定します

    let oauth_access_token = env::var("RUST_API_EXAMPLE_OAUTH_ACCESS_TOKEN").expect("アクセストークンの取得に失敗しました");

    let config = Configuration {
        base_path: "https://api.freee.co.jp".to_string(),
        user_agent: None,
        client: reqwest::Client::new(),
        basic_auth: None,
        oauth_access_token: Some(oauth_access_token),
        bearer_access_token: None,
        api_key: None
    };
  • アクセストークンに設定されたfreeeユーザーに紐付いた事業所の一覧を取得する
    let companies = companies_api::get_companies(&config).await.expect("事業所一覧の取得に失敗しました");
  • 上記で取得した事業所情報を標準出力に出力する
    for company in &companies.companies {
        println!("- company.id: {}, company.display_name: {}",
                 company.id,
                 company.display_name.as_ref().unwrap_or(&"".to_string())
        );
    }
  • 環境変数で指定された事業所IDに、取引先を新規登録する

    登録する取引先の情報は、PartnerCreateParams構造体オブジェクトを作成して、partners_api::create_partner関数に渡します

    let params = PartnerCreateParams {
        company_id,
        name: "Rust API SDKテスト".to_string(),
        code: None,
        shortcut1: None,
        shortcut2: None,
        org_code: None,
        country_code: None,
        long_name: None,
        name_kana: None,
        default_title: None,
        phone: None,
        contact_name: None,
        email: None,
        payer_walletable_id: None,
        transfer_fee_handling_side: None,
        address_attributes: None,
        partner_doc_setting_attributes: None,
        partner_bank_account_attributes: None,
        payment_term_attributes: None,
        invoice_payment_term_attributes: None
    };

    // 取引先を作成する
    let new_partner = partners_api::create_partner(&config, params).await.expect("取引先の作成に失敗しました");
    println!("created: partner.id={}, partner.name={}", new_partner.partner.id, new_partner.partner.name);
  • 上記で登録した取引先の情報を取得する

    作成した取引先IDを使って取得します

    // 取引先を取得する
    let partner = partners_api::get_partner(&config, new_partner.partner.id, company_id).await.expect("取引先の取得に失敗しました");
    println!("got: partner.id={}, partner.name={}", partner.partner.id, partner.partner.name);
  • 同取り取引先を削除する

    作成した取引先IDを使って削除します

    // 取引先を削除する
    partners_api::destroy_partner(&config, new_partner.partner.id, company_id).await.expect("取引先の削除に失敗しました");
    println!("destroy: deleted the partner.");

Webサーバーとして動くサンプル

次にWebサーバーとして動作するサンプルの実行方法を説明します。

freeeのアプリストアの開発者画面で、新しいアプリを作成します

こちらも、事業所を開発用テスト事業所に切り替えてから作成するのがよいでしょう。

コマンドラインアプリと同様に必須入力項目だけを入力すればOKです。

ただ、今回は、「コールバックURL」に http://localhost:8080/auth_callback を設定します。 このURLは、freeeでアプリの認可を行った後に、リダイレクトされるURLです。 そのリダイレクト先は、今回動かすWebサーバアプリのページです。

コールバックURLに値を設定したフィールドのスクリーンショット

環境変数の設定

アプリストアのアプリ情報にある、クライアントIDとクライアントシークレットを下記のように環境変数に設定します。

% export RUST_API_EXAMPLE_CLIENT_ID=<クライアントID>
% export RUST_API_EXAMPLE_CLIENT_SECRET=<クライアントシークレット>

Webサーバーの実行

先にcloneしたSDKにあるサンプルコードのディレクトリに入ります。

% cd freee-accounting-sdk-rust/examples/web

cargo run と打ちます。

% cargo run

すると、プログラムが使うライブラリのダウンロードとコンパイルが行われた後に、プログラムが実行されます。

コンパイル時にワーニングが表示されますが、気にしないで下さい。

コンソールに下記が表示されたら、Webサービスが動いているのでブラウザで http://localhost:8080/ を開いてみて下さい。 開くと、下記のような画面が表示されると思います。

Webサンプルプログラムの初期表示

画面の「freeeで認証する」リンクをクリックします。

すると、freeeの認可画面に遷移して、下記のような画面が表示されます。 アプリ名は、自分で入力したものが表示されます。

freeeの認可画面

「許可する」をクリックします。

すると、再びWebサンプル画面に戻って、ログインユーザ名とログインに関連付いている事業所情報が一覧されます。

Webサンプルのログイン後画面

ログインユーザー名と事業所の一覧情報は、freeeのAPIを使って取得しています。

Webサンプルの説明は以上です。

Webサーバーの実装

このサンプルの処理は、 examples/web/src/main.rs に書かれています。

Webフレームワーク

このサンプルでは、actix-webというフレームワークを使用しています。

このサンプルアプリケーション作成時点でactix-webのシェア率は、トップか2位くらいだったと思います。

シンプルに使うことができ、今回のサンプルを丁度いいサイズで書くことができました(ロジックは200行未満)。 サンプルのソースは、main.rs の中に全てのロジックを含み、テンプレートエンジン用のテンプレートを画面毎に作成しました。

テンプレートエンジンは、askamaテンプレートエンジンを使っています。サンプルアプリ開発時点で、CLionのプラグインでサポートされていたのですが、CLionのバージョンを上げるとプラグインが利用できなくなってしまいました(残念・・・)。

記述は、Laravelで標準のBLADEに似た感じで、個人的にとても書きやすいと思います。

リクエストのルーティング

Webサンプルの全体像を把握するために、ルーティングを確認してどのような画面がサーバーで提供されるかを見てみます。

main.rsファイルの最後には、下記のルーティング定義があります。

    // webサービスを開始する
    HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(AppState {
                oauth: client.clone(),
                pool: pool.clone(),
            }))
            .wrap(middleware::Logger::default())
            .service(web::resource("/").route(web::get().to(index)))
            .service(web::resource("/login").route(web::get().to(login)))
            .service(web::resource("/auth_callback").route(web::get().to(auth_callback)))
            .service(web::resource("/home").route(web::get().to(home)))
    })
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
  • / :Webサンプルに最初にアクセスする画面
  • /login :ログインするリンクをクリックされたときに遷移します
  • /auth_callback :freeeで認可された後にリダイレクトされます
  • /home :/auth_callbackからリダイレクトされる画面

これらのページは、下図のように遷移します。

webサンプルのページ遷移図

/loginへの遷移

/loginは、/indexページに表示される「freeeで認証する」リンクからGETで遷移します。 遷移後は、main関数で予め設定されたfreeeの認証ページURLにリダイレクトします。 したがって、/loginはページを持ちません。

このURLは固定値です。

  • main関数での定義
    let auth_url = AuthUrl::new("https://accounts.secure.freee.co.jp/public_api/authorize".to_string())
        .expect("認可画面のURLが正しくありません");
  • /login遷移時の処理
// ログインlinkがクリックされたら、freeeの認可画面にリダイレクトする
async fn login(app_state: web::Data<AppState>) -> Result<HttpResponse, AWError> {
    let (auth_url, _csrf_token) = app_state.oauth
        .authorize_url(CsrfToken::new_random)
        .url();

    Ok(HttpResponse::TemporaryRedirect()
        .append_header((header::LOCATION, auth_url.to_string()))
        .finish()
    )
}

/auth_callbackへの遷移

/loginからfreeeの認可画面に遷移後、ユーザーが「許可する」ボタンをクリックすると、freeeのアプリ設定である「コールバックURL」にリダイレクトされます。このコールバックURLは、先述したとおりアプリ情報作成時に、本webサンプルアプリの /auth_callback に設定してあります。

/auth_callbackは、ユーザーがfreeeの認可画面で「許可する」をクリックした後に、freeeの仕組みにより認可コードをパラメータに含んでリダイレクトされます。/auth_callbackへの遷移後、APIを使ってアクセストークンを取得し、後の処理のためにアクセストークンをDBに保存します。

アクセストークンの保存後、このサンプルでは、/homeページにリダイレクトします。 よって、/auth_callbackも画面を持ちません。

認証コードを/auth_callback遷移時のクエリパラメータから取得し、それを使ってアクセストークンを取得する

    let code = params.code.clone();
    let code = AuthorizationCode::new(code.to_string());
    :
    // アクセストークンを取得する
    let token_res = web::block(move|| app_state.oauth.exchange_code(code).request(http_client).unwrap()).await;

アクセストークンをDBに保存する

    // アクセストークンとリフレッシュトークンをDBに保存する
    conn.execute("INSERT INTO token VALUES ('EXAMPLE', ?1, ?2)
                    ON CONFLICT(application_id) do update set refresh_token = ?1, token = ?2;",
                 params![refresh_token, access_token])
        .expect("アクセストークンの保存に失敗しました");

リフレッシュトークンも合わせて保存していますが、今回のアプリでは利用していません。

このアプリでは、DBにSQLiteを使用しています。

/home画面

/home画面は、/auth_callbackの処理で保存したアクセストークンを用いて、freeeのAPIにアクセスし、画面に表示する情報を取得します。

    // DBからアクセストークンを取得する
    let conn = app_state.pool.get()
        .expect("DB接続の取得に失敗しました");
    let mut statement = conn.prepare("SELECT token FROM token WHERE application_id = 'EXAMPLE'").unwrap();
    let rows = statement.query_map(params![], |row| {
        let token: String = row.get(0).unwrap();
        Ok(token)
    })
        .expect("DBからのアクセストークンの取得に失敗しました");
    let token = rows.reduce(|_a, t| t).unwrap().unwrap();

あとは、先に書いたコンソール版のサンプルアプリと同様に、Configuration構造体と共にAPIを呼び出します。

    // APIで事業所の一覧を取得する
    let companies = companies_api::get_companies(&config).await
        .expect("事業所一覧の取得に失敗しました");

    // APIでユーザー情報を取得する
    let me = users_api::get_users_me(&config, Some(false), Some(false)).await
        .expect("ユーザ情報の取得に失敗しました");
    let last_name = me.user.last_name.unwrap_or("".to_string());
    let first_name = me.user.first_name.unwrap_or("".to_string());

SDKを実装するときにやったこと

openapi-generatorの利用

リポジトリsdk/srcディレクトリの内容は、openapi-generatorというオープンソースのツールを使っています。

私は、Homebrewを使ってインストールしました。

% brew install openapi-generator

コマンドは、このリポジトリにある update_sdk_src.sh スクリプトで実行できます。

#/bin/bash
openapi-generator generate -i https://raw.githubusercontent.com/freee/freee-api-schema/master/v2020_06_15/open-api-3/api-schema.json -g rust -o /tmp/api-rust
cp -rp /tmp/api-rust/src sdk/

コマンドラインで参照するスキーマJSONファイルは、swagger editorを使って表示することもできます。