
# 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
```