|
| 1 | +# Autograph |
| 2 | +## Description |
| 3 | + |
| 4 | +**Autograph** provides instruments for building source code generation utilities (command line applications) on top of the |
| 5 | +[Synopsis](https://github.com/RedMadRobot/synopsis) framework. |
| 6 | + |
| 7 | +## Installation |
| 8 | +### Swift Package Manager dependency |
| 9 | + |
| 10 | +```swift |
| 11 | +Package.Dependency.package( |
| 12 | + url: "https://github.com/RedMadRobot/autograph", |
| 13 | + from: "1.0.0" |
| 14 | +) |
| 15 | +``` |
| 16 | + |
| 17 | +## Usage |
| 18 | +### Overview |
| 19 | + |
| 20 | +First of all, in order to build a console executable using Swift there needs to be an execution entry point, a `main.swift` file. |
| 21 | + |
| 22 | +**Autograph** uses a common approach when during the `main.swift` file execution your utility app instantiates a special |
| 23 | +«Application» class object and passes control flow to it: |
| 24 | + |
| 25 | +```swift |
| 26 | +// main.swift sample code |
| 27 | +import Foundation |
| 28 | + |
| 29 | +exit(AutographApplication().run()) |
| 30 | +``` |
| 31 | + |
| 32 | +macOS console utilities are expected to return an `Int32` code after their execution, and any code different from `0` should be |
| 33 | +treated as an error, thus `AutographApplication` method `run()` returns `Int32`. The method looks pretty much like this: |
| 34 | + |
| 35 | +```swift |
| 36 | +// class AutographApplication { ... |
| 37 | + |
| 38 | +func run() -> Int32 { |
| 39 | + do { |
| 40 | + try someDangerousOperation() |
| 41 | + try someOtherDangerousOperation() |
| 42 | + ... |
| 43 | + } catch let error { |
| 44 | + print(error) |
| 45 | + return 1 |
| 46 | + } |
| 47 | + return 0 |
| 48 | +} |
| 49 | +``` |
| 50 | + |
| 51 | +Considering everything above, the entry point for you is an `AutographApplication` class. |
| 52 | + |
| 53 | +### AutographApplication class |
| 54 | + |
| 55 | +In order to create your own utility you'll need to create your own `main.swift` file following the example above, |
| 56 | +and make your own `AutographApplication` subclass. |
| 57 | + |
| 58 | +`AutographApplication` provides several convenient extension points for you to complete the execution process. When the app |
| 59 | +runs, it goes through seven major steps: |
| 60 | + |
| 61 | +##### 1. Gather execution parameters |
| 62 | + |
| 63 | +`AutographApplication` console app supports three arguments by default: |
| 64 | + |
| 65 | +* `-help` — print help; |
| 66 | +* `-verbose` — print additional information during execution; |
| 67 | +* `-project_name [name]` — provide project name to be used in generated code; if not set, "GEN" is used as a default project name. |
| 68 | + |
| 69 | +All arguments along with current working directory are aggregated in an `ExecutionParameters` instance: |
| 70 | + |
| 71 | +```swift |
| 72 | +class ExecutionParameters { |
| 73 | + let projectName: String |
| 74 | + let verbose: Bool |
| 75 | + let printHelp: Bool |
| 76 | + let workingDirectory: String |
| 77 | +} |
| 78 | +``` |
| 79 | + |
| 80 | +An `ExecutionParameters` instance acts like a dictionary, so that you may query it for your own arguments: |
| 81 | + |
| 82 | +``` |
| 83 | +/* |
| 84 | + ./MyUtility -verbose -my_argument value |
| 85 | + */ |
| 86 | + |
| 87 | + let parameters: ExecutionParameters = getParameters() |
| 88 | + let myArgument: String = parameters["-my_argument"] ?? "default_value" |
| 89 | +``` |
| 90 | + |
| 91 | +Arguments without values are stored in this dictionary with an empty `String` value. |
| 92 | + |
| 93 | +##### 2. Print help |
| 94 | + |
| 95 | +When your app is run with a `-help` argument, the execution is interrupted, and the `AutographApplication.printHelp()` method is called. |
| 96 | + |
| 97 | +It's the first extension point for you. You may extend this method in order to provide your own help message like this: |
| 98 | + |
| 99 | +```swift |
| 100 | +// class App: AutographApplication { |
| 101 | + |
| 102 | +override func printHelp() { |
| 103 | + super.printHelp() |
| 104 | + print(""" |
| 105 | + -input |
| 106 | + Input folder with model source files. |
| 107 | + If not set, current working directory is used as an input folder. |
| 108 | +
|
| 109 | + -output |
| 110 | + Where to put generated files. |
| 111 | + If not set, current working directory is used as an input folder. |
| 112 | +
|
| 113 | +
|
| 114 | + """) |
| 115 | +} |
| 116 | +``` |
| 117 | + |
| 118 | +Don't forget to leave an empty line after your help message. |
| 119 | + |
| 120 | +##### 3. Provide list of folders with source code files |
| 121 | + |
| 122 | +`AutographApplication` asks `provideInputFoldersList(fromParameters:)` method for a list of input folders. This method |
| 123 | +returns an empty list by default. |
| 124 | + |
| 125 | +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, |
| 126 | +whence the app should search for the source code files to be analysed. |
| 127 | + |
| 128 | +You may override this method like this: |
| 129 | + |
| 130 | +```swift |
| 131 | +// class App: AutographApplication { |
| 132 | + |
| 133 | +override func provideInputFoldersList( |
| 134 | + fromParameters parameters: ExecutionParameters |
| 135 | +) throws -> [String] { |
| 136 | + let input: String = parameters["-input"] ?? "" |
| 137 | + return [input] |
| 138 | +} |
| 139 | +``` |
| 140 | + |
| 141 | +Such that, you query the `ExecutionParameters` for an `-input` argument, and provide a default `""` value, which stands for the |
| 142 | +current working directory. |
| 143 | + |
| 144 | +`AutographApplication` later transforms all relative paths into absolute paths by concatenating with the current working directory, |
| 145 | +thus the empty string `""` will result in the working directory as a default input folder. |
| 146 | + |
| 147 | +If you think it's crucial for the execution to have an explicit `-input` argument value, you may throw an exception like this: |
| 148 | + |
| 149 | +```swift |
| 150 | +// class App: AutographApplication { |
| 151 | + |
| 152 | +enum ExecutionError: Error, CustomStringConvertible { |
| 153 | + case noInputFolder |
| 154 | + |
| 155 | + var description: String { |
| 156 | + switch self { |
| 157 | + case .noInputFolder: return "!!! PLEASE PROVIDE AN -input FOLDER !!!" |
| 158 | + } |
| 159 | + } |
| 160 | +} |
| 161 | + |
| 162 | +override func provideInputFoldersList( |
| 163 | + fromParameters parameters: ExecutionParameters |
| 164 | +) throws -> [String] { |
| 165 | + guard let input: String = parameters["-input"] |
| 166 | + else { throw ExecutionError.noInputFolder } |
| 167 | + return [input] |
| 168 | +} |
| 169 | +``` |
| 170 | + |
| 171 | +##### 4. Find all *.swift files in provided input folders |
| 172 | + |
| 173 | +When the step #3 is complete, `AutographApplication` recursively scans input folders and their subfolders for `*.swift` files. |
| 174 | +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. |
| 175 | + |
| 176 | +There's not much you can do about this process, though there's an `open` calculated property |
| 177 | +`AutographApplication.fileFinder`, where you may return your own `FileFinder` subclass instance if you want, for example, |
| 178 | +to prohibit a recursive file search. |
| 179 | + |
| 180 | +##### 5. Make a Synopsis out of all found source code |
| 181 | + |
| 182 | +Step #5 is pretty straightforward, as it makes a `Synopsis` instance using the list of `URL` entities of source code files found in the |
| 183 | +previous step. |
| 184 | + |
| 185 | +Also, it calls `Synopsis.printToXcode()` in case your app is running in `-verbose` mode. |
| 186 | + |
| 187 | +You can't extend or override this step. |
| 188 | + |
| 189 | +##### 6. Compose utilities |
| 190 | + |
| 191 | +A `Synopsis` instance is passed into the `AutographApplication.compose(forSynopsis:parameters:)` method, where you need |
| 192 | +to generate new source code. At last! |
| 193 | + |
| 194 | +This method returns a list of `Implementation` objects, each one contains the generated source code and a file path, where this |
| 195 | +source code needs to be stored: |
| 196 | + |
| 197 | +```swift |
| 198 | +struct Implementation { |
| 199 | + let filePath: String |
| 200 | + let sourceCode: String |
| 201 | +} |
| 202 | +``` |
| 203 | + |
| 204 | +Usually, this composition process is divided into several steps. |
| 205 | + |
| 206 | +First, you'll need to define an output folder path. `AutographApplication` won't transform this path into absolute path, thus you |
| 207 | +may use the relative one, like `"."`. |
| 208 | + |
| 209 | +Second, you'll need to extract all necessary information out of the obtained `Synopsis` entity. |
| 210 | + |
| 211 | +At last, you'll generate the actual source code. |
| 212 | + |
| 213 | +During each step you may throw errors in case if something went wrong. Consider using an `XcodeMessage` errors in case you want |
| 214 | +your app to rant over some particular source code. |
| 215 | + |
| 216 | +```swift |
| 217 | +// class App: AutographApplication { |
| 218 | + |
| 219 | +override func compose( |
| 220 | + forSynopsis synopsis: Synopsis, |
| 221 | + parameters: ExecutionParameters |
| 222 | +) throws -> [Implementation] { |
| 223 | + // use current directory as a default output folder: |
| 224 | + let output: String = parameters["-output"] ?? "." |
| 225 | + |
| 226 | + // make sure everything is annotated properly: |
| 227 | + try synopsis.classes.forEach { (classDescription: ClassDescription) in |
| 228 | + guard classDescription.annotations.contains(annotationName: "model") |
| 229 | + else { |
| 230 | + throw XcodeMessage( |
| 231 | + declaration: classDescription.declaration, |
| 232 | + message: "[MY GENERATOR] THIS CLASS IS NOT A MODEL" |
| 233 | + ) |
| 234 | + } |
| 235 | + } |
| 236 | + |
| 237 | + // my composer may also throw: |
| 238 | + return try MyComposer().composeSourceCode(outOfModels: synopsis.classes) |
| 239 | +} |
| 240 | +``` |
| 241 | + |
| 242 | +##### 7. Write down to disk |
| 243 | + |
| 244 | +Finally, your `Implementation` instances are being written to the hard drive. |
| 245 | + |
| 246 | +All necessary output folders are created, if needed. Also, if there's a generated source code file already, and the source code didn't |
| 247 | +change — `FileWriter` won't touch it. |
| 248 | + |
| 249 | +Shall you want to adjust this process, there's an `open` calculated property `AutographApplication.fileWriter`, where you may |
| 250 | +return your own `FileWriter` subclass instance. |
| 251 | + |
| 252 | +### Log class — in development |
| 253 | + |
| 254 | +During the app execution through steps mentioned above, different utilities like `FileFinder` or `FileWriter` may print debug |
| 255 | +messages in case the app is running in a `-verbose` mode. These utilities use the same `Log.v(message:)` class method that you |
| 256 | +can override in order to redirect log messages. |
| 257 | + |
| 258 | +### Running tests |
| 259 | + |
| 260 | +Use `spm_resolve.command` to load all dependencies and `spm_generate_xcodeproj.command` to assemble an Xcode project file. |
| 261 | +Also, ensure Xcode targets macOS. |
| 262 | + |
0 commit comments