Kitura さわってみた

# はじめに

最初は Windows 用の Swift コンパイラのビルド方法について書いてみようかと思ったのですが、残念ながら使い物になるコンパイラができませんでした。そこでお題をかえて Swift で動く Web アプリケーションフレームワークの Kitura をちょっといじってみたいと思います。今思えば [Server Side Swift Advent Calendar 2017](https://qiita.com/advent-calendar/2017/server-side-swift) でやるべきネタでした。[@SatoTakeshiX](https://qiita.com/SatoTakeshiX) さんごめんなさい...

# Kitura

Swift で Web サービスをつくるフレームワークらしい。IBM 製。

# インストール

Homebrew に Kitura のリポジトリを追加してインストールします。


```
$ brew tap ibm-swift/kitura
$ brew install kitura
```

# Hello World

まず `npm init` みたくプロジェクトディレクトリを作ってから `kitura init` します。


```
$ mkdir HelloKitura
$ cd HelloKitura
$ kitura init
```

するとなにやら大量にファイルがインストール & ビルドされてプロジェクトディレクトリができあがります。IBM のクラウドサービスである Bluemix にデプロイするためっぽいファイルもありますね。

```
$ ls -a
.                             .swiftservergenerator-project README.md
..                            .yo-rc.json                   Sources
.bluemix                      Dockerfile                    Tests
.build                        Dockerfile-tools              chart
.cfignore                     HelloKitura.xcodeproj         cli-config.yml
.dockerignore                 LICENSE                       manifest.yml
.gitignore                    Package.resolved              spec.json
.swift-version                Package.swift
```

HelloKitura.xcodeproj を Xcode で開いて Source/Applicaiton/Application.swift の `postInit()` の末尾に GET に対するハンドラを書きます。

```swift
router.get("/") { request, response, next in
    response.send("Hello, World!")
    next()
}
```

<img width="1072" alt="スクリーンショット 2017-12-11 0.02.06.png" src="https://qiita-image-store.s3.amazonaws.com/0/110491/b635cb87-1ee4-fcaa-db30-8c2e8db4e4ae.png">

ターゲットが大量にありますが、 その中の HelloKitura を選んで実行します。
<img width="357" alt="スクリーンショット 2017-12-11 0.01.19.png" src="https://qiita-image-store.s3.amazonaws.com/0/110491/f9e431a5-4999-58fd-68f5-2fe09cc9ff8b.png">

すると localhost:8080 で HTTP サーバーが動くのでブラウザ http://localhost:8080/ をひらくと Hello, World! と表示されるはずです。簡単ですね！

# Memo サービス

要素として ID とテキストだけをもつメモを GET, POST, PUT, DELETE 出来る簡単な Web サービスを作ってみましょう。

## モデルの作成

まずはさっきと同じように KituraMemo ディレクトリを作り、その中で `kitura init` して Kitura のプロジェクトを作ってください。KituraMemo.xcodeproj を開き、Application.swift にメモを表す構造体を追加します。

```swift
public struct Memo: Codable {
    public var id: Int?
    public var text: String?
}
```

`Codable` に準拠していると Kitura が JSON へのシリアライゼーション/デシリアライゼーションを自動的にやってくれます。この `Codable` 対応が Kitura 2 の目玉のひとつだそうです。

## POST

新しいメモを投稿できるようにするため POST メソッドを実装しましょう。まず、`let couldEnv = CloudEnv()` の下に次を追加してください。

```swfit
private var memos = [Int: Memo]()
private var nextID = 0
```

`memos` は投稿されたメモを保持する辞書でキーはメモの ID です。`nextID` が次に投稿されるメモにつける ID で、新しいメモが投稿される度にインクリメントすることにします。次に POST のハンドラを Kitura に登録します。`postInit()` の最後に次を追加してください。

```swift
router.post("/memos") { (memo: Memo, respondWith: (Memo?, RequestError?) -> Void) in
     let id = self.nextID
     self.nextID += 1

     let new = Memo(id: id, text: memo.text)
     self.memos[id] = new

     respondWith(new, nil)
}
```

これで /memos に POST するとその JSON の内容がクロージャ引数の `memo` に格納されて呼び出されるので辞書に保存します。`respoendWith(_:_:)` を呼ぶとクライアントにレスポンスが返ります。

KituraMemo ターゲットを実行している状態で実際に curl で POST してみましょう。Swift エンジニアたるものパフォーマンスは常に気になります。ターミナルで飽くなき速度への想いをぶちまけましょう！

```sh
$ curl -s -X POST -H 'Content-Type:application/json' -d '{"text":"5000兆フロップス欲しい！"}' http://localhost:8080/memos | jq
{
  "id": 0,
  "text": "5000兆フロップス欲しい！"
}
```

jq は JSON 文字列を見やすく整形してくれるコマンドです。`respondWith(_:_:)` の第一引数で返したオブジェクトが返ってきています。

## GET

POST できたものの本当にこの想いがサーバーに伝わっているかちょっと怪しいです。次はサーバーが保持しているメモを取得する GET メソッドを実装してみましょう。先程と同じように `postInit()` に GET に対するハンドラを追加します。

```swift
router.get("/memos") { (respondWith: ([Memo]?, RequestError?) -> Void) in
    respondWith(self.memos.values.map({ $0 }), nil)
}
```

このように Memo の配列を返すだけす。curl で叩いてみましょう。次のようになれば成功です。Yes, we need more power!

```sh
$ curl -s -X POST -H 'Content-Type:application/json' -d '{"text":"欲しい！"}' http://localhost:8080/memos | jq
{
  "id": 1,
  "text": "欲しい！"
}
$ curl -s -X POST -H 'Content-Type:application/json' -d '{"text":"欲しい欲しい！"}' http://localhost:8080/memos | jq
{
  "id": 2,
  "text": "欲しい欲しい！"
}
$ curl -s -X GET http://localhost:8080/memos | jq
[
  {
    "id": 2,
    "text": "欲しい欲しい！"
  },
  {
    "id": 0,
    "text": "5000兆フロップス欲しい！"
  },
  {
    "id": 1,
    "text": "欲しい！"
  }
]
```

全てのメモを取得するだけではなく、ID を指定して特定のメモだけを取得できるようにもしてみましょう。それにはもう一つ GET ハンドラを追加します。

```swift
router.get("/memos") { (id: Int, respondWith: (Memo?, RequestError?) -> Void) in
    if let memo = self.memos[id] {
        respondWith(memo, nil)
    } else {
        respondWith(nil, .notFound)
    }
}
```

`http://localhost:8080/memos/1` というよに ID を指定してリクエストするとこちらのハンドラが呼ばれクロージャ引数の `id` に 1 が渡ってきます。ID に対応するメモが見つからない場合は第二引数に `.notFound` をつけて `respondWith(_:_:)` を呼び、クライアントに 404 を返します。他にも HTTP のエラーが一通り定義されています。

curl はこうなります。

```sh
$ curl -s -X GET http://localhost:8080/memos/0 | jq
{
  "id": 0,
  "text": "5000兆フロップス欲しい！"
}
```

欲しい！

## PUT

既存のメモを更新するため PUT ハンドラを実装します。クロージャ引数に ID と新しいメモの内容が渡ってきます。

```swift
router.put("/memos") { (id: Int, memo: Memo, respondWith: (Memo?, RequestError?) -> Void) in
    if self.memos[id] != nil {
        let modified = Memo(id: id, text: memo.text)
        self.memos[id] = modified
        respondWith(modified, nil)
    } else {
        respondWith(nil, .notFound)
    }
}
```

curl はこうなります。

```sh
$ curl -s -X GET http://localhost:8080/memos | jq
[
  {
    "id": 2,
    "text": "欲しい欲しい！"
  },
  {
    "id": 0,
    "text": "5000兆フロップス欲しい！"
  },
  {
    "id": 1,
    "text": "欲しい！"
  }
]
$ curl -s -X PUT -H 'Content-Type:application/json' -d '{"text":"5000兆円欲しい！"}' http://localhost:8080/memos/0 | jq
{
  "id": 0,
  "text": "5000兆円欲しい！"
}
$ curl -s -X GET http://localhost:8080/memos | jq
[
  {
    "id": 2,
    "text": "欲しい欲しい！"
  },
  {
    "id": 0,
    "text": "5000兆円欲しい！"
  },
  {
    "id": 1,
    "text": "欲しい！"
  }
]
```

そうだ、俺は金が欲しかったんだ！！

## DELETE

( ﾟдﾟ)ﾊｯ!

ハイパフォーマンスを追い求めていたはずなのにうっかりお金のため逮捕されるのは勘弁です。DELETE も実装して証拠隠滅を図りましょう...

```swift
router.delete("/memos") { (id: Int, respondWith: (RequestError?) -> Void) in
    if self.memos[id] != nil {
        self.memos.removeValue(forKey: id)
        respondWith(nil)
    } else {
        respondWith(.notFound)
    }
}
```

では実際に消してみましょう。

```sh
$ curl -s -X GET http://localhost:8080/memos | jq
[
  {
    "id": 2,
    "text": "欲しい欲しい！"
  },
  {
    "id": 0,
    "text": "5000兆円欲しい！"
  },
  {
    "id": 1,
    "text": "欲しい！"
  }
]
$ curl -X DELETE http://localhost:8080/memos/0
$ curl -s -X GET http://localhost:8080/memos | jq
[
  {
    "id": 2,
    "text": "欲しい欲しい！"
  },
  {
    "id": 1,
    "text": "欲しい！"
  }
]
```

消せました。
ﾔｯﾀﾈ...

出来上がりは [GitHub](https://github.com/yamoridon/KituraMemo) に置いておきます。ビルドするには最初にプロジェクトディレクトリで `swift build` してください。

# Docker で動かしてみる

そういえばプロジェクトディレクトリに Dockerfile があるのをきになりました。KituraMemo を Docker で動かしてみましょう。まずは [Docker for Mac](https://www.docker.com/docker-mac) をインストールします。.dmg ファイルをダウンロードしてアプリケーションフォルダに入れて起動するだけです。メニューバーの Docker メニューに Docker is running と表示されたら準備完了です。
<img width="292" alt="スクリーンショット 2017-12-13 22.17.35.png" src="https://qiita-image-store.s3.amazonaws.com/0/110491/34cb395d-dd19-2cd2-6617-730e0e066a3b.png">

## Docker 用にビルド

KituraMemo を Docker で動かすには Docker 用、つまり Linux 用にビルドし直さないといけません。プロジェクトファイルにあった Dockerfile は Kitura アプリの実行用の Docker イメージを作るためのものでビルドは出来ないようです。そこでビルド用の Docker イメージをダウンロードします。

```sh
docker pull ibmcom/swift-ubuntu:4.0
```

次に Docker イメージ内にプロジェクトディレクトリをマウントしつつ、起動します。

```sh
docker run -i -t -v (KituraMemo プロジェクトディレクトリのフルパス):/root/KituraMemo ibmcom/swift-ubuntu:4.0 /bin/bash
```

成功するとプロンプトが `root@176e6bf9af3d:~#` というようになり、Docker イメージ内でシェルを実行している状態になります。そうしたらプロジェクトをビルドします。

```
# cd ~/KituraMemo
# swift build --build-path ./.build-ubuntu --configuration release
```

`~/KituraMemo/.build-ubuntu/release/KituraMemo` ができれば成功です。`exit` でビルド用の Docker イメージから抜けてください。


### Docker 上で実行

もちろんビルドに使ったイメージexで上で実行することもできるのですが、せっかくなのでプロジェクトディレクトリ内の Dockerfile で作ったイメージ上で実行してみましょう。プロジェクトディレクトリで次のコマンドを実行します。(注: ビルド用の Docker イメージ内ではなく、Mac のシェル上での操作です。)

```sh
$ docker build -t kitura_memo .
Sending build context to Docker daemon  322.7MB
(略)
$ docker image ls
REPOSITORY                    TAG                 IMAGE ID            CREATED             SIZE
kitura_memo                   latest              052e942a5439        9 seconds ago       605MB
ibmcom/swift-ubuntu-runtime   4.0                 7a4cf16d6bdd        4 weeks ago         293MB
ibmcom/swift-ubuntu           4.0                 c17f1e0377a2        4 weeks ago         1.36GB
```

イメージができたら起動します。-p オプションで localhost の 8080 ポートを Docker イメージの 8080 に転送するようにしておきます。

```sh
$ docker run -d -p 8080:8080 kitura_memo:latest
```

この状態で curl で叩いてみてレスポンスが返ってくれば成功です！

```sh
$ curl -s -X POST -H 'Content-Type:application/json' -d '{"text":"5000兆円欲しい！"}' http://localhost:8080/memos | jq
{
  "text": "5000兆円欲しい！",
  "id": 0
}
```

# まとめ

駆け足になりましたが、Kitura をつかって簡単な POST/PUT/GET/DELETE ができる Web サービスを作り、Docker 上で動かしてみました。やりたいと思いつつ時間切れになってしまったのですが、Bluemix で動かすのも試してみたいですね。