(RustからGoogle スプレッドシートの編集自体は成功しましたが)タイトルにある通り、実質敗北した記事です。

動機

Rustからスプレッドシートをいじりたいなあ。脳死で真似できる日本語記事が検索しても見つからない・・・。自分で用意するしかない。

(少ししたらアドベントカレンダーで誰かやってくれたか?)

GCPでの前準備

RubyでGoogle スプレッドシートを操作する | GMOアドパートナーズグループ TECH BLOG byGMOを参考に以下のことをします。

API ライブラリ – Google Cloud Platformで新しいプロジェクトを作成
作成したプロジェクトで「Google Drive API」と「Google Sheets API」を有効化
「APIとサービス」> 「認証情報」へと進み、「認証情報を作成」>「OAuth クライアント ID」を選択して「アプリケーション名」を入力して保存
「APIとサービス」> 「認証情報」で作成したクライアントのJSONをダウンロード

スプレッドシートの用意

操作対象となるスプレッドシートを用意します。
適当に開いてスプレッドシートのURLに含まれているIDをメモります。

https://docs.google.com/spreadsheets/d/ここがスプレッドシートのID/edit#gid=0

sheet.newとURLバーに入力してEnterすれば新しいスプレッドシートを作成してくれるって知ってました?
参考:A .new trick for starting Google docs, slides, sheets, and forms — Quartz at Work

Rustで作業

“Rust” “Google” “SpreadSheet”あたりで検索したらgoogle-apis-rs/gen/sheets4 at master · Byron/google-apis-rsが見つかったので、これを使います。

Cargoプロジェクトの作成

[/mnt/p/program/rust]
cargo new rust_google_api_test --vcs=none
     Created binary (application) `rust_google_api_test` package

Cargo.tomlを編集

README.md > Usage > Setting up your Project に従って、Cargo.tomlを編集します。

# 略
[dependencies]
google-sheets4 = "*"
hyper = "^0.10"
hyper-rustls = "^0.6"
serde = "^1.0"
serde_json = "^1.0"
yup-oauth2 = "^1.0"

とりあえず build

[/mnt/p/program/rust/rust_google_api_test]
cargo run
#略
error: failed to run custom build command for `openssl-sys v0.9.52`
#略

色々調べたら rust で failed to run custom build command for openssl-sys が出るときにすること – Qiita が見つかりました。これに従って環境変数を指定して実行します。

[/mnt/p/program/rust/rust_google_api_test]
OPENSSL_LIB_DIR="/usr/lib/x86_64-linux-gnu" OPENSSL_INCLUDE_DIR="/usr/include/openssl" cargo run
   Compiling libc v0.2.65

# 略

    Finished dev [unoptimized + debuginfo] target(s) in 38.87s
     Running `target/debug/rust_google_api_test`
Hello, world!

無事にビルドまでは出来ました。

main.rs を編集

google-apis-rs/gen/sheets4 at master · Byron/google-apis-rsのA complete exampleは動きませんでした。自動生成されたものですし、仕方ないですね。Google Sheets API | Google Developersとgoogle-sheets4が依存するdermesser/yup-oauth2 v1.0.12も参考にして頑張ります。

extern crate google_sheets4 as sheets4;
extern crate hyper;
extern crate hyper_rustls;
extern crate yup_oauth2 as oauth2;
use oauth2::{
    read_application_secret, ApplicationSecret, Authenticator, DefaultAuthenticatorDelegate,
    MemoryStorage,
};
use sheets4::{Sheets, ValueRange};
use std::default::Default;

const CLIENT_SECRET_FILE: &'static str = "example_client_secret.json";
const SPREADSHEET_ID: &'static str = "1TDp1Pc_PKOxvxNzdslNviohdvl-gzZbewhQwh57k7Vg";

fn read_client_secret(file: String) -> ApplicationSecret {
    read_application_secret(std::path::Path::new(&file)).unwrap()
}

fn main() {
    let secret = read_client_secret(CLIENT_SECRET_FILE.to_string());
    let auth = Authenticator::new(
        &secret,
        DefaultAuthenticatorDelegate,
        hyper::Client::with_connector(hyper::net::HttpsConnector::new(
            hyper_rustls::TlsClient::new(),
        )),
        <MemoryStorage as Default>::default(),
        None,
    );
    let hub = Sheets::new(
        hyper::Client::with_connector(hyper::net::HttpsConnector::new(
            hyper_rustls::TlsClient::new(),
        )),
        auth,
    );

    let req = ValueRange {
        range: Some("".to_string()),
        major_dimension: Some("ROWS".to_string()),
        values: Some(vec![
            vec!["123".to_string()],
            vec!["serde_json::value::to_valueされるよ".to_string()],
        ]),
    };
    let result = hub
        .spreadsheets()
        .values_update(req, SPREADSHEET_ID, "A1:A2")
        .value_input_option("RAW")
        .include_values_in_response(false)
        .response_value_render_option("FORMULA")
        .response_date_time_render_option("FORMATTED_STRING")
        .add_scope(sheets4::Scope::Spreadsheet)
        .doit();

    dbg!(&result);
}

先程ダウンロードしたJSONファイルのパスをCLIENT_SECRET_FILEに、スプレッドシートのIDをSPREADSHEET_IDに設定します。

実行

[/mnt/p/program/rust/rust_google_api_test]
OPENSSL_LIB_DIR="/usr/lib/x86_64-linux-gnu" OPENSSL_INCLUDE_DIR="/usr/include/openssl" cargo run
   Compiling rust_google_api_test v0.1.0 (/mnt/p/program/rust/rust_google_api_test)
warning: Error finalizing incremental compilation session directory `/mnt/p/program/rust/rust_google_api_test/target/debug/incremental/rust_google_api_test-5se69cyjfu5z/s-fhxlcq9qf8-1a0vxtn-working`: Permission denied (os error 13)

    Finished dev [unoptimized + debuginfo] target(s) in 4.77s
     Running `target/debug/rust_google_api_test`
Please enter RHN-MXQ-HYS at https://www.google.com/device and grant access to this application
Do not close this application until you either denied or granted access.
You have time until 2019-11-17 21:08:43.023812700 +09:00.

と表示されるので、https://www.google.com/deviceをブラウザで開き、直前のコードを入力して承認していきますと、このような画面になります。

キャプ.jpg

安全なページに戻らず、緑枠で囲んだ詳細ボタンを押すと承認プロセスを継続できます。最終的に下のような画面に行き着けば成功です。

キャプ.jpg

少し時間をおいて、以下のような出力が得られます。

[src/main.rs:55] &result = Ok(
    (
        Response {
            status: Ok,
            headers: Headers { Content-Type: application/json; charset=UTF-8
            , Vary: X-Origin
            Vary: Referer
            Vary: Origin,Accept-Encoding
            , Date: Sun, 17 Nov 2019 11:39:06 GMT
            , Server: ESF
            , Cache-Control: private
            , X-XSS-Protection: 0
            , X-Frame-Options: SAMEORIGIN
            , Alt-Svc: quic=":443"; ma=2592000; v="46,43",h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000
            , Accept-Ranges: none
            , Transfer-Encoding: chunked
            , },
            version: Http11,
            url: "https://sheets.googleapis.com/v4/spreadsheets/1TDp1Pc_PKOxvxNzdslNviohdvl-gzZbewhQwh57k7Vg/values/A1:A2?valueInputOption=RAW&responseValueRenderOption=FORMULA&responseDateTimeRenderOption=FORMATTED_STRING&includeValuesInResponse=false&alt=json",
            status_raw: RawStatus(
                200,
                "OK",
            ),
            message: Http11Message {
                is_proxied: false,
                method: None,
                stream: Wrapper {
                    obj: Some(
                        Reading(
                            ChunkedReader(chunk_remaining=0),
                        ),
                    ),
                },
            },
        },
        UpdateValuesResponse {
            updated_columns: Some(
                1,
            ),
            updated_range: Some(
                "sheet1!A1:A2",
            ),
            updated_rows: Some(
                2,
            ),
            updated_data: None,
            spreadsheet_id: Some(
                "1TDp1Pc_PKOxvxNzdslNviohdvl-gzZbewhQwh57k7Vg",
            ),
            updated_cells: Some(
                2,
            ),
        },
    ),
)

スプレッドシートを確認すると

キャプ.jpg

となっており成功です。

敗北

Rustからスプレッドシートをいじれるようになり、もう何も問題がないかと思っていました。
もう一度実行すれば何が問題か分かります。




[/mnt/p/program/rust/rust_google_api_test]
OPENSSL_LIB_DIR="/usr/lib/x86_64-linux-gnu" OPENSSL_INCLUDE_DIR="/usr/include/openssl" cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.09s
     Running `target/debug/rust_google_api_test`
Please enter ZMY-QKW-BDB at https://www.google.com/device and grant access to this application
Do not close this application until you either denied or granted access.
You have time until 2019-11-17 21:28:49.473924500 +09:00.

再びコードの入力を要求されます。
理由は簡単で、google-sheets4が依存するdermesser/yup-oauth2v1.0.12ではアクセストークン保存機能が未実装でした。

バージョン指定をどうにかできないかと使用しているライブラリのCargo.tomlを確認したら、google-sheets4が`yup-oauth2 = { version = “^ 1.0”, default-features = false }と指定していました。

もう自分には無理だと判断し、「RustからGoogle スプレッドシートを利用する」ことは諦めました。
やっぱりRubyで色々しよう(完)。

振り返りとか

一番の苦労はcompleteではないA complete exampleを動かすことでした。

exampleではoptionが適当な値で埋められていたので、設定しなくてもいいと判断してしまいました。エラーの原因が、option(Query parameters)を正しく設定していないからだと気づくのに時間がかかりました。
optionの値は、公式のドキュメントではenumと表記しているのを、このライブラリでは&strで埋める必要があります。ドキュメントからのコピペが安全です。

Rustで外部ライブラリにがっつり依存して色々するのは初めてだったので、かなり楽しかったです。
vscode+rlsのCtrl+左クリックで定義を見に行ける機能が非常に役立ちました。これがなかったら、そもそもスプレッドシートをいじれずに終わったと思います。

bannerAds