Rust用TwitterAPIv2ライブラリでOAuth1.0a系の認証をサポートした話

# 目的
RustでTwitterAPIv2のライブラリを書いています。
[twapi-v2](https://crates.io/crates/twapi-v2)
TwitterAPIv2からOAuth2.0がサポートされてBearer認証で呼びだせます。
実はTwitterAPIv1で使われていたOAuth認証もまだ使えると気が付いたのでサポートしました。

## OAuth2.0とOAuth1.0aの違い
### OAuth2.0
■ WebなどのOAuth結果
access_token : APIを呼び出すトークン
refresh_token : 有効期限が切れたaccess_tokenの更新のためのトークン
expires_in : access_tokenの有効期間
■ APIを呼び出す時の使い方
ヘッダーに「Authorization: Bearer &lt;access_token&gt;」を設定
■ 有効期限
あり
### OAuth1.0a
■ WebなどのOAuth結果
oauth_token : APIの認証の計算に使用
oauth_token_secret : APIの認証の計算に使用
■ APIを呼び出す時の使い方
以下を使って認証トークンを計算する
- アプリケーションを登録した時に得られるoauth_consumer_key, oauth_consumer_secret
- 上記のoauth_token, oauth_token_secret
- APIのURL, メソッド, クエリーパラメーター

ヘッダーに「Authorization: &lt;認証トークン&gt;」を設定
■ 有効期限
無し



## OAuth2.0しかサポートしなかった0.5系
Bearer認証しかサポートしていないコードは以下のようになります。最初に認証コードを渡しています。
```rust:main.rs
use twapi_v2::api::get_2_tweets_id;

#[tokio::main]
async fn main() {
    let bearer_code = std::env::var("BEARER_CODE").unwrap();
    let tweet_id = std::env::var("TWEET_ID").unwrap();
    let res = get_2_tweets_id::Api::open(&bearer_code, &tweet_id)
        .execute()
        .await;
    if let Some((val, rate_limit)) = res {
        println!("{:?}", val);
    }
}
```

## OAuth2.0とOAuth1.0aをサポートした0.7系
認証方法は実行時に渡すことにしました。以下のようなコードになります。
```rust:main.rs
use twapi_v2::api::{get_2_tweets_id, BearerAuthentication};

#[tokio::main]
async fn main() {
    let bearer_code = std::env::var("BEARER_CODE").unwrap();
    let auth = BearerAuthentication::new(bearer_code);
    let tweet_id = std::env::var("TWEET_ID").unwrap();
    let res = get_2_tweets_id::Api::open(&tweet_id)
        .execute(&auth)
        .await;
    if let Some((val, rate_limit)) = res {
        println!("{:?}", val);
    }
}
```

OAuth1.0aは以下のようになります。
```rust:main.rs
use twapi_v2::api::{get_2_tweets_id, BearerAuthentication};
use twapi_v2::oauth10a::OAuthAuthentication;

#[tokio::main]
async fn main() {
    let auth = OAuthAuthentication::new(
        std::env::var("CONSUMER_KEY").unwrap_or_default(),
        std::env::var("CONSUMER_SECRET").unwrap_or_default(),
        std::env::var("ACCESS_KEY").unwrap_or_default(),
        std::env::var("ACCESS_SECRET").unwrap_or_default(),
    );
    let tweet_id = std::env::var("TWEET_ID").unwrap();
    let res = get_2_tweets_id::Api::open(&tweet_id)
        .execute(&auth)
        .await;
    if let Some((val, rate_limit)) = res {
        println!("{:?}", val);
    }
}
```

# コードの説明
## Authenticationトレイト
まず共通する認証用のトレイトを作成しました。
```rust
pub trait Authentication {
    fn execute(
        &self,
        builder: RequestBuilder,
        method: &str,
        uri: &str,
        options: &[(&str, &str)],
    ) -> RequestBuilder;
}
```
builderにはここで計算した認証情報を設定して返します。それ以外のパラメーターはOAuth1.0aの計算で必要なものです。

## OAuth2.0
OAuth2.0用の実装です。bearer_codeだけが必要になります。
```rust
pub struct BearerAuthentication {
    bearer_code: String,
}

impl BearerAuthentication {
    pub fn new<T: Into<String>>(bearer_code: T) -> Self {
        Self {
            bearer_code: bearer_code.into(),
        }
    }
}

impl Authentication for BearerAuthentication {
    fn execute(
        &self,
        builder: RequestBuilder,
        _method: &str,
        _uri: &str,
        _options: &[(&str, &str)],
    ) -> RequestBuilder {
        builder.bearer_auth(&self.bearer_code)
    }
}
```
Bearer認証はとても簡単です。reqwestが専用のメソッドを用意しています。

## OAuth1.0a
OAuth1.0a用の実装です。4つの値が必要になります。
```rust
pub struct OAuthAuthentication {
    consumer_key: String,
    consumer_secret: String,
    access_key: String,
    access_secret: String,
}

impl OAuthAuthentication {
    pub fn new<T: Into<String>>(
        consumer_key: T,
        consumer_secret: T,
        access_key: T,
        access_secret: T,
    ) -> Self {
        Self {
            consumer_key: consumer_key.into(),
            consumer_secret: consumer_secret.into(),
            access_key: access_key.into(),
            access_secret: access_secret.into(),
        }
    }
}

impl Authentication for OAuthAuthentication {
    fn execute(
        &self,
        builder: RequestBuilder,
        method: &str,
        uri: &str,
        options: &[(&str, &str)],
    ) -> RequestBuilder {
        let auth = oauth1_authorization_header(
            &self.consumer_key,
            &self.consumer_secret,
            &self.access_key,
            &self.access_secret,
            method,
            uri,
            &options.to_vec(),
        );
        builder.header(reqwest::header::AUTHORIZATION, auth)
    }
}
```
この認証ではbuilder以外の引数を全て使って認証コードを計算します。builderにはAuthorizationのヘッダーとして設定します。

## 各API
実際に利用してるコードは以下のようになります。
```rust
pub fn build(self, authentication: &impl Authentication) -> RequestBuilder {
    let mut query_parameters = vec![];
    if let Some(expansions) = self.expansions {
        query_parameters.push(("expansions", expansions.iter().join(",")));
    }
    if let Some(media_fields) = self.media_fields {
        query_parameters.push(("media.fields", media_fields.iter().join(",")));
    }
    if let Some(place_fields) = self.place_fields {
        query_parameters.push(("place.fields", place_fields.iter().join(",")));
    }
    if let Some(poll_fields) = self.poll_fields {
        query_parameters.push(("poll.fields", poll_fields.iter().join(",")));
    }
    if let Some(tweet_fields) = self.tweet_fields {
        query_parameters.push(("tweet.fields", tweet_fields.iter().join(",")));
    }
    if let Some(user_fields) = self.user_fields {
        query_parameters.push(("user.fields", user_fields.iter().join(",")));
    }
    let client = reqwest::Client::new();
    let url = URL.replace(":id", &self.id);
    let builder = client.get(&url).query(&query_parameters);
    authentication.execute(
        builder,
        "GET",
        &url,
        &query_parameters
            .iter()
            .map(|it| (it.0, it.1.as_str()))
            .collect::<Vec<_>>(),
    )
}
```
query_parameterは実際にリクエストに渡すだけでなくOAuthの計算にも必要になります。

# まとめ
共通トレイトを書いて認証方法をそれぞれ実装しました。利用者は使いたい認証を選択するだけでAPIは同じように呼び出すことができます。