Autograph provides instruments for building source code generation utilities (command line applications) on top of the Synopsis framework.
Package.Dependency.package(
url: "https://github.com/RedMadRobot/autograph",
from: "1.0.0"
)
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.
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:
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.
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.
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]
}
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.
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.
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)
}
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.
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.
Use spm_resolve.command
to load all dependencies and spm_generate_xcodeproj.command
to assemble an Xcode project file.
Also, ensure Xcode targets macOS.