add content, client, websocket sections

This commit is contained in:
tanner0101 2018-05-03 16:22:26 -04:00
parent 5d1565e9be
commit 7c8d45905e
5 changed files with 355 additions and 34 deletions

45
3.0/docs/vapor/client.md Normal file
View File

@ -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<Response>
```
## 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<Response>
```
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<Response>
```
See [Content](./content.md) for more information on encoding and decoding content to messages.

View File

@ -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<HTTPStatus> 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<User>
```
### 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<Response> 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>
```
### 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<Response> // from the Client
let user = res.flatMap { try $0.content.decode(User.self) }
print(user) // Future<User>
```
### 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<User>
```
## 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

View File

@ -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.

View File

@ -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<Void> 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<Void>
// wait for the websocket to close
try done.wait()
```

View File

@ -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'