Episode #370

Scripting in Swift with Marathon - Part 2

31 minutes
Published on January 3, 2019

This video is only available to subscribers. Get access to this video and 572 others.

We continue our mini-project to create a Swift script that automatically creates migrations for Vapor projects. In this episode we save the generated templates to disk, render a generated extension so that we can add these migration types to the Vapor service, and see the example running end-to-end.

This is a continuation from Part 1.

Quick correction: In the episode I said that Vapor uses a package to generate xcode project files, but this is incorrect. Vapor uses swift package manager directly, which has a generate-xcodeproj command and does the job.

Creating files

Now that we have our templates ready to go, we need to save these to an actual file on disk.

let template = ....

try migrationsFolder.createFile(named: filename, contents: template)

This adds new files on disk, but the Xcode project doesn't know about them yet, so we'll have to inform the user to run vapor xcode again to get them added to the

Now that we have auto-numbered migration files on disk, we need a way to tell Vapor about these.

Creating a Migration Support file

We want to be able to ingest these migration files during Vapor setup. In configure.swift we'll add this line:

var migrations = MigrationConfig()
migrations.runAutoMigrations() // doesn't exist yet

Next, we need to add an extension that includes all of our generated migrations listed.

func migrationSupportSwiftFile() -> String {
    return """
    /* This file is generated. Do not edit. */
    import Vapor
    import FluentPostgreSQL

    extension MigrationConfig {
        mutating func runAutoMigrations() {
            /// add all migrations here
        }
    }
    """
}

We need to give this method the list of all of our type names, but we also need to know what type of migration it is (model or plain migration). We'll wrap this up in a type:

struct MigrationDescriptor {
    let name: String
    let command: String

    func toMigrationSwift() -> String {
        return "add(\(command): \(name).self, database: .psql)"
    }
}

Next we need to gather all of the migration files, skipping over the support file we're about to generate (so we don't treat that as a migration).

let migrationSupportFilename = "_MigrationSupport.swift"
let descriptors = migrationFiles
    .filter { $0.name != migrationSupportFilename }
    .map { file -> MigrationDescriptor in
    let parts = file.nameExcludingExtension.components(separatedBy: "_")
    let name = parts[1]
    let command = parts.count > 2 && parts[2] == "model" ? "model" : "migration"
    return MigrationDescriptor(name: name, command: command)
}

Here we're using a convention that model migrations will be suffixed with _model.

Now we can pass these in to the function we created earlier:

func migrationSupportSwiftFile(_ migrationDescriptors: [MigrationDescriptor]) -> String {
    let migrationList = migrationDescriptors
        .map { $0.toMigrationSwift() }
        .joined(separator: "\n        ")

    return """
    /* This file is generated. Do not edit. */
    import Vapor
    import FluentPostgreSQL

    extension MigrationConfig {
        mutating func runAutoMigrations() {
            \(migrationList)
        }
    }
    """
}

Finally we need to save this file to disk:

let supportFile = migrationSupportSwiftFile(descriptors)
try migrationsFolder.createFile(named: migrationSupportFilename, contents: supportFile)

print("Created \(filename) 🥳. Make sure to run vapor xcode to regenerate your project.")

This episode uses Swift 4.2, Marathon 3.1.0, Xcode 10.1.