From 14939c0748ce87bdb02e921aa0386b1a83b7327f Mon Sep 17 00:00:00 2001 From: yuxialuozi <139631989+yuxialuozi@users.noreply.github.com> Date: Sun, 2 Jun 2024 22:21:27 +0800 Subject: [PATCH] feat:a restful api template (#191) * feat:a restful api template * fix:http only and Modify parameter name * fix:Modify parameter name * fix: server directory structure --- pkg/consts/const.go | 1 + pkg/server/hz.go | 6 + tpl/hertz/server/standard_v2/layout.yaml | 538 ++++++++++++++++++++++ tpl/hertz/server/standard_v2/package.yaml | 167 +++++++ 4 files changed, 712 insertions(+) create mode 100644 tpl/hertz/server/standard_v2/layout.yaml create mode 100644 tpl/hertz/server/standard_v2/package.yaml diff --git a/pkg/consts/const.go b/pkg/consts/const.go index 5dee3a1b..5767d1bd 100644 --- a/pkg/consts/const.go +++ b/pkg/consts/const.go @@ -73,6 +73,7 @@ const ( DefaultDocModelOutDir = "biz/doc/model" DefaultDocDaoOutDir = "biz/doc/dao" Standard = "standard" + StandardV2 = "standard_v2" CurrentDir = "." ) diff --git a/pkg/server/hz.go b/pkg/server/hz.go index 057844a8..d5c4d710 100644 --- a/pkg/server/hz.go +++ b/pkg/server/hz.go @@ -66,6 +66,12 @@ func convertHzArgument(sa *config.ServerArgument, hzArgument *hzConfig.Argument) if isExist { hzArgument.CustomizeLayoutData = layoutDataPath } + + if sa.Template == consts.StandardV2 && sa.Type == consts.HTTP { + hzArgument.CustomizeLayout = path.Join(tpl.HertzDir, consts.Server, consts.StandardV2, consts.LayoutFile) + hzArgument.CustomizePackage = path.Join(tpl.HertzDir, consts.Server, consts.StandardV2, consts.PackageLayoutFile) + } + } else { hzArgument.CustomizeLayout = path.Join(tpl.HertzDir, consts.Server, consts.Standard, consts.LayoutFile) hzArgument.CustomizePackage = path.Join(tpl.HertzDir, consts.Server, consts.Standard, consts.PackageLayoutFile) diff --git a/tpl/hertz/server/standard_v2/layout.yaml b/tpl/hertz/server/standard_v2/layout.yaml new file mode 100644 index 00000000..d7634ca9 --- /dev/null +++ b/tpl/hertz/server/standard_v2/layout.yaml @@ -0,0 +1,538 @@ +layouts: + - path: main.go + delims: + - "" + - "" + body: |- + // Code generated by hertz generator. + + package main + + import ( + "context" + "time" + + "github.com/cloudwego/hertz/pkg/app" + "github.com/cloudwego/hertz/pkg/app/middlewares/server/recovery" + "github.com/cloudwego/hertz/pkg/app/server" + "github.com/cloudwego/hertz/pkg/common/hlog" + "github.com/cloudwego/hertz/pkg/common/utils" + "github.com/cloudwego/hertz/pkg/protocol/consts" + "github.com/hertz-contrib/cors" + "github.com/hertz-contrib/gzip" + "github.com/hertz-contrib/logger/accesslog" + hertzlogrus "github.com/hertz-contrib/logger/logrus" + "github.com/hertz-contrib/pprof" + "{{.GoModule}}/biz/router" + "{{.GoModule}}/conf" + "go.uber.org/zap/zapcore" + "gopkg.in/natefinch/lumberjack.v2" + ) + + func main() { + // init dal + // dal.Init() + address := conf.GetConf().Hertz.Address + h := server.New(server.WithHostPorts(address)) + + registerMiddleware(h) + + // add a ping route to test + h.GET("/ping", func(c context.Context, ctx *app.RequestContext) { + ctx.JSON(consts.StatusOK, utils.H{"ping": "pong"}) + }) + + router.GeneratedRegister(h) + + h.Spin() + } + + func registerMiddleware(h *server.Hertz) { + // log + logger := hertzlogrus.NewLogger() + hlog.SetLogger(logger) + hlog.SetLevel(conf.LogLevel()) + asyncWriter := &zapcore.BufferedWriteSyncer{ + WS: zapcore.AddSync(&lumberjack.Logger{ + Filename: conf.GetConf().Hertz.LogFileName, + MaxSize: conf.GetConf().Hertz.LogMaxSize, + MaxBackups: conf.GetConf().Hertz.LogMaxBackups, + MaxAge: conf.GetConf().Hertz.LogMaxAge, + }), + FlushInterval: time.Minute, + } + hlog.SetOutput(asyncWriter) + h.OnShutdown = append(h.OnShutdown, func(ctx context.Context) { + asyncWriter.Sync() + }) + + // pprof + if conf.GetConf().Hertz.EnablePprof { + pprof.Register(h) + } + + // gzip + if conf.GetConf().Hertz.EnableGzip { + h.Use(gzip.Gzip(gzip.DefaultCompression)) + } + + // access log + if conf.GetConf().Hertz.EnableAccessLog { + h.Use(accesslog.New()) + } + + // recovery + h.Use(recovery.Recovery()) + + // cores + h.Use(cors.Default()) + } + + - path: go.mod + delims: + - '{{' + - '}}' + body: |- + module {{.GoModule}} + {{- if .UseApacheThrift}} + replace github.com/apache/thrift => github.com/apache/thrift v0.13.0 + {{- end}} + + - 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! + } + + - path: conf/conf.go + delims: + - "" + - "" + body: |- + package conf + + import ( + "io/ioutil" + "os" + "path/filepath" + "sync" + + "github.com/cloudwego/hertz/pkg/common/hlog" + "github.com/kr/pretty" + "gopkg.in/validator.v2" + "gopkg.in/yaml.v2" + ) + + var ( + conf *Config + once sync.Once + ) + + type Config struct { + Env string + + Hertz Hertz `yaml:"hertz"` + MySQL MySQL `yaml:"mysql"` + Redis Redis `yaml:"redis"` + } + + type MySQL struct { + DSN string `yaml:"dsn"` + } + + + type Redis struct { + Address string `yaml:"address"` + Password string `yaml:"password"` + Username string `yaml:"username"` + DB int `yaml:"db"` + } + + type Hertz struct { + Address string `yaml:"address"` + EnablePprof bool `yaml:"enable_pprof"` + EnableGzip bool `yaml:"enable_gzip"` + EnableAccessLog bool `yaml:"enable_access_log"` + LogLevel string `yaml:"log_level"` + LogFileName string `yaml:"log_file_name"` + LogMaxSize int `yaml:"log_max_size"` + LogMaxBackups int `yaml:"log_max_backups"` + LogMaxAge int `yaml:"log_max_age"` + } + + // GetConf gets configuration instance + func GetConf() *Config { + once.Do(initConf) + return conf + } + + func initConf() { + prefix := "conf" + confFileRelPath := filepath.Join(prefix, filepath.Join(GetEnv(), "conf.yaml")) + content, err := ioutil.ReadFile(confFileRelPath) + if err != nil { + panic(err) + } + + conf = new(Config) + err = yaml.Unmarshal(content, conf) + if err != nil { + hlog.Error("parse yaml error - %v", err) + panic(err) + } + if err := validator.Validate(conf); err != nil { + hlog.Error("validate config error - %v", err) + panic(err) + } + + conf.Env = GetEnv() + + pretty.Printf("%+v\n", conf) + } + + func GetEnv() string { + e := os.Getenv("GO_ENV") + if len(e) == 0 { + return "test" + } + return e + } + + func LogLevel() hlog.Level { + level := GetConf().Hertz.LogLevel + switch level { + case "trace": + return hlog.LevelTrace + case "debug": + return hlog.LevelDebug + case "info": + return hlog.LevelInfo + case "notice": + return hlog.LevelNotice + case "warn": + return hlog.LevelWarn + case "error": + return hlog.LevelError + case "fatal": + return hlog.LevelFatal + default: + return hlog.LevelInfo + } + } + + + - path: conf/dev/conf.yaml + delims: + - "" + - "" + body: |- + hertz: + address: ":8080" + enable_pprof: true + enable_gzip: true + enable_access_log: true + log_level: info + log_file_name: "log/hertz.log" + log_max_size: 10 + log_max_age: 3 + log_max_backups: 50 + + mysql: + dsn: "gorm:gorm@tcp(127.0.0.1:3306)/gorm?charset=utf8mb4&parseTime=True&loc=Local" + + redis: + address: "127.0.0.1:6379" + username: "" + password: "" + db: 0 + + - path: conf/online/conf.yaml + delims: + - "" + - "" + body: |- + hertz: + address: ":8080" + enable_pprof: false + enable_gzip: true + enable_access_log: true + log_level: info + log_file_name: "log/hertz.log" + log_max_size: 10 + log_max_age: 3 + log_max_backups: 50 + + mysql: + dsn: "gorm:gorm@tcp(127.0.0.1:3306)/gorm?charset=utf8mb4&parseTime=True&loc=Local" + + redis: + address: "127.0.0.1:6379" + username: "" + password: "" + db: 0 + + - path: conf/test/conf.yaml + delims: + - "" + - "" + body: |- + hertz: + address: ":8080" + enable_pprof: true + enable_gzip: true + enable_access_log: true + log_level: info + log_file_name: "log/hertz.log" + log_max_size: 10 + log_max_age: 3 + log_max_backups: 50 + + mysql: + dsn: "gorm:gorm@tcp(127.0.0.1:3306)/gorm?charset=utf8mb4&parseTime=True&loc=Local" + + redis: + address: "127.0.0.1:6379" + username: "" + password: "" + db: 0 + + + - path: biz/dal/init.go + delims: + - "" + - "" + body: |- + package dal + + import ( + "{{.GoModule}}/biz/dal/mysql" + "{{.GoModule}}/biz/dal/redis" + ) + + func Init() { + redis.Init() + mysql.Init() + } + + - path: biz/dal/mysql/init.go + delims: + - "" + - "" + body: |- + package mysql + + import ( + "{{.GoModule}}/conf" + "gorm.io/driver/mysql" + "gorm.io/gorm" + ) + + var ( + DB *gorm.DB + err error + ) + + func Init() { + DB, err = gorm.Open(mysql.Open(conf.GetConf().MySQL.DSN), + &gorm.Config{ + PrepareStmt: true, + SkipDefaultTransaction: true, + }, + ) + if err != nil { + panic(err) + } + } + + - path: biz/dal/redis/init.go + delims: + - "" + - "" + body: |- + package redis + + import ( + "context" + + "github.com/redis/go-redis/v9" + "{{.GoModule}}/conf" + ) + + var RedisClient *redis.Client + + func Init() { + RedisClient = redis.NewClient(&redis.Options{ + Addr: conf.GetConf().Redis.Address, + Username: conf.GetConf().Redis.Username, + Password: conf.GetConf().Redis.Password, + DB: conf.GetConf().Redis.DB, + }) + if err := RedisClient.Ping(context.Background()).Err(); err != nil { + panic(err) + } + } + + - path: docker-compose.yaml + delims: + - "" + - "" + body: |- + version: '3' + services: + mysql: + image: 'mysql:latest' + ports: + - 3306:3306 + environment: + - MYSQL_DATABASE=gorm + - MYSQL_USER=gorm + - MYSQL_PASSWORD=gorm + - MYSQL_RANDOM_ROOT_PASSWORD="yes" + redis: + image: 'redis:latest' + ports: + - 6379:6379 + + - path: readme.md + delims: + - "" + - "" + body: |- + # *** Project + + ## introduce + + - Use the [Hertz](https://github.com/cloudwego/hertz/) framework + - Integration of pprof, cors, recovery, access_log, gzip and other extensions of Hertz. + - Generating the base code for unit tests. + - Provides basic profile functions. + - Provides the most basic MVC code hierarchy. + + ## Directory structure + + | catalog | introduce | + | ---- | ---- | + | conf | Configuration files | + | main.go | Startup file | + | hertz_gen | Hertz generated model | + | biz/handler | Used for request processing, validation and return of response. | + | biz/service | The actual business logic. | + | biz/dal | Logic for operating the storage layer | + | biz/route | Routing and middleware registration | + | biz/utils | Wrapped some common methods | + + ## How to run + + ```shell + sh build.sh + sh output/bootstrap.sh + ``` + + - path: .gitignore + delims: + - "" + - "" + body: |- + *.o + *.a + *.so + _obj + _test + *.[568vq] + [568vq].out + *.cgo1.go + *.cgo2.c + _cgo_defun.c + _cgo_gotypes.go + _cgo_export.* + _testmain.go + *.exe + *.exe~ + *.test + *.prof + *.rar + *.zip + *.gz + *.psd + *.bmd + *.cfg + *.pptx + *.log + *nohup.out + *settings.pyc + *.sublime-project + *.sublime-workspace + !.gitkeep + .DS_Store + /.idea + /.vscode + /output + *.local.yml + + - path: biz/utils/resp.go + delims: + - "{{" + - "}}" + body: |- + package utils + + import ( + "context" + + "github.com/cloudwego/hertz/pkg/app" + ) + + type ErrResponse struct { + Success bool + Code int + ErrorMsg string + } + + // SendErrResponse pack error response + func SendErrResponse(ctx context.Context, c *app.RequestContext, code int, err error) { + // todo edit custom code + c.JSON(code, ErrResponse{Success: false, Code: code, ErrorMsg: err.Error()}) + } + + // SendSuccessResponse pack success response + func SendSuccessResponse(ctx context.Context, c *app.RequestContext, code int, data interface{}) { + // todo edit custom code + c.JSON(code, data) + } + + + - path: build.sh + delims: + - "{{" + - "}}" + body: |- + #!/bin/bash + RUN_NAME={{.ServiceName}} + mkdir -p output/bin output/conf + cp script/bootstrap.sh output 2>/dev/null + chmod +x output/bootstrap.sh + cp -r conf/* output/conf + go build -o output/bin/${RUN_NAME} + + - path: script/bootstrap.sh + delims: + - "{{" + - "}}" + body: |- + #!/bin/bash + CURDIR=$(cd $(dirname $0); pwd) + BinaryName={{.ServiceName}} + echo "$CURDIR/bin/${BinaryName}" + exec $CURDIR/bin/${BinaryName} \ No newline at end of file diff --git a/tpl/hertz/server/standard_v2/package.yaml b/tpl/hertz/server/standard_v2/package.yaml new file mode 100644 index 00000000..052439d5 --- /dev/null +++ b/tpl/hertz/server/standard_v2/package.yaml @@ -0,0 +1,167 @@ +layouts: + - path: handler.go + body: |- + {{$OutDirs := GetUniqueHandlerOutDir .Methods}} + {{$PackageName := .PackageName}} + 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 "" -}} + service "{{$.ProjPackage}}/biz/service/{{$PackageName}}" + {{- 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.StatusBadRequest, 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.StatusInternalServerError, err) + return + } + {{else}} + resp,err := {{$MethodInfo.OutputDir}}.New{{$MethodInfo.Name}}Service(ctx, c).Run(&req) + if err != nil { + utils.SendErrResponse(ctx, c, consts.StatusInternalServerError, 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: "biz/service/{{.GenPackage}}/{{.HandlerGenPath}}/{{ToSnakeCase .MethodName}}.go" + loop_method: true + update_behavior: + type: "skip" + body: |- + package {{.FilePackage}} + import ( + "context" + + "github.com/cloudwego/hertz/pkg/app" + {{- range $k, $v := .Models}} + {{$k}} "{{$v.Package}}" + {{- end}} + ) + type {{.Name}}Service struct { + RequestContext *app.RequestContext + Context context.Context + } + + func New{{.Name}}Service(Context context.Context, RequestContext *app.RequestContext) *{{.Name}}Service { + return &{{.Name}}Service{RequestContext: RequestContext, Context: Context} + } + + func (h *{{.Name}}Service) Run(req *{{.RequestTypeName}}) ( resp *{{.ReturnTypeName}}, err error) { + //defer func() { + // hlog.CtxInfof(h.Context, "req = %+v", req) + // hlog.CtxInfof(h.Context, "resp = %+v", resp) + //}() + // todo edit your code + return + } + + - path: "{{.HandlerDir}}/{{.GenPackage}}/{{ToSnakeCase .ServiceName}}_test.go" + loop_service: true + update_behavior: + type: "append" + append_key: "method" + insert_key: "Test{{$.Name}}" + append_content_tpl: |- + func Test{{.Name}}(t *testing.T) { + h := server.Default() + h.{{.HTTPMethod}}("{{.Path}}", {{.Name}}) + path:= "{{.Path}}" // todo: you can customize query + body:= &ut.Body{Body: bytes.NewBufferString(""), Len: 1} // todo: you can customize body + header:= ut.Header{} // todo: you can customize header + w := ut.PerformRequest(h.Engine, "{{.HTTPMethod}}", path, body,header) + resp := w.Result() + t.Log(string(resp.Body())) + + // todo edit your unit test. + // assert.DeepEqual(t, 200, resp.StatusCode()) + // assert.DeepEqual(t, "null", string(resp.Body())) + } + 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}}) + path:= "{{$MethodInfo.Path}}" // todo: you can customize query + body:= &ut.Body{Body: bytes.NewBufferString(""), Len: 1} // todo: you can customize body + header:= ut.Header{} // todo: you can customize header + w := ut.PerformRequest(h.Engine, "{{$MethodInfo.HTTPMethod}}", path, body,header) + resp := w.Result() + t.Log(string(resp.Body())) + + // todo edit your unit test. + // assert.DeepEqual(t, 200, resp.StatusCode()) + // assert.DeepEqual(t, "null", string(resp.Body())) + } + {{end}} \ No newline at end of file