コンテンツにスキップ

コンテンツ

Vapor のコンテンツ API を使用すると、Codable な構造体を HTTP メッセージに対して簡単にエンコード・デコードできます。標準では JSON 形式でエンコードされており、URL-Encoded FormMultipart についても即座に使えるサポートがあります。この API はカスタマイズ可能であり、特定の HTTP コンテンツタイプに対するエンコーディングの戦略を追加したり、変更したり、置き換えたりすることができます。

概要

Vapor のコンテンツ API の仕組みを理解するためには、HTTP メッセージの基本について知っておく必要があります。以下にリクエストの例を示します。

POST /greeting HTTP/1.1
content-type: application/json
content-length: 18

{"hello": "world"}

このリクエストは、content-type ヘッダーを通じて application/json メディアタイプでJSON形式のデータが含まれていることを示しています。ヘッダーの後のボディ部分には、約束されたJSONデータが続きます。

コンテンツ構造体

この HTTP メッセージをデコードする最初のステップは、期待される構造にマッチする Codable 型を作成することです。

struct Greeting: Content {
    var hello: String
}

型を Content に準拠させると、コンテンツ API を扱うための追加ユーティリティが得られると同時に、Codable にも自動的に準拠するようになります。

コンテンツの構造ができたら、req.content を使ってリクエストからデコードできます。

app.post("greeting") { req in
    let greeting = try req.content.decode(Greeting.self)
    print(greeting.hello) // "world"
    return HTTPStatus.ok
}

このデコードメソッドは、リクエストのコンテンツタイプに基づいて適切なデコーダーを見つけます。デコーダーが見つからない、またはリクエストにコンテンツタイプヘッダーが含まれていない場合、415 エラーが発生します。

つまり、このルートは URL エンコードされたフォームなど、他のサポートされているコンテンツタイプも自動的に受け入れるということです。

POST /greeting HTTP/1.1
content-type: application/x-www-form-urlencoded
content-length: 11

hello=world

ファイルアップロードのケースでは、コンテンツのプロパティは Data 型である必要があります。

struct Profile: Content {
    var name: String
    var email: String
    var image: Data
}

サポートされるメディアタイプ

以下は、コンテンツ API がデフォルトでサポートしているメディアタイプです。

name header value media type
JSON application/json .json
Multipart multipart/form-data .formData
URL-Encoded Form application/x-www-form-urlencoded .urlEncodedForm
Plaintext text/plain .plainText
HTML text/html .html

すべてのメディアタイプが全ての Codable 機能をサポートしているわけではありません。例えば、JSON はトップレベルのフラグメントをサポートしておらず、プレーンテキストはネストされたデータをサポートしていません。

クエリ

Vapor のコンテンツ API は、URL のクエリ文字列にエンコードされたデータの処理に対応しています。

デコード

URL クエリ文字列をデコードする方法を理解するために、以下の例をご覧ください。

GET /hello?name=Vapor HTTP/1.1
content-length: 0

HTTP メッセージのボディ内容を扱う API と同様に、URL クエリ文字列を解析する最初のステップは、期待される構造にあった struct を作成することです。

struct Hello: Content {
    var name: String?
}

name がオプションな String であることに注意して下さい。URL クエリ文字列は常にオプショナルであるべきだからです。パラメーターを必須にしたい場合は、ルートパラメーターを使って下さい。

期待されるクエリ文字列に合わせた Content 構造体が用意できたら、それをデコードできます。

app.get("hello") { req -> String in
    let hello = try req.query.decode(Hello.self)
    return "Hello, \(hello.name ?? "Anonymous")"
}

上記の例のリクエストに基づいて、このルートは次のような応答を返します:

HTTP/1.1 200 OK
content-length: 12

Hello, Vapor

例えば、次のリクエストのようにクエリ文字列が省略された場合は、"Anonymous" が使われます。

GET /hello HTTP/1.1
content-length: 0

単一の値

Content 構造体へのデコードだけでなく、Vapor はクエリ文字列から単一の値を取得することもサポートしています。これはサブスクリプトを使用して行われます。

let name: String? = req.query["name"]

フック

Vapor は、Content タイプに対して beforeEncode および afterDecode を自動的に呼び出します。デフォルトの実装は何もしませんが、これらのメソッドを使用してカスタムロジックを実行することができます。

// この Content がデコードされた後に実行されます。`mutating` は構造体のみに必要で、クラスには必要ありません。
mutating func afterDecode() throws {
    // 名前は渡されないことがありますが、渡される場合は空文字列であってはなりません。
    self.name = self.name?.trimmingCharacters(in: .whitespacesAndNewlines)
    if let name = self.name, name.isEmpty {
        throw Abort(.badRequest, reason: "Name must not be empty.")
    }
}

// この Contents がエンコードされる前に実行されます。`mutating` は構造体のみに必要で、クラスには必要ありません。
mutating func beforeEncode() throws {
    // 名前は渡されないことがありますが、渡される場合は空文字列であってはなりません。
    guard
        let name = self.name?.trimmingCharacters(in: .whitespacesAndNewlines),
        !name.isEmpty
    else {
        throw Abort(.badRequest, reason: "Name must not be empty.")
    }
    self.name = name
}

デフォルトの上書き

Vapor の Content API によって使用されるデフォルトのエンコーダーとデコーダーは設定可能です。

グローバル

ContentConfiguration.global を使用すると、Vapor がデフォルトで使用するエンコーダーやデコーダーを変更できます。これは、アプリケーション全体でデータの解析やシリアライズ方法を変更するのに便利です。

// UNIX タイムスタンプの日付を使用する新しい JSON エンコーダーを作成します。
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .secondsSince1970

// `.json` メディアタイプで使用されるグローバルエンコーダーを上書きします。
ContentConfiguration.global.use(encoder: encoder, for: .json)

ContentConfiguration の変更は通常、configure.swift で行われます。

1回限り

req.content.decode のようなエンコーディングやデコーディングのメソッド呼び出しは、1回限りの使用のためにカスタムコーダーを渡すことをサポートしています。

// UNIX タイムスタンプの日付を使用する新しい JSON デコーダーを作成します。
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970

// カスタムデコーダーを使用して Hello 構造体をデコードします。
let hello = try req.content.decode(Hello.self, using: decoder)

カスタムコーダー

アプリケーションやサードパーティのパッケージは、Vapor がデフォルトでサポートしていないメディアタイプに対応するためにカスタムコーダーを作成することができます。

Content

Vapor は、HTTP メッセージボディのコンテンツを処理するためのコーダーのために、ContentDecoderContentEncoder の2つのプロトコルを指定しています。

public protocol ContentEncoder {
    func encode<E>(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders) throws
        where E: Encodable
}

public protocol ContentDecoder {
    func decode<D>(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders) throws -> D
        where D: Decodable
}

これらのプロトコルに準拠することで、カスタムコーダーを上記で指定されたように ContentConfiguration に登録できます。

URL クエリ

Vapor は、URL クエリ文字列のコンテンツを処理することができる coder のための 2 つのプロトコルを指定しています: URLQueryDecoderURLQueryEncoder

public protocol URLQueryDecoder {
    func decode<D>(_ decodable: D.Type, from url: URI) throws -> D
        where D: Decodable
}

public protocol URLQueryEncoder {
    func encode<E>(_ encodable: E, to url: inout URI) throws
        where E: Encodable
}

これらのプロトコルに準拠することで、use(urlEncoder:) および use(urlDecoder:) メソッドを使用して、URL クエリ文字列の処理のためにカスタムコーダーを ContentConfiguration に登録できます。

カスタム ResponseEncodable

別のアプローチには、タイプに ResponseEncodable を実装するというものがあります。この単純な HTML ラッパータイプを考えてみてください。

struct HTML {
  let value: String
}

その ResponseEncodable の実装は以下のようになります。

extension HTML: ResponseEncodable {
  public func encodeResponse(for request: Request) -> EventLoopFuture<Response> {
    var headers = HTTPHeaders()
    headers.add(name: .contentType, value: "text/html")
    return request.eventLoop.makeSucceededFuture(.init(
      status: .ok, headers: headers, body: .init(string: value)
    ))
  }
}

async/await を使用している場合は、AsyncResponseEncodable を使用できます。

extension HTML: AsyncResponseEncodable {
  public func encodeResponse(for request: Request) async throws -> Response {
    var headers = HTTPHeaders()
    headers.add(name: .contentType, value: "text/html")
    return .init(status: .ok, headers: headers, body: .init(string: value))
  }
}

これにより、Content-Type ヘッダーをカスタマイズできることに注意して下さい。詳細は HTTPHeaders リファレンス を参照して下さい。

その後、ルート内でレスポンスタイプとして HTML を使用できます。

app.get { _ in
  HTML(value: """
  <html>
    <body>
      <h1>Hello, World!</h1>
    </body>
  </html>
  """)
}