Skip to content

Commit 4b01149

Browse files
committed
Initial commit.
0 parents  commit 4b01149

15 files changed

+935
-0
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
/*.xcodeproj

LICENSE

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
MIT License
2+
3+
Copyright (c) 2017 Redmadrobot et al.
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.
22+

Package.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// swift-tools-version:4.0
2+
3+
4+
import PackageDescription
5+
6+
7+
let package = Package(
8+
name: "Autograph",
9+
products: [
10+
Product.library(
11+
name: "Autograph",
12+
targets: ["Autograph"]
13+
)
14+
],
15+
dependencies: [
16+
Package.Dependency.package(
17+
url: "https://github.com/RedMadRobot/synopsis",
18+
from: "1.0.0"
19+
)
20+
],
21+
targets: [
22+
Target.target(
23+
name: "Autograph",
24+
dependencies: ["Synopsis"]
25+
),
26+
Target.testTarget(
27+
name: "AutographTests",
28+
dependencies: ["Autograph"]
29+
),
30+
]
31+
)

README.md

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
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

Comments
 (0)