几个月前,Ariel 对 Ent 的核心做出了一个不声不响但影响深远的贡献 — Extension API。虽然 Ent 长期以来已经具备扩展能力(如 Code‑gen Hooks、External Templates、以及 Annotations),但一直没有一种便捷的方式将这些动态组成合并成一个连贯、可自包含的组件。我们在本文中讨论的 Extension API 正是实现这一点的手段。
许多开源生态系统之所以蓬勃发展,往往是因为它们擅长为开发者提供一种简洁、结构化的方式来扩展一个小型核心系统。针对 Node.js 生态系统曾有不少批评(甚至其原创者 Ryan Dahl也曾发声),但很难否认 npm 模块的易于发布与消费正推动了其爆炸式增长。我在个人博客中讨论过 protoc 的插件系统如何工作,以及这如何让 Protobuf 生态繁荣。简言之,生态系统的形成离不开模块化设计。
今天的文章,我们将通过一个玩具示例,探索 Ent 的 Extension API。
入门
Extension API 仅适用于使用 Ent 的代码生成器以 Go 包形式工作的项目(see docs)。在初始化项目后,新增一个名为 ent/entc.go 的文件:
//+build ignore
package main
import (
"log"
"entgo.io/ent/entc"
"entgo.io/ent/entc/gen"
"entgo.io/ent/schema/field"
)
func main() {
err := entc.Generate("./schema", &gen.Config{})
if err != nil {
log.Fatal("running ent codegen:", err)
}
}
接下来修改 ent/generate.go,让它调用我们的 entc 文件:
package ent
//go:generate go run entc.go
创建我们的 Extension
所有 Extension 必须实现 Extension 接口:
type Extension interface {
// Hooks holds an optional list of Hooks to apply
// on the graph before/after the code-generation.
Hooks() []gen.Hook
// Annotations injects global annotations to the gen.Config object that
// can be accessed globally in all templates. Unlike schema annotations,
// being serializable to JSON raw value is not mandatory.
//
// {{- with $.Config.Annotations.GQL }}
// {{/* Annotation usage goes here. */}}
// {{- end }}
//
Annotations() []Annotation
// Templates specifies a list of alternative templates
// to execute or to override the default.
Templates() []*gen.Template
// Options specifies a list of entc.Options to evaluate on
// the gen.Config before executing the code generation.
Options() []Option
}
为简化新 Extension 的开发,开发者可以嵌入 entc.DefaultExtension,从而无需实现所有方法。在 entc.go 中加入:
// ...
// GreetExtension implements entc.Extension.
type GreetExtension struct {
entc.DefaultExtension
}
目前我们的 Extension 并未真正做任何事。接下来,把它连接到代码生成配置中。在 entc.go 添加:
err := entc.Generate("./schema", &gen.Config{}, entc.Extensions(&GreetExtension{}))
添加模板
外部模板可以被打包进 Extension,以增强 Ent 核心的代码生成能力。以我们的玩具示例为例,我们的目标是为每个实体添加一个名为 Greet 的生成方法,当调用时返回一个包含类型名称的问候语。期望效果类似:
func (u *User) Greet() string {
return "Greetings, User"
}
为此,先新增一个外部模板文件并放置到 ent/templates/greet.tmpl:
{{ define "greet" }}
{{/* Add the base header for the generated file */}}
{{ $pkg := base $.Config.Package }}
{{ template "header" $ }}
{{/* Loop over all nodes and add the Greet method */}}
{{ range $n := $.Nodes }}
{{ $receiver := $n.Receiver }}
func ({{ $receiver }} *{{ $n.Name }}) Greet() string {
return "Greetings, {{ $n.Name }}"
}
{{ end }}
{{ end }}
接下来实现 Templates 方法:
func (*GreetExtension) Templates() []*gen.Template {
return []*gen.Template{
gen.MustParse(gen.NewTemplate("greet").ParseFiles("templates/greet.tmpl")),
}
}
接下来,测试我们的 Extension。新建一个 ent/schema/user.go,添加 User 类型 schema:
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
)
// User holds the schema definition for the User entity.
type User struct {
ent.Schema
}
// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("email_address").
Unique(),
}
}
然后执行:
go generate ./...
你会发现新文件 ent/greet.go 已生成,内容如下:
// Code generated by ent, DO NOT EDIT.
package ent
func (u *User) Greet() string {
return "Greetings, User"
}
太好了!我们的 Extension 成功被 Ent 的代码生成器调用,并产生了我们期望的代码!
添加注解
注解(Annotation)为 Extension 的用户提供了一种 API,能够修改代码生成逻辑的行为。为了给 GreetExtension 添加注解,先实现一个 GreetingWord 注解:
// GreetingWord implements entc.Annotation
type GreetingWord string
func (GreetingWord) Name() string {
return "GreetingWord"
}
随后在 GreetExtension 结构体中添加一个 Word 字段:
type GreetExtension struct {
entc.DefaultExtension
Word GreetingWord
}
实现 Annotations 方法:
func (s *GreetExtension) Annotations() []entc.Annotation {
return []entc.Annotation{
s.Word,
}
}
现在,你可以在模板中访问 GreetingWord 注解。修改 ent/templates/greet.tmpl,使用新的注解:
func ({{ $receiver }} *{{ $n.Name }}) Greet() string {
return "{{ $.Annotations.GreetingWord }}, {{ $n.Name }}"
}
接着调整代码生成配置,以设置 GreetingWord 注解:
err := entc.Generate("./schema",
&gen.Config{},
entc.Extensions(&GreetExtension{
Word: GreetingWord("Shalom"),
}),
)
重新运行:
go generate ./...
你会看到生成的 ent/greet.go 已更新:
func (u *User) Greet() string {
return "Shalom, User"
}
太棒了!我们已通过注解为生成的 Greet 方法添加了可配置的问候词。
更多可能性
除了模板和注解,Extension API 还允许开发者将 gen.Hook 和 entc.Option 打包进 Extension,以进一步控制代码生成行为。本文未展开讨论这些可能性,若你对它们感兴趣,请查看文档。
小结
本文通过一个玩具示例,展示了如何使用 Extension API 创建新的 Ent 代码生成 Extension。正如前面所提,允许任何人扩展软件核心功能的模块化设计对生态系统的成功至关重要。我们正看到这种理念在 Ent 社区的实现,以下列出了一些使用 Extension API 的有趣项目:
- elk — 基于 Ent schema 生成 REST 端点的扩展。
- entgql — 从 Ent schema 生成 GraphQL 服务器。
- entviz — 从 Ent schema 生成 ER 图。
你呢?有没有想要实现的有用 Ent Extension?希望本文让你看到,通过新的 Extension API,实现这一目标并不繁琐。
如有任何问题或需要帮助入门,欢迎加入我们的Discord 服务器或Slack 频道。
::: note For more Ent news and updates:
- Subscribe to our Newsletter
- Follow us on Twitter
- Join us on #ent on the Gophers Slack
- Join us on the Ent Discord Server
:::