描述

Autograph 提供了用于在 Synopsis 框架之上构建源代码生成实用程序(命令行应用程序)的工具。

安装

Swift Package Manager 依赖

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

用法

概述

首先,为了使用 Swift 构建控制台可执行文件,需要有一个执行入口点,即 main.swift 文件。

Autograph 使用了一种通用方法,即在 main.swift 文件执行期间,你的实用程序应用会实例化一个特殊的 «Application» 类对象,并将控制流传递给它

// main.swift sample code
import Foundation

exit(AutographApplication().run())

macOS 控制台实用程序预期在其执行后返回一个 Int32 代码,任何与 0 不同的代码都应被视为错误,因此 AutographApplication 方法 run() 返回 Int32。该方法大致如下所示

// class AutographApplication { ...

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

考虑到以上所有内容,你的入口点是 AutographApplication 类。

AutographApplication 类

为了创建你自己的实用程序,你需要创建你自己的 main.swift 文件,按照上面的示例,并创建你自己的 AutographApplication 子类。

AutographApplication 提供了几个方便的扩展点,供你完成执行过程。当应用运行时,它会经历七个主要步骤

1. 收集执行参数

AutographApplication 控制台应用默认支持三个参数

所有参数以及当前工作目录都聚合在一个 ExecutionParameters 实例中

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

ExecutionParameters 实例就像一个字典,这样你就可以查询它来获取你自己的参数

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

没有值的参数存储在这个字典中,值为空 String

2. 打印帮助

当你的应用使用 -help 参数运行时,执行被中断,并且 AutographApplication.printHelp() 方法被调用。

这是你的第一个扩展点。你可以扩展这个方法,以便提供你自己的帮助信息,如下所示

// 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.


        """)
}

别忘了在你的帮助信息后留一个空行。

3. 提供包含源代码文件的文件夹列表

AutographApplication 调用 provideInputFoldersList(fromParameters:) 方法来获取输入文件夹的列表。此方法默认返回一个空列表。

这是你的下一个主要扩展点。在这里,你需要实现一种方法,让你的实用程序应用确定输入文件夹的列表,应用应从中搜索要分析的源代码文件。

你可以像这样重写此方法

// class App: AutographApplication {

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

这样,你可以查询 ExecutionParameters 以获取 -input 参数,并提供一个默认的 "" 值,它代表当前工作目录。

AutographApplication 稍后通过与当前工作目录连接,将所有相对路径转换为绝对路径,因此,空字符串 "" 将导致工作目录作为默认输入文件夹。

如果你认为对于执行来说,拥有一个显式的 -input 参数值至关重要,你可以像这样抛出异常

// 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. 在提供的输入文件夹中查找所有 *.swift 文件

当步骤 #3 完成时,AutographApplication 递归扫描输入文件夹及其子文件夹以查找 *.swift 文件。此操作的结果是一个 URL 对象列表,然后在步骤 #5 中传递给 Synopsis 框架,见下文。

对于此过程,你没有太多可以做的,尽管有一个 open 计算属性 AutographApplication.fileFinder,你可以在其中返回你自己的 FileFinder 子类实例,如果你想,例如,禁止递归文件搜索。

5. 从所有找到的源代码中创建一个 Synopsis

步骤 #5 非常直接,因为它使用在上一步中找到的源代码文件的 URL 实体列表创建一个 Synopsis 实例。

此外,如果你的应用在 -verbose 模式下运行,它还会调用 Synopsis.printToXcode()

你不能扩展或重写此步骤。

6. 组合实用程序

一个 Synopsis 实例被传递到 AutographApplication.compose(forSynopsis:parameters:) 方法中,你需要在其中生成新的源代码。终于!

此方法返回一个 Implementation 对象列表,每个对象都包含生成的源代码和一个文件路径,源代码需要存储在该文件路径中

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

通常,此组合过程分为几个步骤。

首先,你需要定义一个输出文件夹路径。AutographApplication 不会将此路径转换为绝对路径,因此,你可以使用相对路径,例如 "."

其次,你需要从获得的 Synopsis 实体中提取所有必要的信息。

最后,你将生成实际的源代码。

在每个步骤中,如果出现错误,你可以抛出错误。如果你想让你的应用针对某些特定的源代码发出抱怨,请考虑使用 XcodeMessage 错误。

// 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. 写入磁盘

最后,你的 Implementation 实例正在被写入硬盘驱动器。

如果需要,将创建所有必要的输出文件夹。此外,如果已经存在生成的源代码文件,并且源代码没有更改 — FileWriter 将不会触碰它。

如果你想调整此过程,有一个 open 计算属性 AutographApplication.fileWriter,你可以在其中返回你自己的 FileWriter 子类实例。

Log 类 — 开发中

在应用执行上述步骤期间,如果应用在 -verbose 模式下运行,像 FileFinderFileWriter 这样的不同实用程序可能会打印调试消息。这些实用程序使用相同的 Log.v(message:) 类方法,你可以重写该方法以重定向日志消息。

运行测试

使用 spm_resolve.command 加载所有依赖项,并使用 spm_generate_xcodeproj.command 组装一个 Xcode 项目文件。此外,确保 Xcode 目标是 macOS。