@dynamicMemberLookup と @dynamicCallable でWeb APIアクセス

# 概要

Swiftの機能である `@dynamicMemberLookup` と `@dynamicCallable` を使って、いい感じにWeb APIにアクセスしてみます。
試しにQiitaのAPIにアクセスしようと思います。

#`@dynamicCallable`でパラメータ作成

## GET 

GETアクセスをするためには、queryパラメータを設定する必要があります。
URLComponentsを作成してqueryItemsにURLQueryItemを設定することでリクエストするためのURLが作成できます。

### URLQueryItemを `@dynamicCallable` で作成する
以下のような形で`URLQueryItemsGenerator` を定義してみました。

```swift
@dynamicCallable
struct URLQueryItemsGenerator {
    func dynamicallyCall(withKeywordArguments pairs: KeyValuePairs<String, String?>) -> [URLQueryItem] {
        pairs.map { (key, value) -> URLQueryItem in
            URLQueryItem(name: key, value: value)
        }
    }
}

```

### GET URLの作成
このURLQueryItemsGeneratorを作成し、`（1）`
作成したジェネレーターにAPIドキュメント通りにパラメータを与えると、`(2)`
URLQueryItemが簡単に作れ、URLにqueryパラメータを与えることができます。

```swift
var components = URLComponents()
components.scheme = "https"
components.host = "qiita.com"
components.path = "/api/v2/items"
let generator = URLQueryItemsGenerator() // (1)
components.queryItems = generator(page: "2",
                                  per_page: "10",
                                  query: "SwiftUI") // (2)
```

```
https://qiita.com/api/v2/items?page=2&per_page=10&query=SwiftUI
```

## POST 

QiitaのPOST APIはほとんど `Content-Type: application/json` なので、POST用のbodyのJSON Dataを生成するジェネレータを作ります。

### JSON Dataを `@dynamicCallable` で作成する
以下のような形でdynamicCallableを定義してみました。
Dictionaryを作成する`DictionaryGenerator`とJSONのデータを作成する`JSONDataGenerator`です。


```swift
@dynamicCallable
struct DictionaryGenerator {
    func dynamicallyCall(withKeywordArguments pairs: KeyValuePairs<String, Any?>) -> [String:Any] {
        var dictionary:[String:Any] = [:]
        pairs.forEach { (key, value) in
            dictionary[key] = value
        }
        return dictionary
    }
}

@dynamicCallable
struct JSONDataGenerator {
    func dynamicallyCall(withKeywordArguments pairs: KeyValuePairs<String, Any?>) -> Data? {
        let generator = DictionaryGenerator()
        let dictionary = generator.dynamicallyCall(withKeywordArguments: pairs)
        guard JSONSerialization.isValidJSONObject(dictionary) else {
            return nil
        }
        return try! JSONSerialization.data(withJSONObject: dictionary, options: [])
    }
}
```


### POST Requestの作成
`JSONDataGenerator`を作成し、作成したジェネレーターにAPIドキュメント通りにパラメータを与えると、POST Request用のJSON Dataが作成できます。
/api/v2/itemsのtagsパラメータにはオプジェクトの配列が必要なので、`DictionaryGenerator`も使ってhttpBodyにJSON Dataを設定しています。


```swift
var components = URLComponents()
components.scheme = "https"
components.host = "qiita.com"
components.path = "/api/v2/items"
var request = URLRequest(url: components.url!)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
let jsonDataGenerator = JSONDataGenerator()
let dictionaryDataGenerator = DictionaryGenerator()

request.httpBody = jsonDataGenerator(body:"# Example",
                                     private:"false",
                                     tags:[
                                        dictionaryDataGenerator(name:"Ruby",
                                                                versions:["0.0.1"])
                                     ],
                                     title: "Example title",
                                     tweet: false)
```

```
{
  "body": "# Example",
  "private": false,
  "tags": [
    {
      "name": "Ruby",
      "versions": [
        "0.0.1"
      ]
    }
  ],
  "title": "Example title",
  "tweet": false
}
```

#`@dynamicMemberLookup`でJSON受け取り
リクエストが作成できたので、リクエストをAPIに送信してJSONレスポンスを受け取ります。

## JSON dynamicMemberLookup
だいぶ長いですが、以下のように`@dynamicMemberLookup`を付与したJSON解析用のenumを用意します。

```swift
@dynamicMemberLookup 
enum JSON {
    case dictionaryValue(Dictionary<String, JSON>)
    case arrayValue(Array<JSON>)
    case numberValue(NSNumber)
    case stringValue(String)
    case boolValue(Bool)
    case nullValue
    
    var objectValue: Dictionary<String, JSON>? {
        if case .dictionaryValue(let dictionary) = self {
            return dictionary
        }
        return nil
    }
    
    var arrayValue: Array<JSON>? {
        if case .arrayValue(let array) = self {
            return array
        }
        return nil
    }
    
    var stringValue: String? {
        if case .stringValue(let str) = self {
            return str
        }
        return nil
    }
    
    var numberValue: NSNumber? {
        if case .numberValue(let number) = self {
            return number
        } else if case .boolValue(let b) = self {
            return NSNumber(value: b)
        }
        return nil
    }
    
    var boolValue: Bool? {
        if case .boolValue(let bool) = self {
            return bool
        }
        return nil
    }
    
    var nullValue: NSNull? {
        if case .nullValue = self {
            return NSNull()
        }
        return nil
    }
    
    subscript(index: Int) -> JSON? {
        if case .arrayValue(let array) = self {
            return index < array.count ? array[index] : nil
        }
        return nil
    }
    
    subscript(dynamicMember member: String) -> JSON? {
        if case .dictionaryValue(let dict) = self {
            return dict[member]
        }
        return nil
    }
    
    private init(_ object: Any) {
        switch object {
        case let boolValue as Bool: self = .boolValue(boolValue)
        case let numberValue as NSNumber: self = .numberValue(numberValue)
        case let stringValue as String: self = .stringValue(stringValue)
        case let dictionaryValue as Dictionary<String, Any>: self = JSON.dictionaryValue(dictionaryValue.mapValues{ JSON($0) })
        case let arrayValue as Array<Any>: self = .arrayValue(arrayValue.map { JSON($0)} )
        default: self = .nullValue
        }
    }
    
    init(data: Data) throws {
        let jsonObject = try JSONSerialization.jsonObject(with: data, options: [])
        self = .init(jsonObject)
    }
}
```

### APIにアクセスして記事を取得
先ほどのURLQueryItemsGeneratorも組み合わせてAPIにアクセスして記事のタイトルを取得します。
以下のように、indexやドットを利用して、先頭の記事タイトルが取れます。

```swift
var components = URLComponents()
components.scheme = "https"
components.host = "qiita.com"
components.path = "/api/v2/items"
let generator = URLQueryItemsGenerator()
components.queryItems = generator(page: "2",
                                  per_page: "10",
                                  query: "SwiftUI")
let task = URLSession.shared.dataTask(with: components.url!) { (data, response, error) in
    let json = try! JSON(data: data!)
    print(String(describing: json[0]?.title?.stringValue)) // 先頭の記事のタイトルが取れる
}
task.resume()
```

#なにがいいのか
API処理でクライアントを用意したり、Codableなどでレスポンスの定義をきっちりと行って処理をすることも多いんじゃないかと思いますが、`@dynamicMemberLookup`や`@dynamicCallable`を使うと、その辺りのコードが全て削れます。

外部のAPIと連携するようなアプリの場合、API側の仕様変更が行われたら、APIクライアントやCodableのメンテナンスが必要になりますが、`@dynamicMemberLookup`や`@dynamicCallable`を使うと、リクエスト送信処理やレスポンス受信処理だけAPIのドキュメントとにらめっこしてドットアクセスやパラメータ作成をするだけですむので、そこが気に入ってます。

#おまけ
今回のコードをまとめたものをSwift Package Managerとして提供しています。是非使ってください。スターください。
https://github.com/coe/Dynamics