VaporとMySQLでToDoAPIを作ってみた！

## はじめに

こんにちは。こんばんは。スマートテック・ベンチャーズ 10日目STVオタク担当の @nnsnodnb です。
今後サーバサイドも全部Swiftできるようになるために生まれてきたのではないかと思われている(少なくとも自分の中では)
サーバサイドSwiftについてまた触ってみました。Vaporで行こうと思います。
**Kitura+MySQL** が上手くできなかったいい感じにできた人いらしたら教えてくださいエロい人...

## 環境

- macOS Sierra 10.12.1 (個人所有のMacが修理中のため知人のMacを借りてます)
- Xcode 8.1
- Swift 3.0.1
- mysql  Ver 14.14 Distrib 5.7.16, for osx10.12 (x86_64) using  EditLine wrapper

## 準備

### MySQL準備

Homebrewでインストールを進めます。

```bash
$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
$ brew install mysql
$ sudo mysql.server start
```

あとはDB作成等

### Vapor準備

```bash
$ curl -sL check.vapor.sh | bash
✅  Compatible
$ curl -sL toolbox.vapor.sh | bash
✅  Compatible
Downloading...
Compiling...
Installing...
Vapor Toolbox v1.0.3 Installed
Use vapor --help and vapor <command> --help to learn more.
```

### プロジェクトファイル準備

```bash
$ vapor new vaporTodoApi
Cloning Template [Done]
```

![スクリーンショット 0028-12-04 午前3.55.04.png](https://qiita-image-store.s3.amazonaws.com/0/97280/9e0e09e2-878e-d081-d1c4-939fead3913f.png "スクリーンショット 0028-12-04 午前3.55.04.png")

```bash
$ tree .
.
├── Config
│   ├── app.json
│   ├── clients.json
│   ├── crypto.json
│   ├── droplet.json
│   ├── production
│   │   └── app.json
│   └── servers.json
├── Localization
│   ├── default.json
│   ├── en-US.json
│   ├── es-US.json
│   ├── nl-BE.json
│   └── nl-NL.json
├── Package.swift
├── Procfile
├── Public
│   ├── images
│   │   └── vapor-logo.png
│   └── styles
│       └── app.css
├── README.md
├── Resources
│   └── Views
│       ├── base.leaf
│       └── welcome.leaf
├── Sources
│   └── App
│       ├── Controllers
│       │   └── PostController.swift
│       ├── Models
│       │   └── Post.swift
│       └── main.swift
├── app.json
└── license
```

Vaporコマンドで実行するとここまで自動的にしてくれるのはいいですね！

ちなみにXcodeで作業したいときはnewしたあとに以下のようにやればできます。

```bash
$ vapor xcode -n
$ vapor xcode mysql -y
```

自動的にXcodeが開くはずです。

### MySQL接続設定

`Config/mysql.json` を作成して情報を付加する

```json:mysql.json
{
    "host": "127.0.0.1",
    "user": "root",
    "password": "",
    "database": "todo",
    "encoding": "utf8"
}
```

DATABASEは `todo` という名前で新規作成しておいてください。
ユーザ等は各自適当に変更お願いします:muscle:

### Swift Package Manager設定

```swift:Package.swift
import PackageDescription

let package = Package(
    name: "vaporTodoApi",
    dependencies: [
        .Package(url: "https://github.com/vapor/vapor.git", majorVersion: 1, minor: 1),
        .Package(url: "https://github.com/vapor/mysql-provider.git", majorVersion: 1, minor: 1)  // 追加
    ],
    exclude: [
        "Config",
        "Database",
        "Localization",
        "Public",
        "Resources",
        "Tests",
    ]
)
```

```bash
$ vapor build --mysql
Fetching Dependencies [Done]
Building Project [Done]
```

Xcodeで作業されている方は基本的に `Build & Run` で大丈夫かと思います。
もしダメだったら上のコマンドを叩いてください:bow:

## サンプルソースコード

```swift:Sources/App/main.swift
import Vapor
import VaporMySQL

let drop = Droplet(
  preparations: [Task.self],
  providers: [VaporMySQL.Provider.self]
)

drop.resource("tasks", TaskController())

drop.run()
```

もともとあるかと思われる `Post.swift` を変更するなりなんなり

```swift:Sources/App/Model/Task.swift
import Vapor
import Fluent
import Foundation

final class Task: Model {
    var id: Node?
    var title: String
    var rank: String

    init(title: String, rank: String) {
        self.title = title
        self.rank = rank
    }

    init(node: Node, in context: Context) throws {
        id = try node.extract("id")
        title = try node.extract("title")
        rank = try node.extract("rank")
    }

    func makeNode(context: Context) throws -> Node {
        return try Node(node: [
            "id": id,
            "title": title,
            "rank": rank
        ])
    }
}

extension Task {
    public convenience init?(from title: String, rank: String) throws {
        self.init(title: title, rank: rank)
    }
}

extension Task: Preparation {
    static func prepare(_ database: Database) throws {
      try database.create("tasks") { tasks in
          tasks.id()
          tasks.string("title")
          tasks.string("rank")
      }
    }

    static func revert(_ database: Database) throws {
        try database.delete("tasks")
    }
}
```

こちらも、もともとあるかと思われる `PostViewController.swift` を変更するなりなんなり

```swift:Sources/App/Model/TaskViewController.swift
import Vapor
import HTTP

final class TaskController: ResourceRepresentable {
    func index(request: Request) throws -> ResponseRepresentable {
        return try Task.all().makeNode().converted(to: JSON.self)
    }

    func create(request: Request) throws -> ResponseRepresentable {
        var task = try request.post()
        try task.save()
        return task
    }

    func show(request: Request, task: Task) throws -> ResponseRepresentable {
        return task
    }

    func delete(request: Request, task: Task) throws -> ResponseRepresentable {
        try task.delete()
        return JSON([:])
    }

    func clear(request: Request) throws -> ResponseRepresentable {
        try Task.query().delete()
        return JSON([])
    }

    func update(request: Request, item task: Task) throws -> ResponseRepresentable {
        let new = try request.post()
        var task = task
        task.title = new.title
        task.rank = new.rank
        try task.save()
        return task
    }

    func replace(request: Request, task: Task) throws -> ResponseRepresentable {
        try task.delete()
        return try create(request: request)
    }

    func makeResource() -> Resource<Task> {
        return Resource(
            index: index,
            store: create,
            show: show,
            replace: replace,
            modify: update,
            destroy: delete,
            clear: clear
        )
    }
}

extension Request {
    func post() throws -> Task {
        guard let json = json else {
            throw Abort.badRequest
        }
        return try Task(node: json)
    }
}
```

今回はDBのテーブル構成は `id` , `title` , `rank` というフラグのない謎構成です。

## ソースコードが書けたらとりあえず実行してみよか？

```bash
$ vapor build --mysql
$ vapor run
```

Xcodeの方はそのまま `Build & Run` で行けるかと思います。

Runが通れば自動的にDBにテーブルを作成してくれます。

## 404ページ

![スクリーンショット 2016-12-07 午前1.19.38.png](https://qiita-image-store.s3.amazonaws.com/0/97280/f1015b5e-e271-0226-9efb-2b07805243f9.png "スクリーンショット 2016-12-07 午前1.19.38.png")

404ページは始めからパッケージに同梱されているらしいです！感謝:pray: 

## HTTPメソッドアクセスについて

### GET

GETリクエストについては `index()` と `show()` が呼ばれます。

#### index()

```swift
func index(request: Request) throws -> ResponseRepresentable {
    return try Task.all().makeNode().converted(to: JSON.self)
}
```

見ての通り全てを取り出してJSON形式にしてレスポンスしてくれます。

![スクリーンショット 2016-12-07 午前1.24.44.png](https://qiita-image-store.s3.amazonaws.com/0/97280/799445ae-da46-dbfa-0cdd-ce6f24ca9af9.png "スクリーンショット 2016-12-07 午前1.24.44.png")

```bash
$ curl http://127.0.0.1:8080/tasks
```

#### show()

```swift
func show(request: Request, task: Task) throws -> ResponseRepresentable {
    return task
}
```

詳細情報取得みたいなやつです。

![スクリーンショット 2016-12-07 午前1.27.19.png](https://qiita-image-store.s3.amazonaws.com/0/97280/f6537827-f012-115f-636b-91b935a4b338.png "スクリーンショット 2016-12-07 午前1.27.19.png")

```bash
$ curl http://127.0.0.1:8080/tasks/id
```

`id` のところはデータのIDです。

### POST

POSTリクエストは `create()` が呼ばれます。

```swift
func create(request: Request) throws -> ResponseRepresentable {
    var task = try request.post()
    try task.save()
    return task
}
```

POSTで受け取ったデータをインサートしてるだけですね。Realmとか使ってる人なら見覚えがあるかと...あとはNoSQL使いの皆様とか

![スクリーンショット 2016-12-07 午前1.32.52.png](https://qiita-image-store.s3.amazonaws.com/0/97280/fad55029-4c4d-9c65-6aaa-ed2a715668ae.png "スクリーンショット 2016-12-07 午前1.32.52.png")

```bash
$ curl http://127.0.0.1:8080/tasks -X POST -d '{"title": "hoge", "rank": "中"}' -H "Content-Type: application/json" -H "Accept: application/json"
```

### PUT

PUTリクエストは `update()` が呼ばれます。

```swift
func update(request: Request, item task: Task) throws -> ResponseRepresentable {
    let new = try request.post()
    var task = task
    task.title = new.title
    task.rank = new.rank
    try task.save()
    return task
}
```

これについてはちょっと仕様がわからないんですが、古いデータを削除して新しくインサートしているような動作をしているようです。
もしかしたら私のやり方が間違っているのかもしれませんが...

![スクリーンショット 2016-12-07 午前1.38.29.png](https://qiita-image-store.s3.amazonaws.com/0/97280/c82c91b6-88e8-6a5c-fbd8-97e6b3c57858.png "スクリーンショット 2016-12-07 午前1.38.29.png")

```bash
$ curl http://127.0.0.1:8080/tasks/id -X PUT -d '{"title": "あっぷでーと", "rank": "低"}' -H "Content-Type: application/json" -H "Accept: application/json"
```

### DELETE

DELETEリクエストは `delete()` が呼ばれます。

```swift
func delete(request: Request, task: Task) throws -> ResponseRepresentable {
    try task.delete()
    return JSON([:])
}
```

Vaporの中身をみていないのでなんともわかりませんが、 `id` でfilterして削除しているのではないかと思います。

![スクリーンショット 2016-12-07 午前1.40.57.png](https://qiita-image-store.s3.amazonaws.com/0/97280/4ada01bd-07b6-c821-bd94-fa8fe031b05e.png "スクリーンショット 2016-12-07 午前1.40.57.png")

```bash
$ curl http://127.0.0.1:8080/tasks/id -X DELETE
```

### Clearについて

`clear()` なんですが、呼び方がわからないのでとりあえず放置しますが、(知ってる人教えてください)
呼ぶと全てのキーを削除してしまうので下みたいにならないようにしましょうねｗ

![syazai_kaiken.png](https://qiita-image-store.s3.amazonaws.com/0/97280/471c8673-2464-2f41-ca4a-0d5d9539e065.png "syazai_kaiken.png")

### 補足

```swift
func makeResource() -> Resource<Task> {
    return Resource(
        index: index,
        store: create,
        show: show,
        replace: replace,
        modify: update,
        destroy: delete,
        clear: clear
    )
}
```

`makeResource()` に書かれているようなキーの右側を任意のメソッド名に変更してあげると思い通りにしてくれます。

## 最後に

ざっと、デフォルトでつぎ込まれている機能のみ紹介しましたが、 `filter` なども実装されているとのことなのでGETパラメータでほげほげできそうですね。

https://vapor.github.io/documentation/fluent/query.html

また、今回使用したソースコードはGitHubに公開しています。
よかったら見てみてください！

https://github.com/nnsnodnb/vapor-todo-api

***

明日の11日目は、また私の担当になります。
モバイルバックエンドについての記事になります。主にサーバ周りの話をしていきます。
インフラもちょっと話したいです。お兄さんたちに怒られない程度に...