diff --git a/3.0/docs/vapor/client.md b/3.0/docs/vapor/client.md new file mode 100644 index 00000000..9854b7e2 --- /dev/null +++ b/3.0/docs/vapor/client.md @@ -0,0 +1,45 @@ +# Using Client + +[`Client`](#fixme) is a convenience wrapper around the lower level [HTTP → Client](../http/client.md). It automatically parses things like hostname and port from URIs and helps you encode and decode [Content](content.md). + +```swift +let res = try req.client().get("http://vapor.codes") +print(res) // Future +``` + +## Container + +The first thing you will need is a service [Container](../getting-started/services.md#container) to create your client. + +If you are making this external API request as the result of an incoming request to your server, you should use the `Request` container to create a client. This is most often the case. + +If you need a client during boot, use the `Application` or if you are in a `Command` use the command context's container. + +Once you have a `Container`, use the [`client()`](#fixme) method to create a `Client`. + +```swift +// Creates a generic Client +let client = try container.client() +``` + +## Send + +Once you have a `Client`, you can use the [`send(...)`](#fixme) method to send a `Request`. Note that the request URI must include a scheme and hostname. + +```swift +let req: Request ... +let res = try client.send(req) +print(res) // Future +``` + +You can also use the convenience methods like [`get(...)`](#fixme), [`post(...)`](#fixme), etc. + +```swift +let user: User ... +let res = try client.post("http://api.vapor.codes/users") { post in + try post.content.encode(user) +} +print(res) // Future +``` + +See [Content](./content.md) for more information on encoding and decoding content to messages. diff --git a/3.0/docs/vapor/content.md b/3.0/docs/vapor/content.md index 9fb21928..de225165 100644 --- a/3.0/docs/vapor/content.md +++ b/3.0/docs/vapor/content.md @@ -2,11 +2,15 @@ In Vapor 3, all content types (JSON, protobuf, [URLEncodedForm](../url-encoded-form/getting-started.md), [Multipart](../multipart/getting-started.md), etc) are treated the same. All you need to parse and serialize content is a `Codable` class or struct. -For this introduction, we will use JSON as an example. But keep in mind the API is the same for any supported content type. +For this introduction, we will use mostly JSON as an example. But keep in mind the API is the same for any supported content type. -## Request +## Server -Let's take a look at how you would parse the following HTTP request. +This first section will go over decoding and encoding messages sent between your server and connected clients. See the [client](#client) section for encoding and decoding content in messages sent to external APIs. + +### Request + +Let's take a look at how you would parse the following HTTP request sent to your server. ```http POST /login HTTP/1.1 @@ -18,12 +22,9 @@ Content-Type: application/json } ``` -### Decode Request - -First, create a struct or class that represents the data you expect. +First, create a struct or class that represents the data you expect. ```swift -import Foundation import Vapor struct LoginRequest: Content { @@ -32,24 +33,42 @@ struct LoginRequest: Content { } ``` -Then simply conform this struct or class to `Content`. -Now we are ready to decode that HTTP request. +Notice the key names exactly match the keys in the request data. The expected data types also match. Next conform this struct or class to `Content`. + +#### Decode + +Now we are ready to decode that HTTP request. Every [`Request`](#fixme) has a [`ContentContainer`](#fixme) that we can use to decode content from the message's body. ```swift -router.post("login") { req -> Future in - return req.content.decode(LoginRequest.self).map(to: HTTPStatus.self) { loginRequest in +router.post("login") { req -> Future in + return req.content.decode(LoginRequest.self).map { loginRequest in print(loginRequest.email) // user@vapor.codes print(loginRequest.password) // don't look! - return .ok + return HTTPStatus.ok } } ``` -We use `.map(to:)` here since `req.content.decode(_:)` returns a [future](../async/getting-started.md). +We use `.map(to:)` here since `decode(...)` returns a [future](../async/getting-started.md). -### Other Request Types +!!! note + Decoding content from requests is asynchronous because HTTP allows bodies to be split into multiple parts using chunked transfer encoding. -Since the request in the previous example declared JSON as its content type, Vapor knows to use a JSON decoder automatically. This same method would work just as well for the following request. +#### Router + +To help make decoding content from incoming requests easier, Vapor offers a few extensions on [`Router`](#fixme) to do this automatically. + +```swift +router.post(LoginRequest.self, at: "login") { req, loginRequest in + print(loginRequest.email) // user@vapor.codes + print(loginRequest.password) // don't look! + return HTTPStatus.ok +} +``` + +#### Detect Type + +Since the HTTP request in this example declared JSON as its content type, Vapor knows to use a JSON decoder automatically. This same method would work just as well for the following request. ```http POST /login HTTP/1.1 @@ -58,12 +77,23 @@ Content-Type: application/x-www-form-urlencoded email=user@vapor.codes&don't+look! ``` +All HTTP requests must include a content type to be valid. Because of this, Vapor will automatically choose an appropriate decoder or error if it encounters an unknown media type. + !!! tip - You can configure which encoders/decoders Vapor uses. Read on to learn more. + You can [configure](#configure) the default encoders and decoders Vapor uses. + +#### Custom -## Response +You can always override Vapor's default decoder and pass in a custom one if you want. -Let's take a look at how you would create the following HTTP response. +```swift +let user = try req.content.decode(User.self, using: JSONDecoder()) +print(user) // Future +``` + +### Response + +Let's take a look at how you would create the following HTTP response from your server. ```http HTTP/1.1 200 OK @@ -75,12 +105,9 @@ Content-Type: application/json } ``` -### Encode Response - Just like decoding, first create a struct or class that represents the data that you are expecting. ```swift -import Foundation import Vapor struct User: Content { @@ -89,40 +116,196 @@ struct User: Content { } ``` -Then just conform this struct or class to `Content`. Now we are ready to encode that HTTP response. +Then just conform this struct or class to `Content`. + +#### Encode + +Now we are ready to encode that HTTP response. ```swift router.get("user") { req -> User in - return User( - name: "Vapor User", - email: "user@vapor.codes" - ) + return User(name: "Vapor User", email: "user@vapor.codes") } ``` -### Other Response Types +This will create a default `Response` with `200 OK` status code and minimal headers. You can customize the response using a convenience [`encode(...)`](#fixme) method. + +```swift +router.get("user") { req -> Future in + return User(name: "Vapor User", email: "user@vapor.codes") + .encode(status: .created) +} +``` + +#### Override Type Content will automatically encode as JSON by default. You can always override which content type is used using the `as:` parameter. ```swift -try res.content.encode(user, as: .formURLEncoded) +try res.content.encode(user, as: .urlEncodedForm) ``` You can also change the default media type for any class or struct. ```swift struct User: Content { - /// See Content.defaultMediaType - static let defaultMediaType: MediaType = .formURLEncoded + /// See `Content`. + static let defaultContentType: MediaType = .urlEncodedForm ... } ``` -## Configuring Content +## Client -Use `ContentConfig` to register custom encoder/decoders for your application. These custom coders will be used anywhere you do `content.encode`/`content.decode`. +Encoding content to HTTP requests sent by [`Client`](#fixme)s is similar to encoding HTTP responses returned by your server. + +### Request + +Let's take a look at how we can encode the following request. + +```http +POST /login HTTP/1.1 +Host: api.vapor.codes +Content-Type: application/json + +{ + "email": "user@vapor.codes", + "password": "don't look!" +} +``` + +#### Encode + +First, create a struct or class that represents the data you expect. + +```swift +import Vapor + +struct LoginRequest: Content { + var email: String + var password: String +} +``` + +Now we are ready to make our request. Let's assume we are making this request inside of a route closure, so we will use the _incoming_ request as our container. + +```swift +let loginRequest = LoginRequest(email: "user@vapor.codes", password: "don't look!") +let res = try req.client().post("https://api.vapor.codes/login") { loginReq in + // encode the loginRequest before sending + try loginReq.content.encode(loginRequest) +} +print(res) // Future +``` + +### Response + +Continuing from our example in the encode section, let's see how we would decode content from the client's response. + +```http +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "name": "Vapor User", + "email": "user@vapor.codes" +} +``` + +First of course we must create a struct or class to represent the data. + +```swift +import Vapor + +struct User: Content { + var name: String + var email: String +} +``` + +#### Decode + +Now we are ready to decode the client response. + +```swift +let res: Future // from the Client + +let user = res.flatMap { try $0.content.decode(User.self) } +print(user) // Future +``` + +### Example + +Let's now take a look at our complete [`Client`](#fixme) request that both encodes and decodes content. + +```swift +// Create the LoginRequest data +let loginRequest = LoginRequest(email: "user@vapor.codes", password: "don't look!") +// POST /login +let user = try req.client().post("https://api.vapor.codes/login") { loginReq in + // Encode Content before Request is sent + return try loginReq.content.encode(loginRequest) +}.flatMap { loginRes in + // Decode Content after Response is received + return try loginRes.content.decode(User.self) +} +print(user) // Future +``` + +## Query String + +URL-Encoded Form data can be encoded and decoded from an HTTP request's URI query string just like content. All you need is a class or struct that conforms to [`Content`](#fixme). In these examples, we will be using the following struct. + +```swift +struct Flags: Content { + var search: String? + var isAdmin: Bool? +} +``` + +### Decode + +All [`Request`](#fixme)s have a [`QueryContainer`](#fixme) that you can use to decode the query string. + +```swift +let flags = try req.query.decode(Flags.self) +print(flags) // Flags +``` + +### Encode + +You can also encode content. This is useful for encoding query strings when using [`Client`](#fixme). + +```swift +let flags: Flags ... +try req.query.encode(flags) +``` + +## JSON + +JSON is a very popular encoding format for APIs and the way in which dates, data, floats, etc are encoded is non-standard. Because of this, Vapor makes it easy to use custom [`JSONDecoder`](#fixme)s when you interact with other APIs. + +```swift +// Conforms to Encodable +let user: User ... +// Encode JSON using custom date encoding strategy +try req.content.encode(json: user, using: .custom(dates: .millisecondsSince1970)) +``` + +You can also use this method for decoding. + +```swift +// Decode JSON using custom date encoding strategy +let user = try req.content.decode(json: User.self, using: .custom(dates: .millisecondsSince1970)) +``` + +If you would like to set a custom JSON encoder or decoder globally, you can do so using [configuration](#configure). + +## Configure + +Use [`ContentConfig`](#fixme) to register custom encoder/decoders for your application. These custom coders will be used anywhere you do `content.encode`/`content.decode`. ```swift /// Create default content config diff --git a/3.0/docs/vapor/getting-started.md b/3.0/docs/vapor/getting-started.md index a4624d3f..11842d29 100644 --- a/3.0/docs/vapor/getting-started.md +++ b/3.0/docs/vapor/getting-started.md @@ -26,3 +26,6 @@ let package = Package( Use `import Vapor` to access the APIs. +## API Docs + +The rest of this guide will give you an overview of what is available in the Vapor package. As always, feel free to visit the [API docs](http://api.vapor.codes/vapor/latest/Vapor/index.html) for more in-depth information. \ No newline at end of file diff --git a/3.0/docs/vapor/websocket.md b/3.0/docs/vapor/websocket.md new file mode 100644 index 00000000..3c9937e3 --- /dev/null +++ b/3.0/docs/vapor/websocket.md @@ -0,0 +1,88 @@ +# Using WebSockets + +Vapor includes convenience methods for working with the lower level WebSocket [client](../websocket/overview.md#client) and [server](../websocket/overview.md#server). + +## Server + +Vapor's WebSocket server includes the ability to route incoming requests just like its HTTP server. + +When Vapor's main HTTP [`Server`](#fixme) boots it will attempt to create a [`WebSocketServer`](#fixme). If one is registered, it will be added as an HTTP upgrade handler to the server. + +So to create a WebSocket server, all you need to do is register one in [`configure.swift`](../getting-started/structure.md#configureswift). + +```swift +// Create a new NIO websocket server +let wss = NIOWebSocketServer.default() + +// Add WebSocket upgrade support to GET /echo +wss.get("echo") { ws, req in + // Add a new on text callback + ws.onText { ws, text in + // Simply echo any received text + ws.send(text) + } +} + +// Register our server +services.register(wss, as: WebSocketServer.self) +``` + +That's it. Next time you boot your server, you will be able to perform a WebSocket upgrade at `GET /echo`. You can test this using a simple command line tool called [`wsta`](https://github.com/esphen/wsta) available for macOS and Linux. + +```sh +$ wsta ws://localhost:8080/echo +Connected to ws://localhost:8080/echo +hello, world! +hello, world! +``` + +### Parameters + +Like Vapor's HTTP router, you can also use routing parameters with your WebSocket server. + +```swift +// Add WebSocket upgrade support to GET /chat/:name +wss.get("chat", String.parameter) { ws, req in + let name = try req.parameters.next(String.self) + ws.send("Welcome, \(name)!") + + // ... +} +``` + +Now let's test this new route: + +```sh +$ wsta ws://localhost:8080/chat/Vapor +Connected to ws://localhost:8080/chat/Vapor +Welcome, Vapor! +``` + +## Client + +Vapor also supports connecting to WebSocket servers as a client. The easiest way to connect to a WebSocket server is through the [`webSocket(...)`](#fixme) method on [`Client`](#fixme). + +For this example, we will assume our application connects to a WebSocket server in [`boot.swift`](../getting-started/structure.md#bootswift) + +```swift +// connect to echo.websocket.org +let done = try app.client().webSocket("ws://echo.websocket.org").flatMap { ws -> Future in + // setup an on text callback that will print the echo + ws.onText { ws, text in + print("rec: \(text)") + // close the websocket connection after we recv the echo + ws.close() + } + + // when the websocket first connects, send message + ws.send("hello, world!") + + // return a future that will complete when the websocket closes + return ws.onClose +} + +print(done) // Future + +// wait for the websocket to close +try done.wait() +``` \ No newline at end of file diff --git a/3.0/mkdocs.yml b/3.0/mkdocs.yml index 032fd07a..7721b9fa 100644 --- a/3.0/mkdocs.yml +++ b/3.0/mkdocs.yml @@ -106,14 +106,16 @@ pages: - 'Overview': 'validation/overview.md' - 'Vapor': - 'Getting Started': 'vapor/getting-started.md' + - 'Client': 'vapor/client.md' - 'Content': 'vapor/content.md' + - 'WebSocket': 'vapor/websocket.md' - 'WebSocket': - 'Getting Started': 'websocket/getting-started.md' - 'Overview': 'websocket/overview.md' -- 'Version (3.0-rc)': +- 'Version (3.0)': - '1.5': 'version/1_5.md' - '2.0': 'version/2_0.md' - - '3.0-rc': 'version/3_0.md' + - '3.0': 'version/3_0.md' - 'Support': 'version/support.md'