gobuffalo/genny

GitHub: gobuffalo/genny

一个用于在 Go 中编写模块化生成器的框架,通过将生成蓝图与执行引擎分离来简化代码生成流程的开发和测试。

Stars: 64 | Forks: 14

Actions Status GoDoc Build Status Go Report Card

# Genny ## 什么是 Genny? Genny 是一个用于编写模块化生成器的 _framework_,但它本身实际上并不生成任何东西。它只是让你更容易做到这一点。 :) ## 核心概念 ### Generators [`github.com/gobuffalo/genny#Generator`](https://godoc.org/github.com/gobuffalo/genny#Generator) 用于构建你想要生成的内容的蓝图。 可以添加到 `Generator` 中的内容包括: * [`github.com/gobuffalo/genny#File`](https://godoc.org/github.com/gobuffalo/genny#File) * [`os/exec#Cmd`](https://godoc.org/os/exec#Cmd) * [`github.com/gobuffalo/packd#Box`](https://godoc.org/github.com/gobuffalo/packd#Box) * [`net/http#Request`](https://godoc.org/net/http#Request) * 以及更多 [`github.com/gobuffalo/genny#Generator`](https://godoc.org/github.com/gobuffalo/genny#Generator) 实际上*并不*生成任何内容;需要一个 [`github.com/gobuffalo/genny#Runner`](https://godoc.org/github.com/gobuffalo/genny#Runner) 来运行生成器。 ``` g := genny.New() // add a file g.File(genny.NewFileS("index.html", "Hello\n")) // execute a command g.Command(exec.Command("go", "env")) // run a function at run time g.RunFn(func(r *genny.Runner) error { // look for the `genny` executable if _, err := r.LookPath("genny"); err != nil { // it wasn't found, so install it c := gogen.Get("github.com/gobuffalo/genny/genny") if err := r.Exec(c); err != nil { return err } } // call the `genny` executable with the `-h` flag. return r.Exec(exec.Command("genny", "-h")) }) ``` 当 [`github.com/gobuffalo/genny#Generator`](https://godoc.org/github.com/gobuffalo/genny#Generator) 运行时,添加到其中的每个项目都将按 FIFO(先进先出)顺序执行。在上面的例子中,这意味着会发生以下情况: 1. 创建一个新文件 `r.Root/index.html` 2. 运行命令 `go env` 3. 运行一个安装 `genny` 的函数 #### 运行时检查 Genny 有两个不同的组件:“generator”(或蓝图)和执行它的“runner”。通常,只有在运行生成器时才需要执行某些代码,而不是在构建时。例如,检查可执行文件是否存在,如果缺失则安装它。 在这些情况下,你需要使用 [`github.com/gobuffalo/genny#RunFn`](https://godoc.org/github.com/gobuffalo/genny#RunFn) 函数。 在这个例子中,在运行时会调用 `RunFn` 并传入调用它的 `*Runner`。调用时,该函数会向 [`github.com/gobuffalo/genny#Runner.LookPath`](https://godoc.org/github.com/gobuffalo/genny#Runner.LookPath) 函数询问 `genny` 可执行文件的位置。 在 [`github.com/gobuffalo/genny#DryRunner`](https://godoc.org/github.com/gobuffalo/genny#DryRunner) 中,它只会简单地回显被请求的可执行文件的名称,在这种情况下是 `return "genny", nil`。 在 [`github.com/gobuffalo/genny#WetRunner`](https://godoc.org/github.com/gobuffalo/genny#WetRunner) 中,它将调用 [`os/exec#LookPath`](https://godoc.org/os/exec#LookPath) 并返回其结果。 如果没有找到 `genny` 二进制文件,它将尝试安装它。如果成功,该方法将返回调用 `genny -h` 的执行结果。 ``` g.RunFn(func(r *genny.Runner) error { // look for the `genny` executable if _, err := r.LookPath("genny"); err != nil { // it wasn't found, so install it c := gogen.Get("github.com/gobuffalo/genny/genny") if err := r.Exec(c); err != nil { return err } } // call the `genny` executable with the `-h` flag. return r.Exec(exec.Command("genny", "-h")) }) ``` `*Fn` 函数的灵活性,结合 [`github.com/gobuffalo/genny#RunFn`](https://godoc.org/github.com/gobuffalo/genny#RunFn),构成了一个强大的测试组合。 ### Runners [`github.com/gobuffalo/genny#Runner`](https://godoc.org/github.com/gobuffalo/genny#Runner) 用于运行生成器并控制这些生成器运行的环境。 Genny 自带了三种 `Runner` 实现,涵盖了_大多数_情况。它们也可以为自定义的 `Runner` 实现提供良好的起点。 * [`github.com/gobuffalo/genny#DryRunner`](https://godoc.org/github.com/gobuffalo/genny#DryRunner) * [`github.com/gobuffalo/genny#WetRunner`](https://godoc.org/github.com/gobuffalo/genny#WetRunner) * [`github.com/gobuffalo/genny/gentest#NewRunner`](https://godoc.org/github.com/gobuffalo/genny/gentest#NewRunner) #### 添加 Generators 要将 [`github.com/gobuffalo/genny#Generator`](https://godoc.org/github.com/gobuffalo/genny#Generator) 添加到 [`github.com/gobuffalo/genny#Runner`](https://godoc.org/github.com/gobuffalo/genny#Runner),可以使用 [`github.com/gobuffalo/genny#Runner.With`](https://godoc.org/github.com/gobuffalo/genny#Runner.With) 函数。 ``` run := genny.DryRunner(context.Background()) // add a generator from the `simple` package g := simple.New() run.With(g) // add a generator from the `notsimple` package g := notsimple.New() run.With(g) ``` 每个 [`github.com/gobuffalo/genny#Generator`](https://godoc.org/github.com/gobuffalo/genny#Generator) 都按照其添加到 [`github.com/gobuffalo/genny#Runner`](https://godoc.org/github.com/gobuffalo/genny#Runner) 中的 FIFO(先进先出)顺序运行。 通常会有一个函数用于构建新的 [`github.com/gobuffalo/genny#Generator`](https://godoc.org/github.com/gobuffalo/genny#Generator),或者在出现问题时返回一个 `error`。 ``` func New() (*genny.Generator, error) { g := simple.New() // do work which might error return g, nil } ``` [`github.com/gobuffalo/genny#Runner.WithNew`](https://godoc.org/github.com/gobuffalo/genny#Runner.WithNew) 函数旨在使添加具有这种返回参数签名的 [`github.com/gobuffalo/genny#Generator`](https://godoc.org/github.com/gobuffalo/genny#Generator) 变得更加容易。 ``` if err := run.WithNew(New()); err != nil { log.Fatal(err) } ``` #### Dry 运行 (**非破坏性**) “Dry” 运行的概念意味着不执行任何命令、不向磁盘写入任何文件、不发出任何 HTTP 请求等……相反,这些步骤以“dry”方式运行,在 [`github.com/gobuffalo/genny#DryRunner`](https://godoc.org/github.com/gobuffalo/genny#DryRunner) 中就是这种情况。 ``` func main() { run := genny.DryRunner(context.Background()) g := simple.New() run.With(g) if err := run.Run(); err != nil { log.Fatal(err) } } ``` ``` // output DEBU[2018-12-06T15:13:47-05:00] Step: 4eac628c DEBU[2018-12-06T15:13:47-05:00] Chdir: /go/src/github.com/gobuffalo/genny/internal/_examples/dry DEBU[2018-12-06T15:13:47-05:00] File: /go/src/github.com/gobuffalo/genny/internal/_examples/dry/index.html DEBU[2018-12-06T15:13:47-05:00] Exec: go env DEBU[2018-12-06T15:13:47-05:00] LookPath: genny DEBU[2018-12-06T15:13:47-05:00] Exec: genny -h ``` ``` // file list . └── main.go 0 directories, 1 file ``` 当你不必担心命令运行、文件被写入等问题时,使用 “dry” runner 可以让测试变得更容易。它还可以让你轻松地为你的生成器提供一个 “dry-run” 标志,让人们看到当生成器真正运行时会产生什么。 #### Wet 运行 (**破坏性**) 虽然 “dry” 意味着不执行命令或不写入文件,但 “wet” 运行的意思恰恰相反;它会写入文件并执行命令。 当期望的结果是 “wet” 运行时,请使用 [`github.com/gobuffalo/genny#WetRunner`](https://godoc.org/github.com/gobuffalo/genny#WetRunner)。 ``` func main() { run := genny.WetRunner(context.Background()) g := simple.New() run.With(g) if err := run.Run(); err != nil { log.Fatal(err) } } ``` ``` GOARCH="amd64" GOBIN="" // ... A brief description of your application Usage: genny [command] Available Commands: help Help about any command new generates a new genny stub Flags: -h, --help help for genny Use "genny [command] --help" for more information about a command. ``` ``` // file list . ├── index.html └── main.go 0 directories, 2 files ``` ``` $ cat index.html Hello ``` #### 更改 Runner 行为 要更改 [`github.com/gobuffalo/genny#DryRunner`](https://godoc.org/github.com/gobuffalo/genny#DryRunner) 或 [`github.com/gobuffalo/genny#WetRunner`](https://godoc.org/github.com/gobuffalo/genny#WetRunner) 的工作方式,或者构建你自己的 [`github.com/gobuffalo/genny#Runner`](https://godoc.org/github.com/gobuffalo/genny#Runner),你需要实现 [`github.com/gobuffalo/genny#Runner`](https://godoc.org/github.com/gobuffalo/genny#Runner) 上的 `*Fn` 属性。 ``` type Runner struct { // ... ExecFn func(*exec.Cmd) error // function to use when executing files FileFn func(File) (File, error) // function to use when writing files ChdirFn func(string, func() error) error // function to use when changing directories DeleteFn func(string) error // function used to delete files/folders RequestFn func(*http.Request, *http.Client) (*http.Response, error) // function used to make http requests LookPathFn func(string) (string, error) // function used to make exec.LookPath lookups // ... } ``` 这些 `*Fn` 函数代表了试图运行的任务的 **最终** 端点。 这里有两个 [`github.com/gobuffalo/genny#Runner.FileFn`](https://godoc.org/github.com/gobuffalo/genny#Runner.FileFn) 函数的实现。 第一个会将文件打印到屏幕上。第二个实现将文件写入磁盘。 ``` run.FileFn = func(f packd.SimpleFile) (packd.SimpleFile, error) { io.Copy(os.Stdout, f) return f, nil } run.FileFn = func(f genny.File) (genny.File, error) { if d, ok := f.(genny.Dir); ok { if err := os.MkdirAll(d.Name(), d.Perm); err != nil { return f, err } return d, nil } name := f.Name() if !filepath.IsAbs(name) { name = filepath.Join(run.Root, name) } dir := filepath.Dir(name) if err := os.MkdirAll(dir, 0755); err != nil { return f, err } ff, err := os.Create(name) if err != nil { return f, err } defer ff.Close() if _, err := io.Copy(ff, f); err != nil { return f, err } return f, nil } ``` ### 文件 处理文件(包括创建新文件和现有文件)是编写生成器的一个核心组件。Genny 理解这一点,并提供了几种灵活处理文件的方法,有助于让你更轻松地编写和测试生成器。 [`github.com/gobuffalo/genny#File`](https://godoc.org/github.com/gobuffalo/genny#File) 接口是 Genny 中处理文件的核心。 Genny 自带了几种便捷方法来创建 [`github.com/gobuffalo/genny#File`](https://godoc.org/github.com/gobuffalo/genny#File)。 * [`github.com/gobuffalo/genny#NewFile`](https://godoc.org/github.com/gobuffalo/genny#NewFile) * [`github.com/gobuffalo/genny#NewFileS`](https://godoc.org/github.com/gobuffalo/genny#NewFileS) * [`github.com/gobuffalo/genny#NewFileB`](https://godoc.org/github.com/gobuffalo/genny#NewFileB) * [`github.com/gobuffalo/genny#NewDir`](https://godoc.org/github.com/gobuffalo/genny#NewDir) #### 写入文件 要写入文件,你可以将 [`github.com/gobuffalo/genny#File`](https://godoc.org/github.com/gobuffalo/genny#File) 添加到你的 [`github.com/gobuffalo/genny#Generator.File`](https://godoc.org/github.com/gobuffalo/genny#Generator.File) 中,然后当你的生成器运行时,你的文件将由你的 `*Runner` 处理。 ``` g.File(genny.NewFile("index.html", strings.NewReader("Hello\n"))) g.File(genny.NewFileS("strings/string.html", "Hello\n")) g.File(genny.NewFileB("bytes/byte.html", []byte("Hello\n"))) ``` 在 [`github.com/gobuffalo/genny#WetRunner`](https://godoc.org/github.com/gobuffalo/genny#WetRunner) 的情况下,它会尝试创建你的文件所需的任何目录。 #### 读取文件 在编写生成器时,你可能需要读取现有文件,也许是为了修改它,或者也许是为了读取其内容。这在生成器中带来了一个问题。 第一个问题是,每当我们必须从磁盘读取文件时,都会增加测试的难度。 然而,更大的问题通常出现在 “dry” runner 上(例如测试),而不是 “wet” runner 上。 如果生成器 `A` 创建了一个新文件,而生成器 `B` 想要在测试和 “dry” runner 中修改该文件,这就是个问题,因为该文件可能并未出现在磁盘上供生成器 `B` 访问。 为了解决这个问题,Genny 引入了 [`github.com/gobuffalo/genny#Disk`](https://godoc.org/github.com/gobuffalo/genny#Disk) 的概念。 现在,与其直接向文件系统请求文件,不如转而向 [`github.com/gobuffalo/genny#Runner.Disk`](https://godoc.org/github.com/gobuffalo/genny#Runner.Disk) 请求。 ``` g.RunFn(func(r *genny.Runner) error { // try to find main.go either in the virtual "disk" // or the physical one f, err := r.Disk.Find("main.go") if err != nil { return err } // print the contents of the file fmt.Println(f.String()) return nil }) ``` 当从 [`github.com/gobuffalo/genny#Runner.Disk`](https://godoc.org/github.com/gobuffalo/genny#Runner.Disk) 请求文件时,它会首先检查其内部缓存中是否存在该文件,如果找到则返回。如果文件不在缓存中,那么它会尝试从磁盘的 `filepath.Join(r.Root, name)` 路径读取。 #### 转换文件 有时你可能需要转换单个或全部文件。这可以简单到替换模板名称中的变量以匹配某些用户输入,也可以复杂到通过特定的模板引擎运行任何带有给定扩展名的模板。 [`github.com/gobuffalo/genny#Transformer`](https://godoc.org/github.com/gobuffalo/genny#Transformer) 类型可用于实现这些类型的文件转换。 要创建新的 [`github.com/gobuffalo/genny#Transformer`](https://godoc.org/github.com/gobuffalo/genny#Transformer),你可以使用 [`github.com/gobuffalo/genny#NewTransformer`](https://godoc.org/github.com/gobuffalo/genny#NewTransformer) 函数。 下面的示例取自 [`github.com/gobuffalo/plushgen`](https://godoc.org/github.com/gobuffalo/plushgen) 包。 ``` // Transformer will plushify any file that has a ".plush" extension func Transformer(ctx *plush.Context) genny.Transformer { t := genny.NewTransformer(".plush", func(f genny.File) (genny.File, error) { s, err := plush.RenderR(f, ctx) if err != nil { return f, errors.Wrap(err, f.Name()) } return genny.NewFileS(f.Name(), s), nil }) t.StripExt = true return t } ``` 示例中返回的 [`github.com/gobuffalo/genny#Transformer`](https://godoc.org/github.com/gobuffalo/genny#Transformer) 将仅在名称中包含 `.plush` 扩展名的文件上运行。 如果文件带有 `.plush` 扩展名,它将被发送到 [`github.com/gobuffalo/plush`](https://godoc.org/github.com/gobuffalo/plush) 进行渲染。该渲染的结果将作为一个新的 [`github.com/gobuffalo/genny#File`](https://godoc.org/github.com/gobuffalo/genny#File) 返回。最后,`.plush` 扩展名将从文件名中去掉。 ``` g := genny.New() // add a file g.File(genny.NewFileS("index.html.plush", "Hello <%= name %>\n")) // add the plush transformer ctx := plush.NewContext() ctx.Set("name", "World") g.Transformer(plushgen.Transformer(ctx)) ``` ``` // output DEBU[2018-12-07T10:35:56-05:00] Step: 09c9663e DEBU[2018-12-07T10:35:56-05:00] Chdir: /go/src/github.com/gobuffalo/genny/internal/_examples/dry DEBU[2018-12-07T10:35:56-05:00] File: /go/src/github.com/gobuffalo/genny/internal/_examples/dry/index.html Hello World ``` ### 测试 测试生成器可能很困难,因为在测试期间处理文件的创建、删除和修改可能会很麻烦。运行函数和 HTTP 请求也是如此。 [`github.com/gobuffalo/genny#Runner`](https://godoc.org/github.com/gobuffalo/genny#Runner) 上的 `*Fn` 属性使得模拟不同的测试用例变得更加简单。 大多数情况下,开箱即用的默认设置对于测试来说已经“足够好”了。[`github.com/gobuffalo/genny/gentest`](https://godoc.org/github.com/gobuffalo/genny/gentest) 包提供了几个辅助工具来进一步简化测试。 在这个例子中,我们测试了 [`github.com/gobuffalo/genny#Generator`](https://godoc.org/github.com/gobuffalo/genny#Generator) 的 “正常” (happy) 路径。 ``` func Test_Happy(t *testing.T) { r := require.New(t) run := gentest.NewRunner() run.Disk.Add(genny.NewFileS("main.go", "my main.go file")) g := New() run.With(g) r.NoError(run.Run()) res := run.Results() cmds := []string{"go env", "genny -h"} r.NoError(gentest.CompareCommands(cmds, res.Commands)) files := []string{"index.html", "main.go"} r.NoError(gentest.CompareFiles(files, res.Files)) } ``` 注意,在上面的示例中,我们是如何将 `main.go` 添加到 [`github.com/gobuffalo/genny#Runner.Disk`](https://godoc.org/github.com/gobuffalo/genny#Runner.Disk) 的。这是因为该文件不存在于我们的测试目录中。 在下面的示例中,我们测试了当运行 [`github.com/gobuffalo/genny#Generator`](https://godoc.org/github.com/gobuffalo/genny#Generator) 时找不到 `genny` 可执行文件会发生什么。 我们可以通过使用 [`github.com/gobuffalo/genny#Runner.LookPathFn`](https://godoc.org/github.com/gobuffalo/genny#Runner.LookPathFn) 来模拟这种体验,让它在被询问该特定可执行文件时返回一个错误。 ``` func Test_Missing_Genny(t *testing.T) { r := require.New(t) run := gentest.NewRunner() run.Disk.Add(genny.NewFileS("main.go", "my main.go file")) g := New() run.With(g) // pretend we can't find genny run.LookPathFn = func(s string) (string, error) { if s == "genny" { return "", errors.New("can't find genny") } return s, nil } r.NoError(run.Run()) res := run.Results() cmds := []string{"go env", "go get github.com/gobuffalo/genny/genny", "genny -h"} r.NoError(gentest.CompareCommands(cmds, res.Commands)) files := []string{"index.html", "main.go"} r.NoError(gentest.CompareFiles(files, res.Files)) } ``` ## `genny` 可执行文件 Genny 自带了一个可执行文件,可帮助生成新的生成器。 ### 安装 ``` $ go get -u github.com/gobuffalo/genny/genny ``` ### 用法 ``` $ genny -h tools for working with genny Usage: genny [command] Available Commands: help Help about any command new generates a new genny stub Flags: -h, --help help for genny Use "genny [command] --help" for more information about a command. ``` ### 生成新的 Generator ``` $ genny new coke -h DEBU[2018-12-07T11:07:01-05:00] Step: a1d8eb2f DEBU[2018-12-07T11:07:01-05:00] Chdir: /go/src/github.com/gobuffalo DEBU[2018-12-07T11:07:01-05:00] File: /go/src/github.com/gobuffalo/coke/coke.go DEBU[2018-12-07T11:07:01-05:00] File: /go/src/github.com/gobuffalo/coke/coke_test.go DEBU[2018-12-07T11:07:01-05:00] File: /go/src/github.com/gobuffalo/coke/options.go DEBU[2018-12-07T11:07:01-05:00] File: /go/src/github.com/gobuffalo/coke/options_test.go DEBU[2018-12-07T11:07:01-05:00] File: /go/src/github.com/gobuffalo/coke/templates/example.txt ```
标签:EVTX分析, Go, Ruby工具, SOC Prime, 代码生成器, 开发工具, 开发框架, 日志审计, 脚手架