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.


Last modified August 2, 2023 : chore: update readme (#742) (945d264)