Sourcery stencil files for generating Vapor 2 boilerplate.
Writing Vapor apps usually involves a lot of boilerplate code and since Vapor doesn't have any convenience for this in the current toolbox, we decided to look at metaprogramming to fix this issue. This repository contains the templates we have found useful to have in our toolbelt when developing Vapor projects. The overall guidelines for the templates are:
- It should be easy to opt-in and out of.
- It should be easy to mix and match between Sourcery-generated files and custom-made files.
- It should be very clear what has been generated by Sourcery to avoid manual edits that could potentially be overwritten when Sourcery is being run again.
- The template files should cover the common case, but not all cases. To avoid maintenance overload in this repo, we generally try to solve the general cases. Edge cases should be solved by opting out of Sourcery in the specific cases.
This repo contains a Sourcery configuration file (.sourcery.yml
) which can be copied/moved/linked into the root of your project. Using this configuration all Sourcery-generated files will be created in Sourcers/App/Generated/
, but please note that this Generated
folder needs to be created manually before running Sourcery.
All Vapor related templates support a import
and imports
annotation that allows you to declare imports in the generated source. Please not that, due to a limitation in Stencil, import
and imports
have different behaviour. Use the imports
annotation if you need to import more than one module.
Example:
// sourcery: imports = JWTKeychain, imports = Storage
final class User: Model {
...
This collection of templates is related to models and automating their conversions to common Vapor types. To make Sourcery pick up your model, annotate your model with model
:
// sourcery: model
final class User: Model {
// ...
}
Automatically generates an initializer and an enum for MySQL enum types.
Example:
// sourcery: model
final class User: Model {
var name: String
var age: Int?
}
Becomes:
// sourcery: model
final class User: Model {
var name: String
var age: Int?
// sourcery:inline:auto:User.Models
let storage = Storage()
internal init(
name: String,
age: Int? = nil
) {
self.name = name
self.age = age
}
// sourcery:end
}
Generates a list of database keys, automates prepare
and revert
functions.
Example:
// sourcery: model
final class User: Model {
var name: String
var age: Int?
}
Becomes:
import Vapor
import Fluent
extension User: Preparation {
internal enum DatabaseKeys {
static let id = User.idKey
static let name = "name"
static let age = "age"
}
// MARK: - Preparations (User)
internal static func prepare(_ database: Database) throws {
try database.create(self) {
$0.id()
$0.string(DatabaseKeys.name)
$0.int(DatabaseKeys.age, optional: true)
}
}
internal static func revert(_ database: Database) throws {
try database.delete(self)
}
}
Key | Description |
---|---|
databaseKey |
Set the database key (default is the name of the member). |
preparation |
Set the database preparation type for the given member. For example preparation = string will generate $0.string(...) |
length |
Length of the field. |
unique |
Whether or not the field is unique. |
foreignTable |
The table to use while configuring foreign ids. This field is only valid if preparation is set to foreignId . |
foreignIdKey |
The foreign key to use while configuring foreign ids. |
foreignKeyName |
The foreign key's name. note: this annotation should only be used when you run into Fluent issues with the auto-generated names. |
ignore |
Prevents the preparation from being included in the generated code. |
ignorePreparation |
Prevents the preparation from being included in the generated Preparation code. |
Automates init (row: Row)
and makeRow
boilerplate.
Example:
// sourcery: model
final class User: Model {
var name: String
var age: Int?
}
Becomes:
import Vapor
import Fluent
extension User: RowConvertible {
// MARK: - RowConvertible (User)
convenience internal init (row: Row) throws {
try self.init(
name: row.get(DatabaseKeys.name),
age: row.get(DatabaseKeys.age)
)
}
internal func makeRow() throws -> Row {
var row = Row()
try row.set(DatabaseKeys.name, name)
try row.set(DatabaseKeys.age, age)
return row
}
}
Key | Description |
---|---|
databaseKey |
Set the database key (default is the name of the member). |
ignore |
Prevents the property from being included in the generated code. |
ignoreRowConvertible |
Prevents the property from being included in the generated RowConvertible code. |
Generates a list of node keys and makeNode(in context: Context?)
.
Example:
// sourcery: model
final class User: Model {
var name: String
var age: Int?
}
Becomes:
import Vapor
import Fluent
extension User: NodeRepresentable {
internal enum NodeKeys: String {
case name
case age
}
// MARK: - NodeRepresentable (User)
func makeNode(in context: Context?) throws -> Node {
var node = Node([:])
try node.set(User.idKey, id)
try node.set(NodeKeys.name.rawValue, name)
try node.set(NodeKeys.age.rawValue, age)
return node
}
}
Key | Description |
---|---|
nodeKey |
Set the key for node (de)serialization. |
ignore |
Prevents the property from being included in the generated code. |
ignoreNodeRepresentable |
Prevents the property from being included in the generated NodeRepresentable code. |
Generates a list of JSON keys, init(json: JSON)
and makeJSON
.
Example:
// sourcery: model
final class User: Model {
var name: String
var age: Int?
}
Becomes:
extension User {
internal enum JSONKeys {
static let name = "name"
static let age = "age"
}
}
// MARK: - JSONInitializable (User)
extension AppUser: JSONInitializable {
internal convenience init(json: JSON) throws {
let name: String = try json.get(JSONKeys.name)
let age: Int? = try json.get(JSONKeys.age)
self.init(
name: name,
age: age
)
}
}
// MARK: - JSONRepresentable (User)
extension User: JSONRepresentable {
internal func makeJSON() throws -> JSON {
var json = JSON()
try json.set(User.idKey, id)
try json.set(JSONKeys.name, name)
try json.set(JSONKeys.age, age)
return json
}
}
extension User: JSONConvertible {}
extension User: ResponseRepresentable {}
Key | Description |
---|---|
jsonKey |
Set the key for JSON serialization. |
jsonValue |
Set the value for JSON serialization. |
ignore |
Prevents the property from being included in the generated code. |
ignoreJSONConvertible |
On type: Prevents any JSON-related code to be generated. On property: Prevents the property from being included in the generated JSONConvertible code. |
ignoreJSONInitializable |
On type: Prevents the JSONInitializable extension to be generated. On property: Prevents the property from being included in the JSONInitializable extension. |
ignoreJSONRepresentable |
On type: Prevents the JSONRepresentable extension to be generated. On property: Prevents the property from being included in the JSONRepresentable extension. |
These templates are for controllers and route collections. To make Sourcery pick up your controller, annotate your controller with controller
:
// sourcery: controller
final class UserController {
// ...
}
Generates route collection for controller logic.
Example:
// sourcery: controller
// sourcery: group = users
final class UserController {
// sourcery: route, method = get, path = /
func index(_ req: Request) throws -> ResponseRepresentable {
return try User.all().makeJSON()
}
// sourcery: route, method = get, path = /:userId
func show(_ req: Request) throws -> ResponseRepresentable {
let user: User = try req.parameters.next()
return user
}
}
Becomes:
extension UserController: RouteCollection {
func build(_ builder: RouteBuilder) throws {
builder.group("users") { routes in
// GET /users/
routes.get("/", handler: index)
// GET /users/:userId
routes.get("/:userId", handler: show)
}
}
}
Key | Description |
---|---|
group |
Set the group prefix for all routes in the controller. |
route |
Set the controller method to be generated as a route. This would require method and path to be set as well. |
method |
The method of the route. |
path |
The path for the route. |
This template makes working with Forms more convenient by annotating them with "form".
| Note: Make sure to postfix your field properties with "Field" so that the generated value accessors don't conflict with the field names.
Example:
// sourcery: form
struct UserForm: Form {
let nameField: FormField<String>
let ageField: FormField<Int>
...
}
Becomes:
extension UserForm {
var fields: [FieldType] {
return [
nameField,
ageField
]
}
}
extension UserForm {
var name: String? {
return nameField.value
}
var age: Int? {
return ageField.value
}
}
extension UserForm {
internal func assertValues(errorOnNil: Error = Abort(.internalServerError)) throws -> (
name: String,
age: Int
) {
guard
let name = name,
let age = age
else {
throw errorOnNil
}
return (
name,
age
)
}
}
Key | Description |
---|---|
form |
Opt-in a type conforming to Form to use this template. |
ignoreField |
Opt-out a specific field from all extensions |
ignoreAssertValues |
Opt-out a specific field from being included in the assertValues function. |
These templates are related to unit testing with XCTest.
Generates a static allTests
for every XCTestCase
and registers them in the LinuxMain.swift
.
Example:
class UserControllerTests: TestCase {
func testShowOneUser() throws {}
func testShowAllusers() throws {}
}
Becomes:
// sourcery:inline:auto:LinuxMain
extension UserControllerTests {
static var allTests = [
("testShowOneUser", testShowOneUser),
("testShowAllusers", testShowAllusers),
]
}
XCTMain([
testCase(UserControllerTests.allTests)
])
// sourcery:end
Please note that if you're using one of the official templates (api or web) then you would need to make sure that the definition of TestCase
gets excluded from LinuxMain.swift
using the excludeFromLinuxMain
annotation.
Key | Description |
---|---|
excludeFromLinuxMain |
Prevents the test case from being included in the generated code. |
These templates are not related to any specific part of your Vapor project.
Generates convenient accessors for getting a list of all cases. The MySQL preparation will use this as well.
Example:
// sourcery: enum
enum TestEnum: String, RawStringConvertible {
case a, b, c
}
Becomes:
extension TestEnum {
static var all: [TestEnum] = [
.a,
.b,
.c,
]
static let allRaw = TestEnum.all.map { $0.rawValue }
}
Key | Description |
---|---|
enum |
In the situation where enums has been globally turned off, this flag can be used to opt-in on specific enum types. |
ignore |
Prevents the enum from being included in the generated code. |
Most of our templates are handled on a opt-in basis per type, however we also have some configurations which can be handled on a global level.
Key | Description |
---|---|
enumOptOut |
Globally turns off generation of enum accessors. To enable for only some specific enum types, use the enum annotation. If this is not set all enums within the module will have convenience accessors created. |
This package is developed and maintained by the Vapor team at Nodes. The package owner for this project is Steffen.
This package is open-sourced software licensed under the MIT license.