mike-neckのブログ

Java or Groovy or Swift or Golang

Amazon 製 Server-side Swift フレームワーク smoke-framework について

f:id:mike_neck:20180609045321p:plain

これは Qiita のアドベントカレンダー 2018 の初日のエントリーです。

特に Swift コミュニティに何の貢献もしていないのですが、空いてたのでついポチッと登録してしまいました。

qiita.com


2018 年の 10 月 4 日に Prime Video のエンジニアの Simon さんのツイートが僕のTLに流れてきました。

Amazon が Swift 製の軽量サーバーサイドフレームワーク smoke-framework を作ったとのこと。

早速ためしてみたかったのですが、閃の軌跡というゲームをやるために忙しくて、ずっと試せていませんでした。 ほぼ2ヶ月経過して日本語の紹介記事等がなさそうなので、紹介してみることにしました。


smoke-framework は Swift-NIO の上に作られた軽量 Web フレームワークです。 JSON でのリクエスト-レスポンスの機能だけを提供しているだけのシンプルなものとなっています。

github.com


ここでは例として POST されたメッセージをそのまま返す Echo HTTP アプリケーションを作ってみます。

インストール

Swift Package Manager を使います。 swift package init でプロジェクトを初期化します。

mkdir ExampleResponseApp
cd ExampleResponseApp
swift package init --type executable

この後に、 Package.swift が次の通りになるように dependencies/targets/products に追記します。

// swift-tools-version:4.2

import PackageDescription

let package = Package(
    name: "ExampleResponseApp",
    products: [
        .executable(name: "ExampleResponseApp", targets: ["ExampleResponseApp"]),
    ],
    dependencies: [
        .package(url: "https://github.com/amzn/smoke-framework.git", .upToNextMajor(from: "0.6.0")),
    ],
    targets: [
        .target(name: "ExampleResponseApp", dependencies: ["SmokeOperationsHTTP1"]),
        .testTarget(name: "ExampleResponseAppTests", dependencies: ["ExampleResponseApp"]),
    ]
)

アプリケーション

このフレームワークでアプリケーションを作る場合、以下の3ステップで作れます。

  1. オペレーション関数を記述する
  2. パスとオペレーション関数を結びつけたハンドラーセレクターを作る
  3. ハンドラーセレクターをサーバーにわたす

以下のプログラムは次のモジュールをインポートしておきます。なお、 LoggerAPI は smoke-framework が依存している IBM-Swift の LoggerAPI です。

import SmokeHTTP1
import SmokeOperationsHTTP1
import SmokeOperations
import NIOHTTP1
import LoggerAPI

オペレーション関数

オペレーション関数として次の型を満たす関数を作ります

(InputType, ContextType) throws -> OutputType

オペレーション関数に挙げられている型は特定のプロトコルを満たしている必要があります。README には書かれていますがチュートリアルの部分には書いていないため、最初戸惑いました。

ContextType

ContextType はアプリケーションを実行する際の様々な情報を持つデータで、ユーザーは任意の型を作れます。

とりあえずは、このような struct を作ってみます

struct MyAppContext {}
InputTypeOutputType

次に、 InputTypeOutputType ですが、こちらは ValidatableCodable プロトコルを満たす必要があります。 ValidatableCodable プロトコルの定義は次のようになっています。

public typealias ValidatableCodable = Validatable & Codable

public protocol Validatable {
  func validate() throws
}

これを満たすように InputTypeOutputType を作ります。なお、ここにある ErrorResponse 型については次で述べます。

struct Message {
  let text: String
}

extension Message: ValidatableCodable {
  func validate() throws {
    if self.text.isEmpty {
      throw ErrorResponse.badRequest
    }
  }
}
エラーの型

投げるエラーの型は ErrorIdentifiableByDescription プロトコルを満たす必要があります。これの定義は次のとおりです。

typealias ErrorIdentifiableByDescription = Swift.Error & CustomStringConvertible

これを満たすように ErrorResponse を書いてみます。

enum ErrorResponse {
  case badRequest
}

extension ErrorResponse: ErrorIdentifiableByDescription {
  var description: String {
    return "bad request"
  }

  var statusCode: Int {
    return 400
  }
}
オペレーション関数を組み立て

上記の条件を満たした後、オペレーション関数を組み立てます。届いたメッセージをそのまま返す単純な関数です。

func echo(message: Message, context: MyAppContext) throws -> Message {
  Log.info("message: \(message.text)")
  return message
}

ハンドラーセレクターを作る

作成したオペレーション関数とパスのマッピングを作ります。まずは次のような型エイリアスをつくります。

public typealias HandlerSelector =
    StandardSmokeHTTP1HandlerSelector<MyAppContext, JSONPayloadHTTP1OperationDelegate>

StandardSmokeHTTP1HandlerSelector には addHandlerForUri(_ uri: httpMethod: operation: allowedErrors: operationDelegate:) という関数が生えているので、この関数にパスとオペレーション関数を渡します。

func handlerSelector() -> HandlerSelector {
  var selector = HandlerSelector()
  selector.addHandlerForUri(
      "/example",
      httpMethod: .POST,
      operation: echo,
      allowedErrors: [(ErrorResponse.badRequest, ErrorResponse.badRequest.statusCode)],
      operationDelegate: nil)
  return selector
}

アプリケーション組み立て

アプリケーションを組み立てます。先程準備したコンテキストオブジェクトとハンドラーセレクターを SmokeHTTP1Server の class 関数 startAsOperationServer に渡します。

do {
  try SmokeHTTP1Server.startAsOperationServer(
      withHandlerSelector: handlerSelector(),
      andContext: MyAppContext(),
      defaultOperationDelegate: JSONPayloadHTTP1OperationDelegate())
} catch {
  Log.error("failed to start server: \(error)")
}

これをそのまま実行してもよいのですが、デフォルトの LoggerAPI はログを出力しません。そこで、次のような Logger をつくって、 Logger に登録します。

struct StandardOutLogger: Logger {
  func log(_ type: LoggerMessageType, msg: String, functionName: String, lineNum: Int, fileName: String) {
    if isLogging(type) {
      print("\(type) (\(fileName) - \(functionName) -\(lineNum)) - \(msg)")
    }
  }
  func isLogging(_ level: LoggerMessageType) -> Bool {
    switch level {
    case .info, .error, .warning: return true
    default: return false
    }
  }
}

アプリケーションを実行

これでアプリケーションの完成です。次のコマンドでアプリケーションを実行してみます。

swift run ExampleResponseApp

次のようなログが出てきて、ポート 8080 でサーバーが起動していることがわかります。ポートの選択の仕方が Java っぽいです。

INFO (/Users/user/ExampleResponseApp/.build/checkouts/smoke-framework.git-5627467287507921261/Sources/SmokeOperationsHTTP1/SmokeHTTP1Server+startAsOperationServer.swift - startAsOperationServer(withHandlerSelector:andContext:defaultOperationDelegate:andPort:invocationStrategy:) -49) - Server starting on port 8080...
INFO (/Users/user/ExampleResponseApp/.build/checkouts/smoke-framework.git-5627467287507921261/Sources/SmokeOperationsHTTP1/SmokeHTTP1Server+startAsOperationServer.swift - startAsOperationServer(withHandlerSelector:andContext:defaultOperationDelegate:andPort:invocationStrategy:) -53) - Server started on port 8080...

curl で動作確認してみます。このようなレスポンスが返ってくるのではないかと思います。

$ curl -i http://localhost:8080/example -d '{"text":"hello, smoke-framework!"}'
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 40

{
  "text" : "hello, smoke-framework!"
}

比較的簡単に Swift で web アプリケーションを書けるので、普段は iOS を触っている方も、モックの HTTP サーバーを書くのに使ってみてはいかがでしょうか?


smoke-frmaework シリーズ

現在 GitHub の amzn のレポジトリーに smoke を冠したレポジトリーは smoke-framework を含めて 4 つほどあります。

  • smoke-framework
  • smoke-http
    • Swift-NIO ベースの http client
  • smoke-aws
    • smoke-http ベースの aws sdk
  • smoke-aws-credentials
    • ECS で使うことが前提っぽい短時間の aws クレデンシャル
  • smoke-dynamoDB
    • Swift で dynamoDB を扱いやすくするためのライブラリー

これらのレポジトリーが 10 月中旬ころからできており、また smoke-aws のコミットログを見るとモデルクラスの生成を aws-sdk-go から行っているようです。割とすごい速さで充実していってるような印象を持ちました。


というわけで、 Amazon 製の smoke-framework の紹介でした。

明日は @fumiyasac さんが UIについて書いてくれるそうです。






余談話

2018/11/30 に開催された 中央線Meetup 用にデモとして、 Azure で smoke-framework アプリケーションをビルドしてみたのですが、このようなログが出てきました。

2018-11-25T12:30:56.2632921Z ==============================================================================
2018-11-25T12:30:56.2633017Z Task         : Command Line
2018-11-25T12:30:56.2633076Z Description  : Run a command line script using cmd.exe on Windows and bash on macOS and Linux.
2018-11-25T12:30:56.2633167Z Version      : 2.142.2
2018-11-25T12:30:56.2633216Z Author       : Microsoft Corporation
2018-11-25T12:30:56.2633314Z Help         : [More Information](https://go.microsoft.com/fwlink/?LinkID=613735)
2018-11-25T12:30:56.2633377Z ==============================================================================
2018-11-25T12:30:57.1610232Z Generating script.
2018-11-25T12:30:57.1634662Z Script contents:
2018-11-25T12:30:57.1635377Z swift build --configuration release --product ExampleResponseApp
2018-11-25T12:30:57.1666791Z [command]/bin/bash --noprofile --norc /__w/_temp/5c9a12a6-7144-410f-b5ff-96bb137424b9.sh
2018-11-25T12:30:57.5146866Z Fetching https://github.com/apple/swift-nio.git
2018-11-25T12:30:57.5159040Z Fetching https://github.com/IBM-Swift/LoggerAPI.git
2018-11-25T12:30:57.5173837Z Fetching https://github.com/apple/swift-nio-zlib-support.git
2018-11-25T12:30:58.5142300Z Fetching https://github.com/amzn/smoke-framework.git
2018-11-25T12:31:03.6976063Z Completed resolution in 6.19s

おわかりいただけただろうか一つのログの中に

の巨大な4つの会社の名前が出ているのである。