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: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, 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}}
  # 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: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

# After that, the package template rendering data will be provided, so the form of "k-v" is retained when entering the command, and ":" needs to be added after customize_package.
hz new --mod=github.com/hertz/hello --handler_dir=handler_test --idl=hertzDemo/hello.thrift --customize_package=template/package.yaml:

Default package template

Note: Custom package templates do not provide the ability to render data.

layouts:
  # Path only represents handler.go template, 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 represents router.go template, its 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 represents register.go template, 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"
      	{{$.PkgAlias}} "{{$.Pkg}}"
      )

      // GeneratedRegister registers routers generated by IDL.
      func GeneratedRegister(r *server.Hertz){
      	//INSERT_POINT: DO NOT DELETE THIS LINE!
      	{{$.PkgAlias}}.Register(r)
      }
  - path: model.go
    delims:
      - ""
      - ""
    body: ""
  # path only represents middleware.go template, 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 represents the template of the client.go, the generation path of the client code 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, which is 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, used to update each new middleware 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.

Here is an example of a simple custom handler template, adding some comments to the handler:

template:

layouts:
  # Path only represents handler.go template, the specific handler path is determined by the default path and handler_dir
  - path: handler.go
    delims:
      - '{{'
      - '}}'
    body: |-
      // this is my custom handler.

      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) {
        //  you can code something
      	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 represents router.go template, its 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 represents register.go template, 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"
      	{{$.PkgAlias}} "{{$.Pkg}}"
      )

      // GeneratedRegister registers routers generated by IDL.
      func GeneratedRegister(r *server.Hertz){
      	//INSERT_POINT: DO NOT DELETE THIS LINE!
      	{{$.PkgAlias}}.Register(r)
      }
  - path: model.go
    delims:
      - ""
      - ""
    body: ""
  # path only represents middleware.go template, 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 represents the template of the client.go, the generation path of the client code 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, which is 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, used to update each new middleware when updating
  - path: middleware_single.go
    delims:
      - '{{'
      - '}}'
    body: |+
      func {{.MiddleWare}}Mw() []app.HandlerFunc {
      	// your code...
      	return nil
      }

Command:

# After that, the package template rendering data will be provided, so the form of "k-v" is retained when entering the command, and ":" needs to be added after customize_package.
hz new --mod=github.com/hertz/hello --handler_dir=handler_test --idl=hertzDemo/hello.thrift --customize_package=template/package.yaml:

The handler is generated as follows:

// this is my custom handler.

package example

import (
	"context"

	"github.com/cloudwego/hertz/pkg/app"
	example "test/test2/biz/model/hello/example"
)

// HelloMethod .
// @router /hello [GET]
func HelloMethod(ctx context.Context, c *app.RequestContext) {
	//  you can code something
	var err error
	var req example.HelloReq
	err = c.BindAndValidate(&req)
	if err != nil {
		c.String(400, err.Error())
		return
	}

	resp := new(example.HelloResp)

	c.JSON(200, resp)
}

// OtherMethod .
// @router /other [POST]
func OtherMethod(ctx context.Context, c *app.RequestContext) {
	//  you can code something
	var err error
	var req example.OtherReq
	err = c.BindAndValidate(&req)
	if err != nil {
		c.String(400, err.Error())
		return
	}

	resp := new(example.OtherResp)

	c.JSON(200, resp)
}

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.


Last modified December 2, 2022 : chore: add lark footer link (#460) (f41a884)