From aa96e440fbd542051f0a7c65023df857689d80e5 Mon Sep 17 00:00:00 2001 From: Jimmy McDermott Date: Thu, 9 Aug 2018 09:28:34 -0500 Subject: [PATCH 01/12] bring style guide over --- 3.0/docs/styleguide/styleguide.md | 797 ++++++++++++++++++++++++++++++ 3.0/mkdocs.yml | 1 + 2 files changed, 798 insertions(+) create mode 100644 3.0/docs/styleguide/styleguide.md diff --git a/3.0/docs/styleguide/styleguide.md b/3.0/docs/styleguide/styleguide.md new file mode 100644 index 00000000..15a0420f --- /dev/null +++ b/3.0/docs/styleguide/styleguide.md @@ -0,0 +1,797 @@ +# 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. + +## Maintainers +This style guide was written and is maintained by the following Vapor members: + +- Andrew ([@andrewangeta](https://github.com/andrewangeta)) +- Jimmy ([@mcdappdev](https://github.com/mcdappdev)) (Project manager) +- Jonas ([@joscdk](https://github.com/joscdk)) +- Tanner ([@tanner0101](https://github.com/tanner0101)) +- Tim ([@0xtim](https://github.com/0xtim)) + +## Contributing +To contribute to this guide, please submit a pull request that includes your proposed changes as well as logic to support your addition or modification. Pull requests will be reviewed by the maintainers and the rationale behind the maintainers’ decision to accept or deny the changes will be posted in the pull request. + +## 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 +│ ├── configure.swift +│ ├── migrate.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) +- configure.swift +- migrate.swift +- routes.swift +- repositories.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. + +#### 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. + +#### 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. + +## 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 +To-do + +## 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..dea3533c 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': 'styleguide/styleguide.md' - 'Async': - 'Getting Started': 'async/getting-started.md' - 'Overview': 'async/overview.md' From fb268fde7ca165a8a4cc25393996fc097036df72 Mon Sep 17 00:00:00 2001 From: Jimmy McDermott Date: Thu, 9 Aug 2018 11:01:27 -0500 Subject: [PATCH 02/12] services folder --- 3.0/docs/styleguide/styleguide.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/3.0/docs/styleguide/styleguide.md b/3.0/docs/styleguide/styleguide.md index 15a0420f..2bbc40a8 100644 --- a/3.0/docs/styleguide/styleguide.md +++ b/3.0/docs/styleguide/styleguide.md @@ -25,6 +25,7 @@ The preferred way to structure your application is by separating the application - Models - Setup - Utilities +- Services The structure ensures that new members working on your project can easily find the file or asset they are looking for. @@ -49,6 +50,9 @@ The setup folder has all of the necessary pieces that are called on application #### 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. +#### Services Folder +The services folder is used to hold any custom services that are created and registered. + The final application structure (inside the Sources folder) looks like this: ``` @@ -68,7 +72,8 @@ The final application structure (inside the Sources folder) looks like this: │ ├── configure.swift │ ├── migrate.swift │ └── routes.swift -├── Utils +├── Utilities +├── Services ``` ## Configuration From 8896063e0569c4479b75e319f22b0e8bdd363d53 Mon Sep 17 00:00:00 2001 From: Jimmy McDermott Date: Thu, 9 Aug 2018 15:56:00 -0500 Subject: [PATCH 03/12] update to ServiceType --- 3.0/docs/styleguide/styleguide.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/3.0/docs/styleguide/styleguide.md b/3.0/docs/styleguide/styleguide.md index 2bbc40a8..1e22387e 100644 --- a/3.0/docs/styleguide/styleguide.md +++ b/3.0/docs/styleguide/styleguide.md @@ -219,7 +219,7 @@ import Vapor import FluentMySQL import Foundation -protocol UserRepository: Service { +protocol UserRepository: ServiceType { func find(id: Int, on connectable: DatabaseConnectable) -> Future func all(on connectable: DatabaseConnectable) -> Future<[User]> func find(email: String, on connectable: DatabaseConnectable) -> Future @@ -228,6 +228,10 @@ protocol UserRepository: Service { } final class MySQLUserRepository: UserRepository { + static func makeService(for worker: Container) throws -> Self { + return .init() + } + func find(id: Int, on connectable: DatabaseConnectable) -> EventLoopFuture { return User.find(id, on: connectable) } From 59f20268b39ab2ccebeaecad37841929afab71c9 Mon Sep 17 00:00:00 2001 From: Jimmy McDermott Date: Tue, 14 Aug 2018 07:27:16 -0500 Subject: [PATCH 04/12] move over changes from PRs on vapor-community/styleguide --- 3.0/docs/styleguide/styleguide.md | 482 +++++++++++++++++++++--------- 1 file changed, 340 insertions(+), 142 deletions(-) diff --git a/3.0/docs/styleguide/styleguide.md b/3.0/docs/styleguide/styleguide.md index 1e22387e..d01c8dd3 100644 --- a/3.0/docs/styleguide/styleguide.md +++ b/3.0/docs/styleguide/styleguide.md @@ -1,59 +1,69 @@ -# Vapor Style Guide +# 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. +## Motivation -## Maintainers -This style guide was written and is maintained by the following Vapor members: +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. + +## Maintainers + +This style guide was written and is maintained by the following Vapor members: - Andrew ([@andrewangeta](https://github.com/andrewangeta)) - Jimmy ([@mcdappdev](https://github.com/mcdappdev)) (Project manager) - Jonas ([@joscdk](https://github.com/joscdk)) - Tanner ([@tanner0101](https://github.com/tanner0101)) - Tim ([@0xtim](https://github.com/0xtim)) +- Gustavo ([@gperdomor](https://github.com/gperdomor)) -## Contributing -To contribute to this guide, please submit a pull request that includes your proposed changes as well as logic to support your addition or modification. Pull requests will be reviewed by the maintainers and the rationale behind the maintainers’ decision to accept or deny the changes will be posted in the pull request. +## Contributing -## 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. +To contribute to this guide, please submit a pull request that includes your proposed changes as well as logic to support your addition or modification. Pull requests will be reviewed by the maintainers and the rationale behind the maintainers’ decision to accept or deny the changes will be posted in the pull request. -The preferred way to structure your application is by separating the application into a few main parts: +## Application Structure -- Controllers -- Middleware -- Models +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 +- Utilities - Services -The structure ensures that new members working on your project can easily find the file or asset they are looking for. +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. + +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. + +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. + +“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. +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. +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. + +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 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. #### Services Folder The services folder is used to hold any custom services that are created and registered. -The final application structure (inside the Sources folder) looks like this: +The final application structure (inside the Sources folder) looks like this: ``` ├── Controllers @@ -69,32 +79,47 @@ The final application structure (inside the Sources folder) looks like this: ├── Setup │ ├── app.swift │ ├── boot.swift +│ ├── commands.swift │ ├── configure.swift +│ ├── content.swift +│ ├── databases.swift +│ ├── middlewares.swift │ ├── migrate.swift +│ ├── repositories.swift │ └── routes.swift ├── Utilities ├── Services ``` -## 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. +## 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: + +Depending on your application you should have some or all of the following files: - app.swift (use the default template version) - boot.swift (use the default template version) +- commands.swift (Optional) - configure.swift -- migrate.swift +- content.swift +- databases.swift (Optional) +- middlewares.swift +- migrate.swift (Optional) +- repositories.swift (Optional) - routes.swift -- repositories.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. + +Use this file to register your services, providers, and any other code that needs to run as part of the Vapor application setup process. + +We recommend registering all services (with some exceptions, like `BlockingIOThreadPool`, that have internal synchronization code) using the closure method. The closure gets called each time a container requests that service. There's one container per thread, meaning that you get one service per thread. As a result, you don't need to think about synchronizing access and state in the object, which is otherwise difficult. The tradeoff to this method is memory usage, which is typically negligible for a small class, but you gain performance. #### routes.swift + The routes.swift file is used to declare route registration for your application. Typically, the routes.swift file looks like this: ```swift @@ -105,18 +130,133 @@ public func routes(_ router: Router) throws { } ``` -You should call this function from `configure.swift` like this: +You should call this function from `configure.swift` like this: ```swift - let router = EngineRouter.default() - try routes(router) - services.register(router, as: Router.self) + services.register(Router.self) { _ -> EngineRouter in + let router = EngineRouter.default() + try routes(router) + return router + } ``` 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 omit 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 omit 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 omit 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: + +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 @@ -137,9 +277,12 @@ And then call this function from `configure.swift` like this: } ``` -As you continue to add models to your application, make sure that you add them to the migration file as well. +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 omit this file. + +#### repositories.swift -#### repositories.swift The `repositories.swift` file is responsible for registering each repository during the configuration stage. This file should look like this: ```swift @@ -149,7 +292,7 @@ public func setupRepositories(services: inout Services, config: inout Config) { services.register(UserRepository.self) { _ -> MySQLUserRepository in return MySQLUserRepository() } - + preferDatabaseRepositories(config: &config) } @@ -158,24 +301,27 @@ private func preferDatabaseRepositories(config: inout Config) { } ``` -Call this function from `configure.swift` like this: +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. +For more information on the repository pattern, see the “Architecture” section. + +> If your app doesn't use `Fluent` you can omit 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: +Assuming we have the following credential storage service: ```swift import Vapor -struct APIKeyStorage: Service { +struct APIKeyStorage: Service { let apiKey: String } ``` @@ -184,33 +330,34 @@ struct APIKeyStorage: Service { ```swift services.register { container -> APIKeyStorage in - return APIKeyStorage(apiKey: “MY-SUPER-SECRET-API-KEY”) + return APIKeyStorage(apiKey: "MY-SUPER-SECRET-API-KEY") } ``` -****Good:**** +\***\*Good:\*\*** ```swift -guard let apiKey = Environment.get(“api-key”) else { throw Abort(.internalServerError) } +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`. +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. -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`. +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`. -## 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. +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`. -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. +## Architecture -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. +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`: @@ -231,30 +378,30 @@ final class MySQLUserRepository: UserRepository { static func makeService(for worker: Container) throws -> Self { return .init() } - + 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: +Then, in the controller: ```swift let repository = try req.make(UserRepository.self) @@ -263,14 +410,15 @@ let userQuery = repository .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. +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 +```swift func publicUser() -> PublicUser { return PublicUser(user: self) } @@ -279,7 +427,7 @@ func publicUser() -> PublicUser { **Good:** ```swift -var public: PublicUser { +var `public`: PublicUser { return PublicUser(user: self) } ``` @@ -292,14 +440,16 @@ func userWithSiblings(on connectable: DatabaseConnectable) throws -> Future + +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 { } +extension User: Model { } ``` **Good:** @@ -308,6 +458,8 @@ extension User: Model { } extension User: MySQLModel { } ``` +
+ Extending the model with other conformances (Migration, Parameter, etc) should be done at the file scope via an extension. **Bad:** @@ -321,7 +473,7 @@ public final class User: Model, Parameter, Content, Migration { **Good:** ```swift -public final class User { +public final class User { //.. } @@ -331,9 +483,11 @@ extension User: Migration { } extension User: Content { } ``` +
+ Property naming styles should remain consistent throughout all models. -**Bad:** +**Bad:** ```swift public final class User { @@ -343,7 +497,7 @@ public final class User { } ``` -**Good:** +**Good:** ```swift public final class User { @@ -355,10 +509,11 @@ public final class User { 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 and Controllers -Routes should be separated into functions in the controller that take a `Request` parameter and return a `ResponseEncodable` type. +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:** @@ -379,18 +534,20 @@ 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. +
+ +When creating these route functions, the return type should always be as specific as possible. **Bad:** -```swift +```swift func login(req: Request) throws -> ResponseEncodable { return "string" } @@ -398,55 +555,60 @@ func login(req: Request) throws -> ResponseEncodable { **Good:** -```swift +```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. +When creating a path like `/user/:userId`, always use the most specific `Parameter` instance available. **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 - } -} +router.get("/user", Int.parameter, use: user) ``` **Good:** ```swift -router.post(User.self, at: “/update, use: update) +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 +## Async + Where possible, avoid specifying the type information in flatMap and map calls. **Bad:** @@ -467,6 +629,8 @@ return stringFuture.map { string in } ``` +
+ 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:** @@ -475,7 +639,7 @@ When returning two objects from a chain to the next chain, use the `and(result: let stringFuture: Future return stringFuture.flatMap(to: (String, String).self) { original in let otherStringFuture: Future - + return otherStringFuture.map { other in return (other, original) } @@ -496,6 +660,8 @@ return stringFuture.flatMap(to: (String, String).self) { original in } ``` +
+ 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:** @@ -503,7 +669,7 @@ When returning more than two objects from one chain to the next, do not rely on ```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)) @@ -519,7 +685,7 @@ return flatMap(to: (String, (String, String)).self, stringFuture, secondFuture) ```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 @@ -530,6 +696,8 @@ return flatMap(to: (String, String, String).self, stringFuture, secondFuture) { } ``` +
+ Always use the global `flatMap` and `map` methods to execute futures concurrently when the functions don’t need to wait on each other. **Bad:** @@ -537,7 +705,7 @@ Always use the global `flatMap` and `map` methods to execute futures concurrentl ```swift let stringFuture: Future let secondFuture: Future - + return stringFuture.flatMap { string in print(string) return secondFuture @@ -545,7 +713,6 @@ return stringFuture.flatMap { string in print(second) //finish chain } - ``` **Good:** @@ -553,16 +720,18 @@ return stringFuture.flatMap { string in ```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. +
+ +Avoid nesting async functions more than once per chain, as it becomes unreadable and unsustainable. **Bad:** @@ -571,15 +740,15 @@ 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) } } @@ -596,18 +765,20 @@ return stringFuture.flatMap(to: (String, String).self) { first in 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:** @@ -627,10 +798,11 @@ 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. +## Testing -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. +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:** @@ -639,12 +811,12 @@ func login(req: Request) throws -> Future { if req.environment != .testing { try req.verifyCSRF() } - + //rest of the route } ``` -**Good:** +**Good:** ```swift func login(req: Request) throws -> Future { @@ -654,16 +826,17 @@ func login(req: Request) throws -> Future { } ``` -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. +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. +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. +## Fluent -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`. +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:** @@ -689,9 +862,11 @@ 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. +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. +
+ +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:** @@ -699,16 +874,18 @@ In addition to reducing Fluent inefficiencies, opt for using native Fluent queri conn.raw("SELECT * FROM users;") ``` -**Good:** +**Good:** ```swift User.query(on: req).all() ``` -## Leaf +## 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 @@ -717,7 +894,7 @@ Creating clean, readable Leaf files is important. One of the ways to go about do - + #get(title) @@ -727,9 +904,10 @@ Creating clean, readable Leaf files is important. One of the ways to go about do ``` -Notice the calls to `#get` and `#embed` which piece together the supplied variables from the view and create the final HTML page. +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 } @@ -740,12 +918,13 @@ Notice the calls to `#get` and `#embed` which piece together the supplied variab #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. +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. +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. +## 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. @@ -762,14 +941,14 @@ enum CustomError: Error { ```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" @@ -778,6 +957,7 @@ enum CustomError: Debuggable { } ``` +
Include a `reason` when throwing generic `Abort` errors to indicate the context of the situation. @@ -790,17 +970,35 @@ throw Abort(.badRequest) **Good:** ```swift -throw Abort(.badRequest, reason: “Could not get data from external API.”) +throw Abort(.badRequest, reason: "Could not get data from external API.") ``` ## 3rd Party Providers -To-do +When building third party providers for Vapor, it's important to have a certain consistency that users will be able to become familiar 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 g=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 `Provider` 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. +- 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. +- Reuse `DateFormatters` where possible (while also maintaining thread safety). In particular, don’t create a date formatter inside of a loop as they are 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` +- Eliminate stringly-typed code where possible by storing frequently used strings in a file like `Constants.swift`. From 471e9c736ac0893b5e0426dc5762fff068953e86 Mon Sep 17 00:00:00 2001 From: Jimmy McDermott Date: Wed, 22 Aug 2018 07:50:48 -0500 Subject: [PATCH 05/12] Update repository to thread-safe method --- 3.0/docs/styleguide/styleguide.md | 72 +++++++++++++++++-------------- 1 file changed, 40 insertions(+), 32 deletions(-) diff --git a/3.0/docs/styleguide/styleguide.md b/3.0/docs/styleguide/styleguide.md index d01c8dd3..fdacffe5 100644 --- a/3.0/docs/styleguide/styleguide.md +++ b/3.0/docs/styleguide/styleguide.md @@ -289,10 +289,7 @@ The `repositories.swift` file is responsible for registering each repository dur import Vapor public func setupRepositories(services: inout Services, config: inout Config) { - services.register(UserRepository.self) { _ -> MySQLUserRepository in - return MySQLUserRepository() - } - + services.register(MySQLUserRepository.self) preferDatabaseRepositories(config: &config) } @@ -334,7 +331,7 @@ services.register { container -> APIKeyStorage in } ``` -\***\*Good:\*\*** +**Good:** ```swift guard let apiKey = Environment.get("api-key") else { throw Abort(.internalServerError) } @@ -367,36 +364,47 @@ import FluentMySQL import Foundation protocol UserRepository: ServiceType { - 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 + func find(id: Int) -> Future + func all() -> Future<[User]> + func find(email: String) -> Future + func findCount(email: String) -> Future + func save(user: User) -> Future } final class MySQLUserRepository: UserRepository { + let db: DatabaseConnectionPool + + init(_ db: DatabaseConnectionPool) { + self.db = db + } + + func find(id: Int) -> EventLoopFuture { + return User.find(id, on: db) + } + + func all() -> EventLoopFuture<[User]> { + return User.query(on: db).all() + } + + func find(email: String) -> EventLoopFuture { + return User.query(on: db).filter(\.email == email).first() + } + + func findCount(email: String) -> EventLoopFuture { + return User.query(on: db).filter(\.email == email).count() + } + + func save(user: User) -> EventLoopFuture { + return user.save(on: db) + } +} + +//MARK: - ServiceType conformance +extension MySQLUserRepository { + static let serviceSupports: [Any.Type] = [UserRepository.self] + static func makeService(for worker: Container) throws -> Self { - return .init() - } - - 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) + return .init(db: worker.connectionPool(to: .mysql)) } } ``` @@ -406,7 +414,7 @@ Then, in the controller: ```swift let repository = try req.make(UserRepository.self) let userQuery = repository - .find(email: content.email, on: req) + .find(email: content.email) .unwrap(or: Abort(.unauthorized, reason: "Invalid Credentials")) ``` From 57e8cf28f636418873979ce515b12eaa7f031d79 Mon Sep 17 00:00:00 2001 From: Jimmy McDermott Date: Wed, 22 Aug 2018 07:56:18 -0500 Subject: [PATCH 06/12] update controller to thread-safe method --- 3.0/docs/styleguide/styleguide.md | 49 ++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/3.0/docs/styleguide/styleguide.md b/3.0/docs/styleguide/styleguide.md index fdacffe5..738503d0 100644 --- a/3.0/docs/styleguide/styleguide.md +++ b/3.0/docs/styleguide/styleguide.md @@ -125,17 +125,17 @@ The routes.swift file is used to declare route registration for your application ```swift import Vapor -public func routes(_ router: Router) throws { - try router.register(collection: MyControllerHere()) +public func routes(_ router: Router, _ container: Container) throws { + try router.register(collection: MyControllerHere(db: container.connectionPool(to: .mysql))) } ``` You should call this function from `configure.swift` like this: ```swift - services.register(Router.self) { _ -> EngineRouter in + services.register(Router.self) { container -> EngineRouter in let router = EngineRouter.default() - try routes(router) + try routes(router, container) return router } ``` @@ -613,6 +613,47 @@ func update(req: Request, content: User) throws -> Future { } ``` +Controllers should follow the thread-safe architecture when possible. This means passing necessary `Service`s to the controller on initialization instead of making them in the routes. + +**Bad:** + +```swift +final class LoginViewController: RouteCollection { + func boot(router: Router) throws { + router.get("/login", use: login) + } + + func login(req: Request) throws -> String { + let userRepository = try req.make(UserRepository.self) + //do something with it + + return "" + } +} +``` + +**Good:** + +```swift +final class LoginViewController: RouteCollection { + private let userRepository: UserRepository + + init(userRepository: UserRepository) { + self.userRepository = userRepository + } + + func boot(router: Router) throws { + router.get("/login", use: login) + } + + func login(req: Request) throws -> String { + //use `self.userRepository` + + return "" + } +} +``` + 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 From 56916fec70e4531d64815952e208352ec6423248 Mon Sep 17 00:00:00 2001 From: Jimmy McDermott Date: Wed, 22 Aug 2018 08:00:12 -0500 Subject: [PATCH 07/12] remove parameter label --- 3.0/docs/styleguide/styleguide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/3.0/docs/styleguide/styleguide.md b/3.0/docs/styleguide/styleguide.md index 738503d0..1f58ad9b 100644 --- a/3.0/docs/styleguide/styleguide.md +++ b/3.0/docs/styleguide/styleguide.md @@ -404,7 +404,7 @@ extension MySQLUserRepository { static let serviceSupports: [Any.Type] = [UserRepository.self] static func makeService(for worker: Container) throws -> Self { - return .init(db: worker.connectionPool(to: .mysql)) + return .init(worker.connectionPool(to: .mysql)) } } ``` From 2c6b089bd9b1ff225771a0e58f2cb4fbd3ec4edf Mon Sep 17 00:00:00 2001 From: Jimmy McDermott Date: Wed, 22 Aug 2018 08:04:23 -0500 Subject: [PATCH 08/12] make repository compile --- 3.0/docs/styleguide/styleguide.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/3.0/docs/styleguide/styleguide.md b/3.0/docs/styleguide/styleguide.md index 1f58ad9b..aba5f36d 100644 --- a/3.0/docs/styleguide/styleguide.md +++ b/3.0/docs/styleguide/styleguide.md @@ -372,9 +372,9 @@ protocol UserRepository: ServiceType { } final class MySQLUserRepository: UserRepository { - let db: DatabaseConnectionPool + let db: MySQLDatabase.ConnectionPool - init(_ db: DatabaseConnectionPool) { + init(_ db: MySQLDatabase.ConnectionPool) { self.db = db } @@ -404,9 +404,13 @@ extension MySQLUserRepository { static let serviceSupports: [Any.Type] = [UserRepository.self] static func makeService(for worker: Container) throws -> Self { - return .init(worker.connectionPool(to: .mysql)) + return .init(try worker.connectionPool(to: .mysql)) } } + +extension Database { + public typealias ConnectionPool = DatabaseConnectionPool> +} ``` Then, in the controller: From 63fd6f0a0ad14e3c891d8c2351f27900b334678b Mon Sep 17 00:00:00 2001 From: Jimmy McDermott Date: Mon, 27 Aug 2018 16:43:54 -0400 Subject: [PATCH 09/12] add withConnection change --- 3.0/docs/styleguide/styleguide.md | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/3.0/docs/styleguide/styleguide.md b/3.0/docs/styleguide/styleguide.md index aba5f36d..a60fa66e 100644 --- a/3.0/docs/styleguide/styleguide.md +++ b/3.0/docs/styleguide/styleguide.md @@ -379,23 +379,33 @@ final class MySQLUserRepository: UserRepository { } func find(id: Int) -> EventLoopFuture { - return User.find(id, on: db) + return db.withConnection { conn in + return User.find(id, on: conn) + } } func all() -> EventLoopFuture<[User]> { - return User.query(on: db).all() + return db.withConnection { conn in + return User.query(on: conn).all() + } } func find(email: String) -> EventLoopFuture { - return User.query(on: db).filter(\.email == email).first() + return db.withConnection { conn in + return User.query(on: conn).filter(\.email == email).first() + } } func findCount(email: String) -> EventLoopFuture { - return User.query(on: db).filter(\.email == email).count() + return db.withConnection { conn in + return User.query(on: conn).filter(\.email == email).count() + } } func save(user: User) -> EventLoopFuture { - return user.save(on: db) + return db.withConnection { conn in + return user.save(on: conn) + } } } From 8532899e7c6f96d6278d668e65e2efb45eae3962 Mon Sep 17 00:00:00 2001 From: Jimmy McDermott Date: Sun, 9 Sep 2018 11:48:58 -0400 Subject: [PATCH 10/12] remove provider naming --- 3.0/docs/styleguide/styleguide.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/3.0/docs/styleguide/styleguide.md b/3.0/docs/styleguide/styleguide.md index a60fa66e..134e50a7 100644 --- a/3.0/docs/styleguide/styleguide.md +++ b/3.0/docs/styleguide/styleguide.md @@ -1039,10 +1039,6 @@ 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 familiar 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 g=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 `Provider` 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( From ab865b906d862c9ce9233a5dc69fbcad798d7ce0 Mon Sep 17 00:00:00 2001 From: Jimmy McDermott Date: Mon, 10 Sep 2018 15:06:43 -0400 Subject: [PATCH 11/12] Move to "extras" folder --- 3.0/docs/{styleguide => extras}/styleguide.md | 0 3.0/mkdocs.yml | 3 ++- 2 files changed, 2 insertions(+), 1 deletion(-) rename 3.0/docs/{styleguide => extras}/styleguide.md (100%) diff --git a/3.0/docs/styleguide/styleguide.md b/3.0/docs/extras/styleguide.md similarity index 100% rename from 3.0/docs/styleguide/styleguide.md rename to 3.0/docs/extras/styleguide.md diff --git a/3.0/mkdocs.yml b/3.0/mkdocs.yml index 83d98f75..fc25ec4b 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': 'styleguide/styleguide.md' - 'Async': - 'Getting Started': 'async/getting-started.md' - 'Overview': 'async/overview.md' @@ -107,6 +106,8 @@ pages: - 'WebSocket': - 'Getting Started': 'websocket/getting-started.md' - 'Overview': 'websocket/overview.md' +- 'Extras': + - 'extras/styleguide.md' - 'Version (3.0)': - '1.5': 'version/1_5.md' - '2.0': 'version/2_0.md' From f15f82f77ae3681e728b42e338c4e253f62d6c0b Mon Sep 17 00:00:00 2001 From: Jimmy McDermott Date: Tue, 11 Sep 2018 08:39:59 -0400 Subject: [PATCH 12/12] Update maintainers and structure --- .../extras/{styleguide.md => style-guide.md} | 23 ++++++++++--------- 3.0/mkdocs.yml | 2 +- 2 files changed, 13 insertions(+), 12 deletions(-) rename 3.0/docs/extras/{styleguide.md => style-guide.md} (99%) diff --git a/3.0/docs/extras/styleguide.md b/3.0/docs/extras/style-guide.md similarity index 99% rename from 3.0/docs/extras/styleguide.md rename to 3.0/docs/extras/style-guide.md index 134e50a7..9180f2ae 100644 --- a/3.0/docs/extras/styleguide.md +++ b/3.0/docs/extras/style-guide.md @@ -4,17 +4,6 @@ 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. -## Maintainers - -This style guide was written and is maintained by the following Vapor members: - -- Andrew ([@andrewangeta](https://github.com/andrewangeta)) -- Jimmy ([@mcdappdev](https://github.com/mcdappdev)) (Project manager) -- Jonas ([@joscdk](https://github.com/joscdk)) -- Tanner ([@tanner0101](https://github.com/tanner0101)) -- Tim ([@0xtim](https://github.com/0xtim)) -- Gustavo ([@gperdomor](https://github.com/gperdomor)) - ## Contributing To contribute to this guide, please submit a pull request that includes your proposed changes as well as logic to support your addition or modification. Pull requests will be reviewed by the maintainers and the rationale behind the maintainers’ decision to accept or deny the changes will be posted in the pull request. @@ -1061,3 +1050,15 @@ This allows for easy to read and clean import statements: - Reuse `DateFormatters` where possible (while also maintaining thread safety). In particular, don’t create a date formatter inside of a loop as they are 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`. + + +## Maintainers + +This style guide was written and is maintained by the following Vapor members: + +- Andrew ([@andrewangeta](https://github.com/andrewangeta)) +- Jimmy ([@mcdappdev](https://github.com/mcdappdev)) (Project manager) +- Jonas ([@joscdk](https://github.com/joscdk)) +- Tanner ([@tanner0101](https://github.com/tanner0101)) +- Tim ([@0xtim](https://github.com/0xtim)) +- Gustavo ([@gperdomor](https://github.com/gperdomor)) \ No newline at end of file diff --git a/3.0/mkdocs.yml b/3.0/mkdocs.yml index 9725874d..3315e644 100644 --- a/3.0/mkdocs.yml +++ b/3.0/mkdocs.yml @@ -108,7 +108,7 @@ pages: - 'Getting Started': 'websocket/getting-started.md' - 'Overview': 'websocket/overview.md' - 'Extras': - - 'extras/styleguide.md' + - 'Style Guide': 'extras/style-guide.md' - 'Version (3.0)': - '1.5': 'version/1_5.md' - '2.0': 'version/2_0.md'