https://github.com/redmadrobot/autograph
Swift source code generation kit.
https://github.com/redmadrobot/autograph
code-generation ios macos sourcekit swift template
Last synced: about 1 month ago
JSON representation
Swift source code generation kit.
- Host: GitHub
- URL: https://github.com/redmadrobot/autograph
- Owner: RedMadRobot
- License: mit
- Created: 2017-11-30T22:14:33.000Z (over 7 years ago)
- Default Branch: master
- Last Pushed: 2018-04-25T19:00:14.000Z (almost 7 years ago)
- Last Synced: 2025-03-08T14:32:28.298Z (about 2 months ago)
- Topics: code-generation, ios, macos, sourcekit, swift, template
- Language: Swift
- Homepage:
- Size: 79.1 KB
- Stars: 5
- Watchers: 8
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README

## Description**Autograph** provides instruments for building source code generation utilities (command line applications) on top of the
[Synopsis](https://github.com/RedMadRobot/synopsis) framework.## Installation
### Swift Package Manager dependency```swift
Package.Dependency.package(
url: "https://github.com/RedMadRobot/autograph",
from: "1.0.0"
)
```## Usage
### OverviewFirst 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:```swift
// main.swift sample code
import Foundationexit(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:```swift
// 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:
```swift
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:
```swift
// 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:
```swift
// 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:
```swift
// 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:```swift
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.```swift
// 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.