mirror of https://github.com/vapor/docs.git
Merge branch 'master' of github.com:vapor/documentation
This commit is contained in:
commit
9268c3cee0
|
|
@ -1,3 +1,4 @@
|
|||
/.couscous
|
||||
.sass-cache
|
||||
.DS_Store
|
||||
leaf-pygment/dist
|
||||
|
|
|
|||
|
|
@ -0,0 +1,211 @@
|
|||
# API Authentication
|
||||
|
||||
This guide will introduce you to stateless authentication—a method of authentication commonly used for protecting API endpoints.
|
||||
|
||||
## Concept
|
||||
|
||||
In Computer Science (especially web frameworks), the concept of Authentication means verifying the _identity_ of a user. This is not to be confused with Authorization which verifies _privileges_ to a given resource
|
||||
|
||||
This package allows you to implement stateless authorization using the following tools:
|
||||
|
||||
- *`"Authorization"` header*: Used to send credentials in an HTTP request.
|
||||
- *Middleware*: Detects credentials in request and fetches authenticated user.
|
||||
- *Model*: Represents an authenticated user and its identifying information.
|
||||
|
||||
### Authorization Header
|
||||
|
||||
This packages makes use of two common authorization header formats: basic and bearer.
|
||||
|
||||
#### Basic
|
||||
|
||||
Basic authorization contains a username and password. They are joined together by a `:` and then base64 encoded.
|
||||
|
||||
A basic authorization header containing the username `Alladin` and password `OpenSesame` would look like this:
|
||||
|
||||
```http
|
||||
Authorization: Basic QWxhZGRpbjpPcGVuU2VzYW1l
|
||||
```
|
||||
|
||||
Although basic authorization can be used to authenticate each request to your server, most web applications usually create an ephemeral token for this purpose instead.
|
||||
|
||||
#### Bearer
|
||||
|
||||
Bearer authorization simply contains a token. A bearer authorization header containing the token `cn389ncoiwuencr` would look like this:
|
||||
|
||||
```http
|
||||
Authorization: Bearer cn389ncoiwuencr
|
||||
```
|
||||
|
||||
The bearer authorization header is very common in APIs since it can be sent easily with each request and contain an ephemeral token.
|
||||
|
||||
|
||||
### Middleware
|
||||
|
||||
The usage of Middleware is critical to this package. If you are not familiar with how Middleware works in Vapor, feel free to brush up by reading [Vapor → Middleware](../vapor/middleware.md).
|
||||
|
||||
Authentication middleware is responsible for reading the credentials from the request and fetching the identifier user. This usually means checking the `"Authorization"` header, parsing the credentials, and doing a database lookup.
|
||||
|
||||
For each model / authentication method you use, you will add one middleware to your application. All of this package's middlewares are composable, meaning you can add multiple middlewares to one route and they will work together. If one middleware fails to authorize a user, it will simply forward the request for the next middleware to try.
|
||||
|
||||
If you would like to ensure that a certain model's authentication has succeeded _before_ running your route, you must add an instance of [`GuardAuthenticationMiddleware`](https://api.vapor.codes/auth/latest/Authentication/Classes/GuardAuthenticationMiddleware.html).
|
||||
|
||||
### Model
|
||||
|
||||
Fluent models are _what_ the middlewares authenticate. Learn more about models by reading [Fluent → Models](../fluent/models.md). If authentication is succesful, the middleware will have fetched your model from the database and stored it on the request. This means you can access an authenticated model synchronously in your route.
|
||||
|
||||
In your route closure, you use the following methods to check for authentication:
|
||||
|
||||
- `authenticated(_:)`: Returns type if authenticated, `nil` if not.
|
||||
- `isAuthenticated(_:)`: Returns `true` if supplied type is authenticated.
|
||||
- `requireAuthenticated(_:)`: Returns type if authenticated, `throws` if not.
|
||||
|
||||
Typical usage looks like the following:
|
||||
|
||||
```swift
|
||||
// use middleware to protect a group
|
||||
let protectedGroup = router.group(...)
|
||||
|
||||
// add a protected route
|
||||
protectedGroup.get("test") { req in
|
||||
// require that a User has been authed by middleware or throw
|
||||
let user = try req.requireAuthenticated(User.self)
|
||||
|
||||
// say hello to the user
|
||||
return "Hello, \(user.name)."
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
## Methods
|
||||
|
||||
This package supports two basic types of stateless authentication.
|
||||
|
||||
- _Token_: Uses the bearer authorization header.
|
||||
- _Password_: Uses the basic authorization header.
|
||||
|
||||
For each authentication type, there is a separate middleware and model protocol.
|
||||
|
||||
### Password Authentication
|
||||
|
||||
Password authentication uses the basic authorization header (username and password) to verify a user. With this method, the username and password must be sent with each request to a protected endpoint.
|
||||
|
||||
To use password authentication, you will first need to conform your Fluent model to `PasswordAuthenticatable`.
|
||||
|
||||
```swift
|
||||
extension User: PasswordAuthenticatable {
|
||||
/// See `PasswordAuthenticatable`.
|
||||
static var usernameKey: WritableKeyPath<User, String> {
|
||||
return \.email
|
||||
}
|
||||
|
||||
/// See `PasswordAuthenticatable`.
|
||||
static var passwordKey: WritableKeyPath<User, String> {
|
||||
return \.passwordHash
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that the `passwordKey` should point to the _hashed_ password. Never store passwords in plaintext.
|
||||
|
||||
Once you have created an authenticatable model, the next step is to add middleware to your protected route.
|
||||
|
||||
```swift
|
||||
// Use user model to create an authentication middleware
|
||||
let password = User.basicAuthMiddleware(using: BCryptDigest())
|
||||
|
||||
// Create a route closure wrapped by this middleware
|
||||
router.grouped(password).get("hello") { req in
|
||||
///
|
||||
}
|
||||
```
|
||||
|
||||
Here we are using `BCryptDigest` as the [`PasswordVerifier`](https://api.vapor.codes/auth/latest/Authentication/Protocols/PasswordVerifier.html) since we are assuming the user's password is stored as a BCrypt hash.
|
||||
|
||||
Now, to fetch the authenticated user in the route closure, you can use [`requireAuthenticated(_:)`](https://api.vapor.codes/auth/latest/Authentication/Extensions/Request.html#/s:5Vapor7RequestC14AuthenticationE20requireAuthenticatedxxmKAD15AuthenticatableRzlF).
|
||||
|
||||
```swift
|
||||
let user = try req.requireAuthenticated(User.self)
|
||||
return "Hello, \(user.name)."
|
||||
```
|
||||
|
||||
The `requireAuthenticated` method will automatically throw an appropriate unauthorized error if the valid credentials were not supplied. Because of this, using [`GuardAuthenticationMiddleware`](https://api.vapor.codes/auth/latest/Authentication/Classes/GuardAuthenticationMiddleware.html) to protect the route from unauthenticated access is not required.
|
||||
|
||||
### Token Authentication
|
||||
|
||||
Token authentication uses the bearer authorization header (token) to lookup a token and its related user. With this method, the token must be sent with each request to a protected endpoint.
|
||||
|
||||
Unlike password authentication, token authentication relies on _two_ Fluent models. One for the token and one for the user. The token model should be a _child_ of the user model.
|
||||
|
||||
Here is an example of a very basic `User` and associated `UserToken`.
|
||||
|
||||
```swift
|
||||
struct User: Model {
|
||||
var id: Int?
|
||||
var name: String
|
||||
var email: String
|
||||
var passwordHash: String
|
||||
|
||||
var tokens: Children<User, UserToken> {
|
||||
return children(\.userID)
|
||||
}
|
||||
}
|
||||
|
||||
struct UserToken: Model {
|
||||
var id: Int?
|
||||
var string: String
|
||||
var userID: User.ID
|
||||
|
||||
var user: Parent<UserToken, User> {
|
||||
return parent(\.userID)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The first step to using token authentication is to conform your user and token models to their respective `Authenticatable` protocols.
|
||||
|
||||
```swift
|
||||
extension UserToken: Token {
|
||||
/// See `Token`.
|
||||
typealias UserType = User
|
||||
|
||||
/// See `Token`.
|
||||
static var tokenKey: WritableKeyPath<UserToken, String> {
|
||||
return \.string
|
||||
}
|
||||
|
||||
/// See `Token`.
|
||||
static var userIDKey: WritableKeyPath<UserToken, User.ID> {
|
||||
return \.userID
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Once the token is conformed to `Token`, setting up the user model is easy.
|
||||
|
||||
```swift
|
||||
extension User: TokenAuthenticatable {
|
||||
/// See `TokenAuthenticatable`.
|
||||
typealias TokenType = UserToken
|
||||
}
|
||||
```
|
||||
|
||||
Once you have conformed your models, the next step is to add middleware to your protected route.
|
||||
|
||||
```swift
|
||||
// Use user model to create an authentication middleware
|
||||
let token = User.tokenAuthMiddleware()
|
||||
|
||||
// Create a route closure wrapped by this middleware
|
||||
router.grouped(token).get("hello") {
|
||||
//
|
||||
}
|
||||
```
|
||||
|
||||
Now, to fetch the authenticated user in the route closure, you can use [`requireAuthenticated(_:)`](https://api.vapor.codes/auth/latest/Authentication/Extensions/Request.html#/s:5Vapor7RequestC14AuthenticationE20requireAuthenticatedxxmKAD15AuthenticatableRzlF).
|
||||
|
||||
```swift
|
||||
let user = try req.requireAuthenticated(User.self)
|
||||
return "Hello, \(user.name)."
|
||||
```
|
||||
|
||||
The `requireAuthenticated` method will automatically throw an appropriate unauthorized error if the valid credentials were not supplied. Because of this, using [`GuardAuthenticationMiddleware`](https://api.vapor.codes/auth/latest/Authentication/Classes/GuardAuthenticationMiddleware.html) to protect the route from unauthenticated access is not required.
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
# Getting Started with Auth
|
||||
|
||||
Auth ([vapor/auth](https://github.com/vapor/auth)) is a framework for adding authentication to your application. It builds on top of [Fluent](../fluent/getting-started) by using models as the basis of authentication.
|
||||
|
||||
!!! tip
|
||||
There is a Vapor API template with Auth pre-configured available.
|
||||
See [Getting Started → Toolbox → Templates](../getting-started/toolbox.md#templates).
|
||||
|
||||
Let's take a look at how you can get started using Auth.
|
||||
|
||||
## Package
|
||||
|
||||
The first step to using Auth is adding it as a dependency to your project in your SPM package manifest file.
|
||||
|
||||
```swift
|
||||
// swift-tools-version:4.0
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "MyApp",
|
||||
dependencies: [
|
||||
/// Any other dependencies ...
|
||||
|
||||
// 👤 Authentication and Authorization framework for Fluent.
|
||||
.package(url: "https://github.com/vapor/auth.git", from: "2.0.0"),
|
||||
],
|
||||
targets: [
|
||||
.target(name: "App", dependencies: ["Authentication", ...]),
|
||||
.target(name: "Run", dependencies: ["App"]),
|
||||
.testTarget(name: "AppTests", dependencies: ["App"]),
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
Auth currently provides one module `Authentication`. In the future, there will be a separate module named `Authorization` for performing more advanced auth.
|
||||
|
||||
|
||||
## Provider
|
||||
|
||||
Once you have succesfully added the Auth package to your project, the next step is to configure it in your application. This is usually done in [`configure.swift`](../getting-started/structure.md#configureswift).
|
||||
|
||||
```swift
|
||||
import Authentication
|
||||
|
||||
// register Authentication provider
|
||||
try services.register(AuthenticationProvider())
|
||||
```
|
||||
|
||||
That's it for basic setup. The next step is to create an authenticatable model.
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
# Web Authentication
|
||||
|
||||
This guide will introduce you to session-based authentication—a method of authentication commonly used for protecting web (front-end) pages.
|
||||
|
||||
## Concept
|
||||
|
||||
In Computer Science (especially web frameworks), the concept of Authentication means verifying the _identity_ of a user. This is not to be confused with Authorization which verifies _privileges_ to a given resource
|
||||
|
||||
Session-based authentication uses cookies to re-authenticate users with each request to your website. It performs this logic via a middleware that you add to your application or specific routes.
|
||||
|
||||
You are responsible for initially authenticating the user to your application (either manually or by using methods from the [Stateless (API)](api.md) section). Once you have authenticated the user once, the middleware will use cookies to re-authenticate the user on subsequent requests automatically.
|
||||
|
||||
## Example
|
||||
|
||||
Let's take a look at a simple session-based authentication example.
|
||||
|
||||
### Pre-requisites
|
||||
|
||||
In order to do session-based authentication, you must have a way to initially authenticate your user. In other words, you need a method for logging them in. The [Stateless (API)](api.md) section covers some of these methods, but it's entirely up to you.
|
||||
|
||||
You will also need to have sessions configured for your application. You can learn more about this in [Vapor → Sessions](../vapor/sessions.md). Usually this will require adding the `SessionsMiddleware` and choosing a `KeyedCache`.
|
||||
|
||||
```swift
|
||||
config.prefer(MemoryKeyedCache.self, for: KeyedCache.self)
|
||||
|
||||
var middlewares = MiddlewareConfig()
|
||||
middlewares.use(SessionsMiddleware.self)
|
||||
// ...
|
||||
services.register(middlewares)
|
||||
```
|
||||
|
||||
### Model
|
||||
|
||||
Once you are ready to enable session-based authentication, the first step is to conform your user model to [`SessionAuthenticatable`](https://api.vapor.codes/auth/latest/Authentication/Protocols/SessionAuthenticatable.html).
|
||||
|
||||
```swift
|
||||
extension User: SessionAuthenticatable { }
|
||||
```
|
||||
|
||||
The conformance is empty since all of the required methods have default implementations.
|
||||
|
||||
|
||||
### Middleware
|
||||
|
||||
Once your model is conformed, you can use it to create an `AuthenticationSessionsMiddleware`.
|
||||
|
||||
```swift
|
||||
// create auth sessions middleware for user
|
||||
let session = User.authSessionsMiddleware()
|
||||
|
||||
// create a route group wrapped by this middleware
|
||||
let auth = router.grouped(session)
|
||||
|
||||
// create new route in this route group
|
||||
auth.get("hello") { req -> String in
|
||||
//
|
||||
}
|
||||
```
|
||||
|
||||
Create a route group wrapped by this middleware using the route grouping methods. Any routes you want to support session-based authentication should use this route group.
|
||||
|
||||
You can also apply this middleware globally to your application if you'd like.
|
||||
|
||||
### Route
|
||||
|
||||
Inside of any route closure wrapped by the session auth middleware, we can access our authenticated model using the [`authenticated(_:)`](https://api.vapor.codes/auth/latest/Authentication/Extensions/Request.html#/s:5Vapor7RequestC14AuthenticationE13authenticatedxSgxmKAD15AuthenticatableRzlF) methods.
|
||||
|
||||
```swift
|
||||
let user = try req.requireAuthenticated(User.self)
|
||||
return "Hello, \(user.name)!"
|
||||
```
|
||||
|
||||
Here we are using the method prefixed with `require` to throw an error if the user was not succesfully authenticated.
|
||||
|
||||
If you visit this route now, you should see a message saying no user has been authenticated. Let's resolve this by creating a way for our user to login!
|
||||
|
||||
!!! note
|
||||
Use [`GuardAuthenticationMiddleware`](https://api.vapor.codes/auth/latest/Authentication/Classes/GuardAuthenticationMiddleware.html) to protect routes that do not call `requireAuthenticated(_:)` or otherwise require authentication.
|
||||
|
||||
### Login
|
||||
|
||||
For the sake of this example, we will just log in a pre-defined user with a fixed ID.
|
||||
|
||||
```swift
|
||||
auth.get("login") { req -> Future<String> in
|
||||
return User.find(1, on: req).map { user in
|
||||
guard let user = user else {
|
||||
throw Abort(.badRequest)
|
||||
}
|
||||
try req.authenticate(user)
|
||||
return "Logged in"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Remember that this login route must go through the `AuthenticationSessionsMiddleware`. The middleware is what will detect that we have authenticated a user and later restore the authentication automatically.
|
||||
|
||||
Upon visiting `/hello`, you should recieve an error message stating that you are not logged in. If you then visit `/login` first, followed by `/hello` you should see that you are now successfully logged in!
|
||||
|
||||
If you open the inspector, you should notice a new cookie named `"vapor-session"` has been added to your browser.
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
# TOTP and HOTP
|
||||
|
||||
One-time passwords (OTPs) are commonly used as a form of [two-factor authentication](https://en.wikipedia.org/wiki/Multi-factor_authentication). Crypto can be used to generate both TOTP and HOTP in accordance with [RFC 6238](https://tools.ietf.org/html/rfc6238) and [RFC 4226](https://tools.ietf.org/html/rfc4226
|
||||
) respectively.
|
||||
|
||||
- **TOTP**: Time-based One-Time Password. Generates password by combining shared secret with unix timestamp.
|
||||
- **HOTP**: HMAC-Based One-Time Password. Similar to TOTP, except an incrementing counter is used instead of a timestamp. Each time a new OTP is generated, the counter increments.
|
||||
|
||||
## Generating OTP
|
||||
|
||||
OTP generation is similar for both TOTP and HOTP. The only difference is that HOTP requires the current counter to be passed.
|
||||
|
||||
```swift
|
||||
import Crypto
|
||||
|
||||
// Generate TOTP
|
||||
let code = TOTP.SHA1.generate(secret: "hi")
|
||||
print(code) "123456"
|
||||
|
||||
// Generate HOTP
|
||||
let code = HOTP.SHA1.generate(secret: "hi", counter: 0)
|
||||
print(code) "208503"
|
||||
```
|
||||
|
||||
View the API docs for [`TOTP`](#fixme) and [`HOTP`](#fixme) for more information.
|
||||
|
||||
## Base 32
|
||||
|
||||
TOTP and HOTP shared secrets are commonly transferred using Base32 encoding. Crypto provides conveniences for converting to/from Base32.
|
||||
|
||||
```swift
|
||||
import Crypto
|
||||
|
||||
// shared secret
|
||||
let secret: Data = ...
|
||||
|
||||
// base32 encoded secret
|
||||
let encodedSecret = secret.base32EncodedString()
|
||||
```
|
||||
|
||||
See Crypto's [`Data`](#fixme) extensions for more information.
|
||||
|
|
@ -22,7 +22,7 @@ cd Hello
|
|||
|
||||
## Generate Xcode Project
|
||||
|
||||
Let's now use the [Vapor Toolbox's `xcode`](toolbox#xcode) command to generate an Xcode project.
|
||||
Let's now use the [Vapor Toolbox's `xcode`](toolbox.md) command to generate an Xcode project.
|
||||
This will allow us to build and run our app from inside of Xcode, just like an iOS app.
|
||||
|
||||
```sh
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ a different template by passing the `--template` flag.
|
|||
|------|------------------|-----------------------------------|
|
||||
| API | `--template=api` | JSON API with Fluent database. |
|
||||
| Web | `--template=web` | HTML website with Leaf templates. |
|
||||
| Auth | `--template=auth-template`| JSON API with Fluent DB and Auth. |
|
||||
|
||||
!!! info
|
||||
There are lots of unofficial Vapor templates on GitHub under the <a href="https://github.com/search?utf8=✓&q=topic%3Avapor+topic%3Atemplate&type=Repositories" target="_blank">`vapor` + `template` topcs →</a>.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
# Getting Started with JWT
|
||||
|
||||
JWT ([vapor/jwt](https://github.com/vapor/jwt)) is a package for parsing and serializing **J**SON **W**eb **T**okens supporting both HMAC and RSA signing. JWTs are often used for implementing _decentralized_ authentication and authorization.
|
||||
|
||||
Since all of the authenticated user's information can be embedded _within_ a JWT, there is no need to query a central authentication server with each request to your service. Unlike standard bearer tokens that must be looked up in a centralized database, JWTs contain cryptographic signatures that can be used to independently verify their authenticity.
|
||||
|
||||
If implemented correctly, JWTs can be a powerful tool for making your application [horizontally scalable](https://stackoverflow.com/questions/11707879/difference-between-scaling-horizontally-and-vertically-for-databases). Learn more about JWT at [jwt.io](https://jwt.io).
|
||||
|
||||
!!! tip
|
||||
If your goal is not horizontal scalability, a standard bearer token will likely be a better solution. JWTs have some downsides worth considering such as the inability to revoke a token once it has been issued (until it expires normally).
|
||||
|
||||
Let's take a look at how you can get started using JWT.
|
||||
|
||||
## Package
|
||||
|
||||
The first step to using JWT is adding it as a dependency to your project in your SPM package manifest file.
|
||||
|
||||
```swift
|
||||
// swift-tools-version:4.0
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "MyApp",
|
||||
dependencies: [
|
||||
/// Any other dependencies ...
|
||||
|
||||
// 🔏 JSON Web Token signing and verification (HMAC, RSA).
|
||||
.package(url: "https://github.com/vapor/jwt.git", from: "3.0.0"),
|
||||
],
|
||||
targets: [
|
||||
.target(name: "App", dependencies: ["JWT", ...]),
|
||||
.target(name: "Run", dependencies: ["App"]),
|
||||
.testTarget(name: "AppTests", dependencies: ["App"]),
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
That's it for basic setup. The next section will give you an overview of the package's APIs. As always, feel free to visit the [API Docs](https://api.vapor.codes/jwt/latest/JWT/index.html) for more specific information.
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
# Using JWT
|
||||
|
||||
JSON Web Tokens are a great tool for implementing _decentralized_ authentication and authorization. Once you are finished configuring your app to use the JWT package (see [JWT → Getting Started](getting-started.md)), you are ready to begin using JWTs in your app.
|
||||
|
||||
## Structure
|
||||
|
||||
Like other forms of token-based auth, JWTs are sent using the bearer authorization header.
|
||||
|
||||
```http
|
||||
GET /hello HTTP/1.1
|
||||
Authorization: Bearer <token>
|
||||
...
|
||||
```
|
||||
|
||||
In the example HTTP request above, `<token>` would be replaced by the serialized JWT. [jwt.io](https://jwt.io) hosts an online tool for parsing and serializing JWTs. We will use that tool to create a token for testing.
|
||||
|
||||

|
||||
|
||||
### Header
|
||||
|
||||
The header is mainly used to specify which algorithm was used to generate the token's signature. This is used by the accepting app to verify the token's authenticity.
|
||||
|
||||
Here is the raw JSON data for our header:
|
||||
|
||||
```json
|
||||
{
|
||||
"alg": "HS256",
|
||||
"typ": "JWT"
|
||||
}
|
||||
```
|
||||
|
||||
This specifies the HMAC SHA-256 signing algorithm and that our token is indeed a JWT.
|
||||
|
||||
### Payload
|
||||
|
||||
The payload is where you store information to identify the authenticated user. You can store any data you want here, but be careful not to store too much as some web browsers limit HTTP header sizes.
|
||||
|
||||
The payload is also where you store _claims_. Claims are standardized key / value pairs that many JWT implementations can recognize and act on automatically. A commonly used claim is _Expiration Time_ which stores the token's expiration date as a unix timestamp at key `"exp"`. See a full list of supported claims in [RFC 7519 § 4.1](https://tools.ietf.org/html/rfc7519#section-4.1).
|
||||
|
||||
To keep things simple, we will just include our user's identifier and name in the payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 42,
|
||||
"name": "Vapor Developer"
|
||||
}
|
||||
```
|
||||
|
||||
### Secret
|
||||
|
||||
Last but not least is the secret key used to sign and verify the JWT. For this example, we are using the `HS256` algorithm (specified in the JWT header). HMAC algorithms use a single secret key for both signing and verifying.
|
||||
|
||||
To keep things simple, we will use the following string as our key:
|
||||
|
||||
```
|
||||
secret
|
||||
```
|
||||
|
||||
Other algorithms, like RSA, use asymmetric (public and private) keys. With these types of algorithms, only the _private_ key is able to create (sign) JWTs. Both the _public_ and _private_ keys can verify JWTs. This allows for an added layer of security as you can distribute the public key to services that should only be able to verify tokens, not create them.
|
||||
|
||||
### Serialized
|
||||
|
||||
Finally, here is our fully serialized token. This will be sent via the bearer authorization header.
|
||||
|
||||
```
|
||||
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NDIsIm5hbWUiOiJWYXBvciBEZXZlbG9wZXIifQ.__Dm_tr1Ky2VYhZNoN6XpEkaRHjtRgaM6HdgDFcc9PM
|
||||
```
|
||||
|
||||
Each segment is separated by a `.`. The overall structure of the token is the following:
|
||||
|
||||
```
|
||||
<header>.<payload>.<signature>
|
||||
```
|
||||
|
||||
Note that the header and payload segments are simply base64-url encoded JSON. It is important to remember that all information your store in a normal JWT is publically readable.
|
||||
|
||||
|
||||
## Parse
|
||||
|
||||
Let's take a look at how to parse and verify incoming JWTs.
|
||||
|
||||
### Payload
|
||||
|
||||
First, we need to create a `Codable` type that represents our payload. This should also conform to [`JWTPayload`](https://api.vapor.codes/jwt/latest/JWT/Protocols/JWTPayload.html).
|
||||
|
||||
```swift
|
||||
struct User: JWTPayload {
|
||||
var id: Int
|
||||
var name: String
|
||||
|
||||
func verify(using signer: JWTSigner) throws {
|
||||
// nothing to verify
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Since our simple payload does not include any claims, we can leave the `verify(using:)` method empty for now.
|
||||
|
||||
### Route
|
||||
|
||||
Now that our payload type is ready, we can parse and verify an incoming JWT.
|
||||
|
||||
```swift
|
||||
import JWT
|
||||
import Vapor
|
||||
|
||||
router.get("hello") { req -> String in
|
||||
// fetches the token from `Authorization: Bearer <token>` header
|
||||
guard let bearer = req.http.headers.bearerAuthorization else {
|
||||
throw Abort(.unauthorized)
|
||||
}
|
||||
|
||||
// parse JWT from token string, using HS-256 signer
|
||||
let jwt = try JWT<User>(from: bearer.token, verifiedUsing: .hs256(key: "secret"))
|
||||
return "Hello, \(jwt.payload.name)!"
|
||||
}
|
||||
```
|
||||
|
||||
This snippet creates a new route at `GET /hello`. The first part of the route handler fetches the `<token>` value from the bearer authorization header. The second part uses the [`JWT`](https://api.vapor.codes/jwt/latest/JWT/Structs/JWT.html) struct to parse the token using an `HS256` signer.
|
||||
|
||||
Once the JWT is parsed, we access the [`payload`](https://api.vapor.codes/jwt/latest/JWT/Structs/JWT.html#/s:3JWTAAV7payloadxvp) property which contains an instance of our `User` type. We then access the `name` property to say hello!
|
||||
|
||||
Run the following request and check the output:
|
||||
|
||||
```http
|
||||
GET /hello HTTP/1.1
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NDIsIm5hbWUiOiJWYXBvciBEZXZlbG9wZXIifQ.__Dm_tr1Ky2VYhZNoN6XpEkaRHjtRgaM6HdgDFcc9PM
|
||||
Content-Length: 0
|
||||
```
|
||||
|
||||
You should see the following response:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Content-Length: 23
|
||||
Hello, Vapor Developer!
|
||||
```
|
||||
|
||||
## Serialize
|
||||
|
||||
Let's take a look at how to create and sign a JWT.
|
||||
|
||||
### Payload
|
||||
|
||||
First, we need to create a `Codable` type that represents our payload. This should also conform to [`JWTPayload`](https://api.vapor.codes/jwt/latest/JWT/Protocols/JWTPayload.html).
|
||||
|
||||
```swift
|
||||
struct User: JWTPayload {
|
||||
var id: Int
|
||||
var name: String
|
||||
|
||||
func verify(using signer: JWTSigner) throws {
|
||||
// nothing to verify
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Since our simple payload does not include any claims, we can leave the `verify(using:)` method empty for now.
|
||||
|
||||
### Route
|
||||
|
||||
Now that our payload type is ready, we can generate a JWT.
|
||||
|
||||
```swift
|
||||
router.post("login") { req -> String in
|
||||
// create payload
|
||||
let user = User(id: 42, name: "Vapor Developer")
|
||||
|
||||
// create JWT and sign
|
||||
let data = try JWT(payload: user).sign(using: .hs256(key: "secret"))
|
||||
return String(data: data, encoding: .utf8) ?? ""
|
||||
}
|
||||
```
|
||||
|
||||
This snippet creates a new route at `POST /login`. The first part of the route handler creates an instance of our `User` payload type. The second part creates an instance of `JWT` using our payload, and calls the [`sign(using:)`](https://api.vapor.codes/jwt/latest/JWT/Structs/JWT.html#/s:3JWTAAV4sign10Foundation4DataVAA9JWTSignerC5using_tKF) method. This method returns `Data`, which we convert to a `String`.
|
||||
|
||||
If you visit this route, you should get the following output:
|
||||
|
||||
```
|
||||
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NDIsIm5hbWUiOiJWYXBvciBEZXZlbG9wZXIifQ.__Dm_tr1Ky2VYhZNoN6XpEkaRHjtRgaM6HdgDFcc9PM
|
||||
```
|
||||
|
||||
If you plug that JWT into [jwt.io](https://jwt.io) and enter the secret (`secret`), you should see the encoded data and a message "Signature Verified".
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,252 +0,0 @@
|
|||
# Basics
|
||||
|
||||
Welcome to Leaf. Leaf's goal is to be a simple templating language that can make generating views easier. There are plenty of great templating languages, so use what's best for you – maybe that's Leaf! The goals of Leaf are:
|
||||
|
||||
- Small set of strictly enforced rules
|
||||
- Consistency
|
||||
- Parser first mentality
|
||||
- Extensibility
|
||||
- Asynchronous and reactive
|
||||
|
||||
|
||||
## Rendering a template
|
||||
|
||||
Once you have Leaf installed, you should create a directory called “Resources” inside your project folder, and inside that create another directory called “Views”. This Resources/Views directory is the default location for Leaf templates, although you can change it if you want.
|
||||
|
||||
Firstly, import Leaf to routes.swift
|
||||
|
||||
```swift
|
||||
import Leaf
|
||||
```
|
||||
|
||||
Then, to render a basic Leaf template from a route, add this code:
|
||||
|
||||
```swift
|
||||
router.get { req -> Future<View> in
|
||||
let leaf = try req.make(LeafRenderer.self)
|
||||
let context = [String: String]()
|
||||
return try leaf.render("home", context)
|
||||
}
|
||||
```
|
||||
|
||||
That will load home.leaf in the Resources/Views directory and render it. The `context` dictionary is there to let you provide custom data to render inside the template, but you might find it easier to use codable structs instead because they provide extra type safety. For example:
|
||||
|
||||
```swift
|
||||
struct HomePage: Codable {
|
||||
var title: String
|
||||
var content: String
|
||||
}
|
||||
```
|
||||
|
||||
### Async
|
||||
|
||||
Leaf's engine is completely reactive, supporting both streams and futures. One of the only ones of its kind.
|
||||
|
||||
When working with Future results, simply pass the `Future` in your template context.
|
||||
Streams that carry an encodable type need to be encoded before they're usable within Leaf.
|
||||
|
||||
```swift
|
||||
struct Profile: Encodable {
|
||||
var friends: EncodableStream
|
||||
var currentUser: Future<User>
|
||||
}
|
||||
```
|
||||
|
||||
In the above context, the `currentUser` variable in Leaf will behave as being a `User` type. Leaf will not read the user Future if it's not used during rendering.
|
||||
|
||||
`EncodableStream` will behave as an array of LeafData, only with lower memory impact and better performance. It is recommended to use `EncodableStream` for (large) database queries.
|
||||
|
||||
```
|
||||
Your name is #(currentUser.name).
|
||||
|
||||
#for(friend in friends) {
|
||||
#(friend.name) is a friend of you.
|
||||
}
|
||||
```
|
||||
|
||||
## Template syntax
|
||||
### Structure
|
||||
|
||||
Leaf tags are made up of four elements:
|
||||
|
||||
- Token: `#` is the token
|
||||
- Name: A `string` that identifies the tag
|
||||
- Parameter List: `()` May accept 0 or more arguments
|
||||
- Body (optional): `{}` Must be separated from the parameter list by a space
|
||||
|
||||
There can be many different usages of these four elements depending on the tag's implementation. Let's look at a few examples of how Leaf's built-in tags might be used:
|
||||
|
||||
- `#()`
|
||||
- `#(variable)`
|
||||
- `#embed("template")`
|
||||
- `#set("title") { Welcome to Vapor }`
|
||||
- `#count(friends)`
|
||||
- `#for(friend in friends) { <li>#(friend.name)</li> }`
|
||||
|
||||
|
||||
### Working with context
|
||||
|
||||
In our Swift example from earlier, we used an empty `[String: String]` dictionary for context, which passes no custom data to Leaf. To try rendering content, use this code instead:
|
||||
|
||||
```swift
|
||||
let context = ["title": "Welcome", "message": "Vapor and Leaf work hand in hand"]
|
||||
return try leaf.make("home", context)
|
||||
```
|
||||
|
||||
That will expose `title` and `message` to our Leaf template, which can then be used inside tags. For example:
|
||||
|
||||
```
|
||||
<h1>#(title)</h1>
|
||||
<p>#(message)</p>
|
||||
```
|
||||
|
||||
### Checking conditions
|
||||
|
||||
Leaf is able to evaluate a range of conditions using its `#if` tag. For example, if you provide a variable it will check that variable exists in its context:
|
||||
|
||||
```
|
||||
#if(title) {
|
||||
The title is #(title)
|
||||
} else {
|
||||
No title was provided.
|
||||
}
|
||||
```
|
||||
|
||||
You can also write comparisons, for example:
|
||||
|
||||
```
|
||||
#if(title == "Welcome") {
|
||||
This is a friendly web page.
|
||||
} else {
|
||||
No strangers allowed!
|
||||
}
|
||||
```
|
||||
|
||||
If you want to use another tag as part of your condition, you should omit the `#` for the inner tag. For example:
|
||||
|
||||
```
|
||||
#if(lowercase(title) == "welcome") {
|
||||
This is a friendly web page.
|
||||
} else {
|
||||
No strangers allowed!
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Loops
|
||||
|
||||
If you provide an array of items, Leaf can loop over them and let you manipulate each item individually using its `#for` tag. For example, we could update our Swift code to provide a list of names in a team:
|
||||
|
||||
```swift
|
||||
let context = ["team": ["Malcolm", "Kaylee", "Jayne"]]
|
||||
```
|
||||
|
||||
We could then loop over them in Leaf like this:
|
||||
|
||||
```
|
||||
#for(name in team) {
|
||||
<p>#(name) is in the team.</p>
|
||||
}
|
||||
```
|
||||
|
||||
Leaf provides some extra variables inside a `#for` loop to give you more information about the loop's progress:
|
||||
|
||||
- The `loop.isFirst` variable is true when the current iteration is the first one.
|
||||
- The `loop.isLast` variable is true when it's the last iteration.
|
||||
- The `loop.index` variable will be set to the number of the current iteration, counting from 0.
|
||||
|
||||
Here's how we could use a loop variable to print just the first name in our array:
|
||||
|
||||
```
|
||||
#for(name in team) {
|
||||
#if(isFirst) { <p>#(name) is first!</p> }
|
||||
}
|
||||
```
|
||||
|
||||
### Embedding templates
|
||||
|
||||
Leaf’s `#embed` tag allows you to copy the contents of one template into another. When use this, you should always omit the template file's .leaf extension.
|
||||
|
||||
Embedding is useful for copying in a standard piece of content, for example a page footer or advert code:
|
||||
|
||||
```
|
||||
#embed("footer")
|
||||
```
|
||||
|
||||
This tag is also useful for building one template on top of another. For example, you might have a master.leaf file that includes all the code required to lay out your website – HTML structure, CSS and JavaScript – with some gaps in place that represent where page content varies.
|
||||
|
||||
Using this approach, you would construct a child template that fills in its unique content, then embeds the parent template that places the content appropriately.
|
||||
|
||||
For example, you might create a child.leaf template like this:
|
||||
|
||||
```
|
||||
#set("body") {
|
||||
<p>Welcome to Vapor!</p>
|
||||
}
|
||||
|
||||
#embed("master")
|
||||
```
|
||||
|
||||
That configures one item of context, `body`, but doesn’t display it directly. Instead, it embeds master.leaf, which can render `body` along with any other context variables passed in from Swift. For example, master.leaf might look like this:
|
||||
|
||||
```
|
||||
<html>
|
||||
<head><title>#(title)</title></head>
|
||||
<body>#get(body)</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
When given the context `["title": "Hi there!"]`, child.leaf will render as follows:
|
||||
|
||||
```
|
||||
<html>
|
||||
<head><title>Hi there!</title></head>
|
||||
<body><p>Welcome to Vapor!</p></body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### Other tags
|
||||
|
||||
#### `#capitalize`
|
||||
|
||||
The `#capitalize` tag uppercases the first letter of any string. For example, “taylor” will become “Taylor”.
|
||||
|
||||
```
|
||||
#capitalize(name)
|
||||
```
|
||||
|
||||
#### `#contains`
|
||||
|
||||
The `#contains` tag accepts an array and a value as its two parameters, and returns true if the array in parameter one contains the value in parameter two. For example, given the array `team`:
|
||||
|
||||
```
|
||||
#if(contains(team, "Jayne")) {
|
||||
You're all set!
|
||||
} else {
|
||||
You need someone to do PR.
|
||||
}
|
||||
```
|
||||
|
||||
#### `#count`
|
||||
|
||||
The `#count` tag returns the number of items in an array. For example:
|
||||
|
||||
```
|
||||
Your search matched #count(matches) pages.
|
||||
```
|
||||
|
||||
#### `#lowercase`
|
||||
|
||||
The `#lowercase` tag lowercases all letters in a string. For example, “Taylor” will become “taylor”.
|
||||
|
||||
```
|
||||
#lowercase(name)
|
||||
```
|
||||
|
||||
#### `#uppercase`
|
||||
|
||||
The `#uppercase` tag uppercases all letters in a string. For example, “Taylor” will become “TAYLOR”.
|
||||
|
||||
```
|
||||
#uppercase(name)
|
||||
```
|
||||
|
|
@ -1,54 +1,56 @@
|
|||
# Custom Tags
|
||||
|
||||
You can extend Leaf to provide your own tags that add custom functionality. To demonstrate this, let's look at a basic example by recreating `#uppercase` together. This tag will take one argument, which is the string to uppercase.
|
||||
You can create custom Leaf tags using the [`TagRenderer`](https://api.vapor.codes/template-kit/latest/TemplateKit/Protocols/TagRenderer.html) protocol.
|
||||
|
||||
When working with custom tags, there are four important things to know:
|
||||
To demonstrate this, let's take a look at creating a custom tag `#now` that prints the current timestamp. The tag will also support a single, optional parameter for specifying the date format.
|
||||
|
||||
1. You should call `requireParameterCount()` with the number of parameters you expect to receive. This will throw an error if your tag is used incorrectly.
|
||||
2. If you do or do not require a body, you should use either `requireBody()` or `requireNoBody()`. Again, this will throw an error if your tag is used incorrectly.
|
||||
3. You can read individual parameters using the `parameters` array. Each parameter will be of type `LeafData`, which you can convert to concrete data types using properties such as `.string`, `.dictionary`, and so on.
|
||||
4. You must return a `Future<LeafData?>` containing what should be rendered. In the example below we wrap the resulting uppercase string in a `LeafData` string, then send that back wrapped in a future.
|
||||
## Tag Renderer
|
||||
|
||||
Here’s example code for a `CustomUppercase` Leaf tag:
|
||||
First create a class called `NowTag` and conform it to `TagRenderer`.
|
||||
|
||||
```swift
|
||||
import Async
|
||||
import Leaf
|
||||
|
||||
public final class CustomUppercase: Leaf.LeafTag {
|
||||
public init() {}
|
||||
public func render(parsed: ParsedTag, context: LeafContext, renderer: LeafRenderer) throws -> Future<LeafData?> {
|
||||
// ensure we receive precisely one parameter
|
||||
try parsed.requireParameterCount(1)
|
||||
|
||||
// pull out our lone parameter as a string then uppercase it, or use an empty string
|
||||
let string = parsed.parameters[0].string?.uppercased() ?? ""
|
||||
|
||||
// send it back wrapped in a LeafData
|
||||
return Future(.string(string))
|
||||
final class NowTag: TagRenderer {
|
||||
init() { }
|
||||
|
||||
func render(tag: TagContext) throws -> EventLoopFuture<TemplateData> {
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
We can now register this Tag in our `configure.swift` file with:
|
||||
Now let's implement the `render(tag:)` method. The `TagContext` context passed to this method has everything we should need.
|
||||
|
||||
```swift
|
||||
services.register { container -> LeafConfig in
|
||||
// take a copy of Leaf's default tags
|
||||
var tags = defaultTags
|
||||
let formatter = DateFormatter()
|
||||
switch tag.parameters.count {
|
||||
case 0: formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
case 1:
|
||||
guard let string = tag.parameters[0].string else {
|
||||
throw ...
|
||||
}
|
||||
formatter.dateFormat = string
|
||||
default:
|
||||
throw ...
|
||||
}
|
||||
|
||||
// add our custom tag
|
||||
tags["customuppercase"] = CustomUppercase()
|
||||
let string = formatter.string(from: .init())
|
||||
return tag.container.future(.string(string))
|
||||
```
|
||||
|
||||
// find the location of our Resources/Views directory
|
||||
let directoryConfig = try container.make(DirectoryConfig.self, for: LeafRenderer.self)
|
||||
let viewsDirectory = directoryConfig.workDir + "Resources/Views"
|
||||
## Configure Tag
|
||||
|
||||
// put all that into a new Leaf configuration and return it
|
||||
return LeafConfig(tags: tags, viewsDir: viewsDirectory)
|
||||
Now that we've implemented `NowTag`, we just need to configure it. You can configure any `TagRenderer` like this--even if they come from a separate package.
|
||||
|
||||
```swift
|
||||
services.register { container -> LeafTagConfig in
|
||||
var config = LeafTagConfig.default()
|
||||
config.use(NowTag(), as: "now")
|
||||
return config
|
||||
}
|
||||
```
|
||||
|
||||
Once that is complete, you can use `#customuppercase(some_variable)` to run your custom code.
|
||||
And that's it! We can now use our custom tag in Leaf.
|
||||
|
||||
> Note: Use of non-alphanumeric characters in tag names is **strongly discouraged** and may be disallowed in future versions of Leaf.
|
||||
```leaf
|
||||
The time is #now()
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,73 +1,72 @@
|
|||
!!! warning
|
||||
Leaf 3.0 is still in beta. Some documentation may be missing or out of date.
|
||||
|
||||
# Leaf
|
||||
|
||||
Leaf is a templating language that integrates with Futures, Reactive Streams and Codable. This section outlines how to import the Leaf package into a Vapor project.
|
||||
Leaf is a powerful templating language with Swift-inspired syntax. You can use it to generate dynamic HTML pages for a front-end website or generate rich emails to send from an API.
|
||||
|
||||
## Example Folder Structure
|
||||
## Package
|
||||
|
||||
```
|
||||
Hello
|
||||
├── Package.resolved
|
||||
├── Package.swift
|
||||
├── Public
|
||||
├── Resources
|
||||
│ ├── Views
|
||||
│ │ └── hello.leaf
|
||||
├── Public
|
||||
│ ├── images (images resources)
|
||||
│ ├── styles (css resources)
|
||||
├── Sources
|
||||
│ ├── App
|
||||
│ │ ├── boot.swift
|
||||
│ │ ├── configure.swift
|
||||
│ │ └── routes.swift
|
||||
│ └── Run
|
||||
│ └── main.swift
|
||||
├── Tests
|
||||
│ ├── AppTests
|
||||
│ │ └── AppTests.swift
|
||||
│ └── LinuxMain.swift
|
||||
└── LICENSE
|
||||
```
|
||||
|
||||
## Adding Leaf to your project
|
||||
|
||||
The easiest way to use Leaf with Vapor is to include the Leaf repository as a dependency in Package.swift:
|
||||
The first step to using Leaf is adding it as a dependency to your project in your SPM package manifest file.
|
||||
|
||||
```swift
|
||||
// swift-tools-version:4.0
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "project1",
|
||||
name: "MyApp",
|
||||
dependencies: [
|
||||
// 💧 A server-side Swift web framework.
|
||||
.package(url: "https://github.com/vapor/vapor.git", .branch("beta")),
|
||||
.package(url: "https://github.com/vapor/leaf.git", .branch("beta")),
|
||||
/// Any other dependencies ...
|
||||
.package(url: "https://github.com/vapor/leaf.git", from: "3.0.0"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "App",
|
||||
dependencies: ["Vapor", "Leaf"]
|
||||
),
|
||||
.target(name: "App", dependencies: ["Leaf", ...]),
|
||||
.target(name: "Run", dependencies: ["App"]),
|
||||
.testTarget(name: "AppTests", dependencies: ["App"]),
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
The Leaf package adds Leaf to your project, but to configure it for use you must modify configure.swift:
|
||||
## Configure
|
||||
|
||||
1. Add `import Leaf` to the top of the file so that Leaf is available to use. You will also need to add this to any file that will render templates.
|
||||
2. Add `try services.register(LeafProvider())` to the `configure()` function so that routes may render Leaf templates as needed.
|
||||
Once you have added the package to your project, you can configure Vapor to use it. This is usually done in [`configure.swift`](../getting-started/structure.md#configureswift).
|
||||
|
||||
```swift
|
||||
import Leaf
|
||||
|
||||
try services.register(LeafProvider())
|
||||
```
|
||||
|
||||
If your application supports multiple view renderers, you may need to specify that you would like to use Leaf.
|
||||
|
||||
```swift
|
||||
config.prefer(LeafRenderer.self, for: ViewRenderer.self)
|
||||
```
|
||||
|
||||
## Folder Structure
|
||||
|
||||
Once you have configured Leaf, you will need to ensure you have a `Views` folder to store your `.leaf` files in. By default, Leaf expects the views folder to be a `./Resources/Views` relative to your project's root.
|
||||
|
||||
You will also likely want to enable Vapor's [`FileMiddleware`](https://api.vapor.codes/vapor/latest/Vapor/Classes/FileMiddleware.html) to serve files from your `/Public` folder.
|
||||
|
||||
```
|
||||
VaporApp
|
||||
├── Package.swift
|
||||
├── Resources
|
||||
│ ├── Views
|
||||
│ │ └── hello.leaf
|
||||
├── Public
|
||||
│ ├── images (images resources)
|
||||
│ ├── styles (css resources)
|
||||
└── Sources
|
||||
└── ...
|
||||
```
|
||||
|
||||
## Syntax Highlighting
|
||||
|
||||
You may also wish to install one of these third-party packages that provide support for syntax highlighting in Leaf templates.
|
||||
|
||||
### Sublime
|
||||
|
||||
Install the package [Leaf](https://packagecontrol.io/packages/Leaf) from package control.
|
||||
|
||||
### Atom
|
||||
|
||||
[language-leaf](https://atom.io/packages/language-leaf) by ButkiewiczP
|
||||
|
|
@ -85,3 +84,23 @@ There appears to be a way to [make Xcode file associations persist](http://stack
|
|||
### CLion & AppCode
|
||||
|
||||
Some preliminary work has been done to implement a Leaf Plugin for CLion & AppCode but lack of skill and interest in Java has slowed progress! If you have IntelliJ SDK experience and want to help with this, message Tom Holland on [Vapor Slack](http://vapor.team)
|
||||
|
||||
## Rendering a View
|
||||
|
||||
Now that Leaf is configured, let's render your first template. Inside of the `Resources/Views` folder, create a new file called `hello.leaf` with the following contents:
|
||||
|
||||
```leaf
|
||||
Hello, #(name)!
|
||||
```
|
||||
|
||||
Then, register a route (usually done in `routes.swift` or a controller) to render the view.
|
||||
|
||||
```swift
|
||||
import Leaf
|
||||
|
||||
router.get("hello") { req -> Future<View> in
|
||||
return try req.view().render("hello", ["name": "Leaf"])
|
||||
}
|
||||
```
|
||||
|
||||
Open your browser and visit `/hello`. You should see `Hello, Leaf!`. Congratulations on rendering your first Leaf view!
|
||||
|
|
|
|||
|
|
@ -0,0 +1,287 @@
|
|||
# Leaf Overview
|
||||
|
||||
Leaf is a powerful templating language with Swift-inspired syntax. You can use it to generate dynamic HTML pages for a front-end website or generate rich emails to send from an API.
|
||||
|
||||
This guide will give you an overview of Leaf's syntax and the available tags.
|
||||
|
||||
## Template syntax
|
||||
|
||||
Here is an example of a basic Leaf tag usage.
|
||||
|
||||
```leaf
|
||||
There are #count(users) users.
|
||||
```
|
||||
|
||||
Leaf tags are made up of four elements:
|
||||
|
||||
- Token `#`: This signals the leaf parser to begin looking for a tag.
|
||||
- Name `count`: that identifies the tag.
|
||||
- Parameter List `(users)`: May accept zero or more arguments.
|
||||
- Body: An optional body can be supplied to some tags. This is similar to Swift's trailing-closure syntax.
|
||||
|
||||
There can be many different usages of these four elements depending on the tag's implementation. Let's look at a few examples of how Leaf's built-in tags might be used:
|
||||
|
||||
```leaf
|
||||
#(variable)
|
||||
#embed("template")
|
||||
#set("title") { Welcome to Vapor }
|
||||
#count(friends)
|
||||
#for(friend in friends) { <li>#(friend.name)</li> }
|
||||
```
|
||||
|
||||
Leaf also supports many expressions you are familiar with in Swift.
|
||||
|
||||
- `+`
|
||||
- `>`
|
||||
- `==`
|
||||
- `||`
|
||||
- etc.
|
||||
|
||||
```leaf
|
||||
#if(1 + 1 == 2) {
|
||||
Hello!
|
||||
}
|
||||
```
|
||||
|
||||
## Context
|
||||
|
||||
In the example from [Getting Started](./getting-started.md), we used a `[String: String]` dictionary to pass data to Leaf. However, you can pass anything that conforms to `Encodable`. It's actually preferred to use `Encodable` structs since `[String: Any]` is not supported.
|
||||
|
||||
```swift
|
||||
struct WelcomeContext: Encodable {
|
||||
var title: String
|
||||
var number: Int
|
||||
}
|
||||
return try req.view().make("home", WelcomeContext(title: "Hello!", number: 42))
|
||||
```
|
||||
|
||||
That will expose `title` and `message` to our Leaf template, which can then be used inside tags. For example:
|
||||
|
||||
```leaf
|
||||
<h1>#(title)</h1>
|
||||
<p>#(number)</p>
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Here are some common Leaf usage examples.
|
||||
|
||||
### Conditions
|
||||
|
||||
Leaf is able to evaluate a range of conditions using its `#if` tag. For example, if you provide a variable it will check that variable exists in its context:
|
||||
|
||||
```leaf
|
||||
#if(title) {
|
||||
The title is #(title)
|
||||
} else {
|
||||
No title was provided.
|
||||
}
|
||||
```
|
||||
|
||||
You can also write comparisons, for example:
|
||||
|
||||
```leaf
|
||||
#if(title == "Welcome") {
|
||||
This is a friendly web page.
|
||||
} else {
|
||||
No strangers allowed!
|
||||
}
|
||||
```
|
||||
|
||||
If you want to use another tag as part of your condition, you should omit the `#` for the inner tag. For example:
|
||||
|
||||
```leaf
|
||||
#if(lowercase(title) == "welcome") {
|
||||
This is a friendly web page.
|
||||
} else {
|
||||
No strangers allowed!
|
||||
}
|
||||
```
|
||||
|
||||
Just like in Swift, you can also use `else if` statement.s
|
||||
|
||||
```leaf
|
||||
#if(title == "Welcome") {
|
||||
This is a friendly web page.
|
||||
} else if (1 == 2) {
|
||||
What?
|
||||
} else {
|
||||
No strangers allowed!
|
||||
}
|
||||
```
|
||||
|
||||
### Loops
|
||||
|
||||
If you provide an array of items, Leaf can loop over them and let you manipulate each item individually using its `#for` tag.
|
||||
|
||||
For example, we could update our Swift code to provide a list of planets:
|
||||
|
||||
```swift
|
||||
struct SolarSystem: Codable {
|
||||
let planets = ["Venus", "Earth", "Mars"]
|
||||
}
|
||||
|
||||
return try req.view().render(..., SolarSystem())
|
||||
```
|
||||
|
||||
We could then loop over them in Leaf like this:
|
||||
|
||||
```leaf
|
||||
Planets:
|
||||
<ul>
|
||||
#for(planet in planets) {
|
||||
<li>#(planet)</li>
|
||||
}
|
||||
</ul>
|
||||
```
|
||||
|
||||
This would render a view that looks like:
|
||||
|
||||
```
|
||||
Planets:
|
||||
- Venus
|
||||
- Earth
|
||||
- Mars
|
||||
```
|
||||
|
||||
Leaf provides some extra variables inside a `#for` loop to give you more information about the loop's progress:
|
||||
|
||||
- The `isFirst` variable is true when the current iteration is the first one.
|
||||
- The `isLast` variable is true when it's the last iteration.
|
||||
- The `index` variable will be set to the number of the current iteration, counting from 0.
|
||||
|
||||
Here's how we could use a loop variable to print just the first name in our array:
|
||||
|
||||
```leaf
|
||||
#for(planet in planets) {
|
||||
#if(isFirst) { #(planet) is first! }
|
||||
}
|
||||
```
|
||||
|
||||
### Embedding templates
|
||||
|
||||
Leaf’s `#embed` tag allows you to copy the contents of one template into another. When use this, you should always omit the template file's .leaf extension.
|
||||
|
||||
Embedding is useful for copying in a standard piece of content, for example a page footer or advert code:
|
||||
|
||||
```leaf
|
||||
#embed("footer")
|
||||
```
|
||||
|
||||
This tag is also useful for building one template on top of another. For example, you might have a master.leaf file that includes all the code required to lay out your website – HTML structure, CSS and JavaScript – with some gaps in place that represent where page content varies.
|
||||
|
||||
Using this approach, you would construct a child template that fills in its unique content, then embeds the parent template that places the content appropriately.
|
||||
|
||||
For example, you might create a child.leaf template like this:
|
||||
|
||||
```leaf
|
||||
#set("body") {
|
||||
<p>Welcome to Vapor!</p>
|
||||
}
|
||||
|
||||
#embed("master")
|
||||
```
|
||||
|
||||
That configures one item of context, `body`, but doesn’t display it directly. Instead, it embeds master.leaf, which can render `body` along with any other context variables passed in from Swift. For example, master.leaf might look like this:
|
||||
|
||||
```leaf
|
||||
<html>
|
||||
<head>
|
||||
<title>#(title)</title>
|
||||
</head>
|
||||
<body>#get(body)</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
When given the context `["title": "Hi there!"]`, child.leaf will render as follows:
|
||||
|
||||
```html
|
||||
<html>
|
||||
<head>
|
||||
<title>Hi there!</title>
|
||||
</head>
|
||||
<body><p>Welcome to Vapor!</p></body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### Comments
|
||||
|
||||
You can write single or multiline comments with Leaf. They will be discarded when rendering the view.
|
||||
|
||||
```leaf
|
||||
#// Say hello to the user
|
||||
Hello, #(name)!
|
||||
```
|
||||
|
||||
Multi-line comments are opened with `#/*` and closed with `*/`.
|
||||
|
||||
```leaf
|
||||
#/*
|
||||
Say hello to the user
|
||||
*/
|
||||
Hello, #(name)!
|
||||
```
|
||||
|
||||
### Other tags
|
||||
|
||||
#### `#date`
|
||||
|
||||
The `#date` tag formats dates into a readable string.
|
||||
|
||||
```swift
|
||||
render(..., ["now": Date()])
|
||||
```
|
||||
|
||||
```leaf
|
||||
The time is #date(now)
|
||||
```
|
||||
|
||||
You can pass a custom date formatter string as the second argument. See Swift's [`DateFormatter`](https://developer.apple.com/documentation/foundation/dateformatter) for more information.
|
||||
|
||||
```leaf
|
||||
The date is #date(now, "yyyy-MM-dd")
|
||||
```
|
||||
|
||||
#### `#capitalize`
|
||||
|
||||
The `#capitalize` tag uppercases the first letter of any string.
|
||||
|
||||
```leaf
|
||||
#capitalize(name)
|
||||
```
|
||||
|
||||
#### `#contains`
|
||||
|
||||
The `#contains` tag accepts an array and a value as its two parameters, and returns true if the array in parameter one contains the value in parameter two.
|
||||
|
||||
```leaf
|
||||
#if(contains(planets, "Earth")) {
|
||||
Earth is here!
|
||||
} else {
|
||||
Earth is not in this array.
|
||||
}
|
||||
```
|
||||
|
||||
#### `#count`
|
||||
|
||||
The `#count` tag returns the number of items in an array. For example:
|
||||
|
||||
```leaf
|
||||
Your search matched #count(matches) pages.
|
||||
```
|
||||
|
||||
#### `#lowercase`
|
||||
|
||||
The `#lowercase` tag lowercases all letters in a string.
|
||||
|
||||
```leaf
|
||||
#lowercase(name)
|
||||
```
|
||||
|
||||
#### `#uppercase`
|
||||
|
||||
The `#uppercase` tag uppercases all letters in a string.
|
||||
|
||||
```leaf
|
||||
#uppercase(name)
|
||||
```
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
# Redis basic usage
|
||||
|
||||
To interact with Redis, you first need to construct a Redis client.
|
||||
The Redis library primarily supports TCP sockets.
|
||||
|
||||
This requires a hostname, port and worker. The eventloop will be used for Redis' Socket. The hostname and port have a default. The hostname is defaulted to `localhost`, and the port to Redis' default port `6379`.
|
||||
|
||||
```swift
|
||||
let client = try RedisClient.connect(on: worker) // Future<RedisClient>
|
||||
```
|
||||
|
||||
The `connect` method will return a future containing the TCP based Redis Client.
|
||||
|
||||
## Redis Data Types
|
||||
|
||||
Redis has 6 data types:
|
||||
|
||||
- null
|
||||
- Int
|
||||
- Error
|
||||
- Array
|
||||
- Basic String (used for command names and basic replies only)
|
||||
- Bulk String (used for Strings and binary data blobs)
|
||||
|
||||
You can instantiate one from the static functions and variables on `RedisData`.
|
||||
|
||||
```swift
|
||||
let null = RedisData.null
|
||||
|
||||
let helloWorld = RedisData.bulkString("Hello World")
|
||||
|
||||
let three = RedisData.integer(3)
|
||||
|
||||
let oneThroughTen = RedisData.array([
|
||||
.integer(1),
|
||||
.integer(2),
|
||||
.integer(3),
|
||||
.integer(4),
|
||||
.integer(5),
|
||||
.integer(6),
|
||||
.integer(7),
|
||||
.integer(8),
|
||||
.integer(9),
|
||||
.integer(10)
|
||||
])
|
||||
```
|
||||
|
||||
The above is the explicit way of defining Redis Types. You can also use literals in most scenarios:
|
||||
|
||||
```swift
|
||||
let array = RedisData.array([
|
||||
[
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
|
||||
],
|
||||
"Hello World",
|
||||
"One",
|
||||
"Two",
|
||||
.null,
|
||||
.null,
|
||||
"test"
|
||||
])
|
||||
```
|
||||
|
||||
## CRUD using Redis
|
||||
|
||||
From here on it is assumed that your client has been successfully created and is available in the variable `client` as a `RedisClient`.
|
||||
|
||||
### Creating a record
|
||||
|
||||
Creating a record is done using a `RedisData` for a value and a key.
|
||||
|
||||
```swift
|
||||
client.set("world", forKey: "hello")
|
||||
```
|
||||
|
||||
This returns a future that'll indicate successful or unsuccessful insertion.
|
||||
|
||||
### Reading a record
|
||||
|
||||
Reading a record is similar, only you'll get a warning if you don't use the returned future.
|
||||
|
||||
The `Future<RedisData>` for the key "hello" will be "world" if you created the record as shown above.
|
||||
|
||||
```swift
|
||||
let futureRecord = client.getData(forKey: "hello") // Future<RedisData>
|
||||
```
|
||||
|
||||
### Deleting a record
|
||||
|
||||
Deleting a record is similar but allows querying the keys, too.
|
||||
|
||||
```swift
|
||||
client.delete(keys: ["hello"])
|
||||
```
|
||||
|
||||
Where the above command will remove the key "hello", the next command will delete **all** keys from the Redis database.
|
||||
|
||||
```swift
|
||||
client.delete(keys: ["*"])
|
||||
```
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
# Custom commands
|
||||
|
||||
Many commands are not (yet) implemented by the driver using a convenience function. This does not mean the feature/command is not usable.
|
||||
|
||||
[(Almost) all functions listed here](https://redis.io/commands) work out of the box using custom commands.
|
||||
|
||||
## Usage
|
||||
|
||||
The Redis client has a `run` function that allows you to run these commands.
|
||||
|
||||
The following code demonstrates a "custom" implementation for [GET](https://redis.io/commands/get).
|
||||
|
||||
```swift
|
||||
let future = client.run(command: "GET", arguments: ["my-key"]) // Future<RedisData>
|
||||
```
|
||||
|
||||
This future will contain the result as specified in the article on the redis command page or an error.
|
||||
|
|
@ -1,36 +1,68 @@
|
|||
!!! warning
|
||||
Redis 3.0 is still in beta. Some documentation may be missing or out of date.
|
||||
# Getting Started with Redis
|
||||
|
||||
# Redis
|
||||
Redis ([vapor/redis](https://github.com/vapor/redis)) is a pure-Swift, event-driven, non-blocking Redis client built on top of SwiftNIO.
|
||||
|
||||
Redis is a Redis client library that can communicate with a Redis database.
|
||||
You can use this package to interact send Redis commands to your server directly, or as a cache through Vapor's `KeyedCache` interface.
|
||||
|
||||
### What is Redis?
|
||||
Let's take a look at how you can get started using Redis.
|
||||
|
||||
Redis is an in-memory data store used as a database, cache and message broker. It supports most common data structures. Redis is most commonly used for caching data such as sessions and notifications (between multiple servers).
|
||||
## Package
|
||||
|
||||
Redis works as a key-value store, but allows querying the keys, unlike most databases.
|
||||
|
||||
## With and without Vapor
|
||||
|
||||
To include it in your package, add the following to your `Package.swift` file.
|
||||
The first step to using Redis is adding it as a dependency to your project in your SPM package manifest file.
|
||||
|
||||
```swift
|
||||
// swift-tools-version:4.0
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "Project",
|
||||
name: "MyApp",
|
||||
dependencies: [
|
||||
...
|
||||
.package(url: "https://github.com/vapor/redis.git", .upToNextMajor(from: "3.0.0")),
|
||||
/// Any other dependencies ...
|
||||
|
||||
// ⚡️Non-blocking, event-driven Redis client.
|
||||
.package(url: "https://github.com/vapor/redis.git", from: "3.0.0"),
|
||||
],
|
||||
targets: [
|
||||
.target(name: "Project", dependencies: ["Redis", ... ])
|
||||
.target(name: "App", dependencies: ["Redis", ...]),
|
||||
.target(name: "Run", dependencies: ["App"]),
|
||||
.testTarget(name: "AppTests", dependencies: ["App"]),
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
If this is your first time adding a dependency, you should read our introduction to [Package.swift](../getting-started/spm.md).
|
||||
## Provider
|
||||
|
||||
Once you have succesfully added the Auth package to your project, the next step is to configure it in your application. This is usually done in [`configure.swift`](../getting-started/structure.md#configureswift).
|
||||
|
||||
```swift
|
||||
import Redis
|
||||
|
||||
// register Redis provider
|
||||
try services.register(RedisProvider())
|
||||
```
|
||||
|
||||
That's it for basic setup. The next step is to create a Redis connection and send a command.
|
||||
|
||||
## Command
|
||||
|
||||
First, create a new connection to your Redis database. This package is built on top of DatabaseKit, so you can use any of its convenience methods for creating a new connection. See [DatabaseKit → Overview](../database-kit/overview.md) for more information.
|
||||
|
||||
```swift
|
||||
router.get("redis") { req -> Future<String> in
|
||||
return req.withNewConnection(to: .redis) { redis in
|
||||
// use redis connection
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Once you have a connection, you can use it to send a command. Let's send the `"INFO"` command which should return information about our Redis server.
|
||||
|
||||
```swift
|
||||
// send INFO command to redis
|
||||
return redis.command("INFO")
|
||||
// map the resulting RedisData to a String
|
||||
.map { $0.string ?? "" }
|
||||
```
|
||||
|
||||
Run your app and query `GET /redis`. You should see information about your Redis server printed as output. Congratulations!
|
||||
|
||||
Use `import Redis` to access Redis' APIs.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
# Using Redis
|
||||
|
||||
Redis ([vapor/redis](https://github.com/vapor/redis)) is a pure-Swift, event-driven, non-blocking Redis client built on top of SwiftNIO.
|
||||
|
||||
You can use this package to interact send Redis commands to your server directly, or as a cache through Vapor's `KeyedCache` interface.
|
||||
|
||||
## Redis Commands
|
||||
|
||||
Let's take a look at how to send and recieve data using Redis commands.
|
||||
|
||||
### Connection
|
||||
|
||||
The first thing you will need to send a Redis command is a connection. This package is built on top of DatabaseKit, so you can use any of its convenience methods for creating a new connection.
|
||||
|
||||
For this example, we will use the `withNewConnection(to:)` method to create a new connection to Redis.
|
||||
|
||||
```swift
|
||||
router.get("redis") { req -> Future<String> in
|
||||
return req.withNewConnection(to: .redis) { redis in
|
||||
// use redis connection
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See [DatabaseKit → Overview](../database-kit/overview.md) for more information.
|
||||
|
||||
### Available Commands
|
||||
|
||||
See [`RedisClient`](https://api.vapor.codes/redis/latest/Redis/Classes/RedisClient.html) for a list of all available commands. Here we'll take a look at some common commands.
|
||||
|
||||
#### Get / Set
|
||||
|
||||
Redis's `GET` and `SET` commands allow you to store and later retrieve data from the server. You can pass any `Codable` type as the value to this command.
|
||||
|
||||
```swift
|
||||
router.get("set") { req -> Future<HTTPStatus> in
|
||||
// create a new redis connection
|
||||
return req.withNewConnection(to: .redis) { redis in
|
||||
// save a new key/value pair to the cache
|
||||
return redis.set("hello", to: "world")
|
||||
// convert void future to HTTPStatus.ok
|
||||
.transform(to: .ok)
|
||||
}
|
||||
}
|
||||
|
||||
router.get("get") { req -> Future<String> in
|
||||
// create a new redis connection
|
||||
return req.withNewConnection(to: .redis) { redis in
|
||||
// fetch the key/value pair from the cache, decoding a String
|
||||
return redis.get("hello", as: String.self)
|
||||
// handle nil case
|
||||
.map { $0 ?? "" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Delete
|
||||
|
||||
Redis's `DELETE` command allows you to clear a previously stored key/value pair.
|
||||
|
||||
```swift
|
||||
router.get("del") { req -> Future<HTTPStatus> in
|
||||
// create a new redis connection
|
||||
return req.withNewConnection(to: .redis) { redis in
|
||||
// fetch the key/value pair from the cache, decoding a String
|
||||
return redis.delete("hello")
|
||||
// convert void future to HTTPStatus.ok
|
||||
.transform(to: .ok)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See [`RedisClient`](https://api.vapor.codes/redis/latest/Redis/Classes/RedisClient.html) for a list of all available commands.
|
||||
|
||||
## Keyed Cache
|
||||
|
||||
You can also use Redis as the backend to Vapor's [`KeyedCache`](https://api.vapor.codes/database-kit/latest/DatabaseKit/Protocols/KeyedCache.html) protocol.
|
||||
|
||||
```swift
|
||||
router.get("set") { req -> Future<HTTPStatus> in
|
||||
let string = try req.query.get(String.self, at: "string")
|
||||
return try req.keyedCache(for: .redis).set("string", to: string)
|
||||
.transform(to: .ok)
|
||||
}
|
||||
|
||||
router.get("get") { req -> Future<String> in
|
||||
return try req.keyedCache(for: .redis).get("string", as: String.self)
|
||||
.unwrap(or: Abort(.badRequest, reason: "No string set yet."))
|
||||
}
|
||||
```
|
||||
|
||||
See [DatabaseKit → Overview](../database-kit/overview/#keyed-cache) for more information.
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
# Pipelining
|
||||
|
||||
Pipelining is used for sending multiple commands at once. The performance advantages become apparent when sending a large number of queries. Redis' pipelining cuts down latency by reducing the RTT (Round Trip Time) between the client and server. Pipelining also reduces the amount of IO operations Redis has to perform, this increases the amount of queries per second Redis can handle.
|
||||
|
||||
### Use cases
|
||||
Sometimes multiple commands need to be executed at once. Instead of sending those commands individually in a loop, pipelining allows the commands to be batched and sent in one request. A common scenario might be needing to set a key and increment a count, pipelining those commands would be ideal.
|
||||
|
||||
### Enqueuing Commands
|
||||
|
||||
```swift
|
||||
let pipeline = connection.makePipeline()
|
||||
let result = try pipeline
|
||||
.enqueue(command: "SET", arguments: ["KEY", "VALUE"])
|
||||
.enqueue(command: "INCR", arguments: ["COUNT"])
|
||||
.execute() // Future<[RedisData]>
|
||||
|
||||
```
|
||||
Note: Commands will not be executed until execute is called.
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
# Publish & Subscribe
|
||||
|
||||
Redis' Publish and Subscribe model is really useful for notifications.
|
||||
|
||||
### Use cases
|
||||
|
||||
Pub/sub is used for notifying subscribers of an event.
|
||||
A simple and common event for example would be a chat message.
|
||||
|
||||
A channel consists of a name and group of listeners. Think of it as being `[String: [Listener]]`.
|
||||
When you send a notification to a channel you need to provide a payload.
|
||||
Each listener will get a notification consisting of this payload.
|
||||
|
||||
Channels must be a string. For chat groups, for example, you could use the database identifier.
|
||||
|
||||
### Publishing
|
||||
|
||||
You cannot get a list of listeners, but sending a payload will emit the amount of listeners that received the notification.
|
||||
Sending (publishing) an event is done like so:
|
||||
|
||||
```swift
|
||||
// Any redis data
|
||||
let notification: RedisData = "My-Notification"
|
||||
|
||||
client.publish(notification, to: "my-channel")
|
||||
```
|
||||
|
||||
If you want access to the listener count:
|
||||
|
||||
```swift
|
||||
let notifiedCount = client.publish(notification, to: "my-channel") // Future<Int>
|
||||
```
|
||||
|
||||
### Subscribing
|
||||
|
||||
To subscribe for notifications you're rendering an entire Redis Client useless in exchange for listening to events.
|
||||
|
||||
A single client can listen to one or more channels, which is provided using a set of unique channel names. The result of subscribing is a `SubscriptionStream`.
|
||||
|
||||
```swift
|
||||
let notifications = client.subscribe(to: ["some-notification-channel", "other-notification-channel"])
|
||||
```
|
||||
|
||||
If you try to use the client after subscribing, all operations will fail. These errors are usually emitted through the Future.
|
||||
|
||||
This stream will receive messages asynchronously from the point of `draining`. This works like any other async stream.
|
||||
|
||||
Notifications consist of the channel and payload.
|
||||
|
||||
```swift
|
||||
notifications.drain { notification in
|
||||
print(notification.channel)
|
||||
|
||||
let payload = notification.payload
|
||||
|
||||
// TODO: Process the payload
|
||||
}
|
||||
```
|
||||
|
|
@ -12,6 +12,7 @@ struct User: Codable {
|
|||
var name: String
|
||||
var age: Int
|
||||
var email: String?
|
||||
var profilePictureURL: String?
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -21,6 +22,7 @@ For example, when you decode the above `User` model, Swift will automatically en
|
|||
- `name` is a valid `String` and is _not_ `nil`.
|
||||
- `age` is a valid `Int` and is _not_ `nil`.
|
||||
- `email` is a valid `String` or is `nil`.
|
||||
- `profilePictureURL` is a valid `String` or is `nil`.
|
||||
|
||||
This is a great first step, but there is still room for improvement here. Here are some examples of things Swift and `Codable` would not mind, but are not ideal:
|
||||
|
||||
|
|
@ -28,6 +30,7 @@ This is a great first step, but there is still room for improvement here. Here a
|
|||
- `name` contains non-alphanumeric characters
|
||||
- `age` is a negative number `-42`
|
||||
- `email` is not correctly formatted `test@@vapor.codes`
|
||||
- `profilePictureURL` is not a `URL` without a scheme
|
||||
|
||||
Luckily the Validation package can help.
|
||||
|
||||
|
|
@ -109,6 +112,7 @@ extension User: Validatable {
|
|||
try validations.add(\.name, .alphanumeric && .count(3...))
|
||||
try validations.add(\.age, .range(18...))
|
||||
try validations.add(\.email, .email || .nil)
|
||||
try validations.add(\.profilePictureURL, .url || .nil)
|
||||
return validations
|
||||
}
|
||||
}
|
||||
|
|
@ -126,5 +130,3 @@ router.post(User.self, at: "users") { req, user -> User in
|
|||
When you query that route, you should see that errors are thrown if the data does not meet your validations. If the data is correct, your user model is returned successfully.
|
||||
|
||||
Congratulations on setting up your first `Validatable` model! Check out the [API docs](https://api.vapor.codes/validation/latest/Validation/index.html) for more information and code samples.
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
# Middleware
|
||||
|
||||
Coming soon.
|
||||
|
|
@ -23,6 +23,10 @@ pages:
|
|||
- 'Async':
|
||||
- 'Getting Started': 'async/getting-started.md'
|
||||
- 'Overview': 'async/overview.md'
|
||||
- 'Auth':
|
||||
- 'Getting Started': 'auth/getting-started.md'
|
||||
- 'Stateless (API)': 'auth/api.md'
|
||||
- 'Sessions (Web)': 'auth/web.md'
|
||||
- 'Console':
|
||||
- 'Getting Started': 'console/getting-started.md'
|
||||
- 'Overview': 'console/overview.md'
|
||||
|
|
@ -35,6 +39,7 @@ pages:
|
|||
- 'Ciphers': 'crypto/ciphers.md'
|
||||
- 'Asymmetric': 'crypto/asymmetric.md'
|
||||
- 'Random': 'crypto/random.md'
|
||||
- 'TOTP & HOTP': 'crypto/otp.md'
|
||||
- 'Database Kit':
|
||||
- 'Getting Started': 'database-kit/getting-started.md'
|
||||
- 'Overview': 'database-kit/overview.md'
|
||||
|
|
@ -50,9 +55,12 @@ pages:
|
|||
- 'Client': 'http/client.md'
|
||||
- 'Server': 'http/server.md'
|
||||
- 'Message': 'http/message.md'
|
||||
- 'JWT':
|
||||
- 'Getting Started': 'jwt/getting-started.md'
|
||||
- 'Overview': 'jwt/overview.md'
|
||||
- 'Leaf':
|
||||
- 'Getting Started': 'leaf/getting-started.md'
|
||||
- 'Basics': 'leaf/basics.md'
|
||||
- 'Overview': 'leaf/overview.md'
|
||||
- 'Custom tags': 'leaf/custom-tags.md'
|
||||
- 'Logging':
|
||||
- 'Getting Started': 'logging/getting-started.md'
|
||||
|
|
@ -66,10 +74,7 @@ pages:
|
|||
- 'Getting Started': 'postgresql/getting-started.md'
|
||||
- 'Redis':
|
||||
- 'Getting Started': 'redis/getting-started.md'
|
||||
- 'Basics': 'redis/basics.md'
|
||||
- 'Custom commands': 'redis/custom-commands.md'
|
||||
- 'Publish and Subscribe': 'redis/pub-sub.md'
|
||||
- 'Pipeline': 'redis/pipeline.md'
|
||||
- 'Overview': 'redis/overview.md'
|
||||
- 'Routing':
|
||||
- 'Getting Started': 'routing/getting-started.md'
|
||||
- 'Overview': 'routing/overview.md'
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ COPY . /app
|
|||
WORKDIR /app
|
||||
RUN pip install -r requirements.txt && rm -rf $HOME/.cache/pip
|
||||
|
||||
RUN cd leaf-pygment && ./compile.sh
|
||||
RUN pip install leaf-pygment/dist/leaf-0.1.0-dev.tar.gz
|
||||
RUN cd 3.0 && mkdocs build
|
||||
RUN cd 2.0 && mkdocs build
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
# file GENERATED by distutils, do NOT edit
|
||||
README
|
||||
setup.py
|
||||
leaf/__init__.py
|
||||
leaf/lexer.py
|
||||
|
|
@ -0,0 +1 @@
|
|||
Provides Leaf syntax highlighting for Pygment.
|
||||
|
|
@ -0,0 +1 @@
|
|||
python setup.py sdist
|
||||
Binary file not shown.
|
|
@ -0,0 +1,47 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
pygments.lexers.leaf
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Lexer for Leaf markup.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from pygments.lexer import RegexLexer, ExtendedRegexLexer, include, bygroups, default, using
|
||||
from pygments.token import Text, Comment, Operator, Keyword, Name, String, Punctuation, Number
|
||||
|
||||
__all__ = ['LeafLexer']
|
||||
|
||||
class LeafLexer(RegexLexer):
|
||||
name = 'Leaf'
|
||||
aliases = ['leaf']
|
||||
filenames = ['*.leaf']
|
||||
mimetypes = ['text/leaf', 'application/leaf']
|
||||
tokens = {
|
||||
'root': [
|
||||
(r'\n', Comment),
|
||||
(r'\s+', Comment),
|
||||
(r'else', Keyword),
|
||||
(r'#\/\/.*', String),
|
||||
(r'#\/\*[^\*]*\*\/', String),
|
||||
(r'if\(|if\ \(', Keyword, 'expression'),
|
||||
(r'(\#)([^\(]*)(\()', bygroups(Keyword, Keyword, Punctuation), 'expression'),
|
||||
(r'\{', Name.Builtin.Pseudo),
|
||||
(r'\}', Name.Builtin.Pseudo),
|
||||
(r'[^\#\}]+', Comment),
|
||||
],
|
||||
'expression': [
|
||||
(r'\s+', Text),
|
||||
(r'(")([^"]+)(")', String),
|
||||
(r'[\d]+', Number.Int),
|
||||
(r'in', Keyword),
|
||||
(r',', Text),
|
||||
(r'[\=\!\|\&\+\-\*\%]+', Operator),
|
||||
(r'[\w]+(?=\()', Name.Builtin.Pseudo),
|
||||
(r'[\w]+', Text),
|
||||
(r'\(', Text, '#push'),
|
||||
(r'\)', Text, '#pop'),
|
||||
]
|
||||
}
|
||||
|
||||
Binary file not shown.
|
|
@ -0,0 +1,16 @@
|
|||
from distutils.core import setup
|
||||
|
||||
|
||||
setup (
|
||||
name='leaf',
|
||||
version='0.1.0-dev',
|
||||
url='https://github.com/vapor/leaf',
|
||||
author='tanner0101',
|
||||
author_email='me@tanner.xyz',
|
||||
packages=['leaf'],
|
||||
entry_points =
|
||||
"""
|
||||
[pygments.lexers]
|
||||
leaf = leaf.lexer:LeafLexer
|
||||
""",
|
||||
)
|
||||
Loading…
Reference in New Issue