From 564b9115526c9bf3314d383891e5aeca925ad6e8 Mon Sep 17 00:00:00 2001 From: Natan Rolnik Date: Wed, 8 Aug 2018 20:33:55 +0300 Subject: [PATCH 01/13] Documents URL validation --- 3.0/docs/validation/overview.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/3.0/docs/validation/overview.md b/3.0/docs/validation/overview.md index 09d3b296..f9674264 100644 --- a/3.0/docs/validation/overview.md +++ b/3.0/docs/validation/overview.md @@ -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. - - From cb374e0994d30d8f2a15f23ba82888f416738643 Mon Sep 17 00:00:00 2001 From: tanner0101 Date: Wed, 8 Aug 2018 18:08:47 -0400 Subject: [PATCH 02/13] leaf 3 docs --- .gitignore | 1 + 3.0/docs/leaf/custom-tags.md | 53 +----- 3.0/docs/leaf/getting-started.md | 107 +++++++----- 3.0/docs/leaf/{basics.md => overview.md} | 199 +++++++++++------------ 3.0/mkdocs.yml | 2 +- Dockerfile | 2 + leaf-pygment/MANIFEST | 5 + leaf-pygment/README | 1 + leaf-pygment/compile.sh | 1 + leaf-pygment/leaf/__init__.py | 0 leaf-pygment/leaf/__init__.pyc | Bin 0 -> 155 bytes leaf-pygment/leaf/lexer.py | 47 ++++++ leaf-pygment/leaf/lexer.pyc | Bin 0 -> 1716 bytes leaf-pygment/setup.py | 16 ++ 14 files changed, 237 insertions(+), 197 deletions(-) rename 3.0/docs/leaf/{basics.md => overview.md} (56%) create mode 100644 leaf-pygment/MANIFEST create mode 100644 leaf-pygment/README create mode 100755 leaf-pygment/compile.sh create mode 100644 leaf-pygment/leaf/__init__.py create mode 100644 leaf-pygment/leaf/__init__.pyc create mode 100644 leaf-pygment/leaf/lexer.py create mode 100644 leaf-pygment/leaf/lexer.pyc create mode 100644 leaf-pygment/setup.py diff --git a/.gitignore b/.gitignore index e7aad410..3503f6d1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /.couscous .sass-cache .DS_Store +leaf-pygment/dist diff --git a/3.0/docs/leaf/custom-tags.md b/3.0/docs/leaf/custom-tags.md index 526993a2..006f3d22 100644 --- a/3.0/docs/leaf/custom-tags.md +++ b/3.0/docs/leaf/custom-tags.md @@ -1,54 +1,3 @@ # 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. - -When working with custom tags, there are four important things to know: - -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` 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. - -Here’s example code for a `CustomUppercase` Leaf tag: - -```swift -import Async -import Leaf - -public final class CustomUppercase: Leaf.LeafTag { - public init() {} - public func render(parsed: ParsedTag, context: LeafContext, renderer: LeafRenderer) throws -> Future { - // 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)) - } -} -``` - -We can now register this Tag in our `configure.swift` file with: - -```swift -services.register { container -> LeafConfig in - // take a copy of Leaf's default tags - var tags = defaultTags - - // add our custom tag - tags["customuppercase"] = CustomUppercase() - - // find the location of our Resources/Views directory - let directoryConfig = try container.make(DirectoryConfig.self, for: LeafRenderer.self) - let viewsDirectory = directoryConfig.workDir + "Resources/Views" - - // put all that into a new Leaf configuration and return it - return LeafConfig(tags: tags, viewsDir: viewsDirectory) -} -``` - -Once that is complete, you can use `#customuppercase(some_variable)` to run your custom code. - -> Note: Use of non-alphanumeric characters in tag names is **strongly discouraged** and may be disallowed in future versions of Leaf. +Coming soon. diff --git a/3.0/docs/leaf/getting-started.md b/3.0/docs/leaf/getting-started.md index 76ff0c3c..7af4cec1 100644 --- a/3.0/docs/leaf/getting-started.md +++ b/3.0/docs/leaf/getting-started.md @@ -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 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! diff --git a/3.0/docs/leaf/basics.md b/3.0/docs/leaf/overview.md similarity index 56% rename from 3.0/docs/leaf/basics.md rename to 3.0/docs/leaf/overview.md index dfb7ef17..0ec49c29 100644 --- a/3.0/docs/leaf/basics.md +++ b/3.0/docs/leaf/overview.md @@ -1,110 +1,76 @@ -# Basics +# Leaf Overview -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: +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. -- 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 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 -} -``` - -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. -} -``` +This guide will give you an overview of Leaf's syntax and the available tags. ## Template syntax -### Structure + +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: `#` 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 +- 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 (none): 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: - - `#()` - - `#(variable)` - - `#embed("template")` - - `#set("title") { Welcome to Vapor }` - - `#count(friends)` - - `#for(friend in friends) {
  • #(friend.name)
  • }` +```leaf +#(variable) +#embed("template") +#set("title") { Welcome to Vapor } +#count(friends) +#for(friend in friends) {
  • #(friend.name)
  • } +``` +Leaf also supports many expressions you are familiar with in Swift. -### Working with context +- `+` +- `>` +- `==` +- `||` +- etc. -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: +```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 -let context = ["title": "Welcome", "message": "Vapor and Leaf work hand in hand"] -return try leaf.make("home", context) +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

    #(title)

    -

    #(message)

    +

    #(number)

    ``` -### Checking conditions +## 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 { @@ -114,7 +80,7 @@ Leaf is able to evaluate a range of conditions using its `#if` tag. For example, You can also write comparisons, for example: -``` +```leaf #if(title == "Welcome") { This is a friendly web page. } else { @@ -124,7 +90,7 @@ You can also write comparisons, for example: 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 { @@ -132,6 +98,17 @@ If you want to use another tag as part of your condition, you should omit the `# } ``` +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 @@ -143,7 +120,7 @@ let context = ["team": ["Malcolm", "Kaylee", "Jayne"]] We could then loop over them in Leaf like this: -``` +```leaf #for(name in team) {

    #(name) is in the team.

    } @@ -157,7 +134,7 @@ Leaf provides some extra variables inside a `#for` loop to give you more informa Here's how we could use a loop variable to print just the first name in our array: -``` +```leaf #for(name in team) { #if(isFirst) {

    #(name) is first!

    } } @@ -169,7 +146,7 @@ Leaf’s `#embed` tag allows you to copy the contents of one template into anoth Embedding is useful for copying in a standard piece of content, for example a page footer or advert code: -``` +```leaf #embed("footer") ``` @@ -179,9 +156,9 @@ Using this approach, you would construct a child template that fills in its uniq For example, you might create a child.leaf template like this: -``` +```leaf #set("body") { -

    Welcome to Vapor!

    +

    Welcome to Vapor!

    } #embed("master") @@ -189,29 +166,51 @@ For example, you might create a child.leaf template like this: 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 -#(title) -#get(body) + + #(title) + + #get(body) ``` When given the context `["title": "Hi there!"]`, child.leaf will render as follows: -``` +```html -Hi there! -

    Welcome to Vapor!

    + + Hi there! + +

    Welcome to Vapor!

    ``` +### 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 #### `#capitalize` The `#capitalize` tag uppercases the first letter of any string. For example, “taylor” will become “Taylor”. -``` +```leaf #capitalize(name) ``` @@ -219,7 +218,7 @@ The `#capitalize` tag uppercases the first letter of any string. For example, 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`: -``` +```leaf #if(contains(team, "Jayne")) { You're all set! } else { @@ -231,7 +230,7 @@ The `#contains` tag accepts an array and a value as its two parameters, and retu The `#count` tag returns the number of items in an array. For example: -``` +```leaf Your search matched #count(matches) pages. ``` @@ -239,7 +238,7 @@ Your search matched #count(matches) pages. The `#lowercase` tag lowercases all letters in a string. For example, “Taylor” will become “taylor”. -``` +```leaf #lowercase(name) ``` @@ -247,6 +246,6 @@ The `#lowercase` tag lowercases all letters in a string. For example, “Taylor The `#uppercase` tag uppercases all letters in a string. For example, “Taylor” will become “TAYLOR”. -``` +```leaf #uppercase(name) ``` diff --git a/3.0/mkdocs.yml b/3.0/mkdocs.yml index 825978c7..774b3fed 100644 --- a/3.0/mkdocs.yml +++ b/3.0/mkdocs.yml @@ -52,7 +52,7 @@ pages: - 'Message': 'http/message.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' diff --git a/Dockerfile b/Dockerfile index 15b54a15..c6c8ec4f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,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 FROM nginx:1.13.12-alpine as production-stage diff --git a/leaf-pygment/MANIFEST b/leaf-pygment/MANIFEST new file mode 100644 index 00000000..f8d5cf73 --- /dev/null +++ b/leaf-pygment/MANIFEST @@ -0,0 +1,5 @@ +# file GENERATED by distutils, do NOT edit +README +setup.py +leaf/__init__.py +leaf/lexer.py diff --git a/leaf-pygment/README b/leaf-pygment/README new file mode 100644 index 00000000..acbfe3ba --- /dev/null +++ b/leaf-pygment/README @@ -0,0 +1 @@ +Provides Leaf syntax highlighting for Pygment. \ No newline at end of file diff --git a/leaf-pygment/compile.sh b/leaf-pygment/compile.sh new file mode 100755 index 00000000..770a61ec --- /dev/null +++ b/leaf-pygment/compile.sh @@ -0,0 +1 @@ +python setup.py sdist diff --git a/leaf-pygment/leaf/__init__.py b/leaf-pygment/leaf/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/leaf-pygment/leaf/__init__.pyc b/leaf-pygment/leaf/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0a16537b707b5873bfe314fbf3d3a7d0248e1a42 GIT binary patch literal 155 zcmZSn%**vCB|AEq0SXv_v;zx8Z1h-9~Vx_i2NzOSX)Up?p7)y3HgF0UFszrnx$6Ga5zqqHEh zA+f+m1%EY2Y*01uYoO}j*FhbCe*mfhegjk!{3fUt_${y~x(-Pjlmos4ssnxpR2Td% zs2=z|P<`YQ@2TNK0)D( z@XB`^B~zp4iQLM99^slDzyHVOu;(#Fj@DT*dW^v%6&4p|K6m23@j7uEnAtceJh_ou zjx|TP{C;a>8p-HiX6htPS4kO3(?b2lZc}7st_?XNxem+3m;=;H9j6=eWatjU$i) zI3Kd{c;Cvkeb@&e57~A7YTgg`0XImAKH?TXX8jY=sa^*j;M{qq?xzS915Ze@LNld_ zfhmudCYTL!xM0T+S7Dwf@rwDVj5ZoYs_C^7dd68~qNS$E7>k8i%%^&AsZYtWjnx?rt6A=c{{x~%_9PpVwX7fXFQ@j>;#7umSZQWkZxUn^D?YOS$x@;KdrT(QdOf*gUIL~sIQu2rgL5i~+1bnI>P+3$aw4_M#@aTt093oIW z%j-Ci_Vp6MGf8y{VgsRKWqG|EKHcDvbcNi?!84MUoKvTc~&f(ve4h6pvG)HU Y#Ut`%H56Y_MNO?V?5;hxzOdW&Kc=W)PXGV_ literal 0 HcmV?d00001 diff --git a/leaf-pygment/setup.py b/leaf-pygment/setup.py new file mode 100644 index 00000000..7483e480 --- /dev/null +++ b/leaf-pygment/setup.py @@ -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 + """, +) \ No newline at end of file From 9ca94bfa452ab61203880b299f49247209c547d2 Mon Sep 17 00:00:00 2001 From: tanner0101 Date: Wed, 8 Aug 2018 18:40:48 -0400 Subject: [PATCH 03/13] implement custom tags section --- 3.0/docs/leaf/custom-tags.md | 55 +++++++++++++++++++++++++- 3.0/docs/leaf/overview.md | 76 ++++++++++++++++++++++++++---------- 2 files changed, 110 insertions(+), 21 deletions(-) diff --git a/3.0/docs/leaf/custom-tags.md b/3.0/docs/leaf/custom-tags.md index 006f3d22..1056b865 100644 --- a/3.0/docs/leaf/custom-tags.md +++ b/3.0/docs/leaf/custom-tags.md @@ -1,3 +1,56 @@ # Custom Tags -Coming soon. +You can create custom Leaf tags using the [`TagRenderer`](https://api.vapor.codes/template-kit/latest/TemplateKit/Protocols/TagRenderer.html) protocol. + +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. + +## Tag Renderer + +First create a class called `NowTag` and conform it to `TagRenderer`. + +```swift +final class NowTag: TagRenderer { + init() { } + + func render(tag: TagContext) throws -> EventLoopFuture { + ... + } +} +``` + +Now let's implement the `render(tag:)` method. The `TagContext` context passed to this method has everything we should need. + +```swift +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 ... +} + +let string = formatter.string(from: .init()) +return tag.container.future(.string(string)) +``` + +## Configure Tag + +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 +} +``` + +And that's it! We can now use our custom tag in Leaf. + +```leaf +The time is #now() +``` diff --git a/3.0/docs/leaf/overview.md b/3.0/docs/leaf/overview.md index 0ec49c29..0d080c86 100644 --- a/3.0/docs/leaf/overview.md +++ b/3.0/docs/leaf/overview.md @@ -14,10 +14,10 @@ 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 (none): An optional body can be supplied to some tags. This is similar to Swift's trailing-closure syntax. +- 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: @@ -112,31 +112,49 @@ Just like in Swift, you can also use `else if` statement.s ### 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: +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 -let context = ["team": ["Malcolm", "Kaylee", "Jayne"]] +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 -#for(name in team) { -

    #(name) is in the team.

    +Planets: +
      +#for(planet in planets) { +
    • #(planet)
    • } +
    +``` + +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 `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. +- 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(name in team) { - #if(isFirst) {

    #(name) is first!

    } +#for(planet in planets) { + #if(isFirst) { #(planet) is first! } } ``` @@ -206,9 +224,27 @@ 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. For example, “taylor” will become “Taylor”. +The `#capitalize` tag uppercases the first letter of any string. ```leaf #capitalize(name) @@ -216,13 +252,13 @@ The `#capitalize` tag uppercases the first letter of any string. For example, #### `#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`: +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(team, "Jayne")) { - You're all set! +#if(contains(planets, "Earth")) { + Earth is here! } else { - You need someone to do PR. + Earth is not in this array. } ``` @@ -236,7 +272,7 @@ Your search matched #count(matches) pages. #### `#lowercase` -The `#lowercase` tag lowercases all letters in a string. For example, “Taylor” will become “taylor”. +The `#lowercase` tag lowercases all letters in a string. ```leaf #lowercase(name) @@ -244,7 +280,7 @@ The `#lowercase` tag lowercases all letters in a string. For example, “Taylor #### `#uppercase` -The `#uppercase` tag uppercases all letters in a string. For example, “Taylor” will become “TAYLOR”. +The `#uppercase` tag uppercases all letters in a string. ```leaf #uppercase(name) From 6c8928b35c47c2359fd0605086da248da73ee4cf Mon Sep 17 00:00:00 2001 From: tanner0101 Date: Thu, 9 Aug 2018 11:44:11 -0400 Subject: [PATCH 04/13] add TOTP / HOTP docs --- 3.0/docs/crypto/otp.md | 41 ++ 3.0/docs/style-guide.md | 977 ++++++++++++++++++++++++++++++++++++++++ 3.0/mkdocs.yml | 2 + 3 files changed, 1020 insertions(+) create mode 100644 3.0/docs/crypto/otp.md create mode 100644 3.0/docs/style-guide.md diff --git a/3.0/docs/crypto/otp.md b/3.0/docs/crypto/otp.md new file mode 100644 index 00000000..192be466 --- /dev/null +++ b/3.0/docs/crypto/otp.md @@ -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. diff --git a/3.0/docs/style-guide.md b/3.0/docs/style-guide.md new file mode 100644 index 00000000..1f6779a4 --- /dev/null +++ b/3.0/docs/style-guide.md @@ -0,0 +1,977 @@ +# Vapor Style Guide + +## Motivation + +The Vapor style guide is a perspective on how to write Vapor application code that is clean, readable, and maintainable. It can serve as a jumping off point within your organization (or yourself) for how to write code in a style that aligns with the Vapor ecosystem. We think this guide can help solidify common ideas that occur across most applications and will be a reference for maintainers when starting a new project. This style guide is opinionated, so you should adapt your code in places where you don’t agree. + +## Application Structure + +The structure of your Vapor application is important from a readability standpoint, but also in terms of functionality. Application structure refers to a few different aspects of the Vapor ecosystem, but in particular, it is the way in which you structure your file, folders, and assets. + +The preferred way to structure your application is by separating the application into a few main parts: + +- Controllers +- Middleware +- Models +- Setup +- Utilities + +The structure ensures that new members working on your project can easily find the file or asset they are looking for. + +### Controllers Folder + +The controllers folder houses all of the controllers for your application which correspond to your routes. If you are building an application that serves both API responses and frontend responses, this folder should be further segmented into an `API Controllers` folder and a `View Controllers` folder. + +### Middleware Folder + +The middleware folder contains any custom middleware that you’ve written for your application. Each piece of middleware should be descriptively named and should only be responsible for one piece of functionality. + +### Models Folder + +“Models” in this document means an object that can be used to store or return data throughout the application. Models are not specific to Fluent - Entities, however, include database information that make it possible to persist and query them. + +The Models folder should be broken down into four parts: Entities, Requests, Responses, and View Contexts (if applicable to your application). The `Requests` and `Responses` folder hold object files that are used to decode requests or encode responses. For more information on this, see the “File Naming” section. + +If your application handles view rendering via Leaf, you should also have a folder that holds all of your view contexts. These contexts are the same type of objects as the Request and Response objects, but are specifically for passing data to the view layer. + +The Entities folder is further broken up into a folder for each database model that exists within your application. For example, if you have a `User` model that represents a `users` table, you would have a `Users` folder that contains `User.swift` (the Fluent model representation) and then any other applicable files for this entity. Other common files found at this level include files to extend functionality of the object, repository protocols/implementations, and data transformation extensions. + +### Setup Folder + +The setup folder has all of the necessary pieces that are called on application setup. This includes `app.swift`, `boot.swift`, `configure.swift`, `migrate.swift`, and `routes.swift`. For information on each of these files, see the “Configuration” section. + +### Utilities Folder + +The utilities folder serves as a general purpose location for any objects or helpers that don’t fit the other folders. For example, in your quest to eliminate stringly-typed code (see the “General Advice” section) you might place a `Constants.swift` file in this location. + +The final application structure (inside the Sources folder) looks like this: + +``` +├── Controllers +│ ├── API\ Controllers +│ └── View\ Controllers +├── Middleware +├── Models +│ ├── Entities +│ │ └── User +│ ├── Requests +│ └── Responses +│ └── View\ Contexts +├── Setup +│ ├── app.swift +│ ├── boot.swift +│ ├── commands.swift +│ ├── configure.swift +│ ├── content.swift +│ ├── databases.swift +│ ├── middlewares.swift +│ ├── migrate.swift +│ ├── repositories.swift +│ └── routes.swift +├── Utils +``` + +## Configuration + +Configuring your application correctly is one of the most important parts of a successful Vapor application. The main function of the configuring a Vapor application is correctly registering all of your services and 3rd party providers. + +**Note**: For more information on registering credentials and secrets, see the “Credentials” section. + +### Files + +There are 6 files you should have: + +- app.swift (use the default template version) +- boot.swift (use the default template version) +- commands.swift (Optional) +- configure.swift +- content.swift +- databases.swift (Optional) +- middlewares.swift +- migrate.swift (Optional) +- repositories.swift (Optional) +- routes.swift + +### configure.swift + +Use this file to register your services, providers, and any other code that needs to run as part of the Vapor application setup process. + +### routes.swift + +The routes.swift file is used to declare route registration for your application. Typically, the routes.swift file looks like this: + +```swift +import Vapor + +public func routes(_ router: Router) throws { + try router.register(collection: MyControllerHere()) +} +``` + +You should call this function from `configure.swift` like this: + +```swift + let router = EngineRouter.default() + try routes(router) + services.register(router, as: Router.self) +``` + +For more information on routes, see the “Routes and Controllers” section. + +### commands.swift + +Use this file to add your custom commands to your application. For example: + +```swift +import Vapor + +public func commands(config: inout CommandConfig) { + config.useFluentCommands() + + config.use(MyCustomCommand(), as: "my-custom-command") + ... +} +``` + +You should call this function from `configure.swift` like this: + +```swift + /// Command Config + var commandsConfig = CommandConfig.default() + commands(config: &commandsConfig) + services.register(commandsConfig) +``` + +> If your app doesn't use custom `Command`s you can ommit this file. + +### content.swift + +In this file you can customize the content encoding/decoding configuration for your data models. For example: + +```swift +import Vapor + +public func content(config: inout ContentConfig) throws { + let encoder = JSONEncoder() + let decoder = JSONDecoder() + + encoder.dateEncodingStrategy = .millisecondsSince1970 + decoder.dateDecodingStrategy = .millisecondsSince1970 + + config.use(encoder: encoder, for: .json) + config.use(decoder: decoder, for: .json) +} +``` + +You should call this function from `configure.swift` like this: + +```swift + /// Register Content Config + var contentConfig = ContentConfig.default() + try content(config: &contentConfig) + services.register(contentConfig) +``` + +> If you don't customize the content configuration you can ommit this file. + +### databases.swift + +Use this file to add the databases used in your application. Extracting this logic to a separate file keeps the configure.swift code clean, as it can often get quite long. This file should look something like this: + +```swift +import Vapor +import FluentMySQL //use your database driver here + +public func databases(config: inout DatabasesConfig) throws { + guard let databaseUrl = Environment.get("DATABASE_URL") else { + throw Abort(.internalServerError) + } + + guard let dbConfig = MySQLDatabaseConfig(url: databaseUrl) else { throw Abort(.internalServerError) } + + /// Register the databases + config.add(database: MySQLDatabase(config: dbConfig), as: .mysql) + + ... +} +``` + +And then call this function from `configure.swift` like this: + +```swift + /// Register the configured SQLite database to the database config. + var databasesConfig = DatabasesConfig() + try databases(config: &databasesConfig) + services.register(databasesConfig) +``` + +> If your app doesn't use `Fluent` you can ommit this file. + +### middlewares.swift + +In this file you can customize the middlewares of your application. For example: + +```swift +import Vapor + +public func middlewares(config: inout MiddlewareConfig) throws { + // config.use(FileMiddleware.self) // Serves files from `Public/` directory + config.use(ErrorMiddleware.self) // Catches errors and converts to HTTP response + // Other Middlewares... +} +``` + +You should call this function from `configure.swift` like this: + +```swift + /// Register middlewares + var middlewaresConfig = MiddlewareConfig() + try middlewares(config: &middlewaresConfig) + services.register(middlewaresConfig) +``` + +### migrate.swift + +Use this file to add the migrations to your database. Extracting this logic to a separate file keeps the configure.swift code clean, as it can often get quite long. This file should look something like this: + +```swift +import Vapor +import FluentMySQL //use your database driver here + +public func migrate(migrations: inout MigrationConfig) throws { + migrations.add(model: User.self, database: .mysql) //update this with your database driver +} +``` + +And then call this function from `configure.swift` like this: + +```swift + services.register { container -> MigrationConfig in + var migrationConfig = MigrationConfig() + try migrate(migrations: &migrationConfig) + return migrationConfig + } +``` + +As you continue to add models to your application, make sure that you add them to the migration file as well. + +> If your app doesn't use `Fluent` you can ommit this file. + +### repositories.swift + +The `repositories.swift` file is responsible for registering each repository during the configuration stage. This file should look like this: + +```swift +import Vapor + +public func setupRepositories(services: inout Services, config: inout Config) { + services.register(UserRepository.self) { _ -> MySQLUserRepository in + return MySQLUserRepository() + } + + preferDatabaseRepositories(config: &config) +} + +private func preferDatabaseRepositories(config: inout Config) { + config.prefer(MySQLUserRepository.self, for: UserRepository.self) +} +``` + +Call this function from `configure.swift` like this: + +```swift +setupRepositories(services: &services, config: &config) +``` + +For more information on the repository pattern, see the “Architecture” section. + +> If your app doesn't use `Fluent` you can ommit this file. + +## Credentials + +Credentials are a crucial part to any production-ready application. The preferred way to manage secrets in a Vapor application is via environment variables. These variables can be set via the Xcode scheme editor for testing, the shell, or in the GUI of your hosting provider. + +**Credentials should never, under any circumstances, be checked into a source control repository.** + +Assuming we have the following credential storage service: + +```swift +import Vapor +struct APIKeyStorage: Service { + let apiKey: String +} +``` + +**Bad:** + +```swift +services.register { container -> APIKeyStorage in + return APIKeyStorage(apiKey: "MY-SUPER-SECRET-API-KEY") +} +``` + +\***\*Good:\*\*** + +```swift +guard let apiKey = Environment.get("api-key") else { throw Abort(.internalServerError) } +services.register { container -> APIKeyStorage in + return APIKeyStorage(apiKey: apiKey) +} +``` + +## File Naming + +As the old saying goes, “the two hardest problems in computer science are naming things, cache invalidation, and off by one errors.” To minimize confusion and help increase readability, files should be named succinctly and descriptively. + +Files that contain objects used to decode body content from a request should be appended with `Request`. For example, `LoginRequest`. Files that contain objects used to encode body content to a response should be appended with `Response`. For example, `LoginResponse`. + +Controllers should also be named descriptively for their purpose. If your application contains logic for frontend responses and API responses, each controller’s name should denote their responsibility. For example, `LoginViewController` and `LoginController`. If you combine the login functionality into one controller, opt for the more generic name: `LoginController`. + +## Architecture + +One of the most important decisions to make up front about your app is the style of architecture it will follow. It is incredibly time consuming and expensive to retroactively change your architecture. We recommend that production-level Vapor applications use the repository pattern. + +The basic idea behind the repository pattern is that it creates another abstraction between Fluent and your application code. Instead of using Fluent queries directly in controllers, this pattern encourages abstracting those queries into a more generic protocol and using that instead. + +There are a few benefits to this method. First, it makes testing a lot easier. This is because during the test environment you can easily utilize Vapor’s configuration abilities to swap out which implementation of the repository protocol gets used. This makes unit testing much faster because the unit tests can use a memory version of the protocol rather than the database. The other large benefit to this pattern is that it makes it really easy to switch out the database layer if needed. Because all of the ORM logic is abstracted to this piece of the application (and the controllers don’t know it exists) you could realistically swap out Fluent with a different ORM with minimal changes to your actual application/business logic code. + +Here’s an example of a `UserRepository`: + +```swift +import Vapor +import FluentMySQL +import Foundation + +protocol UserRepository: Service { + func find(id: Int, on connectable: DatabaseConnectable) -> Future + func all(on connectable: DatabaseConnectable) -> Future<[User]> + func find(email: String, on connectable: DatabaseConnectable) -> Future + func findCount(email: String, on connectable: DatabaseConnectable) -> Future + func save(user: User, on connectable: DatabaseConnectable) -> Future +} + +final class MySQLUserRepository: UserRepository { + func find(id: Int, on connectable: DatabaseConnectable) -> EventLoopFuture { + return User.find(id, on: connectable) + } + + func all(on connectable: DatabaseConnectable) -> EventLoopFuture<[User]> { + return User.query(on: connectable).all() + } + + func find(email: String, on connectable: DatabaseConnectable) -> EventLoopFuture { + return User.query(on: connectable).filter(\.email == email).first() + } + + func findCount(email: String, on connectable: DatabaseConnectable) -> EventLoopFuture { + return User.query(on: connectable).filter(\.email == email).count() + } + + func save(user: User, on connectable: DatabaseConnectable) -> EventLoopFuture { + return user.save(on: connectable) + } +} +``` + +Then, in the controller: + +```swift +let repository = try req.make(UserRepository.self) +let userQuery = repository + .find(email: content.email, on: req) + .unwrap(or: Abort(.unauthorized, reason: "Invalid Credentials")) +``` + +In this example, the controller has no idea where the data is coming from, it only knows that it exists. This model has proven to be incredibly effective with Vapor and it is our recommended architecture. + +## Entities + +Oftentimes entities that come from the database layer need to be transformed to make them appropriate for a JSON response or for sending to the view layer. Sometimes these data transformations require database queries as well. If the transformation is simple, use a property and not a function. + +**Bad:** + +```swift +func publicUser() -> PublicUser { + return PublicUser(user: self) +} +``` + +**Good:** + +```swift +var `public`: PublicUser { + return PublicUser(user: self) +} +``` + +Transformations that require more complex processing (fetching siblings and add them to the object) should be functions that accept a DatabaseConnectable object: + +```swift +func userWithSiblings(on connectable: DatabaseConnectable) throws -> Future { + //do the processing here +} +``` + +
    + +We also recommend documenting all functions that exist on entities. + +Unless your entity needs to be database-generic, always conform the model to the most specific model type. + +**Bad:** + +```swift +extension User: Model { } +``` + +**Good:** + +```swift +extension User: MySQLModel { } +``` + +
    + +Extending the model with other conformances (Migration, Parameter, etc) should be done at the file scope via an extension. + +**Bad:** + +```swift +public final class User: Model, Parameter, Content, Migration { + //.. +} +``` + +**Good:** + +```swift +public final class User { + //.. +} + +extension User: MySQLModel { } +extension User: Parameter { } +extension User: Migration { } +extension User: Content { } +``` + +
    + +Property naming styles should remain consistent throughout all models. + +**Bad:** + +```swift +public final class User { + var id: Int? + var firstName: String + var last_name: String +} +``` + +**Good:** + +```swift +public final class User { + var id: Int? + var firstName: String + var lastName: String +} +``` + +As a general rule, try to abstract logic into functions on the models to keep the controllers clean. + +## Routes and Controllers + +We suggest combining your routes into your controller to keep everything central. Controllers serve as a jumping off point for executing logic from other places, namely repositories and model functions. + +Routes should be separated into functions in the controller that take a `Request` parameter and return a `ResponseEncodable` type. + +**Bad:** + +```swift +final class LoginViewController: RouteCollection { + func boot(router: Router) throws { + router.get("/login") { (req) -> ResponseEncodable in + return "" + } + } +} +``` + +**Good:** + +```swift +final class LoginViewController: RouteCollection { + func boot(router: Router) throws { + router.get("/login", use: login) + } + + func login(req: Request) throws -> String { + return "" + } +} +``` + +
    + +When creating these route functions, the return type should always be as specific as possible. + +**Bad:** + +```swift +func login(req: Request) throws -> ResponseEncodable { + return "string" +} +``` + +**Good:** + +```swift +func login(req: Request) throws -> String { + return "string" +} +``` + +
    + +When creating a path like `/user/:userId`, always use the most specific `Parameter` instance available. + +**Bad:** + +```swift +router.get("/user", Int.parameter, use: user) +``` + +**Good:** + +```swift +router.get("/user", User.parameter, use: user) +``` + +
    + +When decoding a request, opt to decode the `Content` object when registering the route instead of in the route. + +**Bad:** + +```swift +router.post("/update", use: update) + +func update(req: Request) throws -> Future { + return req.content.decode(User.self).map { user in + //do something with user + + return user + } +} +``` + +**Good:** + +```swift +router.post(User.self, at: "/update", use: update) + +func update(req: Request, content: User) throws -> Future { + return content.save(on: req) +} +``` + +Controllers should only cover one idea/feature at a time. If a feature grows to encapsulate a large amount of functionality, routes should be split up into multiple controllers and organized under one common feature folder in the `Controllers` folder. For example, an app that handles generating a lot of analytical/reporting views should break up the logic by specific report to avoid cluttering a generic `ReportsViewController.swift` + +## Async + +Where possible, avoid specifying the type information in flatMap and map calls. + +**Bad:** + +```swift +let stringFuture: Future +return stringFuture.map(to: Response.self) { string in + return req.redirect(to: string) +} +``` + +**Good:** + +```swift +let stringFuture: Future +return stringFuture.map { string in + return req.redirect(to: string) +} +``` + +
    + +When returning two objects from a chain to the next chain, use the `and(result: )` function to automatically create a tuple instead of manually creating it (the Swift compiler will most likely require return type information in this case) + +**Bad:** + +```swift +let stringFuture: Future +return stringFuture.flatMap(to: (String, String).self) { original in + let otherStringFuture: Future + + return otherStringFuture.map { other in + return (other, original) + } +}.map { other, original in + //do something +} +``` + +**Good:** + +```swift +let stringFuture: Future +return stringFuture.flatMap(to: (String, String).self) { original in + let otherStringFuture: Future + return otherStringFuture.and(result: original) +}.map { other, original in + //do something +} +``` + +
    + +When returning more than two objects from one chain to the next, do not rely on the `and(result )` method as it can only create, at most, a two object tuple. Use a nested `map` instead. + +**Bad:** + +```swift +let stringFuture: Future +let secondFuture: Future + +return flatMap(to: (String, (String, String)).self, stringFuture, secondFuture) { first, second in + let thirdFuture: Future + return thirdFuture.and(result: (first, second)) +}.map { other, firstSecondTuple in + let first = firstSecondTuple.0 + let second = firstSecondTuple.1 + //do something +} +``` + +**Good:** + +```swift +let stringFuture: Future +let secondFuture: Future + +return flatMap(to: (String, String, String).self, stringFuture, secondFuture) { first, second in + let thirdFuture: Future + return thirdFuture.map { third in + return (first, second, third) + } +}.map { first, second, third in + //do something +} +``` + +
    + +Always use the global `flatMap` and `map` methods to execute futures concurrently when the functions don’t need to wait on each other. + +**Bad:** + +```swift +let stringFuture: Future +let secondFuture: Future + +return stringFuture.flatMap { string in + print(string) + return secondFuture +}.map { second in + print(second) + //finish chain +} +``` + +**Good:** + +```swift +let stringFuture: Future +let secondFuture: Future + +return flatMap(to: Void.self, stringFuture, secondFuture) { first, second in + print(first) + print(second) + + return .done(on: req) +} +``` + +
    + +Avoid nesting async functions more than once per chain, as it becomes unreadable and unsustainable. + +**Bad:** + +```swift +let stringFuture: Future + +return stringFuture.flatMap { first in + let secondStringFuture: Future + + return secondStringFuture.flatMap { second in + let thirdStringFuture: Future + + return thirdStringFuture.flatMap { third in + print(first) + print(second) + print(third) + + return .done(on: req) + } + } +} +``` + +**Good:** + +```swift +let stringFuture: Future + +return stringFuture.flatMap(to: (String, String).self) { first in + let secondStringFuture: Future + return secondStringFuture.and(result: first) +}.flatMap { second, first in + let thirdStringFuture: Future + + //it's ok to nest once + return thirdStringFuture.flatMap { third in + print(first) + print(second) + print(third) + + return .done(on: req) + } +} +``` + +
    + +Use `transform(to: )` to avoid chaining an extra, unnecessary level. + +**Bad:** + +```swift +let stringFuture: Future + +return stringFuture.map { _ in + return .ok +} +``` + +**Good:** + +```swift +let stringFuture: Future +return stringFuture.transform(to: .ok) +``` + +# Testing + +Testing is a crucial part of Vapor applications that helps ensure feature parity across versions. We strongly recommend testing for all Vapor applications. + +While testing routes, avoid changing behavior only to accommodate for the testing environment. Instead, if there is functionality that should differ based on the environment, you should create a service and swap out the selected version during the testing configuration. + +**Bad:** + +```swift +func login(req: Request) throws -> Future { + if req.environment != .testing { + try req.verifyCSRF() + } + + //rest of the route +} +``` + +**Good:** + +```swift +func login(req: Request) throws -> Future { + let csrf = try req.make(CSRF.self) + try csrf.verify(req: req) + //rest of the route +} +``` + +Note how the correct way of handling this situation includes making a service - this is so that you can mock out fake functionality in the testing version of the service. + +Every test should setup and teardown your database. **Do not** try and persist state between tests. + +Tests should be separated into unit tests and integration. If using the repository pattern, the unit tests should use the memory version of the repositories while the integration tests should use the database version of the repositories. + +# Fluent + +ORMs are notorious for making it really easy to write bad code that works but is terribly inefficient or incorrect. Fluent tends to minimize this possibility thanks to the usage of features like KeyPaths and strongly-typed decoding, but there are still a few things to watch out for. + +Actively watch out for and avoid code that produces N+1 queries. Queries that have to be run for every instance of a model are bad and typically produce N+1 problems. Another identifying feature of N+1 code is the combination of a loop (or `map`) with `flatten`. + +**Bad:** + +```swift +//assume this is filled and that each owner can have one pet +let owners = [Owner]() +var petFutures = [Future]() + +for owner in owners { + let petFuture = try Pet.find(owner.petId, on: req).unwrap(or: Abort(.badRequest)) + petFutures.append(petFuture) +} + +let allPets = petFutures.flatten(on: req) +``` + +**Good:** + +```swift +//assume this is filled and that each owner can have one pet +let owners = [Owner]() +let petIds = owners.compactMap { $0.petId } +let allPets = try Pet.query(on: req).filter(\.id ~~ petIds).all() +``` + +Notice the use of the `~~` infix operator which creates an `IN` SQL query. + +
    + +In addition to reducing Fluent inefficiencies, opt for using native Fluent queries over raw queries unless your intended query is too complex to be created using Fluent. + +**Bad:** + +```swift +conn.raw("SELECT * FROM users;") +``` + +**Good:** + +```swift +User.query(on: req).all() +``` + +# Leaf + +Creating clean, readable Leaf files is important. One of the ways to go about doing this is through the use of base templates. Base templates allow you to specify only the different part of the page in the main leaf file for that view, and then base template will sub in the common components of the page (meta headers, the page footer, etc). For example: + +`base.leaf` + +```html + + + + + + + + + #get(title) + + + #get(body) + #embed("Views/footer") + + +``` + +Notice the calls to `#get` and `#embed` which piece together the supplied variables from the view and create the final HTML page. + +`login.leaf` + +```html +#set("title") { Login } + +#set("body") { +

    Add your login page here

    +} + +#embed("Views/base") +``` + +In addition to extracting base components to one file, you should also extract common components to their own file. For example, instead of repeating the snippet to create a bar graph, put it inside of a different file and then use `#embed()` to pull it into your main view. + +Always use `req.view()` to render the views for your frontend. This will ensure that the views will take advantage of caching in production mode, which dramatically speeds up your frontend responses. + +## Errors + +Depending on the type of application you are building (frontend, API-based, or hybrid) the way that you throw and handle errors may differ. For example, in an API-based system, throwing an error generally means you want to return it as a response. However, in a frontend system, throwing an error most likely means that you will want to handle it further down the line to give the user contextual frontend information. + +As a general rule of thumb, conform all of your custom error types to Debuggable. That helps `ErrorMiddleware` print better diagnostics and can lead to easier debugging. + +**Bad:** + +```swift +enum CustomError: Error { + case error +} +``` + +**Good:** + +```swift +enum CustomError: Debuggable { + case error + + //MARK: - Debuggable + var identifier: String { + switch self { + case .error: return "error" + } + } + + var reason: String { + switch self { + case .error: return "Specify reason here" + } + } +} +``` + +
    + +Include a `reason` when throwing generic `Abort` errors to indicate the context of the situation. + +**Bad:** + +```swift +throw Abort(.badRequest) +``` + +**Good:** + +```swift +throw Abort(.badRequest, reason: "Could not get data from external API.") +``` + +## 3rd Party Providers + +When building third party providers for Vapor, it's important to have a certain consistency that users will be able to become famaliar with when switching or adding new providers. Although Vapor is very young, there are already certain patterns that make sense when writing providers. + +### Naming +When naming a provider it's best to name the project itself that will be on github as part of the vapor community organization hyphenated with the extension `-provider`. For example if our provider is named `FooBar` then the project name would be named in the following way: +`foo-bar-provider`. + +When creating a provider library, you should omit phrases like `Provder` or `Package`. Take the StripeProvider for example, while the name of the project itself can be named `StripeProvider` the library name should be just the product itself: +```swift +let package = Package( + name: "StripeProvider", + products: [ + .library(name: "Stripe", targets: ["Stripe"]) + ], +) +``` +This allows for easy to read and clean import statements: +`import Stripe` rather than `import StripeProvider`. + + +## Overall Advice + +- Use `//MARK:` to denote sections of your controllers or configuration so that it is easier for other project members to find critically important areas. +- Only import modules that are needed for that specific file. Adding extra modules creates bloat and makes it difficult to deduce that controller’s responsibility. +- Where possible, use Swift doc-blocks to document methods. This is especially important for methods implements on entities so that other project members understand how the function affects persisted data. +- Do not retrieve environment variables on a repeated basis. Instead, use a custom service and register those variables during the configuration stage of your application (see “Configuration”) +- Reuse `DateFormatters` where possible (while also maintaining thread safety). In particular, don’t create a date formatter inside of a loop as they are incredibly expensive to make. +- Store dates in a computer-readable format until the last possible moment when they must be converted to human-readable strings. That conversion is typically very expensive and is unnecessary when passing dates around internally. Offloading this responsibility to JavaScript is a great tactic as well if you are building a front-end application. +- Eliminate stringly-typed code where possible by storing frequently used strings in a file like `Constants.swift` diff --git a/3.0/mkdocs.yml b/3.0/mkdocs.yml index 774b3fed..bdf392d9 100644 --- a/3.0/mkdocs.yml +++ b/3.0/mkdocs.yml @@ -20,6 +20,7 @@ pages: - 'Async': 'getting-started/async.md' - 'Services': 'getting-started/services.md' - 'Deployment': 'getting-started/cloud.md' +- 'Style Guide': 'style-guide.md' - 'Async': - 'Getting Started': 'async/getting-started.md' - 'Overview': 'async/overview.md' @@ -35,6 +36,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' From 14e55163c8d2ae042e450eef75522d36919cd93d Mon Sep 17 00:00:00 2001 From: tanner0101 Date: Thu, 9 Aug 2018 11:44:54 -0400 Subject: [PATCH 05/13] remove accidental style guide addition --- 3.0/docs/style-guide.md | 977 ---------------------------------------- 3.0/mkdocs.yml | 1 - 2 files changed, 978 deletions(-) delete mode 100644 3.0/docs/style-guide.md diff --git a/3.0/docs/style-guide.md b/3.0/docs/style-guide.md deleted file mode 100644 index 1f6779a4..00000000 --- a/3.0/docs/style-guide.md +++ /dev/null @@ -1,977 +0,0 @@ -# Vapor Style Guide - -## Motivation - -The Vapor style guide is a perspective on how to write Vapor application code that is clean, readable, and maintainable. It can serve as a jumping off point within your organization (or yourself) for how to write code in a style that aligns with the Vapor ecosystem. We think this guide can help solidify common ideas that occur across most applications and will be a reference for maintainers when starting a new project. This style guide is opinionated, so you should adapt your code in places where you don’t agree. - -## Application Structure - -The structure of your Vapor application is important from a readability standpoint, but also in terms of functionality. Application structure refers to a few different aspects of the Vapor ecosystem, but in particular, it is the way in which you structure your file, folders, and assets. - -The preferred way to structure your application is by separating the application into a few main parts: - -- Controllers -- Middleware -- Models -- Setup -- Utilities - -The structure ensures that new members working on your project can easily find the file or asset they are looking for. - -### Controllers Folder - -The controllers folder houses all of the controllers for your application which correspond to your routes. If you are building an application that serves both API responses and frontend responses, this folder should be further segmented into an `API Controllers` folder and a `View Controllers` folder. - -### Middleware Folder - -The middleware folder contains any custom middleware that you’ve written for your application. Each piece of middleware should be descriptively named and should only be responsible for one piece of functionality. - -### Models Folder - -“Models” in this document means an object that can be used to store or return data throughout the application. Models are not specific to Fluent - Entities, however, include database information that make it possible to persist and query them. - -The Models folder should be broken down into four parts: Entities, Requests, Responses, and View Contexts (if applicable to your application). The `Requests` and `Responses` folder hold object files that are used to decode requests or encode responses. For more information on this, see the “File Naming” section. - -If your application handles view rendering via Leaf, you should also have a folder that holds all of your view contexts. These contexts are the same type of objects as the Request and Response objects, but are specifically for passing data to the view layer. - -The Entities folder is further broken up into a folder for each database model that exists within your application. For example, if you have a `User` model that represents a `users` table, you would have a `Users` folder that contains `User.swift` (the Fluent model representation) and then any other applicable files for this entity. Other common files found at this level include files to extend functionality of the object, repository protocols/implementations, and data transformation extensions. - -### Setup Folder - -The setup folder has all of the necessary pieces that are called on application setup. This includes `app.swift`, `boot.swift`, `configure.swift`, `migrate.swift`, and `routes.swift`. For information on each of these files, see the “Configuration” section. - -### Utilities Folder - -The utilities folder serves as a general purpose location for any objects or helpers that don’t fit the other folders. For example, in your quest to eliminate stringly-typed code (see the “General Advice” section) you might place a `Constants.swift` file in this location. - -The final application structure (inside the Sources folder) looks like this: - -``` -├── Controllers -│ ├── API\ Controllers -│ └── View\ Controllers -├── Middleware -├── Models -│ ├── Entities -│ │ └── User -│ ├── Requests -│ └── Responses -│ └── View\ Contexts -├── Setup -│ ├── app.swift -│ ├── boot.swift -│ ├── commands.swift -│ ├── configure.swift -│ ├── content.swift -│ ├── databases.swift -│ ├── middlewares.swift -│ ├── migrate.swift -│ ├── repositories.swift -│ └── routes.swift -├── Utils -``` - -## Configuration - -Configuring your application correctly is one of the most important parts of a successful Vapor application. The main function of the configuring a Vapor application is correctly registering all of your services and 3rd party providers. - -**Note**: For more information on registering credentials and secrets, see the “Credentials” section. - -### Files - -There are 6 files you should have: - -- app.swift (use the default template version) -- boot.swift (use the default template version) -- commands.swift (Optional) -- configure.swift -- content.swift -- databases.swift (Optional) -- middlewares.swift -- migrate.swift (Optional) -- repositories.swift (Optional) -- routes.swift - -### configure.swift - -Use this file to register your services, providers, and any other code that needs to run as part of the Vapor application setup process. - -### routes.swift - -The routes.swift file is used to declare route registration for your application. Typically, the routes.swift file looks like this: - -```swift -import Vapor - -public func routes(_ router: Router) throws { - try router.register(collection: MyControllerHere()) -} -``` - -You should call this function from `configure.swift` like this: - -```swift - let router = EngineRouter.default() - try routes(router) - services.register(router, as: Router.self) -``` - -For more information on routes, see the “Routes and Controllers” section. - -### commands.swift - -Use this file to add your custom commands to your application. For example: - -```swift -import Vapor - -public func commands(config: inout CommandConfig) { - config.useFluentCommands() - - config.use(MyCustomCommand(), as: "my-custom-command") - ... -} -``` - -You should call this function from `configure.swift` like this: - -```swift - /// Command Config - var commandsConfig = CommandConfig.default() - commands(config: &commandsConfig) - services.register(commandsConfig) -``` - -> If your app doesn't use custom `Command`s you can ommit this file. - -### content.swift - -In this file you can customize the content encoding/decoding configuration for your data models. For example: - -```swift -import Vapor - -public func content(config: inout ContentConfig) throws { - let encoder = JSONEncoder() - let decoder = JSONDecoder() - - encoder.dateEncodingStrategy = .millisecondsSince1970 - decoder.dateDecodingStrategy = .millisecondsSince1970 - - config.use(encoder: encoder, for: .json) - config.use(decoder: decoder, for: .json) -} -``` - -You should call this function from `configure.swift` like this: - -```swift - /// Register Content Config - var contentConfig = ContentConfig.default() - try content(config: &contentConfig) - services.register(contentConfig) -``` - -> If you don't customize the content configuration you can ommit this file. - -### databases.swift - -Use this file to add the databases used in your application. Extracting this logic to a separate file keeps the configure.swift code clean, as it can often get quite long. This file should look something like this: - -```swift -import Vapor -import FluentMySQL //use your database driver here - -public func databases(config: inout DatabasesConfig) throws { - guard let databaseUrl = Environment.get("DATABASE_URL") else { - throw Abort(.internalServerError) - } - - guard let dbConfig = MySQLDatabaseConfig(url: databaseUrl) else { throw Abort(.internalServerError) } - - /// Register the databases - config.add(database: MySQLDatabase(config: dbConfig), as: .mysql) - - ... -} -``` - -And then call this function from `configure.swift` like this: - -```swift - /// Register the configured SQLite database to the database config. - var databasesConfig = DatabasesConfig() - try databases(config: &databasesConfig) - services.register(databasesConfig) -``` - -> If your app doesn't use `Fluent` you can ommit this file. - -### middlewares.swift - -In this file you can customize the middlewares of your application. For example: - -```swift -import Vapor - -public func middlewares(config: inout MiddlewareConfig) throws { - // config.use(FileMiddleware.self) // Serves files from `Public/` directory - config.use(ErrorMiddleware.self) // Catches errors and converts to HTTP response - // Other Middlewares... -} -``` - -You should call this function from `configure.swift` like this: - -```swift - /// Register middlewares - var middlewaresConfig = MiddlewareConfig() - try middlewares(config: &middlewaresConfig) - services.register(middlewaresConfig) -``` - -### migrate.swift - -Use this file to add the migrations to your database. Extracting this logic to a separate file keeps the configure.swift code clean, as it can often get quite long. This file should look something like this: - -```swift -import Vapor -import FluentMySQL //use your database driver here - -public func migrate(migrations: inout MigrationConfig) throws { - migrations.add(model: User.self, database: .mysql) //update this with your database driver -} -``` - -And then call this function from `configure.swift` like this: - -```swift - services.register { container -> MigrationConfig in - var migrationConfig = MigrationConfig() - try migrate(migrations: &migrationConfig) - return migrationConfig - } -``` - -As you continue to add models to your application, make sure that you add them to the migration file as well. - -> If your app doesn't use `Fluent` you can ommit this file. - -### repositories.swift - -The `repositories.swift` file is responsible for registering each repository during the configuration stage. This file should look like this: - -```swift -import Vapor - -public func setupRepositories(services: inout Services, config: inout Config) { - services.register(UserRepository.self) { _ -> MySQLUserRepository in - return MySQLUserRepository() - } - - preferDatabaseRepositories(config: &config) -} - -private func preferDatabaseRepositories(config: inout Config) { - config.prefer(MySQLUserRepository.self, for: UserRepository.self) -} -``` - -Call this function from `configure.swift` like this: - -```swift -setupRepositories(services: &services, config: &config) -``` - -For more information on the repository pattern, see the “Architecture” section. - -> If your app doesn't use `Fluent` you can ommit this file. - -## Credentials - -Credentials are a crucial part to any production-ready application. The preferred way to manage secrets in a Vapor application is via environment variables. These variables can be set via the Xcode scheme editor for testing, the shell, or in the GUI of your hosting provider. - -**Credentials should never, under any circumstances, be checked into a source control repository.** - -Assuming we have the following credential storage service: - -```swift -import Vapor -struct APIKeyStorage: Service { - let apiKey: String -} -``` - -**Bad:** - -```swift -services.register { container -> APIKeyStorage in - return APIKeyStorage(apiKey: "MY-SUPER-SECRET-API-KEY") -} -``` - -\***\*Good:\*\*** - -```swift -guard let apiKey = Environment.get("api-key") else { throw Abort(.internalServerError) } -services.register { container -> APIKeyStorage in - return APIKeyStorage(apiKey: apiKey) -} -``` - -## File Naming - -As the old saying goes, “the two hardest problems in computer science are naming things, cache invalidation, and off by one errors.” To minimize confusion and help increase readability, files should be named succinctly and descriptively. - -Files that contain objects used to decode body content from a request should be appended with `Request`. For example, `LoginRequest`. Files that contain objects used to encode body content to a response should be appended with `Response`. For example, `LoginResponse`. - -Controllers should also be named descriptively for their purpose. If your application contains logic for frontend responses and API responses, each controller’s name should denote their responsibility. For example, `LoginViewController` and `LoginController`. If you combine the login functionality into one controller, opt for the more generic name: `LoginController`. - -## Architecture - -One of the most important decisions to make up front about your app is the style of architecture it will follow. It is incredibly time consuming and expensive to retroactively change your architecture. We recommend that production-level Vapor applications use the repository pattern. - -The basic idea behind the repository pattern is that it creates another abstraction between Fluent and your application code. Instead of using Fluent queries directly in controllers, this pattern encourages abstracting those queries into a more generic protocol and using that instead. - -There are a few benefits to this method. First, it makes testing a lot easier. This is because during the test environment you can easily utilize Vapor’s configuration abilities to swap out which implementation of the repository protocol gets used. This makes unit testing much faster because the unit tests can use a memory version of the protocol rather than the database. The other large benefit to this pattern is that it makes it really easy to switch out the database layer if needed. Because all of the ORM logic is abstracted to this piece of the application (and the controllers don’t know it exists) you could realistically swap out Fluent with a different ORM with minimal changes to your actual application/business logic code. - -Here’s an example of a `UserRepository`: - -```swift -import Vapor -import FluentMySQL -import Foundation - -protocol UserRepository: Service { - func find(id: Int, on connectable: DatabaseConnectable) -> Future - func all(on connectable: DatabaseConnectable) -> Future<[User]> - func find(email: String, on connectable: DatabaseConnectable) -> Future - func findCount(email: String, on connectable: DatabaseConnectable) -> Future - func save(user: User, on connectable: DatabaseConnectable) -> Future -} - -final class MySQLUserRepository: UserRepository { - func find(id: Int, on connectable: DatabaseConnectable) -> EventLoopFuture { - return User.find(id, on: connectable) - } - - func all(on connectable: DatabaseConnectable) -> EventLoopFuture<[User]> { - return User.query(on: connectable).all() - } - - func find(email: String, on connectable: DatabaseConnectable) -> EventLoopFuture { - return User.query(on: connectable).filter(\.email == email).first() - } - - func findCount(email: String, on connectable: DatabaseConnectable) -> EventLoopFuture { - return User.query(on: connectable).filter(\.email == email).count() - } - - func save(user: User, on connectable: DatabaseConnectable) -> EventLoopFuture { - return user.save(on: connectable) - } -} -``` - -Then, in the controller: - -```swift -let repository = try req.make(UserRepository.self) -let userQuery = repository - .find(email: content.email, on: req) - .unwrap(or: Abort(.unauthorized, reason: "Invalid Credentials")) -``` - -In this example, the controller has no idea where the data is coming from, it only knows that it exists. This model has proven to be incredibly effective with Vapor and it is our recommended architecture. - -## Entities - -Oftentimes entities that come from the database layer need to be transformed to make them appropriate for a JSON response or for sending to the view layer. Sometimes these data transformations require database queries as well. If the transformation is simple, use a property and not a function. - -**Bad:** - -```swift -func publicUser() -> PublicUser { - return PublicUser(user: self) -} -``` - -**Good:** - -```swift -var `public`: PublicUser { - return PublicUser(user: self) -} -``` - -Transformations that require more complex processing (fetching siblings and add them to the object) should be functions that accept a DatabaseConnectable object: - -```swift -func userWithSiblings(on connectable: DatabaseConnectable) throws -> Future { - //do the processing here -} -``` - -
    - -We also recommend documenting all functions that exist on entities. - -Unless your entity needs to be database-generic, always conform the model to the most specific model type. - -**Bad:** - -```swift -extension User: Model { } -``` - -**Good:** - -```swift -extension User: MySQLModel { } -``` - -
    - -Extending the model with other conformances (Migration, Parameter, etc) should be done at the file scope via an extension. - -**Bad:** - -```swift -public final class User: Model, Parameter, Content, Migration { - //.. -} -``` - -**Good:** - -```swift -public final class User { - //.. -} - -extension User: MySQLModel { } -extension User: Parameter { } -extension User: Migration { } -extension User: Content { } -``` - -
    - -Property naming styles should remain consistent throughout all models. - -**Bad:** - -```swift -public final class User { - var id: Int? - var firstName: String - var last_name: String -} -``` - -**Good:** - -```swift -public final class User { - var id: Int? - var firstName: String - var lastName: String -} -``` - -As a general rule, try to abstract logic into functions on the models to keep the controllers clean. - -## Routes and Controllers - -We suggest combining your routes into your controller to keep everything central. Controllers serve as a jumping off point for executing logic from other places, namely repositories and model functions. - -Routes should be separated into functions in the controller that take a `Request` parameter and return a `ResponseEncodable` type. - -**Bad:** - -```swift -final class LoginViewController: RouteCollection { - func boot(router: Router) throws { - router.get("/login") { (req) -> ResponseEncodable in - return "" - } - } -} -``` - -**Good:** - -```swift -final class LoginViewController: RouteCollection { - func boot(router: Router) throws { - router.get("/login", use: login) - } - - func login(req: Request) throws -> String { - return "" - } -} -``` - -
    - -When creating these route functions, the return type should always be as specific as possible. - -**Bad:** - -```swift -func login(req: Request) throws -> ResponseEncodable { - return "string" -} -``` - -**Good:** - -```swift -func login(req: Request) throws -> String { - return "string" -} -``` - -
    - -When creating a path like `/user/:userId`, always use the most specific `Parameter` instance available. - -**Bad:** - -```swift -router.get("/user", Int.parameter, use: user) -``` - -**Good:** - -```swift -router.get("/user", User.parameter, use: user) -``` - -
    - -When decoding a request, opt to decode the `Content` object when registering the route instead of in the route. - -**Bad:** - -```swift -router.post("/update", use: update) - -func update(req: Request) throws -> Future { - return req.content.decode(User.self).map { user in - //do something with user - - return user - } -} -``` - -**Good:** - -```swift -router.post(User.self, at: "/update", use: update) - -func update(req: Request, content: User) throws -> Future { - return content.save(on: req) -} -``` - -Controllers should only cover one idea/feature at a time. If a feature grows to encapsulate a large amount of functionality, routes should be split up into multiple controllers and organized under one common feature folder in the `Controllers` folder. For example, an app that handles generating a lot of analytical/reporting views should break up the logic by specific report to avoid cluttering a generic `ReportsViewController.swift` - -## Async - -Where possible, avoid specifying the type information in flatMap and map calls. - -**Bad:** - -```swift -let stringFuture: Future -return stringFuture.map(to: Response.self) { string in - return req.redirect(to: string) -} -``` - -**Good:** - -```swift -let stringFuture: Future -return stringFuture.map { string in - return req.redirect(to: string) -} -``` - -
    - -When returning two objects from a chain to the next chain, use the `and(result: )` function to automatically create a tuple instead of manually creating it (the Swift compiler will most likely require return type information in this case) - -**Bad:** - -```swift -let stringFuture: Future -return stringFuture.flatMap(to: (String, String).self) { original in - let otherStringFuture: Future - - return otherStringFuture.map { other in - return (other, original) - } -}.map { other, original in - //do something -} -``` - -**Good:** - -```swift -let stringFuture: Future -return stringFuture.flatMap(to: (String, String).self) { original in - let otherStringFuture: Future - return otherStringFuture.and(result: original) -}.map { other, original in - //do something -} -``` - -
    - -When returning more than two objects from one chain to the next, do not rely on the `and(result )` method as it can only create, at most, a two object tuple. Use a nested `map` instead. - -**Bad:** - -```swift -let stringFuture: Future -let secondFuture: Future - -return flatMap(to: (String, (String, String)).self, stringFuture, secondFuture) { first, second in - let thirdFuture: Future - return thirdFuture.and(result: (first, second)) -}.map { other, firstSecondTuple in - let first = firstSecondTuple.0 - let second = firstSecondTuple.1 - //do something -} -``` - -**Good:** - -```swift -let stringFuture: Future -let secondFuture: Future - -return flatMap(to: (String, String, String).self, stringFuture, secondFuture) { first, second in - let thirdFuture: Future - return thirdFuture.map { third in - return (first, second, third) - } -}.map { first, second, third in - //do something -} -``` - -
    - -Always use the global `flatMap` and `map` methods to execute futures concurrently when the functions don’t need to wait on each other. - -**Bad:** - -```swift -let stringFuture: Future -let secondFuture: Future - -return stringFuture.flatMap { string in - print(string) - return secondFuture -}.map { second in - print(second) - //finish chain -} -``` - -**Good:** - -```swift -let stringFuture: Future -let secondFuture: Future - -return flatMap(to: Void.self, stringFuture, secondFuture) { first, second in - print(first) - print(second) - - return .done(on: req) -} -``` - -
    - -Avoid nesting async functions more than once per chain, as it becomes unreadable and unsustainable. - -**Bad:** - -```swift -let stringFuture: Future - -return stringFuture.flatMap { first in - let secondStringFuture: Future - - return secondStringFuture.flatMap { second in - let thirdStringFuture: Future - - return thirdStringFuture.flatMap { third in - print(first) - print(second) - print(third) - - return .done(on: req) - } - } -} -``` - -**Good:** - -```swift -let stringFuture: Future - -return stringFuture.flatMap(to: (String, String).self) { first in - let secondStringFuture: Future - return secondStringFuture.and(result: first) -}.flatMap { second, first in - let thirdStringFuture: Future - - //it's ok to nest once - return thirdStringFuture.flatMap { third in - print(first) - print(second) - print(third) - - return .done(on: req) - } -} -``` - -
    - -Use `transform(to: )` to avoid chaining an extra, unnecessary level. - -**Bad:** - -```swift -let stringFuture: Future - -return stringFuture.map { _ in - return .ok -} -``` - -**Good:** - -```swift -let stringFuture: Future -return stringFuture.transform(to: .ok) -``` - -# Testing - -Testing is a crucial part of Vapor applications that helps ensure feature parity across versions. We strongly recommend testing for all Vapor applications. - -While testing routes, avoid changing behavior only to accommodate for the testing environment. Instead, if there is functionality that should differ based on the environment, you should create a service and swap out the selected version during the testing configuration. - -**Bad:** - -```swift -func login(req: Request) throws -> Future { - if req.environment != .testing { - try req.verifyCSRF() - } - - //rest of the route -} -``` - -**Good:** - -```swift -func login(req: Request) throws -> Future { - let csrf = try req.make(CSRF.self) - try csrf.verify(req: req) - //rest of the route -} -``` - -Note how the correct way of handling this situation includes making a service - this is so that you can mock out fake functionality in the testing version of the service. - -Every test should setup and teardown your database. **Do not** try and persist state between tests. - -Tests should be separated into unit tests and integration. If using the repository pattern, the unit tests should use the memory version of the repositories while the integration tests should use the database version of the repositories. - -# Fluent - -ORMs are notorious for making it really easy to write bad code that works but is terribly inefficient or incorrect. Fluent tends to minimize this possibility thanks to the usage of features like KeyPaths and strongly-typed decoding, but there are still a few things to watch out for. - -Actively watch out for and avoid code that produces N+1 queries. Queries that have to be run for every instance of a model are bad and typically produce N+1 problems. Another identifying feature of N+1 code is the combination of a loop (or `map`) with `flatten`. - -**Bad:** - -```swift -//assume this is filled and that each owner can have one pet -let owners = [Owner]() -var petFutures = [Future]() - -for owner in owners { - let petFuture = try Pet.find(owner.petId, on: req).unwrap(or: Abort(.badRequest)) - petFutures.append(petFuture) -} - -let allPets = petFutures.flatten(on: req) -``` - -**Good:** - -```swift -//assume this is filled and that each owner can have one pet -let owners = [Owner]() -let petIds = owners.compactMap { $0.petId } -let allPets = try Pet.query(on: req).filter(\.id ~~ petIds).all() -``` - -Notice the use of the `~~` infix operator which creates an `IN` SQL query. - -
    - -In addition to reducing Fluent inefficiencies, opt for using native Fluent queries over raw queries unless your intended query is too complex to be created using Fluent. - -**Bad:** - -```swift -conn.raw("SELECT * FROM users;") -``` - -**Good:** - -```swift -User.query(on: req).all() -``` - -# Leaf - -Creating clean, readable Leaf files is important. One of the ways to go about doing this is through the use of base templates. Base templates allow you to specify only the different part of the page in the main leaf file for that view, and then base template will sub in the common components of the page (meta headers, the page footer, etc). For example: - -`base.leaf` - -```html - - - - - - - - - #get(title) - - - #get(body) - #embed("Views/footer") - - -``` - -Notice the calls to `#get` and `#embed` which piece together the supplied variables from the view and create the final HTML page. - -`login.leaf` - -```html -#set("title") { Login } - -#set("body") { -

    Add your login page here

    -} - -#embed("Views/base") -``` - -In addition to extracting base components to one file, you should also extract common components to their own file. For example, instead of repeating the snippet to create a bar graph, put it inside of a different file and then use `#embed()` to pull it into your main view. - -Always use `req.view()` to render the views for your frontend. This will ensure that the views will take advantage of caching in production mode, which dramatically speeds up your frontend responses. - -## Errors - -Depending on the type of application you are building (frontend, API-based, or hybrid) the way that you throw and handle errors may differ. For example, in an API-based system, throwing an error generally means you want to return it as a response. However, in a frontend system, throwing an error most likely means that you will want to handle it further down the line to give the user contextual frontend information. - -As a general rule of thumb, conform all of your custom error types to Debuggable. That helps `ErrorMiddleware` print better diagnostics and can lead to easier debugging. - -**Bad:** - -```swift -enum CustomError: Error { - case error -} -``` - -**Good:** - -```swift -enum CustomError: Debuggable { - case error - - //MARK: - Debuggable - var identifier: String { - switch self { - case .error: return "error" - } - } - - var reason: String { - switch self { - case .error: return "Specify reason here" - } - } -} -``` - -
    - -Include a `reason` when throwing generic `Abort` errors to indicate the context of the situation. - -**Bad:** - -```swift -throw Abort(.badRequest) -``` - -**Good:** - -```swift -throw Abort(.badRequest, reason: "Could not get data from external API.") -``` - -## 3rd Party Providers - -When building third party providers for Vapor, it's important to have a certain consistency that users will be able to become famaliar with when switching or adding new providers. Although Vapor is very young, there are already certain patterns that make sense when writing providers. - -### Naming -When naming a provider it's best to name the project itself that will be on github as part of the vapor community organization hyphenated with the extension `-provider`. For example if our provider is named `FooBar` then the project name would be named in the following way: -`foo-bar-provider`. - -When creating a provider library, you should omit phrases like `Provder` or `Package`. Take the StripeProvider for example, while the name of the project itself can be named `StripeProvider` the library name should be just the product itself: -```swift -let package = Package( - name: "StripeProvider", - products: [ - .library(name: "Stripe", targets: ["Stripe"]) - ], -) -``` -This allows for easy to read and clean import statements: -`import Stripe` rather than `import StripeProvider`. - - -## Overall Advice - -- Use `//MARK:` to denote sections of your controllers or configuration so that it is easier for other project members to find critically important areas. -- Only import modules that are needed for that specific file. Adding extra modules creates bloat and makes it difficult to deduce that controller’s responsibility. -- Where possible, use Swift doc-blocks to document methods. This is especially important for methods implements on entities so that other project members understand how the function affects persisted data. -- Do not retrieve environment variables on a repeated basis. Instead, use a custom service and register those variables during the configuration stage of your application (see “Configuration”) -- Reuse `DateFormatters` where possible (while also maintaining thread safety). In particular, don’t create a date formatter inside of a loop as they are incredibly expensive to make. -- Store dates in a computer-readable format until the last possible moment when they must be converted to human-readable strings. That conversion is typically very expensive and is unnecessary when passing dates around internally. Offloading this responsibility to JavaScript is a great tactic as well if you are building a front-end application. -- Eliminate stringly-typed code where possible by storing frequently used strings in a file like `Constants.swift` diff --git a/3.0/mkdocs.yml b/3.0/mkdocs.yml index bdf392d9..4f600f6c 100644 --- a/3.0/mkdocs.yml +++ b/3.0/mkdocs.yml @@ -20,7 +20,6 @@ pages: - 'Async': 'getting-started/async.md' - 'Services': 'getting-started/services.md' - 'Deployment': 'getting-started/cloud.md' -- 'Style Guide': 'style-guide.md' - 'Async': - 'Getting Started': 'async/getting-started.md' - 'Overview': 'async/overview.md' From 1be85fa17565a93b286300f560dbf242e19eadc9 Mon Sep 17 00:00:00 2001 From: tanner0101 Date: Thu, 9 Aug 2018 15:03:14 -0400 Subject: [PATCH 06/13] add auth 2 docs --- 3.0/docs/auth/api.md | 211 ++++++++++++++++++++++++++++ 3.0/docs/auth/getting-started.md | 49 +++++++ 3.0/docs/auth/web.md | 102 ++++++++++++++ 3.0/docs/getting-started/toolbox.md | 1 + 3.0/docs/vapor/middleware.md | 3 + 3.0/mkdocs.yml | 4 + 6 files changed, 370 insertions(+) create mode 100644 3.0/docs/auth/api.md create mode 100644 3.0/docs/auth/getting-started.md create mode 100644 3.0/docs/auth/web.md create mode 100644 3.0/docs/vapor/middleware.md diff --git a/3.0/docs/auth/api.md b/3.0/docs/auth/api.md new file mode 100644 index 00000000..7d2df5bd --- /dev/null +++ b/3.0/docs/auth/api.md @@ -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 [`GuardMiddleware`](#fixme). + +### 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 { + return \.email + } + + /// See `PasswordAuthenticatable`. + static var passwordKey: WritableKeyPath { + 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`](#fixme) 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(_:)`](#fixme). + +```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 [`GuardMiddleware`](#fixme) 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 { + return children(\.userID) + } +} + +struct UserToken: Model { + var id: Int? + var string: String + var userID: User.ID + + var user: Parent { + 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 { + return \.string + } + + /// See `Token`. + static var userIDKey: WritableKeyPath { + 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(_:)`](#fixme). + +```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 [`GuardMiddleware`](#fixme) to protect the route from unauthenticated access is not required. diff --git a/3.0/docs/auth/getting-started.md b/3.0/docs/auth/getting-started.md new file mode 100644 index 00000000..2ef2c7de --- /dev/null +++ b/3.0/docs/auth/getting-started.md @@ -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. \ No newline at end of file diff --git a/3.0/docs/auth/web.md b/3.0/docs/auth/web.md new file mode 100644 index 00000000..11b8af3e --- /dev/null +++ b/3.0/docs/auth/web.md @@ -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`](#fixme). + +```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(_:)`](#fixme) 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 [`GuardMiddleware`](#fixme) 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 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. + + diff --git a/3.0/docs/getting-started/toolbox.md b/3.0/docs/getting-started/toolbox.md index 09aa4f1b..8a0d5e3d 100644 --- a/3.0/docs/getting-started/toolbox.md +++ b/3.0/docs/getting-started/toolbox.md @@ -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`| JSON API with Fluent DB and Auth. | !!! info There are lots of unofficial Vapor templates on GitHub under the `vapor` + `template` topcs →. diff --git a/3.0/docs/vapor/middleware.md b/3.0/docs/vapor/middleware.md new file mode 100644 index 00000000..37e83ddf --- /dev/null +++ b/3.0/docs/vapor/middleware.md @@ -0,0 +1,3 @@ +# Middleware + +Coming soon. diff --git a/3.0/mkdocs.yml b/3.0/mkdocs.yml index 4f600f6c..77614a98 100644 --- a/3.0/mkdocs.yml +++ b/3.0/mkdocs.yml @@ -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' From 2a3377b8d98ced2b4304318a13cd51300bd6a667 Mon Sep 17 00:00:00 2001 From: tanner0101 Date: Thu, 9 Aug 2018 15:47:58 -0400 Subject: [PATCH 07/13] replace auth #fixme placeholders --- 3.0/docs/auth/api.md | 12 ++++++------ 3.0/docs/auth/web.md | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/3.0/docs/auth/api.md b/3.0/docs/auth/api.md index 7d2df5bd..ef123aab 100644 --- a/3.0/docs/auth/api.md +++ b/3.0/docs/auth/api.md @@ -47,7 +47,7 @@ Authentication middleware is responsible for reading the credentials from the re 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 [`GuardMiddleware`](#fixme). +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 @@ -119,16 +119,16 @@ router.grouped(password).get("hello") { req in } ``` -Here we are using `BCryptDigest` as the [`PasswordVerifier`](#fixme) since we are assuming the user's password is stored as a BCrypt hash. +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(_:)`](#fixme). +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 [`GuardMiddleware`](#fixme) to protect the route from unauthenticated access is not required. +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 @@ -201,11 +201,11 @@ router.grouped(token).get("hello") { } ``` -Now, to fetch the authenticated user in the route closure, you can use [`requireAuthenticated(_:)`](#fixme). +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 [`GuardMiddleware`](#fixme) to protect the route from unauthenticated access is not required. +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. diff --git a/3.0/docs/auth/web.md b/3.0/docs/auth/web.md index 11b8af3e..f507d90d 100644 --- a/3.0/docs/auth/web.md +++ b/3.0/docs/auth/web.md @@ -31,7 +31,7 @@ services.register(middlewares) ### Model -Once you are ready to enable session-based authentication, the first step is to conform your user model to [`SessionAuthenticatable`](#fixme). +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 { } @@ -63,7 +63,7 @@ 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(_:)`](#fixme) methods. +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) @@ -75,7 +75,7 @@ Here we are using the method prefixed with `require` to throw an error if the us 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 [`GuardMiddleware`](#fixme) to protect routes that do not call `requireAuthenticated(_:)` or otherwise require authentication. + 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 From 360d9a5651e75f3a8671a0ea42a50f52a7a23702 Mon Sep 17 00:00:00 2001 From: tanner0101 Date: Fri, 10 Aug 2018 13:45:04 -0400 Subject: [PATCH 08/13] auth template fix --- 3.0/docs/getting-started/toolbox.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/3.0/docs/getting-started/toolbox.md b/3.0/docs/getting-started/toolbox.md index 8a0d5e3d..a81fd6fa 100644 --- a/3.0/docs/getting-started/toolbox.md +++ b/3.0/docs/getting-started/toolbox.md @@ -40,7 +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`| JSON API with Fluent DB and Auth. | +| Auth | `--template=auth-template`| JSON API with Fluent DB and Auth. | !!! info There are lots of unofficial Vapor templates on GitHub under the `vapor` + `template` topcs →. From b8dc3bced7da7e7361ab9101acf7b2304857e0b2 Mon Sep 17 00:00:00 2001 From: Luka Bratos Date: Tue, 14 Aug 2018 16:24:26 +0100 Subject: [PATCH 09/13] Fix broken link --- 3.0/docs/getting-started/hello-world.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/3.0/docs/getting-started/hello-world.md b/3.0/docs/getting-started/hello-world.md index f8af29d8..23af520c 100644 --- a/3.0/docs/getting-started/hello-world.md +++ b/3.0/docs/getting-started/hello-world.md @@ -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 From 27734c7baf6cac71de1f77faaabc098fa8b27f81 Mon Sep 17 00:00:00 2001 From: tanner0101 Date: Tue, 14 Aug 2018 12:50:50 -0400 Subject: [PATCH 10/13] add jwt 3.0 docs --- 3.0/docs/jwt/getting-started.md | 38 +++++++++++++++++ 3.0/docs/jwt/overview.md | 74 +++++++++++++++++++++++++++++++++ 3.0/mkdocs.yml | 3 ++ 3 files changed, 115 insertions(+) create mode 100644 3.0/docs/jwt/getting-started.md create mode 100644 3.0/docs/jwt/overview.md diff --git a/3.0/docs/jwt/getting-started.md b/3.0/docs/jwt/getting-started.md new file mode 100644 index 00000000..3c20ed18 --- /dev/null +++ b/3.0/docs/jwt/getting-started.md @@ -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](#fixme) for more specific information. diff --git a/3.0/docs/jwt/overview.md b/3.0/docs/jwt/overview.md new file mode 100644 index 00000000..74afe186 --- /dev/null +++ b/3.0/docs/jwt/overview.md @@ -0,0 +1,74 @@ +# 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 +... +``` + +In the example HTTP request above, `` 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. + +![JWT.io](https://user-images.githubusercontent.com/1342803/44101613-ce328e04-9fb5-11e8-9aed-2d9900d0c40c.png) + +### 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" +} +``` + +### 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_. These standardized key / value pairs that many JWT implementations can recognize and act on automatically. See a full list of supported claims in [RFC 7519 § 4.1](https://tools.ietf.org/html/rfc7519#section-4.1). + +```json +{ + "id": 42, + "name": "Vapor Developer" +} +``` + +### Secret + +Last but not least is the secret key used to sign the JWT. For this example, we are using the `HS256` algorithm (specified in the JWT header). This algorithm uses a single secret key to sign and verify: + +``` +secret +``` + +Other algorithms, like RSA, use asymmetric (public and private) keys. + +### 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: + +``` +
    .. +``` + +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 + +First, let's take a look at how to parse and verify incoming JWTs. diff --git a/3.0/mkdocs.yml b/3.0/mkdocs.yml index 77614a98..466d5590 100644 --- a/3.0/mkdocs.yml +++ b/3.0/mkdocs.yml @@ -55,6 +55,9 @@ 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' - 'Overview': 'leaf/overview.md' From 9d66bf4127be918c29aa599695f7075758034478 Mon Sep 17 00:00:00 2001 From: tanner0101 Date: Tue, 14 Aug 2018 14:56:15 -0400 Subject: [PATCH 11/13] add parse + serialize docs --- 3.0/docs/jwt/overview.md | 120 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 116 insertions(+), 4 deletions(-) diff --git a/3.0/docs/jwt/overview.md b/3.0/docs/jwt/overview.md index 74afe186..d1b442d9 100644 --- a/3.0/docs/jwt/overview.md +++ b/3.0/docs/jwt/overview.md @@ -29,11 +29,15 @@ Here is the raw JSON data for our header: } ``` +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_. These standardized key / value pairs that many JWT implementations can recognize and act on automatically. See a full list of supported claims in [RFC 7519 § 4.1](https://tools.ietf.org/html/rfc7519#section-4.1). +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 { @@ -44,13 +48,15 @@ The payload is also where you store _claims_. These standardized key / value pai ### Secret -Last but not least is the secret key used to sign the JWT. For this example, we are using the `HS256` algorithm (specified in the JWT header). This algorithm uses a single secret key to sign and verify: +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. +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 @@ -71,4 +77,110 @@ Note that the header and payload segments are simply base64-url encoded JSON. It ## Parse -First, let's take a look at how to parse and verify incoming JWTs. +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`](#fixme). + +```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 ` 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(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 `` value from the bearer authorization header. The second part uses the [`JWT`](#fixme) struct to parse the token using an `HS256` signer. + +Once the JWT is parsed, we access the [`payload`](#fixme) 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`](#fixme). + +```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:)`](#fixme) 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". + + + From 16b592a7b7e4b982f581a344b969fb795c971089 Mon Sep 17 00:00:00 2001 From: tanner0101 Date: Tue, 14 Aug 2018 15:44:26 -0400 Subject: [PATCH 12/13] replace #fixmes for JWT --- 3.0/docs/jwt/getting-started.md | 2 +- 3.0/docs/jwt/overview.md | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/3.0/docs/jwt/getting-started.md b/3.0/docs/jwt/getting-started.md index 3c20ed18..c5597c7f 100644 --- a/3.0/docs/jwt/getting-started.md +++ b/3.0/docs/jwt/getting-started.md @@ -35,4 +35,4 @@ let package = Package( ) ``` -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](#fixme) for more specific information. +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. diff --git a/3.0/docs/jwt/overview.md b/3.0/docs/jwt/overview.md index d1b442d9..07a10a93 100644 --- a/3.0/docs/jwt/overview.md +++ b/3.0/docs/jwt/overview.md @@ -81,7 +81,7 @@ 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`](#fixme). +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 { @@ -116,9 +116,9 @@ router.get("hello") { req -> String in } ``` -This snippet creates a new route at `GET /hello`. The first part of the route handler fetches the `` value from the bearer authorization header. The second part uses the [`JWT`](#fixme) struct to parse the token using an `HS256` signer. +This snippet creates a new route at `GET /hello`. The first part of the route handler fetches the `` 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`](#fixme) property which contains an instance of our `User` type. We then access the `name` property to say hello! +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: @@ -142,7 +142,7 @@ 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`](#fixme). +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 { @@ -172,7 +172,7 @@ router.post("login") { req -> String in } ``` -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:)`](#fixme) method. This method returns `Data`, which we convert to a `String`. +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: From 025b1c7db2f42572a1c24142203d8abc5d03bef8 Mon Sep 17 00:00:00 2001 From: tanner0101 Date: Fri, 17 Aug 2018 16:16:01 -0400 Subject: [PATCH 13/13] update redis docs --- 3.0/docs/redis/basics.md | 100 ------------------------------ 3.0/docs/redis/custom-commands.md | 17 ----- 3.0/docs/redis/getting-started.md | 66 +++++++++++++++----- 3.0/docs/redis/overview.md | 92 +++++++++++++++++++++++++++ 3.0/docs/redis/pipeline.md | 18 ------ 3.0/docs/redis/pub-sub.md | 58 ----------------- 3.0/mkdocs.yml | 5 +- 7 files changed, 142 insertions(+), 214 deletions(-) delete mode 100644 3.0/docs/redis/basics.md delete mode 100644 3.0/docs/redis/custom-commands.md create mode 100644 3.0/docs/redis/overview.md delete mode 100644 3.0/docs/redis/pipeline.md delete mode 100644 3.0/docs/redis/pub-sub.md diff --git a/3.0/docs/redis/basics.md b/3.0/docs/redis/basics.md deleted file mode 100644 index d74f2f73..00000000 --- a/3.0/docs/redis/basics.md +++ /dev/null @@ -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 -``` - -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` for the key "hello" will be "world" if you created the record as shown above. - -```swift -let futureRecord = client.getData(forKey: "hello") // Future -``` - -### 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: ["*"]) -``` diff --git a/3.0/docs/redis/custom-commands.md b/3.0/docs/redis/custom-commands.md deleted file mode 100644 index cd63e742..00000000 --- a/3.0/docs/redis/custom-commands.md +++ /dev/null @@ -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 -``` - -This future will contain the result as specified in the article on the redis command page or an error. diff --git a/3.0/docs/redis/getting-started.md b/3.0/docs/redis/getting-started.md index 3a73ed6e..82c70ff1 100644 --- a/3.0/docs/redis/getting-started.md +++ b/3.0/docs/redis/getting-started.md @@ -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 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. diff --git a/3.0/docs/redis/overview.md b/3.0/docs/redis/overview.md new file mode 100644 index 00000000..d9f6396a --- /dev/null +++ b/3.0/docs/redis/overview.md @@ -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 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 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 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 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 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 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. diff --git a/3.0/docs/redis/pipeline.md b/3.0/docs/redis/pipeline.md deleted file mode 100644 index 8f546483..00000000 --- a/3.0/docs/redis/pipeline.md +++ /dev/null @@ -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. diff --git a/3.0/docs/redis/pub-sub.md b/3.0/docs/redis/pub-sub.md deleted file mode 100644 index 84e82da1..00000000 --- a/3.0/docs/redis/pub-sub.md +++ /dev/null @@ -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 -``` - -### 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 -} -``` diff --git a/3.0/mkdocs.yml b/3.0/mkdocs.yml index 466d5590..0b894931 100644 --- a/3.0/mkdocs.yml +++ b/3.0/mkdocs.yml @@ -74,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'