今回の記事
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は、会計APIのSDKなので、会計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をクリップボードにコピーして、ブラウザの別タブなどで開きます。
アプリ連携の開始画面が表示されるので、許可するボタンをクリックします。
すると、アクセストークンが表示されるので、それをクリップボードにコピーして、以下のように環境変数にセットします。
% 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版SDKは examples/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では、以下の流れで処理を行います。
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サーバアプリのページです。
環境変数の設定
アプリストアのアプリ情報にある、クライアント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/
を開いてみて下さい。
開くと、下記のような画面が表示されると思います。
画面の「freeeで認証する」リンクをクリックします。
すると、freeeの認可画面に遷移して、下記のような画面が表示されます。 アプリ名は、自分で入力したものが表示されます。
「許可する」をクリックします。
すると、再び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からリダイレクトされる画面
これらのページは、下図のように遷移します。
/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を使って表示することもできます。