hz custom template use
Hertz provides command-line tools (hz) that support custom template features, including:
- Customize the layout template (i.e., the directory structure of the generated code)
- Custom package templates (i.e. service-related code structures, including handler, router, etc.)
Users can provide their own templates and rendering parameters, combined with the ability of hz, to complete the custom code generation structure.
Custom layout template
Users can modify or rewrite according to the default template to meet their own needs
Hz takes advantage of the “go template” capability to support defining templates in “yaml” format and uses “json” format to define rendering data.
The so-called layout template refers to the structure of the entire project. These structures have nothing to do with the specific idl definition, and can be directly generated without idl. The default structure is as follows:
.
├── biz
│ ├── handler
│ │ └── ping.go
│ │ └── ****.go // Set of handlers divided by service, the position can be changed according to handler_dir
│ ├── model
│ │ └── model.go // idl generated struct, the position can be changed according to model_dir
│ └── router //undeveloped custom dir
│ └── register.go // Route registration, used to call the specific route registration
│ └── route.go // Specific route registration location
│ └── middleware.go // Default middleware build location
├── .hz // hz Create code flags
├── go.mod
├── main.go // Start the entrance
├── router.go // User-defined route write location
└── router_gen.go // hz generated route registration call
IDL
// hello.thrift
namespace go hello.example
struct HelloReq {
1: string Name (api.query="name");
}
struct HelloResp {
1: string RespBody;
}
service HelloService {
HelloResp HelloMethod(1: HelloReq request) (api.get="/hello");
}
Command
hz new --mod=github.com/hertz/hello --idl=./hertzDemo/hello.thrift --customize_layout=template/layout.yaml --customize_layout_data_path=template/data.json
The meaning of the default layout template
Note: The following bodies are all go templates
layouts:
# The directory of the generated handler will only be generated if there are files in the directory
- path: biz/handler/
delims:
- ""
- ""
body: ""
# The directory of the generated model will only be generated if there are files in the directory
- path: biz/model/
delims:
- ""
- ""
body: ""
# project main file
- path: main.go
delims:
- ""
- ""
body: |-
// Code generated by hertz generator.
package main
import (
"github.com/cloudwego/hertz/pkg/app/server"
)
func main() {
h := server.Default()
register(h)
h.Spin()
}
# go.mod file, need template rendering data {{.GoModule}} to generate
- path: go.mod
delims:
- '{{'
- '}}'
body: |-
module {{.GoModule}}
{{- if .UseApacheThrift}}
replace github.com/apache/thrift => github.com/apache/thrift v0.13.0
{{- end}}
# .gitignore file
- path: .gitignore
delims:
- ""
- ""
body: "*.o\n*.a\n*.so\n_obj\n_test\n*.[568vq]\n[568vq].out\n*.cgo1.go\n*.cgo2.c\n_cgo_defun.c\n_cgo_gotypes.go\n_cgo_export.*\n_testmain.go\n*.exe\n*.exe~\n*.test\n*.prof\n*.rar\n*.zip\n*.gz\n*.psd\n*.bmd\n*.cfg\n*.pptx\n*.log\n*nohup.out\n*settings.pyc\n*.sublime-project\n*.sublime-workspace\n!.gitkeep\n.DS_Store\n/.idea\n/.vscode\n/output\n*.local.yml\ndumped_hertz_remote_config.json\n\t\t
\ "
# .hz file, containing hz version, is the logo of the project created by hz, no need to transfer rendering data
- path: .hz
delims:
- '{{'
- '}}'
body: |-
// Code generated by hz. DO NOT EDIT.
hz version: {{.hzVersion}}
# ping comes with ping handler
- path: biz/handler/ping.go
delims:
- ""
- ""
body: |-
// Code generated by hertz generator.
package handler
import (
"context"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/common/utils"
)
// Ping .
func Ping(ctx context.Context, c *app.RequestContext) {
c.JSON(200, utils.H{
"message": "pong",
})
}
# `router_gen.go` is the file that defines the route registration
- path: router_gen.go
delims:
- ""
- ""
body: |-
// Code generated by hertz generator. DO NOT EDIT.
package main
import (
"github.com/cloudwego/hertz/pkg/app/server"
router "{{.RouterPkgPath}}"
)
// register registers all routers.
func register(r *server.Hertz) {
router.GeneratedRegister(r)
customizedRegister(r)
}
# Custom route registration file
- path: router.go
delims:
- ""
- ""
body: |-
// Code generated by hertz generator.
package main
import (
"github.com/cloudwego/hertz/pkg/app/server"
handler "{{.HandlerPkgPath}}"
)
// customizeRegister registers customize routers.
func customizedRegister(r *server.Hertz){
r.GET("/ping", handler.Ping)
// your code ...
}
# Default route registration file, do not modify it
- path: biz/router/register.go
delims:
- ""
- ""
body: |-
// Code generated by hertz generator. DO NOT EDIT.
package router
import (
"github.com/cloudwego/hertz/pkg/app/server"
)
// GeneratedRegister registers routers generated by IDL.
func GeneratedRegister(r *server.Hertz){
//INSERT_POINT: DO NOT DELETE THIS LINE!
}
The meaning of template rendering parameter file
When a custom template and render data are specified, the options specified on the command line will not be used as render data, so the render data in the template needs to be defined by the user.
Hz uses “json” to specify render data, as described below
{
// global render parameters
"*": {
"GoModule": "github.com/hz/test", // must be consistent with the command line, otherwise the subsequent generation of model, handler and other code will use the mod specified by the command line, resulting in inconsistency.
"ServiceName": "p.s.m", // as specified on the command line
"UseApacheThrift": false // Set "true"/"false" depending on whether to use "thrift"
},
// router_gen.go route the registered render data,
// "biz/router"points to the module of the routing code registered by the default idl, do not modify it
"router_gen.go": {
"RouterPkgPath": "github.com/hz/test/biz/router"
}
}
Customize a layout template
At present, the project layout generated by hz is already the most basic skeleton of a hertz project, so it is not recommended to delete the files in the existing template.
However, if the user wants a different layout, of course, you can also delete the corresponding file according to your own needs (except “biz/register.go”, the rest can be modified)
We welcome users to contribute their own templates
Assuming that the user only wants “main.go” and “go.mod” files, then we modify the default template as follows:
template
# layout.yaml
layouts:
# project main file
- path: main.go
delims:
- ""
- ""
body: |-
// Code generated by hertz generator.
package main
import (
"github.com/cloudwego/hertz/pkg/app/server"
"{{.GoModule}}/biz/router"
)
func main() {
h := server.Default()
router.GeneratedRegister(h)
// do what you wanted
// add some render data: {{.MainData}}
h.Spin()
}
# go.mod file, requires template rendering data {{.GoModule}} to generate
- path: go.mod
delims:
- '{{'
- '}}'
body: |-
module {{.GoModule}}
{{- if .UseApacheThrift}}
replace github.com/apache/thrift => github.com/apache/thrift v0.13.0
{{- end}}
# Default route registration file, no need to modify
- path: biz/router/register.go
delims:
- ""
- ""
body: |-
// Code generated by hertz generator. DO NOT EDIT.
package router
import (
"github.com/cloudwego/hertz/pkg/app/server"
)
// GeneratedRegister registers routers generated by IDL.
func GeneratedRegister(r *server.Hertz){
//INSERT_POINT: DO NOT DELETE THIS LINE!
}
render data
{
"*": {
"GoModule": "github.com/hertz/hello",
"ServiceName": "hello",
"UseApacheThrift": true
},
"main.go": {
"MainData": "this is customized render data"
}
}
Command:
hz new --mod=github.com/hertz/hello --idl=./hertzDemo/hello.thrift --customize_layout=template/layout.yaml --customize_layout_data_path=template/data.json
Custom package template
The template address of the hz template:
Users can modify or rewrite according to the default template to meet their own needs
- The so-called package template refers to the code related to the idl definition. This part of the code involves the service, go_package/namespace, etc. specified when defining idl. It mainly includes the following parts:
- handler.go: Handling function logic
- router.go: the route registration logic of the service defined by the specific idl
- register.go: logic for calling the content in
router.go
Model code: generated go struct; however, since the tool that uses plugins to generate model code currently does not have permission to modify the model template, this part of the function is not open for now
Command
hz new --mod=github.com/hertz/hello --handler_dir=handler_test --idl=hertzDemo/hello.thrift --customize_package=template/package.yaml
Default package template
Note: The custom package template does not provide the function of rendering data, mainly because the rendering data is generated by the hz tool, so it does not provide the function of writing your own rendering data for now. You can modify the parts of the template that have nothing to do with rendering data to meet your own needs.
layouts:
# path only indicates the template of handler.go, the specific handler path is determined by the default path and handler_dir
- path: handler.go
delims:
- '{{'
- '}}'
body: |-
// Code generated by hertz generator.
package {{.PackageName}}
import (
"context"
"github.com/cloudwego/hertz/pkg/app"
{{- range $k, $v := .Imports}}
{{$k}} "{{$v.Package}}"
{{- end}}
)
{{range $_, $MethodInfo := .Methods}}
{{$MethodInfo.Comment}}
func {{$MethodInfo.Name}}(ctx context.Context, c *app.RequestContext) {
var err error
{{if ne $MethodInfo.RequestTypeName "" -}}
var req {{$MethodInfo.RequestTypeName}}
err = c.BindAndValidate(&req)
if err != nil {
c.String(400, err.Error())
return
}
{{end}}
resp := new({{$MethodInfo.ReturnTypeName}})
c.{{.Serializer}}(200, resp)
}
{{end}}
# path only indicates the router.go template, whose path is fixed at: biz/router/namespace/
- path: router.go
delims:
- '{{'
- '}}'
body: |-
// Code generated by hertz generator. DO NOT EDIT.
package {{$.PackageName}}
import (
"github.com/cloudwego/hertz/pkg/app/server"
{{range $k, $v := .HandlerPackages}}{{$k}} "{{$v}}"{{end}}
)
/*
This file will register all the routes of the services in the master idl.
And it will update automatically when you use the "update" command for the idl.
So don't modify the contents of the file, or your code will be deleted when it is updated.
*/
{{define "g"}}
{{- if eq .Path "/"}}r
{{- else}}{{.GroupName}}{{end}}
{{- end}}
{{define "G"}}
{{- if ne .Handler ""}}
{{- .GroupName}}.{{.HttpMethod}}("{{.Path}}", append({{.MiddleWare}}Mw(), {{.Handler}})...)
{{- end}}
{{- if ne (len .Children) 0}}
{{.MiddleWare}} := {{template "g" .}}.Group("{{.Path}}", {{.MiddleWare}}Mw()...)
{{- end}}
{{- range $_, $router := .Children}}
{{- if ne .Handler ""}}
{{template "G" $router}}
{{- else}}
{ {{template "G" $router}}
}
{{- end}}
{{- end}}
{{- end}}
// Register register routes based on the IDL 'api.${HTTP Method}' annotation.
func Register(r *server.Hertz) {
{{template "G" .Router}}
}
# path only indicates the template of register.go, the path of register is fixed to biz/router/register.go
- path: register.go
delims:
- ""
- ""
body: |-
// Code generated by hertz generator. DO NOT EDIT.
package router
import (
"github.com/cloudwego/hertz/pkg/app/server"
{{$.DepPkgAlias}} "{{$.Pkg}}"
)
// GeneratedRegister registers routers generated by IDL.
func GeneratedRegister(r *server.Hertz){
//INSERT_POINT: DO NOT DELETE THIS LINE!
{{$.DepPkgAlias}}.Register(r)
}
- path: model.go
delims:
- ""
- ""
body: ""
# path only indicates the template of middleware.go, the path of middleware is the same as router.go: biz/router/namespace/
- path: middleware.go
delims:
- '{{'
- '}}'
body: |-
// Code generated by hertz generator.
package {{$.PackageName}}
import (
"github.com/cloudwego/hertz/pkg/app"
)
{{define "M"}}
func {{.MiddleWare}}Mw() []app.HandlerFunc {
// your code...
return nil
}
{{range $_, $router := $.Children}}{{template "M" $router}}{{end}}
{{- end}}
{{template "M" .Router}}
# path only indicates the template of client.go, the path of client code generation is specified by the user "${client_dir}"
- path: client.go
delims:
- '{{'
- '}}'
body: |-
// Code generated by hertz generator.
package {{$.PackageName}}
import (
"github.com/cloudwego/hertz/pkg/app/client"
"github.com/cloudwego/hertz/pkg/common/config"
)
type {{.ServiceName}}Client struct {
client * client.Client
}
func New{{.ServiceName}}Client(opt ...config.ClientOption) (*{{.ServiceName}}Client, error) {
c, err := client.NewClient(opt...)
if err != nil {
return nil, err
}
return &{{.ServiceName}}Client{
client: c,
}, nil
}
# handler_single means a separate handler template, used to update each new handler when updating
- path: handler_single.go
delims:
- '{{'
- '}}'
body: |+
{{.Comment}}
func {{.Name}}(ctx context.Context, c *app.RequestContext) {
// this my demo
var err error
{{if ne .RequestTypeName "" -}}
var req {{.RequestTypeName}}
err = c.BindAndValidate(&req)
if err != nil {
c.String(400, err.Error())
return
}
{{end}}
resp := new({{.ReturnTypeName}})
c.{{.Serializer}}(200, resp)
}
# middleware_single means a separate middleware template, which is used to update each new middleware_single when updating
- path: middleware_single.go
delims:
- '{{'
- '}}'
body: |+
func {{.MiddleWare}}Mw() []app.HandlerFunc {
// your code...
return nil
}
Customize a package template
Like layout templates, users can also customize package templates.
As far as the templates provided by the package are concerned, the average user may only need to customize handler.go templates, because router.go/middleware.go/register.go are generally related to the idl definition and the user does not need to care, so hz currently also fixes the location of these templates, and generally does not need to be modified.
Therefore, users can customize the generated handler template according to their own needs to speed up development; however, since the default handler template integrates some model information and package information, the hz tool is required to provide rendering data. This part of the user can modify it according to their own situation, and it is generally recommended to leave model information.
Add a new template
Considering that sometimes you may need to add your own implementation for some information of IDL, such as adding a single test for each generated handler. Therefore, the hz templates allow users to customize new templates and provide data sources for the template rendering parameters.
Template format:
- path: biz/handler/{{$HandlerName}}.go // path+filename, support rendering data
loop_method: bool // Whether to generate multiple files according to the method defined in idl, to be used with path rendering
loop_service: bool // whether to generate multiple files according to the service defined in idl, to be used with path rendering
update_behavior: // The update behavior of the file when using hz update
type: string // update behavior: skip/cover/append
append_key: "method"/"service" // Specify the appended rendering data source, method/service, in the append behavior
insert_key: string // The "key" of the append logic in the append behavior, based on which to determine if appending is needed
append_content_tpl: string // Specify the template for appending content in the append behavior
import_tpl: []string // The template to be added to the import
body: string // The template content of the generated file
Template Data Source
- File path rendering: The following rendering data can be used when specifying a file path
type FilePathRenderInfo struct {
MasterIDLName string // master IDL name
GenPackage string // master IDL generate code package
HandlerDir string // handler generate dir
ModelDir string // model generate dir
RouterDir string // router generate dir
ProjectDir string // projectDir
GoModule string // go module
ServiceName string // service name, changed as services are traversed
MethodName string // method name, changed as methods are traversed
HandlerGenPath string // "api.gen_path" value
}
- Single file rendering data: the rendering data used when defining a single file, all IDL information can be extracted according to the definition of “IDLPackageRenderInfo”
type CustomizedFileForIDL struct {
*IDLPackageRenderInfo
FilePath string
FilePackage string
}
- Method level rendering data: when “loop_method” is specified, the rendering data used will be generated as a file for each method
type CustomizedFileForMethod struct {
*HttpMethod // The specific information parsed for each method definition
FilePath string // When the method file is generated in a loop, the path to the file
FilePackage string // When the method file is generated in a loop, the go package name of the file
ServiceInfo *Service // The information defined by the service to which the method belongs
}
type HttpMethod struct {
Name string
HTTPMethod string
Comment string
RequestTypeName string
ReturnTypeName string
Path string
Serializer string
OutputDir string
Models map[string]*model.Model
}
- Service level rendering data: when “loop_service” is specified, the rendering data will be used and a file will be generated for each service unit
type CustomizedFileForService struct {
*Service // specific information about the service, including the service name, information about the method defined in the servide, etc.
FilePath string // When the service file is generated in a loop, the path to the file
FilePackage string // When the service file is looped, the go package name of the file
IDLPackageInfo *IDLPackageRenderInfo // Information about the IDL definition to which the service belongs
}
type Service struct {
Name string
Methods []*HttpMethod
ClientMethods []*ClientMethod
Models []*model.Model // all dependency models
BaseDomain string // base domain for client code
}
A simple example of a custom handler template is given below:
example
example: https://github.com/cloudwego/hertz-examples/tree/main/hz/template
-
Modify the content of the default handler
-
Add a single test file for handler
layouts:
- path: handler.go
body: |-
{{$OutDirs := GetUniqueHandlerOutDir .Methods}}
package {{.PackageName}}
import (
"context"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/protocol/consts"
{{- range $k, $v := .Imports}}
{{$k}} "{{$v.Package}}"
{{- end}}
{{- range $_, $OutDir := $OutDirs}}
{{if eq $OutDir "" -}}
"{{$.ProjPackage}}/biz/service"
{{- else -}}
"{{$.ProjPackage}}/biz/service/{{$OutDir}}"
{{- end -}}
{{- end}}
"{{$.ProjPackage}}/biz/utils"
)
{{range $_, $MethodInfo := .Methods}}
{{$MethodInfo.Comment}}
func {{$MethodInfo.Name}}(ctx context.Context, c *app.RequestContext) {
var err error
{{if ne $MethodInfo.RequestTypeName "" -}}
var req {{$MethodInfo.RequestTypeName}}
err = c.BindAndValidate(&req)
if err != nil {
utils.SendErrResponse(ctx, c, consts.StatusOK, err)
return
}
{{end}}
{{if eq $MethodInfo.OutputDir "" -}}
resp,err := service.New{{$MethodInfo.Name}}Service(ctx, c).Run(&req)
if err != nil {
utils.SendErrResponse(ctx, c, consts.StatusOK, err)
return
}
{{else}}
resp,err := {{$MethodInfo.OutputDir}}.New{{$MethodInfo.Name}}Service(ctx, c).Run(&req)
if err != nil {
utils.SendErrResponse(ctx, c, consts.StatusOK, err)
return
}
{{end}}
utils.SendSuccessResponse(ctx, c, consts.StatusOK, resp)
}
{{end}}
update_behavior:
import_tpl:
- |-
{{$OutDirs := GetUniqueHandlerOutDir .Methods}}
{{- range $_, $OutDir := $OutDirs}}
{{if eq $OutDir "" -}}
"{{$.ProjPackage}}/biz/service"
{{- else -}}
"{{$.ProjPackage}}/biz/service/{{$OutDir}}"
{{end}}
{{- end}}
- path: handler_single.go
body: |+
{{.Comment}}
func {{.Name}}(ctx context.Context, c *app.RequestContext) {
var err error
{{if ne .RequestTypeName "" -}}
var req {{.RequestTypeName}}
err = c.BindAndValidate(&req)
if err != nil {
utils.SendErrResponse(ctx, c, consts.StatusOK, err)
return
}
{{end}}
{{if eq .OutputDir "" -}}
resp,err := service.New{{.Name}}Service(ctx, c).Run(&req)
{{else}}
resp,err := {{.OutputDir}}.New{{.Name}}Service(ctx, c).Run(&req)
{{end}}
if err != nil {
utils.SendErrResponse(ctx, c, consts.StatusOK, err)
return
}
utils.SendSuccessResponse(ctx, c, consts.StatusOK, resp)
}=
- path: "{{.HandlerDir}}/{{.GenPackage}}/{{ToSnakeCase .ServiceName}}_test.go"
loop_service: true
update_behavior:
type: "append"
append_key: "method"
insert_key: "Test{{$.Name}}"
append_tpl: |-
func Test{{.Name}}(t *testing.T) {
h := server.Default()
h.{{.HTTPMethod}}("{{.Path}}", {{.Name}})
w := ut.PerformRequest(h.Engine, "{{.HTTPMethod}}", "{{.Path}}", &ut.Body{Body: bytes.NewBufferString(""), Len: 1},
ut.Header{})
resp := w.Result()
assert.DeepEqual(t, 201, resp.StatusCode())
assert.DeepEqual(t, "", string(resp.Body()))
// todo edit your unit test.
}
body: |-
package {{.FilePackage}}
import (
"bytes"
"testing"
"github.com/cloudwego/hertz/pkg/app/server"
"github.com/cloudwego/hertz/pkg/common/test/assert"
"github.com/cloudwego/hertz/pkg/common/ut"
)
{{range $_, $MethodInfo := $.Methods}}
func Test{{$MethodInfo.Name}}(t *testing.T) {
h := server.Default()
h.{{$MethodInfo.HTTPMethod}}("{{$MethodInfo.Path}}", {{$MethodInfo.Name}})
w := ut.PerformRequest(h.Engine, "{{$MethodInfo.HTTPMethod}}", "{{$MethodInfo.Path}}", &ut.Body{Body: bytes.NewBufferString(""), Len: 1},
ut.Header{})
resp := w.Result()
assert.DeepEqual(t, 201, resp.StatusCode())
assert.DeepEqual(t, "", string(resp.Body()))
// todo edit your unit test.
}
{{end}}
MVC Template Practice
Hertz provides a best practice for customizing templates for MVC, see code for code details.
Precautions
Precautions for using layout templates
When the user uses the layout custom template, the generated layout and rendering data are taken over by the user, so the user needs to provide the rendering data of the defined layout.
Precautions for using package templates
Generally speaking, when users use package templates, most of them are to modify the default handler template; however, hz does not provide a single handler template at present, so when updating an existing handler file, the default handler template will be used to append a new handler function to the end of the handler file. When the corresponding handler file does not exist, a custom template will be used to generate the handler file.