はじめに
最初は Windows 用の Swift コンパイラのビルド方法について書いてみようかと思ったのですが、残念ながら使い物になるコンパイラができませんでした。そこでお題をかえて Swift で動く Web アプリケーションフレームワークの Kitura をちょっといじってみたいと思います。今思えば Server Side Swift Advent Calendar 2017 でやるべきネタでした。@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 に対するハンドラを書きます。
router.get("/") { request, response, next in
response.send("Hello, World!")
next()
}
ターゲットが大量にありますが、 その中の HelloKitura を選んで実行します。
すると localhost:8080 で HTTP サーバーが動くのでブラウザ http://localhost:8080/ をひらくと Hello, World! と表示されるはずです。簡単ですね!
Memo サービス
要素として ID とテキストだけをもつメモを GET, POST, PUT, DELETE 出来る簡単な Web サービスを作ってみましょう。
モデルの作成
まずはさっきと同じように KituraMemo ディレクトリを作り、その中で kitura init
して Kitura のプロジェクトを作ってください。KituraMemo.xcodeproj を開き、Application.swift にメモを表す構造体を追加します。
public struct Memo: Codable {
public var id: Int?
public var text: String?
}
Codable
に準拠していると Kitura が JSON へのシリアライゼーション/デシリアライゼーションを自動的にやってくれます。この Codable
対応が Kitura 2 の目玉のひとつだそうです。
POST
新しいメモを投稿できるようにするため POST メソッドを実装しましょう。まず、let couldEnv = CloudEnv()
の下に次を追加してください。
private var memos = [Int: Memo]()
private var nextID = 0
memos
は投稿されたメモを保持する辞書でキーはメモの ID です。nextID
が次に投稿されるメモにつける ID で、新しいメモが投稿される度にインクリメントすることにします。次に POST のハンドラを Kitura に登録します。postInit()
の最後に次を追加してください。
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 エンジニアたるものパフォーマンスは常に気になります。ターミナルで飽くなき速度への想いをぶちまけましょう!
$ 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 に対するハンドラを追加します。
router.get("/memos") { (respondWith: ([Memo]?, RequestError?) -> Void) in
respondWith(self.memos.values.map({ $0 }), nil)
}
このように Memo の配列を返すだけす。curl で叩いてみましょう。次のようになれば成功です。Yes, we need more power!
$ 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 ハンドラを追加します。
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 はこうなります。
$ curl -s -X GET http://localhost:8080/memos/0 | jq
{
"id": 0,
"text": "5000兆フロップス欲しい!"
}
欲しい!
PUT
既存のメモを更新するため PUT ハンドラを実装します。クロージャ引数に ID と新しいメモの内容が渡ってきます。
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 はこうなります。
$ 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 も実装して証拠隠滅を図りましょう...
router.delete("/memos") { (id: Int, respondWith: (RequestError?) -> Void) in
if self.memos[id] != nil {
self.memos.removeValue(forKey: id)
respondWith(nil)
} else {
respondWith(.notFound)
}
}
では実際に消してみましょう。
$ 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 に置いておきます。ビルドするには最初にプロジェクトディレクトリで swift build
してください。
Docker で動かしてみる
そういえばプロジェクトディレクトリに Dockerfile があるのをきになりました。KituraMemo を Docker で動かしてみましょう。まずは Docker for Mac をインストールします。.dmg ファイルをダウンロードしてアプリケーションフォルダに入れて起動するだけです。メニューバーの Docker メニューに Docker is running と表示されたら準備完了です。
Docker 用にビルド
KituraMemo を Docker で動かすには Docker 用、つまり Linux 用にビルドし直さないといけません。プロジェクトファイルにあった Dockerfile は Kitura アプリの実行用の Docker イメージを作るためのものでビルドは出来ないようです。そこでビルド用の Docker イメージをダウンロードします。
docker pull ibmcom/swift-ubuntu:4.0
次に Docker イメージ内にプロジェクトディレクトリをマウントしつつ、起動します。
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 のシェル上での操作です。)
$ 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 に転送するようにしておきます。
$ docker run -d -p 8080:8080 kitura_memo:latest
この状態で curl で叩いてみてレスポンスが返ってくれば成功です!
$ 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 で動かすのも試してみたいですね。