goat gen:implement 和 implement:overwrite

Subterminal goat gen 提供了两条用于注册生成实现的命令:

  • implement
  • implement:overwrite

实现可以在主生成脚本中声明,也可以在任意已加载的模块中声明。

注册阶段结束后,所有最终选定的实现将并发执行。


implement

命令 implement 会以唯一名称注册一个实现。

语法

implement --name <nazwa> --body=<<EOF
    ...
EOF

示例

implement --name assets --body=<<IMPLEOF
    gen:files:onchange \
        --from="assets" \
        --cache="cache/assets-mtimes.json" \
        --foreach=<<EOF
            gen:force \
                --tmpl="asset.txt.tmpl" \
                --out="generated/{{.Data.Name.SnakeCase}}.txt"
        EOF \
        --index=<<EOF
            gen:force \
                --tmpl="index.txt.tmpl" \
                --out="generated/index.txt"
        EOF
IMPLEOF

通过参数 --name 传入的值用于标识该实现。

每个名称只能存在一个 implement 定义。

例如,注册后:

implement --name assets --body=<<EOF
    ...
EOF

再次注册同名实现:

implement --name assets --body=<<EOF
    ...
EOF

必须报错。


implement:overwrite

命令 implement:overwrite 会在最终解析实现注册表时替换实现主体。

语法

implement:overwrite --name <nazwa> --body=<<EOF
    ...
EOF

示例

implement:overwrite --name assets --body=<<IMPLEOF
    gen:files:onchange \
        --from="assets" \
        --cache="cache/assets-mtimes.json" \
        --foreach=<<EOF
            gen:force \
                --tmpl="other_asset.txt.tmpl" \
                --out="generated/{{.Data.Name.SnakeCase}}.txt"
        EOF \
        --index=<<EOF
            gen:force \
                --tmpl="other_index.txt.tmpl" \
                --out="generated/index.txt"
        EOF
IMPLEOF

每个实现名称最多只能存在一个 implement:overwrite 定义。

例如:

implement:overwrite --name assets --body=<<EOF
    ...
EOF

随后再有:

implement:overwrite --name assets --body=<<EOF
    ...
EOF

必须报错。


注册顺序无关紧要

生成模块可以并发加载。

因此,遇到 implement 和 implement:overwrite 命令的顺序无法保证。

因此,implement:overwrite 不要求基础实现必须先被注册。

例如,以下情况是有效的:

implement:overwrite --name assets --body=<<EOF
    # 被覆盖的实现
EOF

即使基础实现稍后才被遇到:

implement --name assets --body=<<EOF
    # 基础实现
EOF

只有在主生成脚本和所有已加载模块完成注册后,才会确定最终实现。

每个名称适用以下规则:

注册允许
一个 implement是
两个或更多 implement否
一个 implement + 一个 implement:overwrite是
一个 implement + 多个 implement:overwrite否
多个 implement + 一个 implement:overwrite否

如果存在 implement:overwrite,其主体将替换由 implement 注册的主体。


实现不能嵌套

implement 和 implement:overwrite 是注册命令。

它们只能在实现注册阶段使用。

不能从另一个实现的主体内部调用它们。

无效:implement 位于 implement 内部

implement --name assets --body=<<EOF
    implement --name nested --body=<<INNER
        ...
    INNER
EOF

无效:implement:overwrite 位于 implement 内部

implement --name assets --body=<<EOF
    implement:overwrite --name nested --body=<<INNER
        ...
    INNER
EOF

无效:implement 位于 implement:overwrite 内部

implement:overwrite --name assets --body=<<EOF
    implement --name nested --body=<<INNER
        ...
    INNER
EOF

无效:implement:overwrite 位于 implement:overwrite 内部

implement:overwrite --name assets --body=<<EOF
    implement:overwrite --name nested --body=<<INNER
        ...
    INNER
EOF

上述所有情况都必须报错。


生命周期

goat gen 中的实现处理分为两个独立阶段。

1. 注册

主生成脚本及所有已加载模块均使用以下方式注册实现:

implement
implement:overwrite

在此阶段,实现体会被保存,但尚不会执行。

注册完成后,将验证实现注册表,然后确定最终实现。

对于每个名称:

  • 必须恰好存在一个 implement 定义;
  • 可以存在零个或一个 implement:overwrite 定义;
  • 重复的 implement 定义会导致错误;
  • 重复的 implement:overwrite 定义会导致错误;
  • 如果存在 implement:overwrite,其实现体将成为最终实现体。

2. 执行

注册完成后,所有最终实现都会并发执行。

goat gen 会等待所有实现完成后才结束。

在执行阶段,注册已关闭。

如果实现体尝试调用:

implement

或:

implement:overwrite

该调用必须以错误结束。

实现执行期间返回的错误必须被正确传递给 goat gen。


在模块之间使用实现

模块可以提供默认实现:

# 模块:assets

implement --name assets --body=<<EOF
    gen:files:onchange \
        --from="assets" \
        --cache="cache/assets-mtimes.json" \
        --foreach=<<FILE
            gen:force \
                --tmpl="asset.txt.tmpl" \
                --out="generated/{{.Data.Name.SnakeCase}}.txt"
        FILE
EOF

其他模块或主生成脚本可以替换该实现:

implement:overwrite --name assets --body=<<EOF
    gen:files:onchange \
        --from="assets" \
        --cache="cache/assets-mtimes.json" \
        --foreach=<<FILE
            gen:force \
                --tmpl="custom-asset.txt.tmpl" \
                --out="generated/{{.Data.Name.SnakeCase}}.txt"
        FILE
EOF

无论先遇到哪一项声明都没有影响。

注册完成后,assets 的最终实现将是传递给 implement:overwrite 的实现体。

原始实现体不会被执行。


多个独立实现

不同的实现可以由不同模块注册。

示例:

implement --name assets --body=<<EOF
    # 生成资源
EOF
implement --name routes --body=<<EOF
    # 生成路由
EOF
implement --name models --body=<<EOF
    # 生成模型
EOF

注册完成后,这三个实现都会并发执行。

示意图:

注册
    |
    +-- assets
    +-- routes
    +-- models
    |
    v
解析注册表
    |
    +--> 执行 assets ----+
    +--> 执行 routes ----+--> 等待全部完成 --> 结束
    +--> 执行 models ----+

独立实现之间不存在有保证的执行顺序。

因此,实现不应假定其他实现在它之前或之后执行。


覆盖示例

假设存在两个模块。

第一个提供默认实现:

implement --name assets --body=<<EOF
    echo "default assets implementation"
EOF

第二个将其替换:

implement:overwrite --name assets --body=<<EOF
    echo "custom assets implementation"
EOF

最终实现将是:

echo "custom assets implementation"

仅会执行 implement:overwrite 中的实现体。

以下顺序是等价的,同样正确:

implement:overwrite --name assets --body=<<EOF
    echo "custom assets implementation"
EOF

implement --name assets --body=<<EOF
    echo "default assets implementation"
EOF

声明顺序不会影响结果。


无效重复项

重复的 implement

implement --name assets --body=<<EOF
    echo "first"
EOF

implement --name assets --body=<<EOF
    echo "second"
EOF

此情况必须以错误结束,因为 assets 存在多个基础实现。

重复的 implement:overwrite

implement --name assets --body=<<EOF
    echo "default"
EOF

implement:overwrite --name assets --body=<<EOF
    echo "first overwrite"
EOF

implement:overwrite --name assets --body=<<EOF
    echo "second overwrite"
EOF

此情况必须以错误结束,因为 assets 存在多个覆盖实现。


核心规则

使用实现机制时,适用以下规则:

  • implement 定义基础实现;
  • 每个名称必须恰好有一个基础定义;
  • implement:overwrite 可以替换基础定义;
  • 同一个名称最多只能存在一个 implement:overwrite;
  • implement:overwrite 可以在其对应的 implement 之前注册;
  • 不应依赖模块之间的注册顺序;
  • 实现体不会在注册期间执行;
  • 不得从实现体中调用 implement 和 implement:overwrite;
  • 最终实现会并发执行;
  • 不保证实现的执行顺序;
  • goat gen 会等待所有实现完成;
  • 实现执行期间产生的错误会传播到 goat gen。