Skip to content

RedMadRobot/autograph

Repository files navigation

Description

Autograph provides instruments for building source code generation utilities (command line applications) on top of the Synopsis framework.

Installation

Swift Package Manager dependency

Package.Dependency.package(
    url: "https://github.com/RedMadRobot/autograph",
    from: "1.0.0"
)

Usage

Overview

First of all, in order to build a console executable using Swift there needs to be an execution entry point, a main.swift file.

Autograph uses a common approach when during the main.swift file execution your utility app instantiates a special «Application» class object and passes control flow to it:

// main.swift sample code
import Foundation

exit(AutographApplication().run())

macOS console utilities are expected to return an Int32 code after their execution, and any code different from 0 should be treated as an error, thus AutographApplication method run() returns Int32. The method looks pretty much like this:

// class AutographApplication { ...

func run() -> Int32 {
    do {
        try someDangerousOperation()
        try someOtherDangerousOperation()
        ...
    } catch let error {
        print(error)
        return 1
    }
    return 0
}

Considering everything above, the entry point for you is an AutographApplication class.

AutographApplication class

In order to create your own utility you'll need to create your own main.swift file following the example above, and make your own AutographApplication subclass.

AutographApplication provides several convenient extension points for you to complete the execution process. When the app runs, it goes through seven major steps:

1. Gather execution parameters

AutographApplication console app supports three arguments by default:

  • -help — print help;
  • -verbose — print additional information during execution;
  • -project_name [name] — provide project name to be used in generated code; if not set, "GEN" is used as a default project name.

All arguments along with current working directory are aggregated in an ExecutionParameters instance:

class ExecutionParameters {
    let projectName:      String
    let verbose:          Bool
    let printHelp:        Bool
    let workingDirectory: String
}

An ExecutionParameters instance acts like a dictionary, so that you may query it for your own arguments:

/*
    ./MyUtility -verbose -my_argument value
 */
 
 let parameters: ExecutionParameters = getParameters()
 let myArgument: String              = parameters["-my_argument"] ?? "default_value"

Arguments without values are stored in this dictionary with an empty String value.

2. Print help

When your app is run with a -help argument, the execution is interrupted, and the AutographApplication.printHelp() method is called.

It's the first extension point for you. You may extend this method in order to provide your own help message like this:

// class App: AutographApplication {

override func printHelp() {
    super.printHelp()
    print("""
        -input
        Input folder with model source files.
        If not set, current working directory is used as an input folder.

        -output
        Where to put generated files.
        If not set, current working directory is used as an input folder.


        """)
}

Don't forget to leave an empty line after your help message.

3. Provide list of folders with source code files

AutographApplication asks provideInputFoldersList(fromParameters:) method for a list of input folders. This method returns an empty list by default.

It's the next major extension point for you. Here, you need to implement a way your utility app determines the list of input folders, whence the app should search for the source code files to be analysed.

You may override this method like this:

// class App: AutographApplication {

override func provideInputFoldersList(
    fromParameters parameters: ExecutionParameters
) throws -> [String] {
    let input: String = parameters["-input"] ?? ""
    return [input]
}

Such that, you query the ExecutionParameters for an -input argument, and provide a default "" value, which stands for the current working directory.

AutographApplication later transforms all relative paths into absolute paths by concatenating with the current working directory, thus the empty string "" will result in the working directory as a default input folder.

If you think it's crucial for the execution to have an explicit -input argument value, you may throw an exception like this:

// class App: AutographApplication {

enum ExecutionError: Error, CustomStringConvertible {
    case noInputFolder
    
    var description: String {
        switch self {
            case .noInputFolder: return "!!! PLEASE PROVIDE AN -input FOLDER !!!"
        }
    }
}

override func provideInputFoldersList(
    fromParameters parameters: ExecutionParameters
) throws -> [String] {
    guard let input: String = parameters["-input"]
    else { throw ExecutionError.noInputFolder }
    return [input]
}
4. Find all *.swift files in provided input folders

When the step #3 is complete, AutographApplication recursively scans input folders and their subfolders for *.swift files. The result of this operation is a list of URL objects, which is then passed to the Synopsis framework in the step #5, see below.

There's not much you can do about this process, though there's an open calculated property AutographApplication.fileFinder, where you may return your own FileFinder subclass instance if you want, for example, to prohibit a recursive file search.

5. Make a Synopsis out of all found source code

Step #5 is pretty straightforward, as it makes a Synopsis instance using the list of URL entities of source code files found in the previous step.

Also, it calls Synopsis.printToXcode() in case your app is running in -verbose mode.

You can't extend or override this step.

6. Compose utilities

A Synopsis instance is passed into the AutographApplication.compose(forSynopsis:parameters:) method, where you need to generate new source code. At last!

This method returns a list of Implementation objects, each one contains the generated source code and a file path, where this source code needs to be stored:

struct Implementation {
    let filePath:   String
    let sourceCode: String
}

Usually, this composition process is divided into several steps.

First, you'll need to define an output folder path. AutographApplication won't transform this path into absolute path, thus you may use the relative one, like ".".

Second, you'll need to extract all necessary information out of the obtained Synopsis entity.

At last, you'll generate the actual source code.

During each step you may throw errors in case if something went wrong. Consider using an XcodeMessage errors in case you want your app to rant over some particular source code.

// class App: AutographApplication {

override func compose(
    forSynopsis synopsis: Synopsis,
    parameters: ExecutionParameters
) throws -> [Implementation] {
    // use current directory as a default output folder:
    let output: String = parameters["-output"] ?? "."
    
    // make sure everything is annotated properly:
    try synopsis.classes.forEach { (classDescription: ClassDescription) in
        guard classDescription.annotations.contains(annotationName: "model")
        else {
            throw XcodeMessage(
                declaration: classDescription.declaration,
                message: "[MY GENERATOR] THIS CLASS IS NOT A MODEL"
            )
        }
    }
    
    // my composer may also throw:
    return try MyComposer().composeSourceCode(outOfModels: synopsis.classes)
}
7. Write down to disk

Finally, your Implementation instances are being written to the hard drive.

All necessary output folders are created, if needed. Also, if there's a generated source code file already, and the source code didn't change — FileWriter won't touch it.

Shall you want to adjust this process, there's an open calculated property AutographApplication.fileWriter, where you may return your own FileWriter subclass instance.

Log class — in development

During the app execution through steps mentioned above, different utilities like FileFinder or FileWriter may print debug messages in case the app is running in a -verbose mode. These utilities use the same Log.v(message:) class method that you can override in order to redirect log messages.

Running tests

Use spm_resolve.command to load all dependencies and spm_generate_xcodeproj.command to assemble an Xcode project file. Also, ensure Xcode targets macOS.