@dynamicCallableのdynamicallyCallはasync/awaitでも使える

# 概要

タイトルそのままですが、Swiftの`@dynamicCallable`を使う上で実装が必要になる`dynamicallyCall`で、async/awaitが使えます。

## dynamicCallableについて

dynamicCallableはSwift 5で追加された機能です。
`@dynamicCallable`がマークされた型のインスタンスは、好きな名前で引数をつけて関数呼び出しが可能です。

`@dynamicCallable`がマークされた型は`dynamicallyCall(withArguments:)`もしくは`dynamicallyCall(withKeywordArguments:)` のfuncを実装します。

dynamicallyCallは、引数の名前によって動作させることができます。dynamicallyCallは好きな返り値を設定することができます。

```swift
@dynamicCallable
struct Foo {
    func dynamicallyCall(withKeywordArguments pairs: KeyValuePairs<String, String?>) -> String? {
        pairs.first.map {
            "\($0.key):\($0.value ?? "")"
        }
    }
}

let foo = Foo()
print(foo(bar: "hoge") ?? "") //bar:hoge
```

# 本題

## async/awaitを利用する
このdynamicCallableは好きな返り値を設定できると言いましたが、試してみたらasync/awaitも使えました。調べた限りドキュメント等に書かれているわけではないですが、インスタンス呼び出しの際のパラメータで非同期で呼び出せます。
非同期で呼び出すといえばWeb APIなので、Web APIの実行例としてQiita APIで試してみます。

## Web APIの仕様に合わせてdynamicCallableをasync/awaitで組み立てる(Qiita編)

まずは[Qiita API v2](https://qiita.com/api/v2/docs)の仕様について見ていきましょう。


> APIとの全ての通信にはHTTPSプロトコルを利用します。アクセス先のホストには、Qiitaのデータを利用する場合には qiita.com を利用し、Qiita Teamのデータを利用する場合には *.qiita.com を利用します (*には所属しているTeamのIDが入ります)。

とあるので、URLのscheme部分は"https"で固定ですが、hostに関してはQiitaとQiita Teamでは異なることがわかります。
なのでdynamicCallableに対応したクラスはhostをパラメータとして受け入れられる形にします。

こんな形で`Qiita`クラスを作成し、配下に`@dynamicCallable`付きの`Get`クラスを作ります。
そして引数の部分は、名前のない場合はpath、名前のある引数はqueryパラメータとしてURLを組み立てて、URLSessionでネットワーク通信を行うものを作成してみます。結果は`try await`で受け取ります。

```swift 
struct Qiita {
    internal init(host: String) {
        self.get = Get(host: host)
    }
    
    private static let scheme = "https"
    
    let get: Get

    @dynamicCallable
    struct Get {
        let host: String
        func dynamicallyCall(withKeywordArguments pairs: KeyValuePairs<String, String?>) async throws -> (Data, URLResponse) {
            var components = URLComponents()
            components.scheme = Qiita.scheme
            components.host = host
            components.path = (pairs.filter { $0.key.isEmpty }.first?.value)!
            components.queryItems = pairs.filter({ !$0.key.isEmpty}).map({
                URLQueryItem(name: $0.key, value: $0.value)
            })
            return try await URLSession.shared.data(from: components.url!)
        }
    }
}
```

## 使う
実際にQiitaクラスを使用してみます。
まずはタグの取得を行なってみます。
[GET /api/v2/tags](https://qiita.com/api/v2/docs#get-apiv2tags)の仕様を見て、

> page
ページ番号 (1から100まで)
Example: 1
Type: string
Pattern: /^[0-9]+$/
per_page
1ページあたりに含まれる要素数 (1から100まで)
Example: 20
Type: string
Pattern: `/^[0-9]+$/`
sort
並び順 (countで記事数順、nameで名前順)
Example: "count"
Type: string

とあるので

```swift
    let qiita = Qiita(host: "qiita.com")
    Task {
        let (data, _) = try await qiita.get("/api/v2/tags",
                                            page: "11",
                                            per_page: "3",
                                            sort: "count")
        print(String(data: data, encoding: .utf8)!) // レスポンスデータ
    }
```

こんな感じで`try await`を使って実際にQiita上のタグのデータを使うことができます。

記事の取得など、違うAPIを使いたい場合もパラメータを変えるだけで済みます。

```swift
    Task {
        let (data, _) = try await qiita.get("/api/v2/items",
                                            page: "1",
                                            per_page: "3",
                                            query: "qiita user:Qiita")
        print(String(data: data, encoding: .utf8)!) // レスポンスデータ
    }
```