flipfrogの技術ブログ

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

macOSのDocker Desktopで、No route to hostエラーを回避する方法

とても限定的な環境ですが、仕事で使っているIntel MacBook Pro上のDocker環境で表題のエラーが発生して困っていたのを、おそらく、解消できたのでメモします。

  • ハードウェア:13-inch, 2018, Four Thunderbolt 3 Ports
  • OS:13.0.1
  • Docker Desktop:4.15.0 (93002)

PHPのライブラリであるGuzzlehttpを使って、Box APIにファイルをチャンクに分割してアップロードする、Uploads (Chunked) APIを使い、並列度10以上でファイル転送していると頻繁に下記のエラーが発生しました。

cURL error 7: Failed to connect to api.box.com port 443: No route to host (see https://curl.haxx.se/libcurl/c/libcurl-errors.html) for https://api.box.com/2.0/folders/<folder_id>/items in <ソースのファイル名>

一度これが出ると、Docker Desktopをrestartするまで出続けるという現象が発生していました。

対処方法は、下記のスクリーンショットにある、Preferences > General > Use Virtualization frameworkのチェックを外します。これにより、前述のエラーが発生しなくなりました。

Docker DesktopのPreferences画面のスクリーンショット

この対処方法は、下記の記事を参考にしました。

https://forums.docker.com/t/192-168-65-1-connect-no-route-to-host/109264/6

Box APIでGuzzlehttpを使ってファイルをアップロードする方法

Box APIを使ってファイルをアップロードする方法を調べたので記事を書きます。

経緯

仕事でBox APIを使っているのですが、ファイルアップロードがうまく行かずに詰まっていました。

帰宅後に調べて解決したので記事に書きます(スッキリしました)。

APIエンドポイントのリファレンス

サンプルコード

GitHubに置いておきました。

https://github.com/flipfrog/box-api-upload-using-guzzlehttp

実装上のポイント

リファレンスにかかれている、cURLでの使い方をGuzzlehttpでどのように実装するかでした。

-F attributes='{"name":"Contract.pdf", "parent":{"id":"11446498"}}' \
-F file=@<FILE_NAME>

POSTMANを使うと、ほぼそのままリクエストパラメータを設定すればアップロードできたのですが、私は直感的にGuzzlehttpを使った実装ができませんでした。Guzzlehttpでは下記のように、multipartフォームを記述します。

    'multipart' => [
        [
            'name' => 'parent_id',
            'contents' => 0, // 0: root folder
        ],
        [
            'name' => 'file',
            'contents' => \GuzzleHttp\Psr7\Utils::tryFopen('./data/test.txt', 'rb'),
            'filename' => 'my_test.txt',
        ],
    ],

GitHub上のソース

最初のブロックの、name=parent_idの書き方は、Boxが提供するOpen API specification schemaから、openapi-generatorを使ってPHPSDKを生成したときの src/Command/Content/File/UploadFile.php ファイルに記載されていた方法を参考にしました。

2つ目のブロックのcontentsに、文字列やファイルが紐付かないstreamを指定したときには、同ブロックのfilenameを指定しないと400エラーになります。

詰まっていた理由

いくつかの理由でうまくいかずに困っていました。

アップロードするサーバーは他のエンドポイントとは別

リファレンス をよく見ていなかった自分が悪いですね。

APIを使ったファイルのアップロードに慣れていなかった

ほぼ、リファレンスに書かれている感じでPOSTMANを使うとファイルをアップロードできますが、Guzzlehttpを使って同じことをやろうとするとうまく行かない。というか、その方法が分からなかった。

下図はPOSTMANでのリクエストパラメータ設定です。 POSTMANでのリクエストの設定

情報が少ない

ネットで検索すると、同じような質問が幾つかありましたが、どれも解決に至っていませんでした。 Box公式のSDKが無く、個人的に作成されたものをGitHubで見たのですが、使うGuzzlehttpのバージョンが4.xと古く、最新の7.xでは参考になりませんでした。

追記

記事を書いたあとに情報を見つけたのですが、multipartブロックを下記のように書けば直感的になるようです。

    'multipart' => [
        [
            'name' => 'attributes',
            'contents' => json_encode([
                'name' => 'my_test.txt',
                'parent' => ['id' => 0],
            ]),
        ],
        [
            'name' => 'file',
            'contents' => \GuzzleHttp\Psr7\Utils::tryFopen('./data/test.txt', 'rb'),
            'filename' => 'my_test.txt', // optional to specify file name on box
        ],
    ],

Boxに作成されるファイルのファイル名は、1つ目のブロックのJSON内のname属性が優先して採用されるようです。

出典: https://support.box.com/hc/ja/community/posts/5289184800019-API-upload-did-not-contain-a-file-part

PHP7.2 + xdebug 3.1.5 + PHPStorm(2022.2.3) で、ステップ実行したときにSegmentation faultする件

仕事で、PHP7.2実行環境とxdebug3.1.5の組み合わせで、ブレークポイントで止まったあとにステップ実行すると、Segmentation faultする現象の対応に時間を割いてしまったのでメモ。

  • IDEは、PHPStorm(2022.2.3)です。
  • VSCodeをインストールして同じことをすると、コアダンプしないで使うことができました。
  • PHP7.4では問題なく動作するようなので、PHP7.2との組み合わせ固有の現象のようです。

対処方法は、PHPStormのPreferences... > PHP > Debug にある、Evaluation欄で Enable '__toString' object viewのチェックを外すとコアダンプしなくなりました。

PHPStormの環境設定画面のスクリーンショット

php -vを実行したときの出力は、以下の通りです。

PHP 7.2.24 (cli) (built: Oct 22 2019 08:28:36) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
    with Xdebug v3.1.5, Copyright (c) 2002-2022, by Derick Rethans
    with Zend OPcache v7.2.24, Copyright (c) 1999-2018, by Zend Technologies

php.iniのxdebugセクションの抜粋は、以下の通りです。

[xdebug]
zend_extension=/usr/lib64/php/modules/xdebug.so
xdebug.mode=debug
xdebug.start_with_request=yes
xdebug.discover_client_host = 0
xdebug.remote_handler = "dbgp"
xdebug.client_port=9003
xdebug.client_host=host.docker.internal
xdebug.idekey=phpstorm

OSは、AlmaLinux 8をDocker環境で実行しています。

# cat /etc/redhat-release 
AlmaLinux release 8.6 (Sky Tiger)

gdbを使って実行させたときのバックトレースです。

(gdb) run test.php
Starting program: /usr/bin/php test.php
warning: Error disabling address space randomization: Operation not permitted
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".

Program received signal SIGSEGV, Segmentation fault.
0x00005583000a56dd in _zval_ptr_dtor ()
(gdb) bt
#0  0x00005583000a56dd in _zval_ptr_dtor ()
#1  0x00007f6e28dd85c0 in xdebug_dbgp_handle_eval (retval=0x7fff3966c850, 
    context=<optimized out>, args=<optimized out>)
    at /var/tmp/xdebug/src/debugger/handler_dbgp.c:1055
#2  0x00007f6e28ddcdc6 in xdebug_dbgp_parse_option (flags=0, 
    retval=<optimized out>, 
    line=0x55830245fa10 "eval -i 24 -- KHN0cmluZykoJHNtYXJ0eSk=", 
    context=0x7f6e28ffdcb8 <xdebug_globals+376>)
    at /var/tmp/xdebug/src/debugger/handler_dbgp.c:2230
#3  xdebug_dbgp_cmdloop (context=0x7f6e28ffdcb8 <xdebug_globals+376>, bail=1)
    at /var/tmp/xdebug/src/debugger/handler_dbgp.c:2340
#4  0x00007f6e28dddd99 in xdebug_dbgp_breakpoint (
    context=0x7f6e28ffdcb8 <xdebug_globals+376>, stack=<optimized out>, 
    filename=<optimized out>, lineno=23, type=<optimized out>, exception=0x0, 
    code=0x0, message=0x0, brk_info=0x558302416b60)
    at /var/tmp/xdebug/src/debugger/handler_dbgp.c:2701
#5  0x00007f6e28dd3534 in xdebug_debugger_statement_call (
    filename=0x7f6e29076030, lineno=23)
    at /var/tmp/xdebug/src/debugger/debugger.c:294
#6  0x00005583000a940a in zend_llist_apply_with_argument ()
#7  0x00005583001457dd in ZEND_EXT_STMT_SPEC_HANDLER ()
#8  0x0000558300159a50 in execute_ex ()
#9  0x00007f6e28dbe60a in xdebug_execute_ex (execute_data=0x7f6e2901c030)
--Type <RET> for more, q to quit, c to continue without paging--
   r/tmp/xdebug/src/base/base.c:779
#10 0x000055830015d776 in zend_execute ()
#11 0x00005583000b74cb in zend_execute_scripts ()
#12 0x0000558300052a40 in php_execute_script ()
#13 0x000055830015fd5c in do_cli ()
#14 0x00005582fff0d632 in main ()
(gdb) 

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を使って表示することもできます。

A Tour of Goの最後の課題

はじめまして

愛知県で働く、ソフトウェアエンジニアのflipfrogといいます。

Go言語を知ろうと思い、A Tour of Goを一通りやってみました。

A Tour of Goをやっただけなので、他にも知らない機能があると思いますが、Go言語はシンプルで言語仕様が頭の中に入りやすかったです。

説明を読みながら課題をこなして、最後までたどり着きました。

最後の課題は、明確に正解が提示されていないので、どのような実装にしたかを記録しておこうと思います。

この課題は、並列処理と排他制御を使った疑似Webクローラーの実装でした。

修正後のコードは、GitHubのリポジトリに保存してあります。

課題で提示された実装からの修正差分は、コミットを参照して下さい。

課題の指示

  • ウェブクローラ( web crawler )の並列化
  • 同じURLを2度取ってくることなく並列してURLを取ってくる

ベースのコードは提供されているので、この課題ではそれを修正します。

2つめの「同じURLを2度取ってくることなく並列してURLを取ってくる」という指示には、URLを重複して結果に含めないのと、並列で実行するスレッドから重複を管理するデータへのアクセスに排他制御をかけるということが含まれていると考えました。

どのように実装を修正したか

  • ウェブクローラ( web crawler )の並列化

Crawl関数は、与えられた1つのurlから再帰的にリソースに含まれる下位のurlを探索します。 下位urlの探索には、再帰呼びをしているので、その処理を別スレッドで実行するように修正しました。

上位のurlから下位のurlを呼び出しますが、上位側では下位urlの探索が終わるまで待ってから制御を上位に戻すようにしました。

+       var wg sync.WaitGroup
        for _, u := range urls {
-               Crawl(u, depth-1, fetcher)
+               wg.Add(1)
+               go func(u string) {
+                       defer wg.Done()
+                       Crawl(u, depth-1, fetcher, c)
+               }(u)
        }
+       wg.Wait()
        return
 }

https://github.com/flipfrog/a-tour-of-go-the-last-exercise/blob/4f97abfa23041d1d1967b7ecc3ddfcfc2a1c4816/exercise.go#L43-L51

並列化の制御のために、Crawl関数の呼び出しにクロージャをはさみました。

wg.Add(1)を呼び出したあとにgoroutineを起動、クロージャの処理が終了した時点でwg.Done()が呼び出されるようにしています。 呼び出し元では、全てのスレッドが終了するのをwg.Wait()を使って待ちます。

また、この記事を書いている時点で、main関数でsync.WaitGroupを作成して、Crawl関数の引数渡しで持ち回った方がより並列度が上がるかもしれないと思いました。

  • 同じURLを2度取ってくることなく並列してURLを取ってくる

重複しないURLを作成するために、mapを使うことにしました。

更に、排他制御も必要なので、前の課題で作成したSafeCounterを使うことにしました。

SafeCounterオブジェクトへのアドレスを、Crawl関数に渡します。

    c := SafeCounter{v: make(map[string]int)}
    Crawl("https://golang.org/", 4, fetcher, &c)

https://github.com/flipfrog/a-tour-of-go-the-last-exercise/blob/4f97abfa23041d1d1967b7ecc3ddfcfc2a1c4816/exercise.go#L56-L57

Crawl関数では、SafeCounterをロック後、渡されたurlが未登録の場合、そのurlをmapに登録しロックを解除します。 既に登録されている場合は、ロックを解除して制御を呼び元に戻します。 これにより、テストしてセットする処理をアトミックに処理できるようになりました。

   c.mu.Lock()
    if c.v[url] > 0 {
        c.mu.Unlock()
        return
    }
    c.v[url]++
    c.mu.Unlock()

https://github.com/flipfrog/a-tour-of-go-the-last-exercise/blob/4f97abfa23041d1d1967b7ecc3ddfcfc2a1c4816/exercise.go#L34-L40

全てのスレッドの処理が終わったら、SafeCounterオブジェクトに格納されたurlを表示してmain関数の処理が終わります。

   for url, _ := range c.v {
        fmt.Println("*** ", url)
    }

https://github.com/flipfrog/a-tour-of-go-the-last-exercise/blob/4f97abfa23041d1d1967b7ecc3ddfcfc2a1c4816/exercise.go#L58-L60

以下は、このプログラムをコンソールで実行したときの出力です。

$ ./exercise 
found: https://golang.org/ "The Go Programming Language"
not found: https://golang.org/cmd/
found: https://golang.org/pkg/ "Packages"
not found: https://golang.org/cmd/
found: https://golang.org/pkg/os/ "Package os"
found: https://golang.org/pkg/fmt/ "Package fmt"
***  https://golang.org/
***  https://golang.org/pkg/
***  https://golang.org/pkg/os/
***  https://golang.org/pkg/fmt/
$ 

***が先頭に付いた行が、収集されたurlの一覧です。

IDEを使うメリット

この課題を実装する過程で、処理結果が想定どおりにならなくて困っていたときがありました。 原因は、ループ変数を、クロージャ内部で使っていたことでした。 そして、ループ変数をクロージャ関数の引数として渡すことで問題を解消しました。

そのとき、たまたまコードをIDEで編集してみようと思い、コードを開いたところ下記のようなワーニングが表示されて気づくことができました。 IDEを使っていなければ、更に余計な時間を使ったと思います。

GOLandで表示されたワーニングのスクリーンショット