diff --git a/docs/basics/async.ja.md b/docs/basics/async.ja.md new file mode 100644 index 00000000..9ac2b3d0 --- /dev/null +++ b/docs/basics/async.ja.md @@ -0,0 +1,431 @@ +# 非同期 + +## Async Await + +Swift 5.5では、`async`/`await`の形で言語に非同期性が導入されました。これにより、SwiftおよびVaporアプリケーションで非同期コードを扱うための第一級の方法が提供されます。 + +Vaporは、低レベルの非同期プログラミングのためのプリミティブ型を提供する[SwiftNIO](https://github.com/apple/swift-nio.git)の上に構築されています。これらは(そして依然として)`async`/`await`が到来する前のVapor全体で使用されていました。しかし、ほとんどのアプリケーションコードは、`EventLoopFuture`を使用する代わりに`async`/`await`を使用して書かれるようになりました。これにより、あなたのコードが簡素化され、その理由を理解しやすくなります。 + +VaporのAPIの多くは、`EventLoopFuture`と`async`/`await`の両方のバージョンを提供するようになり、どちらが最適かを選択できるようになりました。一般的には、1つのルートハンドラーにつき1つのプログラミングモデルのみを使用し、コードの中で混在させないようにすべきです。イベントループを明示的に制御する必要があるアプリケーションや、非常に高性能が求められるアプリケーションについては、カスタムエグゼキュータが実装されるまで`EventLoopFuture`を使用し続けるべきです。それ以外の人々には、読みやすさや保守性の利点が小さなパフォーマンスのペナルティをはるかに上回るため、`async`/`await`を使用すべきです。 + +### async/awaitへの移行 + +async/awaitに移行するにはいくつかのステップが必要です。まず、macOSを使用している場合は、macOS 12 Monterey以降とXcode 13.1以降が必要です。他のプラットフォームでは、Swift 5.5以降を実行している必要があります。次に、すべての依存関係を更新したことを確認してください。 + +Package.swiftで、ファイルの先頭にあるツールバージョンを5.5に設定します: + +```swift +// swift-tools-version:5.5 +import PackageDescription + +// ... +``` + +次に、プラットフォームバージョンをmacOS 12 に設定します。: + +```swift + platforms: [ + .macOS(.v12) + ], +``` + +最後に、`Run` ターゲットを実行可能ターゲットとしてマークを更新します。: + +```swift +.executableTarget(name: "Run", dependencies: [.target(name: "App")]), +``` + +Note: Linuxにデプロイする場合は、そちらのSwiftバージョンも更新してください。例えばHerokuやDockerfileで。例えばDockerfileでは、以下のように変更します: + +```diff +-FROM swift:5.2-focal as build ++FROM swift:5.5-focal as build +... +-FROM swift:5.2-focal-slim ++FROM swift:5.5-focal-slim +``` + +これで既存のコードを移行することができます。一般的には、EventLoopFutureを返す関数は今やasyncになっています。例えば: + +```swift +routes.get("firstUser") { req -> EventLoopFuture in + User.query(on: req.db).first().unwrap(or: Abort(.notFound)).flatMap { user in + user.lastAccessed = Date() + return user.update(on: req.db).map { + return user.name + } + } +} +``` + +それが以下のようになります。: + +```swift +routes.get("firstUser") { req async throws -> String in + guard let user = try await User.query(on: req.db).first() else { + throw Abort(.notFound) + } + user.lastAccessed = Date() + try await user.update(on: req.db) + return user.name +} +``` + +### 古い API と新しい API の使用 + +まだ `async`/`await` バージョンを提供していない API に遭遇した場合は、`EventLoopFuture` を返す関数に `.get()` を呼び出して変換することができます。 + +例えば、 + +```swift +return someMethodCallThatReturnsAFuture().flatMap { futureResult in + // use futureResult +} +``` + +これを、 + +```swift +let futureResult = try await someMethodThatReturnsAFuture().get() +``` + +に変換できます。 +逆にやりたい場合は、 + +```swift +let myString = try await someAsyncFunctionThatGetsAString() +``` + +を + +```swift +let promise = request.eventLoop.makePromise(of: String.self) +promise.completeWithTask { + try await someAsyncFunctionThatGetsAString() +} +let futureString: EventLoopFuture = promise.futureResult +``` + +に変換できます。 + +## `EventLoopFuture` + +Vapor のいくつかの API が一般的な `EventLoopFuture` タイプを期待したり返したりすることに気づいたかもしれません。もしこれが futures について初めて聞いたのであれば、最初は少し混乱するかもしれません。しかし心配しないでください、このガイドは彼らの強力な API を利用する方法をお見せします。 + +プロミスとフューチャーは関連しているが、異なるタイプです。プロミスはフューチャーを_作成する_ために使用されます。ほとんどの場合、Vapor の API によって返されるフューチャーを扱っており、プロミスを作成することについて心配する必要はありません。 + +|タイプ|説明|変更可能性| +|-|-|-| +|`EventLoopFuture`|まだ利用できない可能性がある値への参照|read-only| +|`EventLoopPromise`|非同期になんらかの値を提供するという約束|read/write| + +フューチャーは、コールバックベースの非同期 API への代替手段です。フューチャーは、単純なクロージャではできない方法で連鎖させたり変換したりすることができます。 + +## 変換 + +Swiftのオプショナルや配列のように、フューチャーはマップやフラットマップで変換できます。これらは、フューチャーに対して行う最も一般的な操作です。 + +|メソッド|引数|説明| +|-|-|-| +|[`map`](#map)|`(T) -> U`|フューチャーの値を別の値にマップします。| +|[`flatMapThrowing`](#flatmapthrowing)|`(T) throws -> U`|フューチャーの値を別の値にマップするか、エラーを投げます。| +|[`flatMap`](#flatmap)|`(T) -> EventLoopFuture`|フューチャーの値を別の_フューチャー_の値にマップします。| +|[`transform`](#transform)|`U`|既に利用可能な値にフューチャーをマップします。| + +`Optional`や`Array`の`map`や`flatMap`のメソッドシグネチャを見ると、`EventLoopFuture`で利用可能なメソッドと非常に似ていることが分かります。 + +### map + +`map`メソッドを使用すると、フューチャーの値を別の値に変換できます。フューチャーの値はまだ利用可能でない可能性があるため(非同期タスクの結果である場合があります)、値を受け取るクロージャを提供する必要があります。 + +```swift +/// あるAPIからフューチャー文字列を取得すると仮定します +let futureString: EventLoopFuture = ... + +/// フューチャー文字列を整数にマップします +let futureInt = futureString.map { string in + print(string) // 実際のString + return Int(string) ?? 0 +} + +/// 今度はフューチャー整数を持っています +print(futureInt) // EventLoopFuture +``` + +### flatMapThrowing + +`flatMapThrowing` メソッドを使用すると、フューチャーの値を別の値に変換するか、エラーを投げることができます。 + +!!! info + エラーを投げるためには内部で新しいフューチャーを作成する必要があるため、このメソッドは `flatMap` というプレフィックスが付いていますが、クロージャーはフューチャーを返す必要はありません。 + +```swift +/// Assume we get a future string back from some API +let futureString: EventLoopFuture = ... + +/// Map the future string to an integer +let futureInt = futureString.flatMapThrowing { string in + print(string) // The actual String + // Convert the string to an integer or throw an error + guard let int = Int(string) else { + throw Abort(...) + } + return int +} + +/// We now have a future integer +print(futureInt) // EventLoopFuture +``` + +### flatMap + +`flatMap` メソッドを使用すると、フューチャーの値を別のフューチャーの値に変換できます。これは、ネストされたフューチャー(例えば、`EventLoopFuture>`)を作成するのを避けることができるため、"flat" map と呼ばれます。つまり、ジェネリックをフラットに保つのに役立ちます。 + +```swift +/// Assume we get a future string back from some API +let futureString: EventLoopFuture = ... + +/// Assume we have created an HTTP client +let client: Client = ... + +/// flatMap the future string to a future response +let futureResponse = futureString.flatMap { string in + client.get(string) // EventLoopFuture +} + +/// We now have a future response +print(futureResponse) // EventLoopFuture +``` + +!!! info + 上記の例で `map` を使用した場合、結果は `EventLoopFuture>` になります。 + +`flatMap` 内でエラーを投げるメソッドを呼び出す場合は、Swiftの `do` / `catch` キーワードを使用し、[completed future](#makefuture) を作成します。 + +```swift +/// Assume future string and client from previous example. +let futureResponse = futureString.flatMap { string in + let url: URL + do { + // Some synchronous throwing method. + url = try convertToURL(string) + } catch { + // Use event loop to make pre-completed future. + return eventLoop.makeFailedFuture(error) + } + return client.get(url) // EventLoopFuture +} +``` + +### transform + +`transform` メソッドを使用すると、既存の値を無視してフューチャーの値を変更できます。これは、フューチャーの実際の値が重要でない `EventLoopFuture` の結果を変換する場合に特に便利です。 + +!!! tip + `EventLoopFuture` は、時にシグナルと呼ばれ、その唯一の目的は、非同期操作の完了または失敗を通知することです。 + +```swift +/// Assume we get a void future back from some API +let userDidSave: EventLoopFuture = ... + +/// Transform the void future to an HTTP status +let futureStatus = userDidSave.transform(to: HTTPStatus.ok) +print(futureStatus) // EventLoopFuture +``` + +`transform` に既に利用可能な値を提供しているとしても、これはまだ_変換_です。前のフューチャーが完了する(または失敗する)まで、フューチャーは完了しません。 + +### Chaining + +フューチャーの変換の素晴らしい点は、それらがチェーンできることです。これにより、多くの変換やサブタスクを簡単に表現できます。 + +上記の例を変更して、チェーンを利用する方法を見てみましょう。 + +```swift +/// Assume we get a future string back from some API +let futureString: EventLoopFuture = ... + +/// Assume we have created an HTTP client +let client: Client = ... + +/// Transform the string to a url, then to a response +let futureResponse = futureString.flatMapThrowing { string in + guard let url = URL(string: string) else { + throw Abort(.badRequest, reason: "Invalid URL string: \(string)") + } + return url +}.flatMap { url in + client.get(url) +} + +print(futureResponse) // EventLoopFuture +``` + +最初の map 呼び出しの後、一時的な `EventLoopFuture` が作成されます。このフューチャーはすぐに `EventLoopFuture` にフラットマップされます。 + +## Future + +`EventLoopFuture` を使用する他の方法について見てみましょう。 + +### makeFuture + +イベントループを使用して、値またはエラーを持つ事前に完了したフューチャーを作成できます。 + +```swift +// Create a pre-succeeded future. +let futureString: EventLoopFuture = eventLoop.makeSucceededFuture("hello") + +// Create a pre-failed future. +let futureString: EventLoopFuture = eventLoop.makeFailedFuture(error) +``` + +### whenComplete + +`whenComplete` を使用して、フューチャーが成功または失敗したときに実行されるコールバックを追加できます。 + +```swift +/// Assume we get a future string back from some API +let futureString: EventLoopFuture = ... + +futureString.whenComplete { result in + switch result { + case .success(let string): + print(string) // The actual String + case .failure(let error): + print(error) // A Swift Error + } +} +``` + +!!! note + Future には、好きなだけコールバックを追加できます。 + +### Wait + +`.wait()`を使用して、フューチャーが完了するまで同期的に待つことができます。フューチャーが失敗する可能性があるため、この呼び出しは投げられます。 + +```swift +/// Assume we get a future string back from some API +let futureString: EventLoopFuture = ... + +/// Block until the string is ready +let string = try futureString.wait() +print(string) /// String +``` + +`wait()` は、バックグラウンドスレッドまたはメインスレッド、つまり `configure.swift` で使用できます。イベントループスレッド、つまりルートクロージャで使用することは_できません_。 + +!!! warning + イベントループスレッドで `wait()` を呼び出そうとすると、アサーションエラーが発生します。 + + +## Promise + +ほとんどの場合、Vapor の API からの呼び出しによって返されるフューチャーを変換します。しかし、ある時点で自分自身の約束を作成する必要があるかもしれません。 + +約束を作成するには、`EventLoop` へのアクセスが必要です。`Application` または `Request` からコンテキストに応じてイベントループにアクセスできます。 + +```swift +let eventLoop: EventLoop + +// Create a new promise for some string. +let promiseString = eventLoop.makePromise(of: String.self) +print(promiseString) // EventLoopPromise +print(promiseString.futureResult) // EventLoopFuture + +// Completes the associated future. +promiseString.succeed("Hello") + +// Fails the associated future. +promiseString.fail(...) +``` + +!!! info + 約束は一度だけ完了できます。その後の完了は無視されます。 + +約束はどのスレッドからでも完了(`succeed` / `fail`)できます。これが、初期化にイベントループが必要な理由です。約束は、完了アクションがそのイベントループに戻されて実行されることを保証します。 + +## イベントループ + +アプリケーションが起動すると、通常は実行中の CPU の各コアに対して1つのイベントループが作成されます。各イベントループには1つのスレッドがあります。Node.js からのイベントループに精通している場合、Vapor のものは似ています。主な違いは、Swift がマルチスレッディングをサポートしているため、Vapor は 1 つのプロセスで複数のイベントループを実行できることです。 + +クライアントがサーバーに接続するたびに、そのイベントループの 1 つに割り当てられます。その時点から、サーバーとそのクライアントとの間のすべての通信は、同じイベントループ(および関連するイベントループのスレッド)で行われます。 + +イベントループは、接続された各クライアントの状態を追跡する責任があります。クライアントからのリクエストが読み取りを待っている場合、イベントループは読み取り通知をトリガーし、データが読み取られます。リクエスト全体が読み取られると、そのリクエストのデータを待っている任意のフューチャーが完了します。 + +ルートクロージャで、`Request` 経由で現在のイベントループにアクセスできます。 + +```swift +req.eventLoop.makePromise(of: ...) +``` + +!!! warning + Vapor はルートクロージャが `req.eventLoop` に留まることを期待しています。スレッドを移動する場合、`Request` と最終的な応答フューチャーへのアクセスがすべてリクエストのイベントループ上で行われることを確認する必要があります。 + +ルートクロージャの外では、`Application` を介して利用可能なイベントループの1つを取得できます。 + +```swift +app.eventLoopGroup.next().makePromise(of: ...) +``` + +### hop + +`hop` を使用して、フューチャーのイベントループを変更できます。 + +```swift +futureString.hop(to: otherEventLoop) +``` + +## Blocking + +イベントループスレッドでブロッキングコードを呼び出すと、アプリケーションがタイムリーに受信リクエストに応答することができなくなる可能性があります。ブロッキングコールの例としては、`libc.sleep(_:)` のようなものがあります。 + +```swift +app.get("hello") { req in + /// Puts the event loop's thread to sleep. + sleep(5) + + /// Returns a simple string once the thread re-awakens. + return "Hello, world!" +} +``` + +`sleep(_:)` は指定された秒数だけ現在のスレッドをブロックするコマンドです。イベントループで直接このようなブロッキング作業を行うと、その作業の期間、イベントループはそれに割り当てられた他のクライアントに応答することができなくなります。言い換えると、イベントループで `sleep(5)` を行うと、そのイベントループに接続されている他のクライアント(数百または数千)が少なくとも5秒遅れることになります。 + +ブロッキング作業は背景で実行し、この作業がブロッキングしない方法で完了したときにイベントループに通知するためにプロミスを使用してください。 + +```swift +app.get("hello") { req -> EventLoopFuture in + /// Dispatch some work to happen on a background thread + return req.application.threadPool.runIfActive(eventLoop: req.eventLoop) { + /// Puts the background thread to sleep + /// This will not affect any of the event loops + sleep(5) + + /// When the "blocking work" has completed, + /// return the result. + return "Hello world!" + } +} +``` + +すべてのブロッキングコールが `sleep(_:)` ほど明白ではありません。使用している呼び出しがブロッキングかどうか疑わしい場合は、そのメソッド自体を調査するか、誰かに尋ねてください。以下のセクションでは、メソッドがどのようにブロッキングする可能性があるかについて、詳しく説明します。 + +### I/O バウンド + +I/O バウンドのブロッキングとは、ネットワークやハードディスクなど、CPU よりも桁違いに遅いリソースを待つことを意味します。これらのリソースを待っている間に CPU をブロックすると、時間が無駄になります。 + +!!! danger + イベントループで直接ブロッキングI/Oバウンドコールを行わないでください。 + +Vapor のすべてのパッケージは SwiftNIO に基づいており、ノンブロッキング I/O を使用しています。しかし、ブロッキング I/O を使用する Swift のパッケージや C ライブラリが多く存在します。関数がディスクやネットワーク IO を行っており、同期 API(コールバックやフューチャーがない)を使用している場合、ブロッキングしている可能性が高いです。 + +### CPU バウンド + +リクエスト中のほとんどの時間は、データベースのクエリやネットワークリクエストなどの外部リソースを待っているために費やされます。Vapor と SwiftNIO はノンブロッキングなので、このダウンタイムは他の受信リクエストを満たすために使用できます。しかし、アプリケーションのいくつかのルートは、リクエストの結果として重い CPU バウンド作業を行う必要があるかもしれません。 + +イベントループが CPU バウンド作業を処理している間、他の受信リクエストに応答することができません。これは通常問題ありません。CPU は高速であり、Web アプリケーションが行うほとんどの CPU 作業は軽量です。しかし、長時間実行される CPU 作業のルートが、他のルートへの迅速な応答を妨げる場合、問題になる可能性があります。 + +アプリ内の長時間実行される CPU 作業を特定し、それをバックグラウンドスレッドに移動することで、サービスの信頼性と応答性を向上させることができます。CPU バウンド作業は I/O バウンド作業よりもグレーエリアであり、線を引く場所を決定するのは最終的にはあなた次第です。 + +重い CPU バウンド作業の一般的な例は、ユーザーのサインアップとログイン時の Bcrypt ハッシュ化です。Bcrypt はセキュリティ上の理由から意図的に非常に遅く、CPU を集中的に使用します。これは、シンプルな Web アプリケーションが実際に行う作業の中で最も CPU 集中的な作業かもしれません。ハッシングをバックグラウンドスレッドに移動すると、CPU はイベントループの作業とハッシュの計算を交互に行うことができ、結果として高い並行性が実現されます。 diff --git a/docs/basics/client.ja.md b/docs/basics/client.ja.md new file mode 100644 index 00000000..18da420f --- /dev/null +++ b/docs/basics/client.ja.md @@ -0,0 +1,74 @@ +# クライアント + +Vapor のクライアント API では、外部のリソースに対して HTTP 通信を行うことができます。これは [async-http-client](https://github.com/swift-server/async-http-client) に基づいており、[コンテンツ](content.ja.md) API と統合されています。 + +## 概要 + +`Application` やルートハンドラー内の `Request` から、デフォルトクライアントにアクセスできます。 + +```swift +app.client // Client + +app.get("test") { req in + req.client // Client +} +``` + +アプリケーションのクライアントは、設定時に HTTP リクエストを送る際に便利です。ルートハンドラー内で HTTP リクエストを行う場合は、リクエストに紐づくクライアントを使うべきです。 + +### メソッド + +`GET` リクエストを行う際には、目的の URL を `get` メソッドに渡します。 + +```swift +let response = try await req.client.get("https://httpbin.org/status/200") +``` + +`get`、`post`、`delete` など、各種 HTTP メソッドに対応したメソッドがあります。クライアントからのレスポンスは将来的に返され、HTTPステータス、ヘッダー、ボディが含まれます。 + +### コンテンツ + +Vapor の [コンテンツ](content.ja.md) を使うと、クライアントリクエストやレスポンスのデータを扱うことができます。コンテンツやクエリパラメータをエンコードしたり、ヘッダーを追加するには、`beforeSend` クロージャを使います。 + +```swift +let response = try await req.client.post("https://httpbin.org/status/200") { req in + // リクエストURLにクエリ文字列をエンコードします。 + try req.query.encode(["q": "test"]) + + // JSONをリクエストボディにエンコードします。 + try req.content.encode(["hello": "world"]) + + // リクエストに認証ヘッダーを追加します。 + let auth = BasicAuthorization(username: "something", password: "somethingelse") + req.headers.basicAuthorization = auth +} +//レスポンスを扱う +``` + +レスポンスボディを `Content` を使ってデコードすることもできます。: + +```swift +let response = try await req.client.get("https://httpbin.org/json") +let json = try response.content.decode(MyJSONResponse.self) +``` + +もし、futures を使っている場合は、`flatMapThrowing` を使うことができます。: + +```swift +return req.client.get("https://httpbin.org/json").flatMapThrowing { res in + try res.content.decode(MyJSONResponse.self) +}.flatMap { json in + // Use JSON here +} +``` + +## 設定 + +アプリケーションを通じて、基本となる HTTP クライアントを設定することができます。 + +```swift +// Disable automatic redirect following. +app.http.client.configuration.redirectConfiguration = .disallow +``` + +初めてデフォルトクライアントを使用する前に、必ず設定を完了させておく必要があります。 diff --git a/docs/basics/content.ja.md b/docs/basics/content.ja.md new file mode 100644 index 00000000..feee471e --- /dev/null +++ b/docs/basics/content.ja.md @@ -0,0 +1,283 @@ +# コンテンツ + +Vapor のコンテンツ API を使用すると、Codable な構造体を HTTP メッセージに対して簡単にエンコード・デコードできます。標準では [JSON](https://tools.ietf.org/html/rfc7159) 形式でエンコードされており、[URL-Encoded Form](https://en.wikipedia.org/wiki/Percent-encoding#The_application/x-www-form-urlencoded_type) や [Multipart](https://tools.ietf.org/html/rfc2388) についても即座に使えるサポートがあります。この API はカスタマイズ可能であり、特定の HTTP コンテンツタイプに対するエンコーディングの戦略を追加したり、変更したり、置き換えたりすることができます。 + +## 概要 + +Vapor のコンテンツ API の仕組みを理解するためには、HTTP メッセージの基本について知っておく必要があります。以下にリクエストの例を示します。 + +```http +POST /greeting HTTP/1.1 +content-type: application/json +content-length: 18 + +{"hello": "world"} +``` + +このリクエストは、`content-type` ヘッダーを通じて `application/json` メディアタイプでJSON形式のデータが含まれていることを示しています。ヘッダーの後のボディ部分には、約束されたJSONデータが続きます。 + +### コンテンツ構造体 + +この HTTP メッセージをデコードする最初のステップは、期待される構造にマッチする Codable 型を作成することです。 + +```swift +struct Greeting: Content { + var hello: String +} +``` + +型を `Content` に準拠させると、コンテンツ API を扱うための追加ユーティリティが得られると同時に、`Codable` にも自動的に準拠するようになります。 + + +コンテンツの構造ができたら、`req.content` を使ってリクエストからデコードできます。 + +```swift +app.post("greeting") { req in + let greeting = try req.content.decode(Greeting.self) + print(greeting.hello) // "world" + return HTTPStatus.ok +} +``` + +このデコードメソッドは、リクエストのコンテンツタイプに基づいて適切なデコーダーを見つけます。デコーダーが見つからない、またはリクエストにコンテンツタイプヘッダーが含まれていない場合、`415` エラーが発生します。 + +つまり、このルートは URL エンコードされたフォームなど、他のサポートされているコンテンツタイプも自動的に受け入れるということです。 + +```http +POST /greeting HTTP/1.1 +content-type: application/x-www-form-urlencoded +content-length: 11 + +hello=world +``` + +ファイルアップロードのケースでは、コンテンツのプロパティは `Data` 型である必要があります。 + +```swift +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 クエリ文字列をデコードする方法を理解するために、以下の例をご覧ください。 + +```http +GET /hello?name=Vapor HTTP/1.1 +content-length: 0 +``` + +HTTP メッセージのボディ内容を扱う API と同様に、URL クエリ文字列を解析する最初のステップは、期待される構造にあった `struct` を作成することです。 + +```swift +struct Hello: Content { + var name: String? +} +``` + +`name` がオプションな `String` であることに注意して下さい。URL クエリ文字列は常にオプショナルであるべきだからです。パラメーターを必須にしたい場合は、ルートパラメーターを使って下さい。 + +期待されるクエリ文字列に合わせた `Content` 構造体が用意できたら、それをデコードできます。 + +```swift +app.get("hello") { req -> String in + let hello = try req.query.decode(Hello.self) + return "Hello, \(hello.name ?? "Anonymous")" +} +``` + +上記の例のリクエストに基づいて、このルートは次のような応答を返します: + +```http +HTTP/1.1 200 OK +content-length: 12 + +Hello, Vapor +``` + +例えば、次のリクエストのようにクエリ文字列が省略された場合は、"Anonymous" が使われます。 + +```http +GET /hello HTTP/1.1 +content-length: 0 +``` + +### 単一の値 + +`Content` 構造体へのデコードだけでなく、Vapor はクエリ文字列から単一の値を取得することもサポートしています。これはサブスクリプトを使用して行われます。 + +```swift +let name: String? = req.query["name"] +``` + +## フック + +Vapor は、`Content` タイプに対して `beforeEncode` および `afterDecode` を自動的に呼び出します。デフォルトの実装は何もしませんが、これらのメソッドを使用してカスタムロジックを実行することができます。 + +```swift +// この 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 がデフォルトで使用するエンコーダーやデコーダーを変更できます。これは、アプリケーション全体でデータの解析やシリアライズ方法を変更するのに便利です。 + +```swift +// UNIX タイムスタンプの日付を使用する新しい JSON エンコーダーを作成します。 +let encoder = JSONEncoder() +encoder.dateEncodingStrategy = .secondsSince1970 + +// `.json` メディアタイプで使用されるグローバルエンコーダーを上書きします。 +ContentConfiguration.global.use(encoder: encoder, for: .json) +``` + +`ContentConfiguration` の変更は通常、`configure.swift` で行われます。 + +### 1回限り + +`req.content.decode` のようなエンコーディングやデコーディングのメソッド呼び出しは、1回限りの使用のためにカスタムコーダーを渡すことをサポートしています。 + +```swift +// UNIX タイムスタンプの日付を使用する新しい JSON デコーダーを作成します。 +let decoder = JSONDecoder() +decoder.dateDecodingStrategy = .secondsSince1970 + +// カスタムデコーダーを使用して Hello 構造体をデコードします。 +let hello = try req.content.decode(Hello.self, using: decoder) +``` + +## カスタムコーダー + +アプリケーションやサードパーティのパッケージは、Vapor がデフォルトでサポートしていないメディアタイプに対応するためにカスタムコーダーを作成することができます。 + +### Content + +Vapor は、HTTP メッセージボディのコンテンツを処理するためのコーダーのために、`ContentDecoder` と `ContentEncoder` の2つのプロトコルを指定しています。 + +```swift +public protocol ContentEncoder { + func encode(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders) throws + where E: Encodable +} + +public protocol ContentDecoder { + func decode(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders) throws -> D + where D: Decodable +} +``` + +これらのプロトコルに準拠することで、カスタムコーダーを上記で指定されたように `ContentConfiguration` に登録できます。 + +### URL クエリ + +Vapor は、URL クエリ文字列のコンテンツを処理することができる coder のための 2 つのプロトコルを指定しています: `URLQueryDecoder` と `URLQueryEncoder` + +```swift +public protocol URLQueryDecoder { + func decode(_ decodable: D.Type, from url: URI) throws -> D + where D: Decodable +} + +public protocol URLQueryEncoder { + func encode(_ encodable: E, to url: inout URI) throws + where E: Encodable +} +``` + +これらのプロトコルに準拠することで、`use(urlEncoder:)` および `use(urlDecoder:)` メソッドを使用して、URL クエリ文字列の処理のためにカスタムコーダーを `ContentConfiguration` に登録できます。 + +### カスタム `ResponseEncodable` + +別のアプローチには、タイプに `ResponseEncodable` を実装するというものがあります。この単純な `HTML` ラッパータイプを考えてみてください。 + +```swift +struct HTML { + let value: String +} +``` + +その `ResponseEncodable` の実装は以下のようになります。 + +```swift +extension HTML: ResponseEncodable { + public func encodeResponse(for request: Request) -> EventLoopFuture { + 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` を使用できます。 + +```swift +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` リファレンス](https://api.vapor.codes/vapor/documentation/vapor/response/headers) を参照して下さい。 + +その後、ルート内でレスポンスタイプとして `HTML` を使用できます。 + +```swift +app.get { _ in + HTML(value: """ + + +

Hello, World!

+ + + """) +} +``` diff --git a/docs/basics/controllers.ja.md b/docs/basics/controllers.ja.md new file mode 100644 index 00000000..abe8179d --- /dev/null +++ b/docs/basics/controllers.ja.md @@ -0,0 +1,70 @@ +# コントローラー + +コントローラーはコードを整理するのに適した方法です。これらは、リクエストを受けてレスポンスを返すメソッドの集まりです。 + +コントローラーを置く良い場所は、[Controllers](../getting-started/folder-structure.ja.md#controllers) フォルダーです。 + +## 概要 + +例としてコントローラーを見てみましょう。 + +```swift +import Vapor + +struct TodosController: RouteCollection { + func boot(routes: RoutesBuilder) throws { + let todos = routes.grouped("todos") + todos.get(use: index) + todos.post(use: create) + + todos.group(":id") { todo in + todo.get(use: show) + todo.put(use: update) + todo.delete(use: delete) + } + } + + func index(req: Request) async throws -> [Todo] { + try await Todo.query(on: req.db).all() + } + + func create(req: Request) async throws -> Todo { + let todo = try req.content.decode(Todo.self) + try await todo.save(on: req.db) + return todo + } + + func show(req: Request) async throws -> Todo { + guard let todo = try await Todo.find(req.parameters.get("id"), on: req.db) else { + throw Abort(.notFound) + } + return todo + } + + func update(req: Request) async throws -> Todo { + guard let todo = try await Todo.find(req.parameters.get("id"), on: req.db) else { + throw Abort(.notFound) + } + let updatedTodo = try req.content.decode(Todo.self) + todo.title = updatedTodo.title + try await todo.save(on: req.db) + return todo + } + + func delete(req: Request) async throws -> HTTPStatus { + guard let todo = try await Todo.find(req.parameters.get("id"), on: req.db) { + throw Abort(.notFound) + } + try await todo.delete(on: req.db) + return .ok + } +} +``` + +コントローラーのメソッドは常に `Request` を受け取り、何か `ResponseEncodable` を返す必要があります。このメソッドは非同期でも同期でも構いません。 + +最後に、コントローラーを `routes.swift` に登録する必要があります: + +```swift +try app.register(collection: TodosController()) +``` diff --git a/docs/basics/environment.ja.md b/docs/basics/environment.ja.md new file mode 100644 index 00000000..07e75b97 --- /dev/null +++ b/docs/basics/environment.ja.md @@ -0,0 +1,146 @@ +# 環境 + +VaporのEnvironment APIは、アプリの動的な設定を支援します。デフォルトでは、あなたのアプリは`development`環境を使用します。`production`や`staging`のような他の有用な環境を定義し、各ケースでアプリがどのように設定されるかを変更できます。また、プロセスの環境や`.env`(dotenv)ファイルから変数を読み込むことも、ニーズに応じて可能です。 + +現在の環境にアクセスするには、`app.environment`を使用します。`configure(_:)`内でこのプロパティにスイッチして、異なる設定ロジックを実行できます。 + +```swift +switch app.environment { +case .production: + app.databases.use(....) +default: + app.databases.use(...) +} +``` + +## 環境の変化 + +デフォルトでは、アプリは `development` 環境で実行されます。アプリ起動時に `--env`(`-e`)フラグを渡すことで、これを変更できます。 + +```swift +swift run App serve --env production +``` + +Vapor は以下の環境を含みます。: + +|名前|略称|説明| +|-|-|-| +|production|prod|ユーザーにデプロイされた状態| +|development|dev|ローカル開発| +|testing|test|ユニットテスト用| + +!!! info + `production` 環境は、特に指定されていない場合、デフォルトで `notice` レベルのログになります。他の環境はデフォルトでinfoです。 + + +`--env`(`-e`)フラグには、フルネームか略称のどちらかを渡すことができます。 + +```swift +swift run App serve -e prod +``` + +## プロセス変数 + +`Environment` は、プロセスの環境変数にアクセスするためのシンプルな文字列ベースの API を提供します。 + +```swift +let foo = Environment.get("FOO") +print(foo) // String? +``` + +`get` に加えて、`Environment` は `process` 経由で動的メンバールックアップ API を提供します。 + +```swift +let foo = Environment.process.FOO +print(foo) // String? +``` + +ターミナルでアプリを実行する際は、`export` を使って環境変数を設定できます。 + +```sh +export FOO=BAR +swift run App serve +``` + +Xcode でアプリを実行する場合は、`App` スキームを編集して環境変数を設定できます。 + +## .env (dotenv) + +Dotenv ファイルには、環境に自動的にロードされるキーと値のペアのリストが含まれています。これらのファイルは、手動で設定することなく環境変数を設定するのを容易にします。 + +Vapor は、現在の作業ディレクトリにある dotenv ファイルを探します。Xcode を使用している場合は、`App` スキームを編集して作業ディレクトリを設定してください。 + +以下の `.env` ファイルがプロジェクトのルートフォルダに配置されているとします: + +```sh +FOO=BAR +``` + +アプリケーションが起動すると、このファイルの内容に他のプロセス環境変数のようにアクセスできます。 + +```swift +let foo = Environment.get("FOO") +print(foo) // String? +``` + +!!! info + `.env` ファイルで指定された変数は、プロセス環境に既に存在する変数を上書きしません。 + + +`.env` と並行して、Vapor は現在の環境の dotenv ファイルも読み込もうとします。例えば、`development` 環境では、`.env.development` がロードされます。特定の環境ファイルにある値は、一般的な `.env` ファイルより優先されます。 + +一般的なパターンとして、プロジェクトはデフォルト値を含む `.env` ファイルをテンプレートとして含めます。特定の環境ファイルは、以下のパターンで `.gitignore` に含まれます: + +```gitignore +.env.* +``` + +プロジェクトが新しいコンピュータにクローンされたとき、テンプレートの `.env` ファイルをコピーして正しい値を挿入できます。 + +```sh +cp .env .env.development +vim .env.development +``` + +!!! warning + パスワードなどの機密情報を含む dotenv ファイルは、バージョン管理にコミットしてはいけません。 + +dotenv ファイルの読み込みに問題がある場合は、`--log debug` を使用してデバッグログを有効にすると、より多くの情報が得られます。 + +## カスタム環境 + +カスタム環境を定義するには、`Environment`を拡張します。 + +```swift +extension Environment { + static var staging: Environment { + .custom(name: "staging") + } +} +``` + +アプリケーションの環境は通常、`entrypoint.swift` で `Environment.detect()` を使って設定されます。 + +```swift +@main +enum Entrypoint { + static func main() async throws { + var env = try Environment.detect() + try LoggingSystem.bootstrap(from: &env) + + let app = Application(env) + defer { app.shutdown() } + + try await configure(app) + try await app.runFromAsyncMainEntrypoint() + } +} +``` + +`detect` メソッドはプロセスのコマンドライン引数を使用し、`--env` フラグを自動的に解析します。カスタム `Environment` 構造体を初期化することで、この動作をオーバーライドできます。 + +```swift +let env = Environment(name: "testing", arguments: ["vapor"]) +``` + +引数配列には、実行可能な名前を表す少なくとも1つの引数が含まれている必要があります。コマンドライン経由で引数を渡すのをシミュレートするために、さらに引数を供給できます。これは特にテストに役立ちます。 diff --git a/docs/basics/errors.ja.md b/docs/basics/errors.ja.md new file mode 100644 index 00000000..25601e91 --- /dev/null +++ b/docs/basics/errors.ja.md @@ -0,0 +1,197 @@ +# エラー + +Vapor は Swift の `Error` プロトコルをベースにしたエラー処理を採用しています。ルートハンドラは、エラーを `throw` するか、失敗した `EventLoopFuture` を返すことができます。Swiftの `Error` を throw するか返すと、`500` ステータスのレスポンスが生成され、エラーがログに記録されます。`AbortError` と `DebuggableError` は、それぞれ結果として得られるレスポンスとログを変更するために使用できます。エラーの処理は `ErrorMiddleware` によって行われます。このミドルウェアはデフォルトでアプリケーションに追加されており、必要に応じてカスタムロジックに置き換えることができます。 + +## Abort + +Vapor は `Abort` というデフォルトのエラー構造体を提供しています。この構造体は `AbortError` と `DebuggableError` の両方に準拠しています。HTTP ステータスとオプショナルな失敗理由を指定して初期化できます。 + +```swift +// 404 error, default "Not Found" reason used. +throw Abort(.notFound) + +// 401 error, custom reason used. +throw Abort(.unauthorized, reason: "Invalid Credentials") +``` + +非同期の状況で throw がサポートされていない場合や `EventLoopFuture` を返す必要がある場合、例えば `flatMap` クロージャ内で、失敗した未来を返すことができます。 + +```swift +guard let user = user else { + req.eventLoop.makeFailedFuture(Abort(.notFound)) +} +return user.save() +``` + +Vapor にはオプショナルな値を持つ未来をアンラップするためのヘルパーエクステンション `unwrap(or:)` が含まれています。 + +```swift +User.find(id, on: db) + .unwrap(or: Abort(.notFound)) + .flatMap +{ user in + // Non-optional User supplied to closure. +} +``` + +`User.find` が `nil` を返した場合、提供されたエラーで未来が失敗します。それ以外の場合は、`flatMap` に非オプショナルな値が提供されます。`async` / `await` を使用している場合は、通常どおりオプショナルを扱うことができます: + +```swift +guard let user = try await User.find(id, on: db) { + throw Abort(.notFound) +} +``` + + +## Abort Error + +デフォルトでは、ルートクロージャによって throw されたり返されたりする任意の Swift の Error は `500 Internal Server Error` レスポンスになります。デバッグモードでビルドされた場合、`ErrorMiddleware` はエラーの説明を含めます。セキュリティ上の理由から、リリースモードでビルドするとこれが除去されます。 + +特定のエラーの結果として得られる `HTTP` レスポンスステータスや理由を設定するには、それを `AbortError` に準拠させます。 + +```swift +import Vapor + +enum MyError { + case userNotLoggedIn + case invalidEmail(String) +} + +extension MyError: AbortError { + var reason: String { + switch self { + case .userNotLoggedIn: + return "User is not logged in." + case .invalidEmail(let email): + return "Email address is not valid: \(email)." + } + } + + var status: HTTPStatus { + switch self { + case .userNotLoggedIn: + return .unauthorized + case .invalidEmail: + return .badRequest + } + } +} +``` + +## Debuggable Error + +`ErrorMiddleware` は、ルートによって throw されたエラーのログを記録するために `Logger.report(error:)` メソッドを使用します。このメソッドは `CustomStringConvertible` や `LocalizedError` などのプロトコルへの準拠をチェックし、読みやすいメッセージをログに記録します。 + +エラーログをカスタマイズするために、エラーを `DebuggableError` に準拠させることができます。このプロトコルには、固有の識別子、ソースの位置、スタックトレースなど、多くの便利なプロパティが含まれています。これらのプロパティのほとんどはオプショナルであるため、準拠を採用するのは容易です。 + +`DebuggableError` に最適に準拠するためには、エラーは構造体であるべきです。これにより、必要に応じてソースとスタックトレース情報を格納できます。以下は、前述の `MyError` 列挙型を構造体に更新し、エラーソース情報をキャプチャする例です。 + +```swift +import Vapor + +struct MyError: DebuggableError { + enum Value { + case userNotLoggedIn + case invalidEmail(String) + } + + var identifier: String { + switch self.value { + case .userNotLoggedIn: + return "userNotLoggedIn" + case .invalidEmail: + return "invalidEmail" + } + } + + var reason: String { + switch self.value { + case .userNotLoggedIn: + return "User is not logged in." + case .invalidEmail(let email): + return "Email address is not valid: \(email)." + } + } + + var value: Value + var source: ErrorSource? + + init( + _ value: Value, + file: String = #file, + function: String = #function, + line: UInt = #line, + column: UInt = #column + ) { + self.value = value + self.source = .init( + file: file, + function: function, + line: line, + column: column + ) + } +} +``` + +`DebuggableError` には、エラーのデバッグ性を向上させるために使用できる `possibleCauses` や `suggestedFixes` など、他にもいくつかのプロパティがあります。より詳細に知りたい場合は、プロトコル自体をご覧ください。 + +## スタックトレース + +Vaporは、通常のSwiftエラーやクラッシュに対するスタックトレースの表示をサポートしています。 + +### Swift バックトレース + +Vapor は、Linux 上で致命的なエラーやアサーションの後にスタックトレースを提供するために、[SwiftBacktrace](https://github.com/swift-server/swift-backtrace) ライブラリを使用しています。これが機能するためには、アプリはコンパイル中にデバッグシンボルを含める必要があります。 + +```sh +swift build -c release -Xswiftc -g +``` + +### エラートレース + +デフォルトでは、`Abort` は初期化されたときに現在のスタックトレースをキャプチャします。カスタムエラータイプは、`DebuggableError` に準拠し、`StackTrace.capture()` を保存することでこれを実現できます。 + +```swift +import Vapor + +struct MyError: DebuggableError { + var identifier: String + var reason: String + var stackTrace: StackTrace? + + init( + identifier: String, + reason: String, + stackTrace: StackTrace? = .capture() + ) { + self.identifier = identifier + self.reason = reason + self.stackTrace = stackTrace + } +} +``` + +アプリケーションの[ログレベル](logging.ja.md#level)が `.debug` 以下に設定されている場合、エラースタックトレースはログ出力に含まれます。 + +ログレベルが `.debug` より大きい場合、スタックトレースはキャプチャされません。この挙動を変更するには、`configure` 内で `StackTrace.isCaptureEnabled` を手動で設定してください。 + +```swift +// Always capture stack traces, regardless of log level. +StackTrace.isCaptureEnabled = true +``` + +## エラーミドルウェア + +`ErrorMiddleware` は、デフォルトでアプリケーションに追加される唯一のミドルウェアです。このミドルウェアは、ルートハンドラーによって投げられたり返されたりした Swift のエラーを HTTP レスポンスに変換します。このミドルウェアがない場合、投げられたエラーは応答なしに接続が閉じられることになります。 + +`AbortError` と `DebuggableError` が提供するものを超えてエラー処理をカスタマイズするには、`ErrorMiddleware` を独自のエラー処理ロジックで置き換えることができます。これを行うには、まず `app.middleware` を空の設定に設定して、デフォルトのエラーミドルウェアを削除します。その後、独自のエラー処理ミドルウェアをアプリケーションに最初のミドルウェアとして追加します。 + +```swift +// Remove all existing middleware. +app.middleware = .init() +// Add custom error handling middleware first. +app.middleware.use(MyErrorMiddleware()) +``` + +エラー処理ミドルウェアの _前に_ 置くべきミドルウェアはほどんどありません。注目すべき例外は `CORSMiddleware` です。 diff --git a/docs/basics/logging.ja.md b/docs/basics/logging.ja.md new file mode 100644 index 00000000..4a9cadfe --- /dev/null +++ b/docs/basics/logging.ja.md @@ -0,0 +1,109 @@ +# ロギング + +Vapor のロギング API は [SwiftLog](https://github.com/apple/swift-log) を基に構築されています。これは、Vapor が SwiftLog の[バックエンド実装](https://github.com/apple/swift-log#backends)と互換性があることを示しています。 + +## ロガー + +`Logger` のインスタンスはログメッセージを出力するために使用されます。Vapor はロガーにアクセスするためのいくつかの簡単な方法を提供しています。 + +### リクエスト + +各入力 `Request` には、そのリクエストに固有のログを使用するためのユニークなロガーがあります。 + +```swift +app.get("hello") { req -> String in + req.logger.info("Hello, logs!") + return "Hello, world!" +} +``` + +リクエストロガーには、ログの追跡を容易にするために、入力リクエストを識別するユニークな UUID が含まれています。 + +``` +[ INFO ] Hello, logs! [request-id: C637065A-8CB0-4502-91DC-9B8615C5D315] (App/routes.swift:10) +``` + +!!! info + ロガーメタデータは、デバッグログレベルまたはそれ以下でのみ表示されます。 + +### アプリケーション + +アプリの起動や設定中のログメッセージには、`Application` のロガーを使用します。 + +```swift +app.logger.info("Setting up migrations...") +app.migrations.use(...) +``` + +### カスタムロガー + +`Application` や `Request` にアクセスできない状況では、新しい `Logger` を初期化できます。 + +```swift +let logger = Logger(label: "dev.logger.my") +logger.info(...) +``` + +カスタムロガーは設定されたロギングバックエンドに出力されますが、リクエスト UUID のような重要なメタデータは付加されません。可能な限りリクエストやアプリケーション固有のロガーを使用してください。 + +## レベル + +SwiftLog はいくつかの異なるログレベルをサポートしています。 + +|nama|description| +|-|-| +|trace|プログラムの実行を追跡する際に通常のみ役立つ情報を含むメッセージに適しています。| +|debug|プログラムをデバッグする際に通常のみ役立つ情報を含むメッセージに適しています。| +|info|情報メッセージに適しています。| +|notice|エラー条件ではないが、特別な処理が必要な条件に適しています。| +|warning|エラー条件ではないが、noticeよりも重大なメッセージに適しています。| +|error|エラー条件に適しています。| +|critical|通常は直ちに注意が必要な重大なエラー条件に適しています。| + +`critical` メッセージがログに記録されると、ログバックエンドはシステム状態をキャプチャするために重い操作(スタックトレースのキャプチャなど)を自由に実行できます。 + +デフォルトでは、Vapor は `info` レベルのログを使用します。`production` 環境で実行する場合は、パフォーマンス向上のためにnoticeが使用されます。 + +### ログレベルの変更 + +環境モードに関係なく、ログの量を増減するためにログレベルをオーバーライドできます。 + +最初の方法は、アプリケーションを起動する際にオプションの `--log` フラグを渡すことです。 + +```sh +swift run App serve --log debug +``` + +2番目の方法は、`LOG_LEVEL` 環境変数を設定することです。 + +```sh +export LOG_LEVEL=debug +swift run App serve +``` + +これらの操作は、Xcodeで `App` スキームを編集することで行うことができます。 + +## 設定 + +SwiftLog は、プロセスごとに一度 `LoggingSystem` をブートストラップすることによって設定されます。Vapor プロジェクトでは、これは通常 `entrypoint.swift` で行われます。 + +```swift +var env = try Environment.detect() +try LoggingSystem.bootstrap(from: &env) +``` + +`bootstrap(from:)` は、コマンドライン引数や環境変数に基づいてデフォルトのログハンドラーを設定するために Vapor が提供するヘルパーメソッドです。デフォルトのログハンドラーは、ANSI カラーサポートを備えた端末へのメッセージ出力をサポートしています。 + +### カスタムハンドラー + +Vaporのデフォルトのログハンドラーをオーバーライドし、独自のものを登録することができます。 + +```swift +import Logging + +LoggingSystem.bootstrap { label in + StreamLogHandler.standardOutput(label: label) +} +``` + +SwiftLog がサポートするすべてのバックエンドは Vapor と互換性があります。ただし、コマンドライン引数や環境変数を使用したログレベルの変更は、Vapor のデフォルトのログハンドラーとのみ互換性があります。 diff --git a/docs/basics/routing.ja.md b/docs/basics/routing.ja.md new file mode 100644 index 00000000..00573895 --- /dev/null +++ b/docs/basics/routing.ja.md @@ -0,0 +1,436 @@ +# ルーティング + +ルーティングは、入ってきたリクエストに対して適切なリクエストハンドラを見つける処理です。Vapor のルーティングの核心には、[RoutingKit](https://github.com/vapor/routing-kit) からの高性能なトライノードルータがあります。 + +## 概要 + +Vapor でのルーティングの仕組みを理解するために、まず HTTP リクエストの基本について理解する必要があります。以下のサンプルリクエストを見てください。 + +```http +GET /hello/vapor HTTP/1.1 +host: vapor.codes +content-length: 0 +``` + +これは、URL `/hello/vapor` への単純な `GET` HTTP リクエストです。この HTTP リクエストは、ブラウザを以下の URL に向けた場合に実行されるものです。 + +``` +http://vapor.codes/hello/vapor +``` + +### HTTP メソッド + +リクエストの最初の部分は HTTP メソッドです。`GET` は最も一般的な HTTP メソッドですが、頻繁に使用されるいくつかの HTTP メソッドがあります。これらの HTTP メソッドは、しばしば [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) セマンティクスと関連付けられています。 + +|Method|CRUD| +|-|-| +|`GET`|読む(Read)| +|`POST`|作成(Create)| +|`PUT`|置換(Replace)| +|`PATCH`|更新(Update)| +|`DELETE`|削除(Delete)| + +### リクエストパス + +HTTP メソッドの直後には、リクエストの URI があります。これは、`/` で始まるパスと、`?` の後のオプションのクエリ文字列で構成されています。HTTP メソッドとパスは、Vapor がリクエストをルーティングするために使用するものです。 + +URI の後には HTTP バージョン、0個以上のヘッダが続き、最後にボディが続きます。これは `GET` リクエストなので、ボディはありません。 + +### ルーターメソッド + +このリクエストが Vapor でどのように処理されるか見てみましょう。 + +```swift +app.get("hello", "vapor") { req in + return "Hello, vapor!" +} +``` + +全ての一般的な HTTP メソッドは、`Application` 上で利用可能としてメソッドが提供されています。リクエストのパスを `/` で区切った 1 つ以上の文字列引数を受け取ります。 + +また、メソッドの後に `on` を使用して、このように書くこともできます。 + +```swift +app.on(.GET, "hello", "vapor") { ... } +``` + +このルートが登録されていると、上記のサンプル HTTP リクエストは、以下の HTTP レスポンスをもたらします。 + +```http +HTTP/1.1 200 OK +content-length: 13 +content-type: text/plain; charset=utf-8 + +Hello, vapor! +``` + +### ルートパラメータ + +HTTP メソッドとパスに基づいてリクエストを正常にルーティングしたので、次にパスを動的にしてみましょう。"vapor" の名前がパスとレスポンスの両方でハードコードされていることに注意してください。これを動的にして、`/hello/<任意の名前>` にアクセスすると、レスポンスが返されるようにしてみましょう。 + +```swift +app.get("hello", ":name") { req -> String in + let name = req.parameters.get("name")! + return "Hello, \(name)!" +} +``` + +":" で始まるパスコンポーネントを使用することで、これが動的なコンポーネントであることをルータに示しています。ここで提供される任意の文字列は、このルートにマッチするようになります。その後、`req.parameters` を使用して、文字列の値にアクセスできます。 + +もう一度サンプルのリクエストを実行すると、まだ vapor に挨拶するレスポンスが返されます。しかし、今度は `/hello/` の後に任意の名前を含めて、それがレスポンスに含まれることを確認できます。 `/hello/swift` を試してみましょう。 + +```http +GET /hello/swift HTTP/1.1 +content-length: 0 +``` +```http +HTTP/1.1 200 OK +content-length: 13 +content-type: text/plain; charset=utf-8 + +Hello, swift! +``` + +基本を理解したところで、各セッションをチェックし、パラメータやグループなどについて詳しく学んでください。 + +## ルート + +ルートは、特定の HTTP メソッドと URI パスに対するリクエストハンドラを指定します。また、追加のメタデータを格納することもできます。 + +### メソッド + +ルートは、様々な HTTP メソッドヘルパーを使用して、`Application` に直接登録できます。 + +```swift +// responds to GET /foo/bar/baz +app.get("foo", "bar", "baz") { req in + ... +} +``` + +ルートハンドラは、`ResponseEncodable` であるものを返すことをサポートしています。これには `Content` 、`async` クロージャ、および未来の値が `ResponseEncodable` である `EventLoopFuture` が含まれます。 + +コンパイラが戻り値のタイプを決定できない状況で、ルートの戻り値のタイプを指定するには、 `in` の前に `-> T` を使用します。 + +```swift +app.get("foo") { req -> String in + return "bar" +} +``` + +これらは、サポートされているルートヘルパーメソッドです: + +- `get` +- `post` +- `patch` +- `put` +- `delete` + +HTTP メソッドヘルパーに加えて、HTTP メソッドを入力パラメータとして受け入れる `on` 関数があります。 + +```swift +// responds to OPTIONS /foo/bar/baz +app.on(.OPTIONS, "foo", "bar", "baz") { req in + ... +} +``` + +### パスコンポーネント + +各ルート登録メソッドは、`PathComponent` の多様なリストを受け入れます。このタイプは文字列リテラルによって表現可能であり、4つのケースがあります: + +- 定数 (`foo`) +- パラーメータ (`:foo`) +- Anything (`*`) +- キャッチオール (`**`) + +#### 定数 + +これは静的なルートコンポーネントです。この位置で正確に一致する文字列のみが許可されます。 + +```swift +// responds to GET /foo/bar/baz +app.get("foo", "bar", "baz") { req in + ... +} +``` + +#### パラメータ + +これは動的なルートコンポーネントです。この位置での任意の文字列が許可されます。パラメータコンポーネントは `:` 接頭辞で指定されます。`:` に続く文字列は、パラメータの名前として使用されます。後でリクエストからパラメータの値にアクセスするために名前を使用できます。 + +```swift +// responds to GET /foo/bar/baz +// responds to GET /foo/qux/baz +// ... +app.get("foo", ":bar", "baz") { req in + ... +} +``` + +#### Anything + +これはパラメータと非常に似ていますが、値は破棄されます。このコンポーネントは、単に `*` として指定されます。 + +```swift +// responds to GET /foo/bar/baz +// responds to GET /foo/qux/baz +// ... +app.get("foo", "*", "baz") { req in + ... +} +``` + +#### キャッチオール + +これは、1つ以上のコンポーネントに一致する動的なルートコンポーネントです。 `**` だけで指定します。この位置以降の文字列はリクエストでマッチします。 + +```swift +// responds to GET /foo/bar +// responds to GET /foo/bar/baz +// ... +app.get("foo", "**") { req in + ... +} +``` + +### パラメータ + +パラメータパスコンポーネント(`:` で接頭辞されたもの) を使用すると、その位置の URI の値が `req.parameters` に格納されます。パスコンポーネントの名前を使用して、値にアクセスできます。 + +```swift +// responds to GET /hello/foo +// responds to GET /hello/bar +// ... +app.get("hello", ":name") { req -> String in + let name = req.parameters.get("name")! + return "Hello, \(name)!" +} +``` + +!!! tip + ルートパスに `:name` が含まれているので、`rep.parameters.get` が `nil` を返すことはないと核心しています。ただし、ミッドウェア内でルートパラメータにアクセスしたり、複数のルートによってトリガされるコード内でこれを行う場合は、`nil` の可能性を処理する必要があります。 + +!!! tip + URL クエリパラメータを取得したい場合、例えば `/hello/?name=foo` 、URLのクエリ文字列で URL エンコードされたデータを処理するために、Vapor の Content API を使用する必要があります。詳しくは[`Content` リファレンス](content.md)を参照してください。 + +`req.parameters.get` は、パラメータを自動的に `LosslessStringConvertible` タイプにキャストすることもサポートしています。 + +```swift +// responds to GET /number/42 +// responds to GET /number/1337 +// ... +app.get("number", ":x") { req -> String in + guard let int = req.parameters.get("x", as: Int.self) else { + throw Abort(.badRequest) + } + return "\(int) is a great number" +} +``` + +Catchall (`**`) によって一致した URI の値は、`req.parameters` に `[String]` として格納されます。これらのコンポーネントにアクセスするには、 `req.parameters.getCatchall` を使用します。 + +```swift +// responds to GET /hello/foo +// responds to GET /hello/foo/bar +// ... +app.get("hello", "**") { req -> String in + let name = req.parameters.getCatchall().joined(separator: " ") + return "Hello, \(name)!" +} +``` + +### Body ストリーミング + +`on` メソッドを使用してルートを登録するとき、リクエストの本文がどのように処理されるかを指定できます。デフォルトでは、ハンドラを呼び出す前にリクエストの本文がメモリに収集されます。これは、アプリケーションが非同期に適切なリクエストを読み取るにもかかわらず、リクエストのコンテンツの複号化を同期的に行うために便利です。 + +デフォルトでは、Vapor はストリーミング本文の収集を 16KB までに制限します。これは `app.routes` を使用して設定できます。 + +```swift +// Increases the streaming body collection limit to 500kb +app.routes.defaultMaxBodySize = "500kb" +``` + +収集されるストリーミングボディが設定された制限を超えた場合、`413 Payload Too Large` エラーが投げられる。 + +個々のルートに対してリクエストボディの収集ストラテジーを設定するには、`body` パラメータを使います。 + +```swift +// Collects streaming bodies (up to 1mb in size) before calling this route. +app.on(.POST, "listings", body: .collect(maxSize: "1mb")) { req in + // Handle request. +} +``` + +`collect` に `maxSize` が渡されると、そのルートのアプリケーションのデフォルトを上書きします。アプリケーションのデフォルトを使用するには、`maxSize` 引数を省略します。 + +大きなリクエスト、例えばファイルのアップロードの場合、リクエスト本文をバッファに収集すると、システムのメモリが逼迫する可能性があります。リクエスト本文が収集されないようにするには、`stream` 戦略を使用します。 + +```swift +// Request body will not be collected into a buffer. +app.on(.POST, "upload", body: .stream) { req in + ... +} +``` + +リクエストの本文がストリームされる場合、`req.body.data` は `nil` になります。各チャンクがルートに送信されるたびに `req.body.drain` を使用して処理する必要があります。 + +### 大文字・小文字を区別しないルーティング + +ルーティングのデフォルトの振る舞いは、大文字・小文字を区別するとともに、大文字・小文字を保持します。`Constant` パスのコンポーネントは、ルーティングの目的のために、大文字・小文字を区別しないが大文字・小文字を保持する方法で扱うことができます。この振る舞いを有効にするには、アプリケーションの起動前に設定して下さい。: +```swift +app.routes.caseInsensitive = true +``` +元のリクエストに変更は加えられません。ルートハンドラは、リクエストのパスコンポーネントを変更せずに受け取ります。 + + +### ルートの表示 + +アプリケーションのルートにアクセスするには、`Routes` サービスを使用するか、`app.routes` を使用します。 + +```swift +print(app.routes.all) // [Route] +``` + +Vapor には `routes` コマンドも同梱されており、利用可能な全てのルートを ASCII 形式のタブで表示してくれます。 + +```sh +$ swift run App routes ++--------+----------------+ +| GET | / | ++--------+----------------+ +| GET | /hello | ++--------+----------------+ +| GET | /todos | ++--------+----------------+ +| POST | /todos | ++--------+----------------+ +| DELETE | /todos/:todoID | ++--------+----------------+ +``` + +### メタデータ + +すべてのルート登録メソッドは、作成された `Route` を返します。これにより、ルートの`userInfo` 辞書にメタデータを追加できます。説明を追加するようなデフォルトのメソッドも用意されています。 + +```swift +app.get("hello", ":name") { req in + ... +}.description("says hello") +``` + +## ルートグループ + +ルートのグループ化により、パスの接頭辞または特定のミドルウェアを持つルートのセットを作成できます。グループ化は、ビルダーとクロージャベースの構文をサポートしています。 + +すべてのグループ化メソッドは `RouteBuilder` を返します。つまり、グループを他のルート構築メソッドと無限に組み合わせたり、ネストにしたりすることができます。 + +### パス接頭辞 + +パス接頭辞付きのルートグループを使用すると、1つ以上のパスコンポーネントをルートグループの先頭に追加できます。 + +```swift +let users = app.grouped("users") +// GET /users +users.get { req in + ... +} +// POST /users +users.post { req in + ... +} +// GET /users/:id +users.get(":id") { req in + let id = req.parameters.get("id")! + ... +} +``` + +`get` や `post` などのメソッドに渡すことができる任意のパスコンポーネントを、`grouped` に渡すことができます。代替として、クロージャベースの構文もあります。 + +```swift +app.group("users") { users in + // GET /users + users.get { req in + ... + } + // POST /users + users.post { req in + ... + } + // GET /users/:id + users.get(":id") { req in + let id = req.parameters.get("id")! + ... + } +} +``` + +パスの接頭辞を持つルートグループをネストすると、CRUD API を簡潔に定義できます。 + +```swift +app.group("users") { users in + // GET /users + users.get { ... } + // POST /users + users.post { ... } + + users.group(":id") { user in + // GET /users/:id + user.get { ... } + // PATCH /users/:id + user.patch { ... } + // PUT /users/:id + user.put { ... } + } +} +``` + +### ミッドウェア + +パスコンポーネントの接頭辞に、ルートグループにミドルウェアを追加することもできます。 + +```swift +app.get("fast-thing") { req in + ... +} +app.group(RateLimitMiddleware(requestsPerMinute: 5)) { rateLimited in + rateLimited.get("slow-thing") { req in + ... + } +} +``` + + +これは特に、異なる認証ミドルウェアでルートのサブセットを保護する場合に便利です。 + +```swift +app.post("login") { ... } +let auth = app.grouped(AuthMiddleware()) +auth.get("dashboard") { ... } +auth.get("logout") { ... } +``` + +## リダイレクト + +リダイレクトは、SEO のために古いロケーションを新しいロケーションに転送したり、認証されていないユーザーをログインページにリダイレクトしたり、新しいバージョンの API との広報互換性を維持したりするなど、多くのシナリオで役立ちます。 + +リクエストをリダイレクトするには、次のようにします: + +```swift +req.redirect(to: "/some/new/path") +``` + +また、リダイレクトのタイプを指定することもできます。例えば、ページを永久にリダイレクトして SEO が正しく更新されるようにするには次のようにします: + +```swift +req.redirect(to: "/some/new/path", type: .permanent) +``` + +異なる `RedirectType` は以下の通りです: + +* `.permanent` - **301 Permanent** をリダイレクトします +* `.normal` - **303 see other** をリダイレクトします。これは Vapor デフォルトで、クライアントにリダイレクトを **GET** リクエストでフォローするように指示します。 +* `.temporary` - **307 Temporary** をリダイレクトします。これにより、リクエストで使用された HTTP メソッドをクライアントが保持するように支持されます。 + +> 適切なリダイレクションステータスコードを選択するには、[the full list](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_redirection) をチェックして下さい。 diff --git a/docs/basics/validation.ja.md b/docs/basics/validation.ja.md new file mode 100644 index 00000000..9f73f9ff --- /dev/null +++ b/docs/basics/validation.ja.md @@ -0,0 +1,291 @@ +# バリデーション + +Vapor のバリデーション API は、データをデコードする前に、[コンテンツ](content.ja.md) API を使って入力リクエストの検証を行うのに役立ちます。 + +## はじめに + +Swift の型安全な `Codable` プロトコルを深く統合している Vapor は、動的型付け言語に比べてデータバリデーションについてそれほど心配する必要はありません。しかし、明示的なバリデーションを選択する理由はいくつかあります。 + +### 人間が読みやすいエラー + +[コンテンツ](content.ja.md) API を使用して構造体をデコードする際には、データが無効である場合にエラーが発生します。しかしながら、これらのエラーメッセージは時として人間が読みやすいものではありません。例えば、次のような文字列ベースの enum を見て下さい。 + +```swift +enum Color: String, Codable { + case red, blue, green +} +``` + +もし、ユーザーが `Color` 型のプロパティに `"purple"` という文字列を渡そうとした場合、次のようなエラーが発生します。 + +``` +Cannot initialize Color from invalid String value purple for key favoriteColor +``` + +このエラーは技術的に正しく、エンドポイントを無効な値から守るのに成功していますが、ユーザーにミスと利用可能な選択肢についてもっとよく情報を伝えることができます。バリデーション API を使用すると、次のようなエラーを生成できます。 + +``` +favoriteColor is not red, blue, or green +``` + +さらに、`Codable` は最初のエラーが発生した時点で型のデコードを試みるのをやめます。つまり、リクエストに多くの無効なプロパティがあっても、ユーザーは最初のエラーしか見ることができません。バリデーション API は、一度のリクエストで全てのバリデーション失敗を報告します。 + +### 特定のバリデーション + +`Codable` は型のバリデーションをうまく扱いますが、時にはそれ以上のことをしたい事もあります。例えば、文字列の内容を検証したり、整数のサイズを検証したりすることです。バリデーション API には、メールアドレス、文字セット、整数の範囲など、データの検証に役立つバリデータがあります。 + +## Validatable + +リクエストをバリデーションするためには、`Validations` コレクションを生成する必要があります。これは通常、既存の型を `Validatable` に準拠させることによって行われます。 + +`POST /users` エンドポイントにバリデーションを追加する方法を見てみましょう。このガイドでは、既に[コンテンツ](content.ja.md) APIについて熟知していることを前提としています。 + +```swift +enum Color: String, Codable { + case red, blue, green +} + +struct CreateUser: Content { + var name: String + var username: String + var age: Int + var email: String + var favoriteColor: Color? +} + +app.post("users") { req -> CreateUser in + let user = try req.content.decode(CreateUser.self) + // Do something with user. + return user +} +``` + +### バリデーションの追加 + +最初のステップは、この場合は `CreateUser` である、デコードする型を `Validatable` に準拠させることです。これは拡張を使って行うことができます。 + +```swift +extension CreateUser: Validatable { + static func validations(_ validations: inout Validations) { + // Validations go here. + } +} +``` + +静的メソッド `validations(_:)` は `CreateUser` がバリデーションされたときに呼び出されます。実行したいバリデーションは、提供された `Validations` コレクションに追加する必要があります。ユーザーのメールが有効であることを要求する簡単なバリデーションを追加する方法を見てみましょう。 + +```swift +validations.add("email", as: String.self, is: .email) +``` + +最初のパラメータは期待される値のキーで、この場合は `"email"` です。これは検証される型のプロパティ名と一致している必要があります。二番目のパラメータ `as` は期待される型で、この場合は `String` です。型は通常、プロパティの型と一致しますが、常にそうとは限りません。最後に、三番目のパラメータ `is` の後に一つまたは複数のバリデータを追加できます。この場合、値がメールアドレスであるかをチェックする単一のバリデータが追加されています。 + +### リクエストコンテンツのバリデーション + +型を `Validatable` に準拠させたら、静的な `validate(content:)` 関数を使ってリクエストコンテンツをバリデーションできます。ルートハンドラの `req.content.decode(CreateUser.self)` の前に次の行を追加してください。 + + +```swift +try CreateUser.validate(content: req) +``` + +これで、次のように無効なメールを含むリクエストを送信してみてください。: + +```http +POST /users HTTP/1.1 +Content-Length: 67 +Content-Type: application/json + +{ + "age": 4, + "email": "foo", + "favoriteColor": "green", + "name": "Foo", + "username": "foo" +} +``` + +次のようなエラーが返されるはずです。: + +``` +email is not a valid email address +``` + +### リクエストクエリのバリデーション + +`Validatable` に準拠している型には、リクエストのクエリ文字列をバリデーションする `validate(query:)` もあります。ルートハンドラに次の行を追加してください。 + +```swift +try CreateUser.validate(query: req) +req.query.decode(CreateUser.self) +``` + +これで、クエリ文字列に無効なメールを含む次のようなリクエストを送信してみてください。 + +```http +GET /users?age=4&email=foo&favoriteColor=green&name=Foo&username=foo HTTP/1.1 + +``` + +次のようなエラーが返されるはずです。: + +``` +email is not a valid email address +``` + +### 整数のバリデーション + +素晴らしい、次に `age` に対する検証を追加してみましょう。 + +```swift +validations.add("age", as: Int.self, is: .range(13...)) +``` + +年齢の検証では、年齢が `13` 歳以上であることを要求します。もし上記と同じリクエストを試したら、今度は新しいエラーが表示されるはずです。: + +``` +age is less than minimum of 13, email is not a valid email address +``` + +### 文字列のバリデーション + +次に、`name` と `username` に対する検証を追加しましょう。 + +```swift +validations.add("name", as: String.self, is: !.empty) +validations.add("username", as: String.self, is: .count(3...) && .alphanumeric) +``` + +名前の検証では、!演算子を使って `.empty` 検証を反転させます。これにより、文字列が空でないことが必要です。 + +ユーザーネームの検証では、`&&` を使って2つのバリデーターを組み合わせます。これにより、文字列が少なくとも3文字以上であり、_かつ_ 英数字のみで構成されていることが必要です。 + +### Enumのバリデーション + +最後に、提供された `favoriteColor` が有効かどうかをチェックする少し高度な検証を見てみましょう。 + + + +```swift +validations.add( + "favoriteColor", as: String.self, + is: .in("red", "blue", "green"), + required: false +) +``` + +無効な値から `Color` をデコードすることは不可能なため、この検証では基本型として `String` を使用しています。`.in` バリデーターを使って、値が有効なオプションであるかどうかを確認します。:赤、青、または緑です。この値はオプショナルなので、このキーがリクエストデータから欠落している場合に検証が失敗しないように、`required` はfalseに設定されます。 + +お気に入りの色の検証は、キーが欠落している場合には通過しますが、`null` が提供された場合には通過しません。`null` をサポートしたい場合は、検証の型を `String?` に変更し、`.nil ||`("is nil or ..."と読みます)を使用します。 + +```swift +validations.add( + "favoriteColor", as: String?.self, + is: .nil || .in("red", "blue", "green"), + required: false +) +``` + +### カスタムエラー + +`Validations` や `Validator` にカスタムで人が読めるエラーを追加したい場合があります。そのためには、デフォルトのエラーを上書きする追加の `customFailureDescription` パラメータを提供するだけです。 + +```swift +validations.add( + "name", + as: String.self, + is: !.empty, + customFailureDescription: "Provided name is empty!" +) +validations.add( + "username", + as: String.self, + is: .count(3...) && .alphanumeric, + customFailureDescription: "Provided username is invalid!" +) +``` + +## バリデーター + +以下は、現在サポートされているバリデーターと、それらが何をするのかの簡単な説明のリストです。 + +|Validation|説明| +|-|-| +|`.ascii`|ASCⅡ 文字のみを使います。| +|`.alphanumeric`|英数字のみを含みます。| +|`.characterSet(_:)`|指定された `CharacterSet` からの文字のみを含みます。| +|`.count(_:)`|コレクションのカウントが指定された範囲内です。| +|`.email`|有効なメールアドレスを含みます。| +|`.empty`|コレクションが空です。| +|`.in(_:)`|値が提供された `Collection` の中にあります。| +|`.nil`|値が `null` です。| +|`.range(_:)`|値が提供された `Range` の内です。| +|`.url`|有効な URL を含みます。| + +バリデーターはまた、演算子を使用して複雑な検証を組み立てるために組み合わせることができます。 + +|演算子|位置|説明| +|-|-|-| +|`!`|前置|バリデーターを反転させ、反対のものを要求します。| +|`&&`|中置|2つのバリデーターを組み合わせ、両方を要求する。| +|`||`|中置|2つのバリデーターを組み合わせ、1つを要求する。| + +## カスタムバリデーション + +郵便番号のカスタムバリデーターを作成することで、バリデーションフレームワークの機能を拡張できます。このセクションでは、郵便番号を検証するカスタムバリデーターを作成する手順を説明します。 + +まず、`ZipCode` 検証結果を表す新しいタイプを作成します。この構造体は、特定の文字列が有効な郵便番号であるかどうかを報告する役割を担います。 + +```swift +extension ValidatorResults { + /// Represents the result of a validator that checks if a string is a valid zip code. + public struct ZipCode { + /// Indicates whether the input is a valid zip code. + public let isValidZipCode: Bool + } +} +``` + +次に、新しいタイプを `ValidatorResult` に適合させます。これは、カスタムバリデーターから期待される振る舞いを定義します。 + +```swift +extension ValidatorResults.ZipCode: ValidatorResult { + public var isFailure: Bool { + !self.isValidZipCode + } + + public var successDescription: String? { + "is a valid zip code" + } + + public var failureDescription: String? { + "is not a valid zip code" + } +} +``` + +最後に、郵便番号のバリデーションロジックを実装します。正規表現を使用して、入力文字列がアメリカの郵便番号の形式に一致しているかをチェックします。 + +```swift +private let zipCodeRegex: String = "^\\d{5}(?:[-\\s]\\d{4})?$" + +extension Validator where T == String { + /// Validates whether a `String` is a valid zip code. + public static var zipCode: Validator { + .init { input in + guard let range = input.range(of: zipCodeRegex, options: [.regularExpression]), + range.lowerBound == input.startIndex && range.upperBound == input.endIndex + else { + return ValidatorResults.ZipCode(isValidZipCode: false) + } + return ValidatorResults.ZipCode(isValidZipCode: true) + } + } +} +``` + +カスタムの `zipCode` バリデーターを定義したので、アプリケーションで郵便番号を検証する際にこれを使用できます。バリデーションコードに以下の行を追加するだけです: + +```swift +validations.add("zipCode", as: String.self, is: .zipCode) +``` diff --git a/mkdocs.yml b/mkdocs.yml index c573efb2..683f3499 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -491,9 +491,10 @@ plugins: Basics: 基礎 Client: クライアント Commands: コマンド - Content: 内容 + Content: コンテンツ Contributing: 貢献 Contributing Guide: 貢献ガイド + Controllers: コントローラー Crypto: 暗号 Custom Tags: カスタムタグ Deploy: デプロイ