RustでTwitterAPIv2のライブラリを作りました

# 目的
実はRustでTwitterAPIv1のライブラリを作っています。
[twapi-reqwest](https://crates.io/crates/twapi-reqwest)
v1ライブラリで一番ダウンロードが多いです(自分のが63684でライバルのが60000でした)。

今回作ったものはこれになります。
[twapi-v2](https://crates.io/crates/twapi-v2)

v2のライブラリは既にありましたが、最新のAPIをサポートしていないし更新が半年止まっているので自分で書いてみようと思いました。ちなみに記述時点で自分のが362, ライバルは3096です。

# 機能
- Builderパターン
- レートリミット情報
- 便利なパラメーター設定済メソッドの提供
- オプションでリトライ、タイムアウト、ログ機能提供
- オプションでOAuthとOAUthを使ったWebのサンプル提供
- ストリーミングのサンプル提供
- 実験的に戻り値の型をサポート

## Builderパターン
```rust
let (res, rate_limit) = get_2_tweets_search_recent::Api::new(&bearer_code, "東京")
    .since_id("111")
    .max_results(10)
    .execute();
```

## レートリミット情報
APIを実行した時にヘッダーにレートリミット情報がある場合、APIの実行結果と一緒に返します。
```rust
pub struct RateLimit {
    pub limit: i64,
    pub remaining: i64,
    pub reset: DateTime<Utc>,
}
```

## 便利なパラメーター設定済メソッドの提供
- new()は必須パラメーターのみ
- all()はOAuth認証したアカウントだけ見られる全データと最大件数設定。OAuth認証していないアカウントのツイートが含まれるとエラー
- open()は公開されている全データと最大件数設定

```rust
pub fn new(bearer_code: &str, query: &str) -> Self {
    Self {
        bearer_code: bearer_code.to_owned(),
        query: query.to_owned(),
        ..Default::default()
    }
}

pub fn all(bearer_code: &str, query: &str) -> Self {
    Self {
        bearer_code: bearer_code.to_owned(),
        query: query.to_owned(),
        expansions: Some(Expansions::all()),
        media_fields: Some(MediaFields::all()),
        place_fields: Some(PlaceFields::all()),
        poll_fields: Some(PollFields::all()),
        tweet_fields: Some(TweetFields::organic()),
        user_fields: Some(UserFields::all()),
        max_results: Some(100),
        ..Default::default()
    }
}

pub fn open(bearer_code: &str, query: &str) -> Self {
    Self {
        bearer_code: bearer_code.to_owned(),
        query: query.to_owned(),
        expansions: Some(Expansions::all()),
        media_fields: Some(MediaFields::open()),
        place_fields: Some(PlaceFields::all()),
        poll_fields: Some(PollFields::all()),
        tweet_fields: Some(TweetFields::open()),
        user_fields: Some(UserFields::all()),
        max_results: Some(100),
        ..Default::default()
    }
}
```

## オプションでリトライ、タイムアウト、ログ機能提供
```rust
pub async fn execute_retry<T>(
    builder: RequestBuilder,
    retry_count: usize,
    retryable_status_codes: &[StatusCode],
    retry_logger: Option<&impl RetryLogger>,
    timeout_duration: Option<Duration>,
    retry_delay_secound_count: Option<u64>,
) -> Result<(T, Option<RateLimit>), Error>
where
    T: DeserializeOwned,
{...}

struct Logger;
impl RetryLogger for Logger {
    fn log(&self, builder: &RequestBuilder) {
        println!("{:?}", builder);
    }
}
async fn execute() {
    let logger = Logger {};
    let res = execute_retry::<get_2_tweets_id::Response>(
        get_2_tweets_id::Api::open("xxx", "xxx").build(),
        2,
        &vec![StatusCode::UNAUTHORIZED],
        Some(&logger),
        Some(Duration::from_secs(10)),
        None,
    )
    .await;
}
```

## オプションでOAuthとOAUthを使ったWebのサンプル提供
```sh
cd examples/oauth-web
API_KEY_CODE=xxx API_SECRET_CODE=XXX cargo run
```
http://localhost:3000

## ストリーミングのサンプル提供
```
cd examples/streaming
BEARER_CODE=XXXXX cargo run
```

## 実験的に戻り値の型をサポート
```rust
let (res, rate_limit) = execute_twitter::<get_2_tweets_search_recent::Response>(
    get_2_tweets_search_recent::Api::open("xxx", "東京")
).await?;
```
execute_twitterはDeserializeOwned型を戻します。
もし未定義のパラメーターが返ってくる場合はextraに保存されます。
ドキュメントに必須だと書いてあるパラメーターがもし降ってこないと、エラーになります。
不安がある場合はserde_json::Valueを使ってください。

```rust
pub struct Response {
    pub data: Option<Vec<Tweets>>,
    pub errors: Option<Vec<Errors>>,
    pub includes: Option<Includes>,
    pub meta: Option<Meta>,
    #[serde(flatten)]
    pub extra: std::collections::HashMap<String, serde_json::Value>,
}
```

# テスト
作成した78APIすべてテストしました。アカデミーで無いと叩けないAPIやリファレンス通りに実行してもエラーになるAPIはテストが失敗しています。
すべての状況は[TEST.md](https://github.com/aoyagikouhei/twapi-v2-rs/blob/main/TEST.md)に記述しています。
extraが空になるまでテストしましたが、想定していない戻りが含まれるとextraに何か値が入るかもしれません。

```rust:get_2_tweets_search_recent.rs
use anyhow::Result;
use twapi_v2::api::{execute_twitter, get_2_tweets_search_recent};

// BEARER_CODE=XXXXX cargo test test_get_2_tweets_search_recent -- --nocapture --test-threads=1

#[tokio::test]
async fn test_get_2_tweets_search_recent() -> Result<()> {
    let bearer_code = std::env::var("BEARER_CODE").unwrap_or_default();
    let builder = get_2_tweets_search_recent::Api::open(&bearer_code, "東京")
        .max_results(10)
        .build();
    let (res, _rate_limit) = execute_twitter::<serde_json::Value>(builder).await?;
    println!("{}", serde_json::to_string(&res).unwrap());
    let response = serde_json::from_value::<get_2_tweets_search_recent::Response>(res)?;
    assert_eq!(response.is_empty_extra(), true);
    Ok(())
}
```

# まとめ
普段仕事で使っているので何か問題があった場合すぐに対応可能になるのもうれしいです。
今後も自分で使ってみて便利な機能があったらどんどん追加していきます。


# おまけ、作り方
- リファレンスを読みます。
[GET /2/tweets/search/recent](https://developer.twitter.com/en/docs/twitter-api/tweets/search/api-reference/get-tweets-search-recent)

- yamlを書きます
```yaml:get_2_tweets_search_recent.yaml
---

url: https://api.twitter.com/2/tweets/search/recent
method: get
queries:
  - name: query
    type: string
    required: true
  - name: end_time
    type: date
  - name: expansions
    type: enum
    value: attachments.poll_ids, attachments.media_keys, author_id, edit_history_tweet_ids, entities.mentions.username, geo.place_id, in_reply_to_user_id, referenced_tweets.id, referenced_tweets.id.author_id
  - name: max_results
    type: integer
    max: 100
    min: 10
  - name: media.fields
    type: enum
  - name: next_token
    type: string
  - name: place.fields
    type: enum
  - name: poll.fields
    type: enum
  - name: since_id
    type: string
  - name: sort_order
    type: enum_single
    value: recency, relevancy
  - name: start_time
    type: date
  - name: tweet.fields
    type: enum
  - name: until_id
    type: string
  - name: user.fields
    type: enum
response:
  type: object
  properties:
    data:
      type: array
      items:
        type: object
        ref: tweets
    errors:
      type: array
      items:
        type: object
        ref: errors
    includes:
      type: object
      ref: includes
    meta:
      type: object
      ref: meta
```

- rubyとerbを使って生成します
```
bundle exec ruby make.rb
```

- テストを書きます
- 実行結果を見てyamlを修正してテストを成功させます。リファレンスは参考程度です。:innocent:

ちなみにライバルはマクロで作ってます。