Building Vapor projects using meta programming with Sourcery
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
Controllers
Forms
General
Models
Tests
.sourcery.yml
LICENSE
README.md

README.md

Sourcery Templates

Swift Version Vapor Version codebeat badge Readme Score GitHub license

Sourcery stencil files for generating Vapor 2 boilerplate.

Introduction

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.

Important notices

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

Table of Contents

Models

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 {
    // ...
}

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
}

Preparation

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)
    }
}

Annotations

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.

RowConvertible

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
    }
}

Annotations

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.

NodeRepresentable

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
    }
}

Annotations

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.

JSONConvertible

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 {}

Annotations

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.

Controllers

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 {
	// ... 
}

RouteCollection

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)
        }
    }
}

Annotations

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.

Forms

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
        )
    }
}

Annotations

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.

Tests

These templates are related to unit testing with XCTest.

LinuxMain

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.

Annotations

Key Description
excludeFromLinuxMain Prevents the test case from being included in the generated code.

General

These templates are not related to any specific part of your Vapor project.

Enums

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 }
}

Annotations

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.

Configuration

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.

Options

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.

🏆 Credits

This package is developed and maintained by the Vapor team at Nodes. The package owner for this project is Steffen.

📄 License

This package is open-sourced software licensed under the MIT license.