├── .gitignore ├── cmd ├── version.go ├── remove.go ├── new.go └── generate.go ├── Makefile ├── tpls ├── react │ ├── services.typings.d.ts.tpl │ ├── services.index.ts.tpl │ ├── pages.components.SaveForm.tsx.tpl │ └── pages.index.tsx.tpl ├── ant-design-pro-v5 │ ├── services.typings.d.ts.tpl │ ├── locales.en.page.ts.tpl │ ├── locales.zh.page.ts.tpl │ ├── services.index.ts.tpl │ ├── pages.components.form.tsx.tpl │ └── pages.index.tsx.tpl └── default │ ├── schema.go.tpl │ ├── api.go.tpl │ ├── dal.go.tpl │ └── biz.go.tpl ├── internal ├── parser │ ├── type.go │ ├── parser_test.go │ ├── tpl.go │ ├── file.go │ ├── struct.go │ ├── parser.go │ └── visitor.go ├── tfs │ ├── embed.go │ └── fs.go ├── schema │ ├── struct_test.go │ └── struct.go ├── utils │ ├── tpl.go │ ├── io.go │ ├── inflections.go │ └── command.go └── actions │ ├── remove.go │ ├── new.go │ └── generate.go ├── go.mod ├── LICENSE ├── main.go ├── examples ├── role.yaml ├── some_table.yaml ├── user.yaml ├── menu.yaml └── parameter.yaml ├── go.sum └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | *.DS_Store 8 | 9 | # Test binary, build with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | /gin-admin-cli 15 | .idea 16 | .vscode 17 | /testdata 18 | /tmp 19 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/urfave/cli/v2" 7 | ) 8 | 9 | func Version(v string) *cli.Command { 10 | return &cli.Command{ 11 | Name: "version", 12 | Usage: "Show the version of the program", 13 | Action: func(c *cli.Context) error { 14 | fmt.Println(v) 15 | return nil 16 | }, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | 3 | RELEASE_VERSION = v10.4.0 4 | 5 | APP = gin-admin-cli 6 | BIN = ${APP} 7 | GIT_COUNT = $(shell git rev-list --all --count) 8 | GIT_HASH = $(shell git rev-parse --short HEAD) 9 | RELEASE_TAG = $(RELEASE_VERSION).$(GIT_COUNT).$(GIT_HASH) 10 | 11 | build: 12 | @go build -ldflags "-w -s -X main.VERSION=$(RELEASE_TAG)" -o $(BIN) 13 | -------------------------------------------------------------------------------- /tpls/react/services.typings.d.ts.tpl: -------------------------------------------------------------------------------- 1 | {{$includeStatus := .Include.Status}} 2 | declare namespace API { 3 | {{with .Comment}}// {{.}}{{end}} 4 | type {{.Name}} = { 5 | {{- range .Fields}}{{$fieldName := .Name}} 6 | {{- with .Comment}} 7 | /** {{.}} */ 8 | {{- end}} 9 | {{lowerUnderline $fieldName}}?: {{convGoTypeToTsType .Type}}; 10 | {{- end}} 11 | {{- if $includeStatus}} 12 | statusChecked?: boolean; 13 | {{- end}} 14 | }; 15 | } -------------------------------------------------------------------------------- /tpls/ant-design-pro-v5/services.typings.d.ts.tpl: -------------------------------------------------------------------------------- 1 | {{$includeStatus := .Include.Status}} 2 | declare namespace API { 3 | {{with .Comment}}// {{.}}{{end}} 4 | type {{.Name}} = { 5 | {{- range .Fields}}{{$fieldName := .Name}} 6 | {{- with .Comment}} 7 | /** {{.}} */ 8 | {{- end}} 9 | {{lowerUnderline $fieldName}}?: {{convGoTypeToTsType .Type}}; 10 | {{- end}} 11 | {{- if $includeStatus}} 12 | statusChecked?: boolean; 13 | {{- end}} 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /internal/parser/type.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | type AstFlag int 4 | 5 | func (f AstFlag) String() string { 6 | switch f { 7 | case AstFlagGen: 8 | return "G" 9 | case AstFlagRem: 10 | return "R" 11 | } 12 | return "?" 13 | } 14 | 15 | const ( 16 | AstFlagGen AstFlag = 1 << iota 17 | AstFlagRem 18 | ) 19 | 20 | type BasicArgs struct { 21 | Dir string 22 | ModuleName string 23 | ModulePath string 24 | StructName string 25 | GenPackages []string 26 | Flag AstFlag 27 | FillRouterPrefix bool 28 | } 29 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gin-admin/gin-admin-cli/v10 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/jinzhu/inflection v1.0.0 7 | github.com/json-iterator/go v1.1.12 8 | github.com/urfave/cli/v2 v2.25.1 9 | go.uber.org/zap v1.24.0 10 | golang.org/x/mod v0.10.0 11 | gopkg.in/yaml.v2 v2.4.0 12 | ) 13 | 14 | require ( 15 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 16 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect 17 | github.com/modern-go/reflect2 v1.0.2 // indirect 18 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 19 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect 20 | go.uber.org/atomic v1.7.0 // indirect 21 | go.uber.org/multierr v1.6.0 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /internal/tfs/embed.go: -------------------------------------------------------------------------------- 1 | package tfs 2 | 3 | import ( 4 | "embed" 5 | "path/filepath" 6 | "runtime" 7 | "strings" 8 | ) 9 | 10 | var efsIns embed.FS 11 | 12 | func SetEFS(fs embed.FS) { 13 | efsIns = fs 14 | } 15 | 16 | func EFS() embed.FS { 17 | return efsIns 18 | } 19 | 20 | type embedFS struct { 21 | } 22 | 23 | func NewEmbedFS() FS { 24 | return &embedFS{} 25 | } 26 | 27 | func (fs *embedFS) ReadFile(name string) ([]byte, error) { 28 | fullname := filepath.Join("tpls", name) 29 | if runtime.GOOS == "windows" { 30 | fullname = strings.ReplaceAll(fullname, "\\", "/") 31 | } 32 | return efsIns.ReadFile(fullname) 33 | } 34 | 35 | func (fs *embedFS) ParseTpl(name string, data interface{}) ([]byte, error) { 36 | tplBytes, err := fs.ReadFile(name) 37 | if err != nil { 38 | return nil, err 39 | } 40 | return parseTplData(string(tplBytes), data) 41 | } 42 | -------------------------------------------------------------------------------- /tpls/ant-design-pro-v5/locales.en.page.ts.tpl: -------------------------------------------------------------------------------- 1 | {{- $name := .Name}} 2 | {{- $lowerCamelName := lowerCamel .Name}} 3 | {{- $parentName := .Extra.ParentName}} 4 | export default { 5 | 'pages.{{with $parentName}}{{.}}.{{end}}{{$lowerCamelName}}.add': 'Add {{$name}}', 6 | 'pages.{{with $parentName}}{{.}}.{{end}}{{$lowerCamelName}}.edit': 'Edit {{$name}}', 7 | 'pages.{{with $parentName}}{{.}}.{{end}}{{$lowerCamelName}}.delTip': 'Are you sure you want to delete this record?', 8 | {{- range .Fields}} 9 | 'pages.{{with $parentName}}{{.}}.{{end}}{{$lowerCamelName}}.form.{{lowerUnderline .Name}}': '{{.Name}}', 10 | 'pages.{{with $parentName}}{{.}}.{{end}}{{$lowerCamelName}}.form.{{lowerUnderline .Name}}.placeholder': 'Please enter the {{lowerSpace .Name}}', 11 | 'pages.{{with $parentName}}{{.}}.{{end}}{{$lowerCamelName}}.form.{{lowerUnderline .Name}}.required': '{{titleSpace .Name}} is required!', 12 | {{- end}} 13 | }; 14 | -------------------------------------------------------------------------------- /tpls/ant-design-pro-v5/locales.zh.page.ts.tpl: -------------------------------------------------------------------------------- 1 | {{- $name := .Name}} 2 | {{- $lowerCamelName := lowerCamel .Name}} 3 | {{- $parentName := .Extra.ParentName}} 4 | export default { 5 | 'pages.{{with $parentName}}{{.}}.{{end}}{{$lowerCamelName}}.add': 'Add {{$name}}', 6 | 'pages.{{with $parentName}}{{.}}.{{end}}{{$lowerCamelName}}.edit': 'Edit {{$name}}', 7 | 'pages.{{with $parentName}}{{.}}.{{end}}{{$lowerCamelName}}.delTip': 'Are you sure you want to delete this record?', 8 | {{- range .Fields}} 9 | 'pages.{{with $parentName}}{{.}}.{{end}}{{$lowerCamelName}}.form.{{lowerUnderline .Name}}': '{{.Name}}', 10 | 'pages.{{with $parentName}}{{.}}.{{end}}{{$lowerCamelName}}.form.{{lowerUnderline .Name}}.placeholder': 'Please enter the {{lowerSpace .Name}}', 11 | 'pages.{{with $parentName}}{{.}}.{{end}}{{$lowerCamelName}}.form.{{lowerUnderline .Name}}.required': '{{titleSpace .Name}} is required!', 12 | {{- end}} 13 | }; 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 LyricTian 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "embed" 5 | "os" 6 | 7 | "github.com/gin-admin/gin-admin-cli/v10/cmd" 8 | "github.com/gin-admin/gin-admin-cli/v10/internal/tfs" 9 | "github.com/urfave/cli/v2" 10 | "go.uber.org/zap" 11 | ) 12 | 13 | //go:embed tpls 14 | var f embed.FS 15 | 16 | var VERSION = "v10.7.0" 17 | 18 | func main() { 19 | defer func() { 20 | _ = zap.S().Sync() 21 | }() 22 | 23 | // Set the embed.FS to the fs package 24 | tfs.SetEFS(f) 25 | 26 | logger, err := zap.NewDevelopmentConfig().Build(zap.WithCaller(false)) 27 | if err != nil { 28 | panic(err) 29 | } 30 | zap.ReplaceGlobals(logger) 31 | 32 | app := cli.NewApp() 33 | app.Name = "gin-admin-cli" 34 | app.Version = VERSION 35 | app.Usage = "A command line tool for [gin-admin](https://github.com/LyricTian/gin-admin)." 36 | app.Authors = append(app.Authors, &cli.Author{ 37 | Name: "LyricTian", 38 | Email: "tiannianshou@gmail.com", 39 | }) 40 | app.Commands = []*cli.Command{ 41 | cmd.Version(VERSION), 42 | cmd.New(), 43 | cmd.Generate(), 44 | cmd.Remove(), 45 | } 46 | 47 | if err := app.Run(os.Args); err != nil { 48 | panic(err) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /internal/parser/parser_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | ) 7 | 8 | const dir = "/home/lyric/go/src/gin-admin" 9 | 10 | func TestModifyModuleMainFile(t *testing.T) { 11 | buf, err := ModifyModuleMainFile(context.Background(), BasicArgs{ 12 | Dir: dir, 13 | ModuleName: "RBAC", 14 | StructName: "Role", 15 | Flag: AstFlagGen, 16 | }) 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | _ = buf 21 | // fmt.Println(string(buf)) 22 | } 23 | 24 | func TestModifyModuleWireFile(t *testing.T) { 25 | buf, err := ModifyModuleWireFile(context.Background(), BasicArgs{ 26 | Dir: dir, 27 | ModuleName: "RBAC", 28 | StructName: "Role", 29 | Flag: AstFlagGen, 30 | GenPackages: []string{"dal", "biz", "api"}, 31 | }) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | 36 | _ = buf 37 | // fmt.Println(string(buf)) 38 | } 39 | 40 | func TestModifyModsFile(t *testing.T) { 41 | buf, err := ModifyModsFile(context.Background(), BasicArgs{ 42 | Dir: dir, 43 | ModuleName: "Configurator", 44 | Flag: AstFlagGen, 45 | }) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | 50 | _ = buf 51 | // fmt.Println(string(buf)) 52 | } 53 | -------------------------------------------------------------------------------- /internal/tfs/fs.go: -------------------------------------------------------------------------------- 1 | package tfs 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "path/filepath" 7 | "text/template" 8 | 9 | "github.com/gin-admin/gin-admin-cli/v10/internal/utils" 10 | ) 11 | 12 | var Ins FS = NewEmbedFS() 13 | 14 | func SetIns(ins FS) { 15 | Ins = ins 16 | } 17 | 18 | type FS interface { 19 | ReadFile(name string) ([]byte, error) 20 | ParseTpl(name string, data interface{}) ([]byte, error) 21 | } 22 | 23 | type osFS struct { 24 | dir string 25 | } 26 | 27 | func NewOSFS(dir string) FS { 28 | return &osFS{dir: dir} 29 | } 30 | 31 | func (fs *osFS) ReadFile(name string) ([]byte, error) { 32 | return os.ReadFile(filepath.Join(fs.dir, name)) 33 | } 34 | 35 | func (fs *osFS) ParseTpl(name string, data interface{}) ([]byte, error) { 36 | tplBytes, err := fs.ReadFile(name) 37 | if err != nil { 38 | return nil, err 39 | } 40 | return parseTplData(string(tplBytes), data) 41 | } 42 | 43 | func parseTplData(text string, data interface{}) ([]byte, error) { 44 | t, err := template.New("").Funcs(utils.FuncMap).Parse(text) 45 | if err != nil { 46 | return nil, err 47 | } 48 | buf := new(bytes.Buffer) 49 | if err := t.Execute(buf, data); err != nil { 50 | return nil, err 51 | } 52 | return buf.Bytes(), nil 53 | } 54 | -------------------------------------------------------------------------------- /internal/parser/tpl.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | var tplModuleMain = ` 4 | package $$LowerModuleName$$ 5 | 6 | import ( 7 | "context" 8 | "$$RootImportPath$$/internal/config" 9 | 10 | "$$ModuleImportPath$$/api" 11 | "$$ModuleImportPath$$/schema" 12 | "github.com/gin-gonic/gin" 13 | "gorm.io/gorm" 14 | ) 15 | 16 | type $$ModuleName$$ struct { 17 | DB *gorm.DB 18 | } 19 | 20 | func (a *$$ModuleName$$) AutoMigrate(ctx context.Context) error { 21 | return a.DB.AutoMigrate() 22 | } 23 | 24 | func (a *$$ModuleName$$) Init(ctx context.Context) error { 25 | if config.C.Storage.DB.AutoMigrate { 26 | if err := a.AutoMigrate(ctx); err != nil { 27 | return err 28 | } 29 | } 30 | return nil 31 | } 32 | 33 | func (a *$$ModuleName$$) RegisterV1Routers(ctx context.Context, v1 *gin.RouterGroup) error { 34 | {{- if .FillRouterPrefix}} 35 | v1 = v1.Group("$$LowerModuleName$$") 36 | {{- end}} 37 | return nil 38 | } 39 | 40 | func (a *$$ModuleName$$) Release(ctx context.Context) error { 41 | return nil 42 | } 43 | ` 44 | 45 | var tplModuleWire = ` 46 | package $$LowerModuleName$$ 47 | 48 | import ( 49 | "$$ModuleImportPath$$/api" 50 | "$$ModuleImportPath$$/biz" 51 | "$$ModuleImportPath$$/dal" 52 | "github.com/google/wire" 53 | ) 54 | 55 | var Set = wire.NewSet( 56 | wire.Struct(new($$ModuleName$$), "*"), 57 | ) 58 | ` 59 | -------------------------------------------------------------------------------- /examples/role.yaml: -------------------------------------------------------------------------------- 1 | - name: Role 2 | comment: Role management for RBAC 3 | fields: 4 | - name: Name 5 | type: string 6 | comment: Display name of role 7 | gorm_tag: "size:128;index" 8 | query: 9 | name: LikeName 10 | in_query: true 11 | form_tag: name 12 | op: LIKE 13 | form: 14 | binding_tag: "required,max=128" 15 | - name: Description 16 | type: string 17 | comment: Details about role 18 | gorm_tag: "size:1024" 19 | form: {} 20 | - name: Sequence 21 | type: int 22 | comment: Sequence for sorting 23 | order: DESC 24 | form: {} 25 | - name: Status 26 | type: string 27 | comment: Status of role (disabled, enabled) 28 | gorm_tag: "size:20;index" 29 | query: 30 | in_query: true 31 | form: 32 | binding_tag: "required,oneof=disabled enabled" 33 | - name: RoleMenu 34 | comment: Role permissions for RBAC 35 | outputs: ["schema", "dal"] 36 | disable_default_fields: true 37 | fields: 38 | - name: ID 39 | type: string 40 | comment: Unique ID 41 | gorm_tag: "size:20;primaryKey" 42 | - name: RoleID 43 | type: string 44 | comment: From Role.ID 45 | gorm_tag: "size:20;index" 46 | query: {} 47 | - name: MenuID 48 | type: string 49 | comment: From Menu.ID 50 | gorm_tag: "size:20;index" 51 | - name: CreatedAt 52 | type: time.Time 53 | comment: Create time 54 | gorm_tag: "index;" 55 | -------------------------------------------------------------------------------- /internal/parser/file.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "bytes" 5 | "text/template" 6 | 7 | "github.com/gin-admin/gin-admin-cli/v10/internal/utils" 8 | ) 9 | 10 | const ( 11 | FileForMods = "mods.go" 12 | FileForModuleMain = "{{lower .ModuleName}}/main.go" 13 | FileForModuleWire = "{{lower .ModuleName}}/wire.go" 14 | FileForModuleAPI = "{{lower .ModuleName}}/api/{{lowerUnderline .StructName}}.api.go" 15 | FileForModuleBiz = "{{lower .ModuleName}}/biz/{{lowerUnderline .StructName}}.biz.go" 16 | FileForModuleDAL = "{{lower .ModuleName}}/dal/{{lowerUnderline .StructName}}.dal.go" 17 | FileForModuleSchema = "{{lower .ModuleName}}/schema/{{lowerUnderline .StructName}}.go" 18 | ) 19 | 20 | func ParseFilePathFromTpl(moduleName, structName string, tpl string) (string, error) { 21 | t := template.Must(template.New("").Funcs(utils.FuncMap).Parse(tpl)) 22 | buf := new(bytes.Buffer) 23 | if err := t.Execute(buf, map[string]interface{}{ 24 | "ModuleName": moduleName, 25 | "StructName": structName, 26 | }); err != nil { 27 | return "", err 28 | } 29 | return buf.String(), nil 30 | } 31 | 32 | func GetModuleMainFilePath(moduleName string) (string, error) { 33 | p, err := ParseFilePathFromTpl(moduleName, "", FileForModuleMain) 34 | if err != nil { 35 | return "", err 36 | } 37 | return p, nil 38 | } 39 | 40 | func GetModuleWireFilePath(moduleName string) (string, error) { 41 | p, err := ParseFilePathFromTpl(moduleName, "", FileForModuleWire) 42 | if err != nil { 43 | return "", err 44 | } 45 | return p, nil 46 | } 47 | -------------------------------------------------------------------------------- /internal/schema/struct_test.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | jsoniter "github.com/json-iterator/go" 8 | "gopkg.in/yaml.v2" 9 | ) 10 | 11 | func TestMarshalS(t *testing.T) { 12 | s := &S{ 13 | Name: "Menu", 14 | Fields: []*Field{ 15 | { 16 | Name: "Name", 17 | Type: "string", 18 | Comment: "Display name of menu", 19 | GormTag: "size:128;index", 20 | Query: &FieldQuery{ 21 | Name: "LikeName", 22 | InQuery: true, 23 | FormTag: "name", 24 | OP: "LIKE", 25 | }, 26 | Form: &FieldForm{ 27 | BindingTag: "required", 28 | }, 29 | }, 30 | { 31 | Name: "Description", 32 | Type: "string", 33 | Comment: "Details about menu", 34 | GormTag: "type:text", 35 | Form: &FieldForm{ 36 | Name: "Desc", 37 | JSONTag: `,omitempty`, 38 | }, 39 | CustomTag: `form:"desc" validate:"max=255"`, 40 | }, 41 | { 42 | Name: "Sequence", 43 | Type: "int", 44 | Comment: "Sequence for sorting", 45 | GormTag: "index;", 46 | Form: &FieldForm{}, 47 | }, 48 | { 49 | Name: "Type", 50 | Type: "string", 51 | Comment: "Type of menu (group/menu/button)", 52 | GormTag: "size:32;index;", 53 | Form: &FieldForm{}, 54 | }, 55 | }, 56 | DisablePagination: true, 57 | } 58 | 59 | buf, err := jsoniter.MarshalIndent([]*S{s}, "", "") 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | fmt.Println("\n" + string(buf) + "\n") 64 | 65 | fmt.Println("=====================================") 66 | 67 | ybuf, err := yaml.Marshal([]*S{s}) 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | fmt.Println("\n" + string(ybuf) + "\n") 72 | } 73 | -------------------------------------------------------------------------------- /internal/utils/tpl.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "html/template" 5 | "strings" 6 | ) 7 | 8 | // FuncMap is a map of functions that can be used in templates. 9 | var FuncMap = template.FuncMap{ 10 | "lower": strings.ToLower, 11 | "upper": strings.ToUpper, 12 | "title": strings.ToTitle, 13 | "lowerUnderline": ToLowerUnderlinedNamer, 14 | "plural": ToPlural, 15 | "lowerPlural": ToLowerPlural, 16 | "lowerSpacePlural": ToLowerSpacePlural, 17 | "lowerHyphensPlural": ToLowerHyphensPlural, 18 | "lowerCamel": ToLowerCamel, 19 | "lowerSpace": ToLowerSpacedNamer, 20 | "titleSpace": ToTitleSpaceNamer, 21 | "convIfCond": tplConvToIfCond, 22 | "convSwaggerType": tplConvToSwaggerType, 23 | "raw": func(s string) template.HTML { return template.HTML(s) }, 24 | "convGoTypeToTsType": func(goType string) string { 25 | if strings.Contains(goType, "int") || strings.Contains(goType, "float") { 26 | return "number" 27 | } else if goType == "bool" { 28 | return "boolean" 29 | } 30 | return "string" 31 | }, 32 | } 33 | 34 | func tplConvToIfCond(t string) template.HTML { 35 | cond := `v != nil` 36 | if strings.HasPrefix(t, "*") { 37 | cond = `v != nil` 38 | } else if t == "string" { 39 | cond = `len(v) > 0` 40 | } else if strings.Contains(t, "int") { 41 | cond = `v != 0` 42 | } else if strings.Contains(t, "float") { 43 | cond = `v != 0` 44 | } else if t == "time.Time" { 45 | cond = `!v.IsZero()` 46 | } 47 | return template.HTML(cond) 48 | } 49 | 50 | func tplConvToSwaggerType(t string) string { 51 | if strings.Contains(t, "int") || strings.Contains(t, "float") { 52 | return "number" 53 | } 54 | return "string" 55 | } 56 | -------------------------------------------------------------------------------- /examples/some_table.yaml: -------------------------------------------------------------------------------- 1 | # use command like [gin-admin-cli gen -d . -m SOME_MODULE --structs SomeTable --fe-dir ./frontend -c ./configs/schema/some_table.yaml] 2 | - name: SomeTable 3 | module: SomeModule 4 | comment: example for generate api code and react component 5 | disable_pagination: false 6 | fill_gorm_commit: true 7 | fill_router_prefix: true # ex) GET /api/some_module/some_tables/:id 8 | force_write: true # overwrite even if exist current file 9 | generate_fe: true # generate react component by fe_mapping 10 | disable_default_fields: false # id, created_at, updated_at columns 11 | tpl_type: crud # crud/tree 12 | fe_tpl: react 13 | fe_mapping: # tpl -> file 14 | services.typings.d.ts.tpl: src/services/some_module/some_table_typing.d.ts 15 | services.index.ts.tpl: src/services/some_module/some_table_service.ts 16 | pages.index.tsx.tpl: src/pages/some_module/some_table/index.tsx 17 | pages.components.SaveForm.tsx.tpl: src/pages/some_module/some_table/components/SaveForm.tsx 18 | extra: # need to generate code 19 | ImportService: some_module/some_table 20 | ActionText: Option 21 | AddTitle: Add 22 | EditTitle: Edit 23 | DelTip: Delete 24 | SubmitText: Submit 25 | ResetText: Reset 26 | SaveSuccessMessage: Saved. 27 | DeleteSuccessMessage: Deleted. 28 | fields: 29 | - name: Order 30 | type: int 31 | unique: true 32 | comment: test text 33 | gorm_tag: "type:int unsigned;" 34 | form: # used in save, edit form 35 | binding_tag: "required" 36 | required: true 37 | extra: # used in generate code 38 | Label: TestText 39 | Required: true 40 | - name: Description 41 | type: string 42 | comment: comment for admin 43 | gorm_tag: "size:128;" 44 | form: 45 | binding_tag: "max=128" 46 | extra: 47 | Label: Description 48 | -------------------------------------------------------------------------------- /internal/utils/io.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "io" 7 | "io/fs" 8 | "os" 9 | 10 | jsoniter "github.com/json-iterator/go" 11 | "gopkg.in/yaml.v2" 12 | ) 13 | 14 | // Scanner scans the given reader line by line and calls the given function 15 | func Scanner(r io.Reader, fn func(string) string) *bytes.Buffer { 16 | buf := new(bytes.Buffer) 17 | scanner := bufio.NewScanner(r) 18 | for scanner.Scan() { 19 | line := scanner.Text() 20 | buf.WriteString(fn(line)) 21 | buf.WriteString("\n") 22 | } 23 | return buf 24 | } 25 | 26 | // ExistsFile checks if the given file exists 27 | func ExistsFile(name string) (bool, error) { 28 | if _, err := os.Stat(name); os.IsNotExist(err) { 29 | return false, nil 30 | } else if err != nil { 31 | return false, err 32 | } 33 | return true, nil 34 | } 35 | 36 | // WriteFile writes the given data to the given file 37 | func WriteFile(name string, data []byte) error { 38 | return os.WriteFile(name, data, 0644) 39 | } 40 | 41 | // Parses the given JSON file 42 | func ParseJSONFile(name string, obj interface{}) error { 43 | f, err := os.Open(name) 44 | if err != nil { 45 | return err 46 | } 47 | defer f.Close() 48 | 49 | return jsoniter.NewDecoder(f).Decode(obj) 50 | } 51 | 52 | // Parses the given YAML file 53 | func ParseYAMLFile(name string, obj interface{}) error { 54 | f, err := os.Open(name) 55 | if err != nil { 56 | return err 57 | } 58 | defer f.Close() 59 | 60 | return yaml.NewDecoder(f).Decode(obj) 61 | } 62 | 63 | // Checks if the given path is a directory 64 | func IsDir(name string) bool { 65 | info, err := os.Stat(name) 66 | if err != nil { 67 | return false 68 | } 69 | return info.IsDir() 70 | } 71 | 72 | func ReplaceFileContent(file string, old, new []byte, m fs.FileMode) error { 73 | f, err := os.ReadFile(file) 74 | if err != nil { 75 | return err 76 | } 77 | return os.WriteFile(file, bytes.Replace(f, old, new, -1), m) 78 | } 79 | -------------------------------------------------------------------------------- /tpls/react/services.index.ts.tpl: -------------------------------------------------------------------------------- 1 | {{- $name := .Name}} 2 | {{- $lowerPluralName := lowerHyphensPlural .Name}} 3 | // @ts-ignore 4 | /* eslint-disable */ 5 | import { request } from 'umi'; 6 | 7 | /** Query list GET /api/v1/{{$lowerPluralName}} */ 8 | export async function fetch{{$name}}(params: API.PaginationParam, options?: { [key: string]: any }) { 9 | return request>('/api/v1/{{$lowerPluralName}}', { 10 | method: 'GET', 11 | params: { 12 | current: '1', 13 | pageSize: '10', 14 | ...params, 15 | }, 16 | ...(options || {}), 17 | }); 18 | } 19 | 20 | /** Create record POST /api/v1/{{$lowerPluralName}} */ 21 | export async function add{{$name}}(body: API.{{$name}}, options?: { [key: string]: any }) { 22 | return request>('/api/v1/{{$lowerPluralName}}', { 23 | method: 'POST', 24 | data: body, 25 | ...(options || {}), 26 | }); 27 | } 28 | 29 | /** Get record by ID GET /api/v1/{{$lowerPluralName}}/${id} */ 30 | export async function get{{$name}}(id: string, options?: { [key: string]: any }) { 31 | return request>(`/api/v1/{{$lowerPluralName}}/${id}`, { 32 | method: 'GET', 33 | ...(options || {}), 34 | }); 35 | } 36 | 37 | /** Update record by ID PUT /api/v1/{{$lowerPluralName}}/${id} */ 38 | export async function update{{$name}}(id: string, body: API.{{$name}}, options?: { [key: string]: any }) { 39 | return request>(`/api/v1/{{$lowerPluralName}}/${id}`, { 40 | method: 'PUT', 41 | data: body, 42 | ...(options || {}), 43 | }); 44 | } 45 | 46 | /** Delete record by ID DELETE /api/v1/{{$lowerPluralName}}/${id} */ 47 | export async function del{{$name}}(id: string, options?: { [key: string]: any }) { 48 | return request>(`/api/v1/{{$lowerPluralName}}/${id}`, { 49 | method: 'DELETE', 50 | ...(options || {}), 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /tpls/ant-design-pro-v5/services.index.ts.tpl: -------------------------------------------------------------------------------- 1 | {{- $name := .Name}} 2 | {{- $lowerPluralName := lowerHyphensPlural .Name}} 3 | // @ts-ignore 4 | /* eslint-disable */ 5 | import { request } from 'umi'; 6 | 7 | /** Query list GET /api/v1/{{$lowerPluralName}} */ 8 | export async function fetch{{$name}}(params: API.PaginationParam, options?: { [key: string]: any }) { 9 | return request>('/api/v1/{{$lowerPluralName}}', { 10 | method: 'GET', 11 | params: { 12 | current: '1', 13 | pageSize: '10', 14 | ...params, 15 | }, 16 | ...(options || {}), 17 | }); 18 | } 19 | 20 | /** Create record POST /api/v1/{{$lowerPluralName}} */ 21 | export async function add{{$name}}(body: API.{{$name}}, options?: { [key: string]: any }) { 22 | return request>('/api/v1/{{$lowerPluralName}}', { 23 | method: 'POST', 24 | data: body, 25 | ...(options || {}), 26 | }); 27 | } 28 | 29 | /** Get record by ID GET /api/v1/{{$lowerPluralName}}/${id} */ 30 | export async function get{{$name}}(id: string, options?: { [key: string]: any }) { 31 | return request>(`/api/v1/{{$lowerPluralName}}/${id}`, { 32 | method: 'GET', 33 | ...(options || {}), 34 | }); 35 | } 36 | 37 | /** Update record by ID PUT /api/v1/{{$lowerPluralName}}/${id} */ 38 | export async function update{{$name}}(id: string, body: API.{{$name}}, options?: { [key: string]: any }) { 39 | return request>(`/api/v1/{{$lowerPluralName}}/${id}`, { 40 | method: 'PUT', 41 | data: body, 42 | ...(options || {}), 43 | }); 44 | } 45 | 46 | /** Delete record by ID DELETE /api/v1/{{$lowerPluralName}}/${id} */ 47 | export async function del{{$name}}(id: string, options?: { [key: string]: any }) { 48 | return request>(`/api/v1/{{$lowerPluralName}}/${id}`, { 49 | method: 'DELETE', 50 | ...(options || {}), 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /internal/parser/struct.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | 8 | "github.com/gin-admin/gin-admin-cli/v10/internal/utils" 9 | "golang.org/x/mod/modfile" 10 | ) 11 | 12 | const ( 13 | StructNamingAPI = "API" 14 | StructNamingBIZ = "BIZ" 15 | StructNamingDAL = "DAL" 16 | ) 17 | 18 | const ( 19 | StructPackageAPI = "api" 20 | StructPackageBIZ = "biz" 21 | StructPackageDAL = "dal" 22 | StructPackageSchema = "schema" 23 | ) 24 | 25 | var StructPackages = []string{ 26 | StructPackageSchema, 27 | StructPackageDAL, 28 | StructPackageBIZ, 29 | StructPackageAPI, 30 | } 31 | 32 | var StructPackageTplPaths = map[string]string{ 33 | StructPackageAPI: FileForModuleAPI, 34 | StructPackageBIZ: FileForModuleBiz, 35 | StructPackageDAL: FileForModuleDAL, 36 | StructPackageSchema: FileForModuleSchema, 37 | } 38 | 39 | func GetStructAPIName(structName string) string { 40 | return structName + StructNamingAPI 41 | } 42 | 43 | func GetStructBIZName(structName string) string { 44 | return structName + StructNamingBIZ 45 | } 46 | 47 | func GetStructDALName(structName string) string { 48 | return structName + StructNamingDAL 49 | } 50 | 51 | func GetStructRouterVarName(structName string) string { 52 | return utils.ToLowerCamel(structName) 53 | } 54 | 55 | func GetStructRouterGroupName(structName string) string { 56 | return utils.ToLowerHyphensPlural(structName) 57 | } 58 | 59 | func GetModuleImportName(moduleName string) string { 60 | return strings.ToLower(moduleName) 61 | } 62 | 63 | func GetRootImportPath(dir string) string { 64 | modBytes, err := os.ReadFile(filepath.Join(dir, "go.mod")) 65 | if err != nil { 66 | return "" 67 | } 68 | return modfile.ModulePath(modBytes) 69 | } 70 | 71 | func GetModuleImportPath(dir, modulePath, moduleName string) string { 72 | return GetRootImportPath(dir) + "/" + modulePath + "/" + GetModuleImportName(moduleName) 73 | } 74 | 75 | func GetUtilImportPath(dir, modulePath string) string { 76 | return GetRootImportPath(dir) + "/pkg/util" 77 | } 78 | -------------------------------------------------------------------------------- /examples/user.yaml: -------------------------------------------------------------------------------- 1 | - name: User 2 | comment: User management for RBAC 3 | fields: 4 | - name: Username 5 | type: string 6 | comment: Username for login 7 | gorm_tag: "size:64;index" 8 | query: 9 | name: LikeUsername 10 | in_query: true 11 | form_tag: username 12 | op: LIKE 13 | form: 14 | binding_tag: "required,max=64" 15 | - name: Name 16 | type: string 17 | comment: Name of user 18 | gorm_tag: "size:64;index" 19 | query: 20 | name: LikeName 21 | in_query: true 22 | form_tag: name 23 | op: LIKE 24 | form: 25 | binding_tag: "required,max=64" 26 | - name: Password 27 | type: string 28 | comment: Password for login (encrypted) 29 | gorm_tag: "size:64;" 30 | form: {} 31 | - name: Phone 32 | type: string 33 | comment: Phone number of user 34 | gorm_tag: "size:32;" 35 | form: {} 36 | - name: Email 37 | type: string 38 | comment: Email of user 39 | gorm_tag: "size:128;" 40 | form: {} 41 | - name: Remark 42 | type: string 43 | comment: Remark of user 44 | gorm_tag: "size:1024;" 45 | form: {} 46 | - name: Status 47 | type: string 48 | comment: Status of user (activated, freezed) 49 | gorm_tag: "size:20;index" 50 | query: 51 | in_query: true 52 | form: 53 | binding_tag: "required,oneof=activated freezed" 54 | - name: UserRole 55 | comment: User roles for RBAC 56 | outputs: ["schema", "dal"] 57 | disable_default_fields: true 58 | fields: 59 | - name: ID 60 | type: string 61 | comment: Unique ID 62 | gorm_tag: "size:20;primaryKey" 63 | - name: UserID 64 | type: string 65 | comment: From User.ID 66 | gorm_tag: "size:20;index" 67 | query: {} 68 | - name: RoleID 69 | type: string 70 | comment: From Role.ID 71 | gorm_tag: "size:20;index" 72 | query: {} 73 | - name: CreatedAt 74 | type: time.Time 75 | comment: Create time 76 | gorm_tag: "index;" 77 | -------------------------------------------------------------------------------- /cmd/remove.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/gin-admin/gin-admin-cli/v10/internal/actions" 7 | "github.com/urfave/cli/v2" 8 | ) 9 | 10 | // Remove returns the remove command. 11 | func Remove() *cli.Command { 12 | return &cli.Command{ 13 | Name: "remove", 14 | Aliases: []string{"rm"}, 15 | Usage: "Remove structs from the module", 16 | Flags: []cli.Flag{ 17 | &cli.StringFlag{ 18 | Name: "dir", 19 | Aliases: []string{"d"}, 20 | Usage: "The directory to remove the struct from", 21 | Required: true, 22 | }, 23 | &cli.StringFlag{ 24 | Name: "module", 25 | Aliases: []string{"m"}, 26 | Usage: "The module to remove the struct from (like: RBAC)", 27 | Required: true, 28 | }, 29 | &cli.StringFlag{ 30 | Name: "module-path", 31 | Usage: "The module path to remove the struct from (default: internal/mods)", 32 | Value: "internal/mods", 33 | }, 34 | &cli.StringFlag{ 35 | Name: "structs", 36 | Aliases: []string{"s"}, 37 | Usage: "The struct to remove (multiple structs can be separated by a comma)", 38 | }, 39 | &cli.StringFlag{ 40 | Name: "config", 41 | Aliases: []string{"c"}, 42 | Usage: "The config file to generate the struct from (JSON/YAML)", 43 | }, 44 | &cli.StringFlag{ 45 | Name: "wire-path", 46 | Usage: "The wire generate path to remove the struct from (default: internal/library/wirex)", 47 | Value: "internal/wirex", 48 | }, 49 | &cli.StringFlag{ 50 | Name: "swag-path", 51 | Usage: "The swagger generate path to remove the struct from (default: internal/swagger)", 52 | Value: "internal/swagger", 53 | }, 54 | }, 55 | Action: func(c *cli.Context) error { 56 | rm := actions.Remove(actions.RemoveConfig{ 57 | Dir: c.String("dir"), 58 | ModuleName: c.String("module"), 59 | ModulePath: c.String("module-path"), 60 | WirePath: c.String("wire-path"), 61 | SwaggerPath: c.String("swag-path"), 62 | }) 63 | 64 | if c.String("config") != "" { 65 | return rm.RunWithConfig(c.Context, c.String("config")) 66 | } else if c.String("structs") != "" { 67 | return rm.Run(c.Context, strings.Split(c.String("structs"), ",")) 68 | } 69 | return nil 70 | }, 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /examples/menu.yaml: -------------------------------------------------------------------------------- 1 | - name: Menu 2 | comment: Menu management for RBAC 3 | disable_pagination: true 4 | tpl_type: "tree" 5 | fields: 6 | - name: Code 7 | type: string 8 | comment: Code of menu 9 | gorm_tag: "size:32;" 10 | form: 11 | binding_tag: "required,max=32" 12 | - name: Name 13 | type: string 14 | comment: Display name of menu 15 | gorm_tag: "size:128;index" 16 | query: 17 | name: LikeName 18 | in_query: true 19 | form_tag: name 20 | op: LIKE 21 | form: 22 | binding_tag: "required,max=128" 23 | - name: Description 24 | type: string 25 | comment: Details about menu 26 | gorm_tag: "size:1024" 27 | form: {} 28 | - name: Sequence 29 | type: int 30 | comment: Sequence for sorting 31 | order: DESC 32 | form: {} 33 | - name: Type 34 | type: string 35 | comment: Type of menu (group, page, button) 36 | gorm_tag: "size:20;index" 37 | form: 38 | binding_tag: "required,oneof=group menu button" 39 | - name: Path 40 | type: string 41 | comment: Access path of menu 42 | gorm_tag: "size:255;" 43 | form: {} 44 | - name: Properties 45 | type: string 46 | comment: Properties of menu (JSON) 47 | gorm_tag: "type:text;" 48 | form: {} 49 | - name: Status 50 | type: string 51 | comment: Status of menu (disabled, enabled) 52 | gorm_tag: "size:20;index" 53 | query: {} 54 | form: 55 | binding_tag: "required,oneof=disabled enabled" 56 | - name: MenuResource 57 | comment: Menu resource management for RBAC 58 | outputs: ["schema", "dal"] 59 | disable_default_fields: true 60 | fields: 61 | - name: ID 62 | type: string 63 | comment: Unique ID 64 | gorm_tag: "size:20;primaryKey" 65 | - name: MenuID 66 | type: string 67 | comment: From Menu.ID 68 | gorm_tag: "size:20;index" 69 | query: {} 70 | - name: Method 71 | type: string 72 | comment: HTTP method 73 | gorm_tag: "size:20;" 74 | - name: Path 75 | type: string 76 | comment: API request path 77 | gorm_tag: "size:255;" 78 | - name: CreatedAt 79 | type: time.Time 80 | comment: Create time 81 | gorm_tag: "index;" 82 | -------------------------------------------------------------------------------- /cmd/new.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gin-admin/gin-admin-cli/v10/internal/actions" 7 | "github.com/urfave/cli/v2" 8 | ) 9 | 10 | // New returns the new project command. 11 | func New() *cli.Command { 12 | return &cli.Command{ 13 | Name: "new", 14 | Usage: "Create a new project", 15 | Flags: []cli.Flag{ 16 | &cli.StringFlag{ 17 | Name: "dir", 18 | Aliases: []string{"d"}, 19 | Usage: "The directory to generate the project (default: current directory)", 20 | Required: true, 21 | }, 22 | &cli.StringFlag{ 23 | Name: "name", 24 | Usage: "The project name", 25 | Required: true, 26 | }, 27 | &cli.StringFlag{ 28 | Name: "app-name", 29 | Usage: "The application name (default: project name)", 30 | }, 31 | &cli.StringFlag{ 32 | Name: "desc", 33 | Usage: "The project description", 34 | }, 35 | &cli.StringFlag{ 36 | Name: "version", 37 | Usage: "The project version (default: 1.0.0)", 38 | }, 39 | &cli.StringFlag{ 40 | Name: "pkg", 41 | Usage: "The project package name (default: project name)", 42 | }, 43 | &cli.StringFlag{ 44 | Name: "git-url", 45 | Usage: "Use git repository to initialize the project (default: https://github.com/LyricTian/gin-admin.git)", 46 | Value: "https://github.com/LyricTian/gin-admin.git", 47 | }, 48 | &cli.StringFlag{ 49 | Name: "git-branch", 50 | Usage: "Use git branch to initialize the project (default: main)", 51 | Value: "main", 52 | }, 53 | &cli.StringFlag{ 54 | Name: "fe-dir", 55 | Usage: "The frontend directory to generate the project (if empty, the frontend project will not be generated)", 56 | }, 57 | &cli.StringFlag{ 58 | Name: "fe-name", 59 | Usage: "The frontend project name (default: frontend)", 60 | Value: "frontend", 61 | }, 62 | &cli.StringFlag{ 63 | Name: "fe-git-url", 64 | Usage: "Use git repository to initialize the frontend project (default: https://github.com/gin-admin/gin-admin-frontend.git)", 65 | Value: "https://github.com/gin-admin/gin-admin-frontend.git", 66 | }, 67 | &cli.StringFlag{ 68 | Name: "fe-git-branch", 69 | Usage: "Use git branch to initialize the frontend project (default: main)", 70 | Value: "main", 71 | }, 72 | }, 73 | Action: func(c *cli.Context) error { 74 | n := actions.New(actions.NewConfig{ 75 | Dir: c.String("dir"), 76 | Name: c.String("name"), 77 | AppName: c.String("app-name"), 78 | Description: c.String("desc"), 79 | PkgName: c.String("pkg"), 80 | Version: c.String("version"), 81 | GitURL: c.String("git-url"), 82 | GitBranch: c.String("git-branch"), 83 | FeDir: c.String("fe-dir"), 84 | FeName: c.String("fe-name"), 85 | FeGitURL: c.String("fe-git-url"), 86 | FeGitBranch: c.String("fe-git-branch"), 87 | }) 88 | return n.Run(context.Background()) 89 | }, 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tpls/ant-design-pro-v5/pages.components.form.tsx.tpl: -------------------------------------------------------------------------------- 1 | {{- $name := .Name}} 2 | {{- $lowerCamelName := lowerCamel .Name}} 3 | {{- $parentName := .Extra.ParentName}} 4 | {{- $includeStatus := .Include.Status}} 5 | import React, { useEffect, useRef } from 'react'; 6 | import { useIntl } from '@umijs/max'; 7 | import { message{{if .Extra.FormAntdImport}}, {{.Extra.FormAntdImport}}{{end}} } from 'antd'; 8 | import { ModalForm{{if .Extra.FormProComponentsImport}}, {{.Extra.FormProComponentsImport}}{{end}} } from '@ant-design/pro-components'; 9 | import type { ProFormInstance } from '@ant-design/pro-components'; 10 | import { add{{$name}}, get{{$name}}, update{{$name}} } from '@/services/{{with $parentName}}{{.}}/{{end}}{{$lowerCamelName}}'; 11 | 12 | type {{$name}}ModalProps = { 13 | onSuccess: () => void; 14 | onCancel: () => void; 15 | visible: boolean; 16 | title: string; 17 | id?: string; 18 | }; 19 | 20 | const {{$name}}Modal: React.FC<{{$name}}ModalProps> = (props: {{$name}}ModalProps) => { 21 | const intl = useIntl(); 22 | const formRef = useRef>(); 23 | 24 | useEffect(() => { 25 | if (!props.visible) { 26 | return; 27 | } 28 | 29 | formRef.current?.resetFields(); 30 | if (props.id) { 31 | get{{$name}}(props.id).then(async (res) => { 32 | if (res.data) { 33 | const data = res.data; 34 | {{- if $includeStatus}} 35 | data.statusChecked = data.status === 'enabled'; 36 | {{- end}} 37 | formRef.current?.setFieldsValue(data); 38 | } 39 | }); 40 | } 41 | }, [props]); 42 | 43 | return ( 44 | 45 | open={props.visible} 46 | title={props.title} 47 | width={800} 48 | formRef={formRef} 49 | layout="horizontal" 50 | grid={true} 51 | rowProps={{`{{`}} gutter: 20 {{`}}`}} 52 | submitTimeout={3000} 53 | submitter={{`{{`}} 54 | searchConfig: { 55 | submitText: intl.formatMessage({ id: 'button.confirm' }), 56 | resetText: intl.formatMessage({ id: 'button.cancel' }), 57 | }, 58 | {{`}}`}} 59 | modalProps={{`{{`}} 60 | destroyOnClose: true, 61 | maskClosable: false, 62 | onCancel: () => { 63 | props.onCancel(); 64 | }, 65 | {{`}}`}} 66 | onFinish={async (values: API.{{$name}}) => { 67 | {{- if $includeStatus}} 68 | values.status = values.statusChecked ? 'enabled' : 'disabled'; 69 | {{- end}} 70 | if (props.id) { 71 | await update{{$name}}(props.id, values); 72 | } else { 73 | await add{{$name}}(values); 74 | } 75 | 76 | message.success(intl.formatMessage({ id: 'component.message.success.save' })); 77 | props.onSuccess(); 78 | return true; 79 | {{`}}`}} 80 | initialValues={{`{{`}} {{if $includeStatus}}statusChecked: true{{end}} {{`}}`}} 81 | > 82 | {{- range .Fields}} 83 | {{- if .Form}} 84 | {{- if .Extra.FormComponent}} 85 | {{.Extra.FormComponent}} 86 | {{- end}} 87 | {{- end}} 88 | {{- end}} 89 | 90 | ); 91 | }; 92 | 93 | export default {{$name}}Modal; 94 | -------------------------------------------------------------------------------- /internal/utils/inflections.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | 7 | "github.com/jinzhu/inflection" 8 | ) 9 | 10 | var commonInitialisms = []string{"API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IP", "JSON", "LHS", "QPS", "RAM", "RHS", "RPC", "SLA", "SMTP", "SSH", "TLS", "TTL", "UID", "UI", "UUID", "URI", "URL", "UTF8", "VM", "XML", "XSRF", "XSS"} 11 | var commonInitialismsReplacer *strings.Replacer 12 | 13 | func init() { 14 | var commonInitialismsForReplacer []string 15 | for _, initialism := range commonInitialisms { 16 | commonInitialismsForReplacer = append(commonInitialismsForReplacer, initialism, strings.Title(strings.ToLower(initialism))) 17 | } 18 | commonInitialismsReplacer = strings.NewReplacer(commonInitialismsForReplacer...) 19 | } 20 | 21 | func ToLowerUnderlinedNamer(name string) string { 22 | const ( 23 | lower = false 24 | upper = true 25 | ) 26 | 27 | if name == "" { 28 | return "" 29 | } 30 | 31 | var ( 32 | value = commonInitialismsReplacer.Replace(name) 33 | buf = bytes.NewBufferString("") 34 | lastCase, currCase, nextCase, nextNumber bool 35 | ) 36 | 37 | for i, v := range value[:len(value)-1] { 38 | nextCase = bool(value[i+1] >= 'A' && value[i+1] <= 'Z') 39 | nextNumber = bool(value[i+1] >= '0' && value[i+1] <= '9') 40 | 41 | if i > 0 { 42 | if currCase == upper { 43 | if lastCase == upper && (nextCase == upper || nextNumber == upper) { 44 | buf.WriteRune(v) 45 | } else { 46 | if value[i-1] != '_' && value[i+1] != '_' { 47 | buf.WriteRune('_') 48 | } 49 | buf.WriteRune(v) 50 | } 51 | } else { 52 | buf.WriteRune(v) 53 | if i == len(value)-2 && (nextCase == upper && nextNumber == lower) { 54 | buf.WriteRune('_') 55 | } 56 | } 57 | } else { 58 | currCase = upper 59 | buf.WriteRune(v) 60 | } 61 | lastCase = currCase 62 | currCase = nextCase 63 | } 64 | 65 | buf.WriteByte(value[len(value)-1]) 66 | 67 | s := strings.ToLower(buf.String()) 68 | return s 69 | } 70 | 71 | func ToPlural(v string) string { 72 | return inflection.Plural(v) 73 | } 74 | 75 | func toLowerPlural(v, sep string) string { 76 | ss := strings.Split(ToLowerUnderlinedNamer(v), "_") 77 | if len(ss) > 0 { 78 | ss[len(ss)-1] = ToPlural(ss[len(ss)-1]) 79 | } 80 | return strings.Join(ss, sep) 81 | } 82 | 83 | func ToLowerPlural(v string) string { 84 | return toLowerPlural(v, "") 85 | } 86 | 87 | func ToLowerSpacePlural(v string) string { 88 | return toLowerPlural(v, " ") 89 | } 90 | 91 | func ToLowerHyphensPlural(v string) string { 92 | return toLowerPlural(v, "-") 93 | } 94 | 95 | func ToLowerCamel(v string) string { 96 | if v == "" { 97 | return "" 98 | } 99 | return strings.ToLower(v[:1]) + v[1:] 100 | } 101 | 102 | func ToLowerSpacedNamer(v string) string { 103 | return strings.Replace(ToLowerUnderlinedNamer(v), "_", " ", -1) 104 | } 105 | 106 | func ToTitleSpaceNamer(v string) string { 107 | vv := strings.Split(ToLowerUnderlinedNamer(v), "_") 108 | if len(vv) > 0 && len(vv[0]) > 0 { 109 | vv[0] = strings.ToUpper(vv[0][:1]) + vv[0][1:] 110 | } 111 | return strings.Join(vv, " ") 112 | } 113 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= 2 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= 3 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 8 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 9 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 10 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 11 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 12 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 13 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 14 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 15 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 16 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 17 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 18 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 19 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 20 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 21 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 22 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 23 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 24 | github.com/urfave/cli/v2 v2.25.1 h1:zw8dSP7ghX0Gmm8vugrs6q9Ku0wzweqPyshy+syu9Gw= 25 | github.com/urfave/cli/v2 v2.25.1/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= 26 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= 27 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= 28 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= 29 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 30 | go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= 31 | go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= 32 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 33 | go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= 34 | go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= 35 | golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= 36 | golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 37 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 38 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 39 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 40 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 41 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 42 | -------------------------------------------------------------------------------- /tpls/react/pages.components.SaveForm.tsx.tpl: -------------------------------------------------------------------------------- 1 | {{- $name := .Name}} 2 | {{- $includeStatus := .Include.Status}} 3 | {{- $statusEnabledText := .Extra.StatusEnabledText}} 4 | {{- $statusDisabledText :=.Extra.StatusDisabledText}} 5 | import React, { useEffect, useRef } from 'react'; 6 | import { message{{if .Extra.FormAntdImport}}, {{.Extra.FormAntdImport}}{{end}} } from 'antd'; 7 | import { ModalForm, ProFormText{{if .Extra.FormProComponentsImport}}, {{.Extra.FormProComponentsImport}}{{end}}} from '@ant-design/pro-components'; 8 | import type { ProFormInstance } from '@ant-design/pro-components'; 9 | import { add{{$name}}, get{{$name}}, update{{$name}} } from '@/services/{{.Extra.ImportService}}'; 10 | 11 | type {{$name}}ModalProps = { 12 | onSuccess: () => void; 13 | onCancel: () => void; 14 | visible: boolean; 15 | title: string; 16 | id?: string; 17 | }; 18 | 19 | const {{$name}}Modal: React.FC<{{$name}}ModalProps> = (props: {{$name}}ModalProps) => { 20 | const formRef = useRef>(); 21 | 22 | useEffect(() => { 23 | if (!props.visible) { 24 | return; 25 | } 26 | 27 | formRef.current?.resetFields(); 28 | if (props.id) { 29 | get{{$name}}(props.id).then(async (res) => { 30 | if (res.data) { 31 | const data = res.data; 32 | {{- if $includeStatus}} 33 | data.statusChecked = data.status === 'enabled'; 34 | {{- end}} 35 | formRef.current?.setFieldsValue(data); 36 | } 37 | }); 38 | } 39 | }, [props]); 40 | 41 | return ( 42 | 43 | visible={props.visible} 44 | title={props.title} 45 | width={800} 46 | formRef={formRef} 47 | layout="horizontal" 48 | grid={true} 49 | submitTimeout={3000} 50 | submitter={{`{{`}} 51 | searchConfig: { 52 | submitText: '{{.Extra.SubmitText}}', 53 | resetText: '{{.Extra.ResetText}}', 54 | }, 55 | {{`}}`}} 56 | modalProps={{`{{`}} 57 | destroyOnClose: true, 58 | maskClosable: false, 59 | onCancel: () => { 60 | props.onCancel(); 61 | }, 62 | {{`}}`}} 63 | onFinish={async (values: API.{{$name}}) => { 64 | {{- if $includeStatus}} 65 | values.status = values.statusChecked ? 'enabled' : 'disabled'; 66 | delete values.statusChecked; 67 | {{- end}} 68 | 69 | if (props.id) { 70 | await update{{$name}}(props.id, values); 71 | } else { 72 | await add{{$name}}(values); 73 | } 74 | 75 | message.success('{{.Extra.SaveSuccessMessage}}'); 76 | props.onSuccess(); 77 | return true; 78 | {{`}}`}} 79 | initialValues={{`{{}}`}} 80 | > 81 | {{- range .Fields}} 82 | {{- $fieldName := .Name}} 83 | {{- $required := .Extra.Required}} 84 | {{- $fieldLabel := .Extra.Label}} 85 | {{- $fieldPlaceholder := .Extra.Placeholder}} 86 | {{- $fieldRulesMessage := .Extra.RulesMessage}} 87 | {{- if .Form}} 88 | {{- if .Extra.FormComponent}} 89 | {{.Extra.FormComponent}} 90 | {{- else}} 91 | 107 | {{- end}} 108 | {{- end}} 109 | {{- end}} 110 | 111 | ); 112 | }; 113 | 114 | export default {{$name}}Modal; 115 | -------------------------------------------------------------------------------- /cmd/generate.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | 7 | "github.com/gin-admin/gin-admin-cli/v10/internal/actions" 8 | "github.com/gin-admin/gin-admin-cli/v10/internal/schema" 9 | "github.com/gin-admin/gin-admin-cli/v10/internal/tfs" 10 | "github.com/urfave/cli/v2" 11 | ) 12 | 13 | // Generate returns the gen command. 14 | func Generate() *cli.Command { 15 | return &cli.Command{ 16 | Name: "generate", 17 | Aliases: []string{"gen"}, 18 | Usage: "Generate structs to the specified module, support config file", 19 | Flags: []cli.Flag{ 20 | &cli.StringFlag{ 21 | Name: "dir", 22 | Aliases: []string{"d"}, 23 | Usage: "The project directory to generate the struct", 24 | Required: true, 25 | }, 26 | &cli.StringFlag{ 27 | Name: "module", 28 | Aliases: []string{"m"}, 29 | Usage: "The module to generate the struct from (like: RBAC)", 30 | Required: false, 31 | }, 32 | &cli.StringFlag{ 33 | Name: "module-path", 34 | Usage: "The module path to generate the struct from (default: internal/mods)", 35 | Value: "internal/mods", 36 | }, 37 | &cli.StringFlag{ 38 | Name: "wire-path", 39 | Usage: "The wire generate path to generate the struct from (default: internal/wirex)", 40 | Value: "internal/wirex", 41 | }, 42 | &cli.StringFlag{ 43 | Name: "swag-path", 44 | Usage: "The swagger generate path to generate the struct from (default: internal/swagger)", 45 | Value: "internal/swagger", 46 | }, 47 | &cli.StringFlag{ 48 | Name: "config", 49 | Aliases: []string{"c"}, 50 | Usage: "The config file or directory to generate the struct from (JSON/YAML)", 51 | }, 52 | &cli.StringFlag{ 53 | Name: "structs", 54 | Aliases: []string{"s"}, 55 | Usage: "The struct name to generate", 56 | }, 57 | &cli.StringFlag{ 58 | Name: "structs-comment", 59 | Usage: "Specify the struct comment", 60 | }, 61 | &cli.BoolFlag{ 62 | Name: "structs-router-prefix", 63 | Usage: "Use module name as router prefix", 64 | }, 65 | &cli.StringFlag{ 66 | Name: "structs-output", 67 | Usage: "Specify the packages to generate the struct (default: schema,dal,biz,api)", 68 | }, 69 | &cli.StringFlag{ 70 | Name: "tpl-path", 71 | Usage: "The template path to generate the struct from (default use tpls)", 72 | }, 73 | &cli.StringFlag{ 74 | Name: "tpl-type", 75 | Usage: "The template type to generate the struct from (default: default)", 76 | Value: "default", 77 | }, 78 | &cli.StringFlag{ 79 | Name: "fe-dir", 80 | Usage: "The frontend project directory to generate the UI", 81 | }, 82 | }, 83 | Action: func(c *cli.Context) error { 84 | if tplPath := c.String("tpl-path"); tplPath != "" { 85 | tfs.SetIns(tfs.NewOSFS(tplPath)) 86 | } 87 | 88 | gen := actions.Generate(actions.GenerateConfig{ 89 | Dir: c.String("dir"), 90 | TplType: c.String("tpl-type"), 91 | Module: c.String("module"), 92 | ModulePath: c.String("module-path"), 93 | WirePath: c.String("wire-path"), 94 | SwaggerPath: c.String("swag-path"), 95 | FEDir: c.String("fe-dir"), 96 | }) 97 | 98 | if c.String("config") != "" { 99 | return gen.RunWithConfig(c.Context, c.String("config")) 100 | } else if name := c.String("structs"); name != "" { 101 | var outputs []string 102 | if v := c.String("structs-output"); v != "" { 103 | outputs = strings.Split(v, ",") 104 | } 105 | return gen.RunWithStruct(c.Context, &schema.S{ 106 | Name: name, 107 | Comment: c.String("structs-comment"), 108 | Outputs: outputs, 109 | FillRouterPrefix: c.Bool("structs-router-prefix"), 110 | }) 111 | } else { 112 | return errors.New("structs or config must be specified") 113 | } 114 | }, 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /tpls/default/schema.go.tpl: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "time" 5 | 6 | {{if .TableName}}"{{.RootImportPath}}/internal/config"{{end}} 7 | "{{.UtilImportPath}}" 8 | ) 9 | 10 | {{$name := .Name}} 11 | {{$includeSequence := .Include.Sequence}} 12 | {{$treeTpl := eq .TplType "tree"}} 13 | 14 | {{with .Comment}}// {{.}}{{else}}// Defining the `{{$name}}` struct.{{end}} 15 | type {{$name}} struct { 16 | {{- range .Fields}}{{$fieldName := .Name}} 17 | {{$fieldName}} {{.Type}} `json:"{{.JSONTag}}"{{with .GormTag}} gorm:"{{.}}"{{end}}{{with .CustomTag}} {{raw .}}{{end}}`{{with .Comment}}// {{.}}{{end}} 18 | {{- end}} 19 | } 20 | 21 | {{- if .TableName}} 22 | func (a {{$name}}) TableName() string { 23 | return config.C.FormatTableName("{{.TableName}}") 24 | } 25 | {{- end}} 26 | 27 | // Defining the query parameters for the `{{$name}}` struct. 28 | type {{$name}}QueryParam struct { 29 | util.PaginationParam 30 | {{if $treeTpl}}InIDs []string `form:"-"`{{- end}} 31 | {{- range .Fields}}{{$fieldName := .Name}}{{$type :=.Type}} 32 | {{- with .Query}} 33 | {{.Name}} {{$type}} `form:"{{with .FormTag}}{{.}}{{else}}-{{end}}"{{with .BindingTag}} binding:"{{.}}"{{end}}{{with .CustomTag}} {{raw .}}{{end}}`{{with .Comment}}// {{.}}{{end}} 34 | {{- end}} 35 | {{- range .Queries}} 36 | {{- with .}} 37 | {{.Name}} {{$type}} `form:"{{with .FormTag}}{{.}}{{else}}-{{end}}"{{with .BindingTag}} binding:"{{.}}"{{end}}{{with .CustomTag}} {{raw .}}{{end}}`{{with .Comment}}// {{.}}{{end}} 38 | {{- end}} 39 | {{- end}} 40 | {{- end}} 41 | } 42 | 43 | // Defining the query options for the `{{$name}}` struct. 44 | type {{$name}}QueryOptions struct { 45 | util.QueryOptions 46 | } 47 | 48 | // Defining the query result for the `{{$name}}` struct. 49 | type {{$name}}QueryResult struct { 50 | Data {{plural .Name}} 51 | PageResult *util.PaginationResult 52 | } 53 | 54 | // Defining the slice of `{{$name}}` struct. 55 | type {{plural .Name}} []*{{$name}} 56 | 57 | {{- if $includeSequence}} 58 | func (a {{plural .Name}}) Len() int { 59 | return len(a) 60 | } 61 | 62 | func (a {{plural .Name}}) Less(i, j int) bool { 63 | if a[i].Sequence == a[j].Sequence { 64 | return a[i].CreatedAt.Unix() > a[j].CreatedAt.Unix() 65 | } 66 | return a[i].Sequence > a[j].Sequence 67 | } 68 | 69 | func (a {{plural .Name}}) Swap(i, j int) { 70 | a[i], a[j] = a[j], a[i] 71 | } 72 | {{- end}} 73 | 74 | {{- if $treeTpl}} 75 | func (a {{plural .Name}}) ToMap() map[string]*{{$name}} { 76 | m := make(map[string]*{{$name}}) 77 | for _, item := range a { 78 | m[item.ID] = item 79 | } 80 | return m 81 | } 82 | 83 | func (a {{plural .Name}}) SplitParentIDs() []string { 84 | parentIDs := make([]string, 0, len(a)) 85 | idMapper := make(map[string]struct{}) 86 | for _, item := range a { 87 | if _, ok := idMapper[item.ID]; ok { 88 | continue 89 | } 90 | idMapper[item.ID] = struct{}{} 91 | if pp := item.ParentPath; pp != "" { 92 | for _, pid := range strings.Split(pp, util.TreePathDelimiter) { 93 | if pid == "" { 94 | continue 95 | } 96 | if _, ok := idMapper[pid]; ok { 97 | continue 98 | } 99 | parentIDs = append(parentIDs, pid) 100 | idMapper[pid] = struct{}{} 101 | } 102 | } 103 | } 104 | return parentIDs 105 | } 106 | 107 | func (a {{plural .Name}}) ToTree() {{plural .Name}} { 108 | var list {{plural .Name}} 109 | m := a.ToMap() 110 | for _, item := range a { 111 | if item.ParentID == "" { 112 | list = append(list, item) 113 | continue 114 | } 115 | if parent, ok := m[item.ParentID]; ok { 116 | if parent.Children == nil { 117 | children := {{plural .Name}}{item} 118 | parent.Children = &children 119 | continue 120 | } 121 | *parent.Children = append(*parent.Children, item) 122 | } 123 | } 124 | return list 125 | } 126 | {{- end}} 127 | 128 | // Defining the data structure for creating a `{{$name}}` struct. 129 | type {{$name}}Form struct { 130 | {{- range .Fields}}{{$fieldName := .Name}}{{$type :=.Type}} 131 | {{- with .Form}} 132 | {{.Name}} {{$type}} `json:"{{.JSONTag}}"{{with .BindingTag}} binding:"{{.}}"{{end}}{{with .CustomTag}} {{raw .}}{{end}}`{{with .Comment}}// {{.}}{{end}} 133 | {{- end}} 134 | {{- end}} 135 | } 136 | 137 | // A validation function for the `{{$name}}Form` struct. 138 | func (a *{{$name}}Form) Validate() error { 139 | return nil 140 | } 141 | 142 | // Convert `{{$name}}Form` to `{{$name}}` object. 143 | func (a *{{$name}}Form) FillTo({{lowerCamel $name}} *{{$name}}) error { 144 | {{- range .Fields}}{{$fieldName := .Name}} 145 | {{- with .Form}} 146 | {{lowerCamel $name}}.{{$fieldName}} = a.{{.Name}} 147 | {{- end}} 148 | {{- end}} 149 | return nil 150 | } 151 | -------------------------------------------------------------------------------- /internal/parser/parser.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "go/ast" 7 | "go/format" 8 | "go/parser" 9 | "go/token" 10 | "path/filepath" 11 | "strings" 12 | "text/template" 13 | 14 | "github.com/gin-admin/gin-admin-cli/v10/internal/utils" 15 | ) 16 | 17 | func ModifyModuleMainFile(ctx context.Context, args BasicArgs) ([]byte, error) { 18 | filename, err := GetModuleMainFilePath(args.ModuleName) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | fullname := filepath.Join(args.Dir, args.ModulePath, filename) 24 | exists, err := utils.ExistsFile(fullname) 25 | if err != nil { 26 | return nil, err 27 | } else if !exists { 28 | tplData := strings.ReplaceAll(tplModuleMain, "$$LowerModuleName$$", GetModuleImportName(args.ModuleName)) 29 | tplData = strings.ReplaceAll(tplData, "$$ModuleName$$", args.ModuleName) 30 | tplData = strings.ReplaceAll(tplData, "$$RootImportPath$$", GetRootImportPath(args.Dir)) 31 | tplData = strings.ReplaceAll(tplData, "$$ModuleImportPath$$", GetModuleImportPath(args.Dir, args.ModulePath, args.ModuleName)) 32 | buf := new(bytes.Buffer) 33 | err := template.Must(template.New("").Parse(tplData)).Execute(buf, args) 34 | if err != nil { 35 | return nil, err 36 | } 37 | if err := utils.WriteFile(fullname, buf.Bytes()); err != nil { 38 | return nil, err 39 | } 40 | } 41 | 42 | fset := token.NewFileSet() 43 | f, err := parser.ParseFile(fset, fullname, nil, parser.ParseComments) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | ast.Walk(&astModuleMainVisitor{ 49 | fset: fset, 50 | args: args, 51 | }, f) 52 | 53 | buf := new(bytes.Buffer) 54 | if err := format.Node(buf, fset, f); err != nil { 55 | return nil, err 56 | } 57 | 58 | buf = utils.Scanner(buf, func(line string) string { 59 | if strings.HasPrefix(strings.TrimSpace(line), "new(schema") { 60 | return strings.ReplaceAll(line, "), new(", "),\n \t\tnew(") 61 | } 62 | return line 63 | }) 64 | 65 | return buf.Bytes(), nil 66 | } 67 | 68 | func ModifyModuleWireFile(ctx context.Context, args BasicArgs) ([]byte, error) { 69 | filename, err := GetModuleWireFilePath(args.ModuleName) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | fullname := filepath.Join(args.Dir, args.ModulePath, filename) 75 | exists, err := utils.ExistsFile(fullname) 76 | if err != nil { 77 | return nil, err 78 | } else if !exists { 79 | tplData := strings.ReplaceAll(tplModuleWire, "$$LowerModuleName$$", GetModuleImportName(args.ModuleName)) 80 | tplData = strings.ReplaceAll(tplData, "$$ModuleName$$", args.ModuleName) 81 | tplData = strings.ReplaceAll(tplData, "$$RootImportPath$$", GetRootImportPath(args.Dir)) 82 | tplData = strings.ReplaceAll(tplData, "$$ModuleImportPath$$", GetModuleImportPath(args.Dir, args.ModulePath, args.ModuleName)) 83 | if err := utils.WriteFile(fullname, []byte(tplData)); err != nil { 84 | return nil, err 85 | } 86 | } 87 | 88 | fset := token.NewFileSet() 89 | f, err := parser.ParseFile(fset, fullname, nil, parser.ParseComments) 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | ast.Walk(&astModuleWireVisitor{ 95 | fset: fset, 96 | args: args, 97 | }, f) 98 | 99 | buf := new(bytes.Buffer) 100 | if err := format.Node(buf, fset, f); err != nil { 101 | return nil, err 102 | } 103 | 104 | buf = utils.Scanner(buf, func(line string) string { 105 | if strings.HasPrefix(strings.TrimSpace(line), "wire.Struct") { 106 | return strings.ReplaceAll(line, "), wire.", "),\n \twire.") 107 | } 108 | return line 109 | }) 110 | 111 | return buf.Bytes(), nil 112 | } 113 | 114 | func ModifyModsFile(ctx context.Context, args BasicArgs) ([]byte, error) { 115 | fullname := filepath.Join(args.Dir, args.ModulePath, FileForMods) 116 | exists, err := utils.ExistsFile(fullname) 117 | if err != nil { 118 | return nil, err 119 | } else if !exists { 120 | return nil, nil 121 | } 122 | 123 | fset := token.NewFileSet() 124 | f, err := parser.ParseFile(fset, fullname, nil, parser.ParseComments) 125 | if err != nil { 126 | return nil, err 127 | } 128 | 129 | ast.Walk(&astModsVisitor{ 130 | fset: fset, 131 | args: args, 132 | }, f) 133 | 134 | buf := new(bytes.Buffer) 135 | if err := format.Node(buf, fset, f); err != nil { 136 | return nil, err 137 | } 138 | 139 | buf = utils.Scanner(buf, func(line string) string { 140 | if strings.Contains(strings.TrimSpace(line), ".Set, ") { 141 | return strings.ReplaceAll(line, ".Set, ", ".Set,\n \t") 142 | } 143 | return line 144 | }) 145 | 146 | result := bytes.ReplaceAll(buf.Bytes(), []byte("ctx,\n\n\t\t\tv1"), []byte("ctx, v1")) 147 | result = bytes.ReplaceAll(result, []byte("RegisterV1Routers(\n\t\tctx, v1)"), []byte("RegisterV1Routers(ctx, v1)")) 148 | result = bytes.ReplaceAll(result, []byte(".\n\t\tInit"), []byte(".Init")) 149 | result = bytes.ReplaceAll(result, []byte(".\n\t\tRegister"), []byte(".Register")) 150 | 151 | return result, nil 152 | } 153 | -------------------------------------------------------------------------------- /examples/parameter.yaml: -------------------------------------------------------------------------------- 1 | - name: Parameter 2 | comment: System parameter management 3 | generate_fe: true 4 | fe_tpl: "ant-design-pro-v5" 5 | extra: 6 | ParentName: system 7 | IndexAntdImport: "Tag" 8 | IndexProComponentsImport: "" 9 | FormAntdImport: "" 10 | FormProComponentsImport: "ProFormText, ProFormTextArea, ProFormSwitch" 11 | IncludeCreatedAt: true 12 | IncludeUpdatedAt: true 13 | fe_mapping: 14 | services.typings.d.ts.tpl: "src/services/system/parameter/typings.d.ts" 15 | services.index.ts.tpl: "src/services/system/parameter/index.ts" 16 | pages.components.form.tsx.tpl: "src/pages/system/Parameter/components/SaveForm.tsx" 17 | pages.index.tsx.tpl: "src/pages/system/Parameter/index.tsx" 18 | locales.en.page.ts.tpl: "src/locales/en-US/pages/system.parameter.ts" 19 | fields: 20 | - name: Name 21 | type: string 22 | comment: Name of parameter 23 | gorm_tag: "size:128;index" 24 | unique: true 25 | query: 26 | name: LikeName 27 | in_query: true 28 | form_tag: name 29 | op: LIKE 30 | form: 31 | binding_tag: "required,max=128" 32 | extra: 33 | ColumnComponent: > 34 | { 35 | title: intl.formatMessage({ id: 'pages.system.parameter.form.name' }), 36 | dataIndex: 'name', 37 | width: 160, 38 | key: 'name', // Query field name 39 | } 40 | FormComponent: > 41 | 54 | - name: Value 55 | type: string 56 | comment: Value of parameter 57 | gorm_tag: "size:1024;" 58 | form: 59 | binding_tag: "max=1024" 60 | extra: 61 | ColumnComponent: > 62 | { 63 | title: intl.formatMessage({ id: 'pages.system.parameter.form.value' }), 64 | dataIndex: 'value', 65 | width: 200, 66 | } 67 | FormComponent: > 68 | 75 | - name: Remark 76 | type: string 77 | comment: Remark of parameter 78 | gorm_tag: "size:255;" 79 | form: {} 80 | extra: 81 | ColumnComponent: > 82 | { 83 | title: intl.formatMessage({ id: 'pages.system.parameter.form.remark' }), 84 | dataIndex: 'remark', 85 | width: 180, 86 | } 87 | FormComponent: > 88 | 94 | - name: Status 95 | type: string 96 | comment: Status of parameter (enabled, disabled) 97 | gorm_tag: "size:20;index" 98 | query: 99 | in_query: true 100 | form: 101 | binding_tag: "required,oneof=enabled disabled" 102 | extra: 103 | ColumnComponent: > 104 | { 105 | title: intl.formatMessage({ id: 'pages.system.parameter.form.status' }), 106 | dataIndex: 'status', 107 | width: 130, 108 | search: false, 109 | render: (status) => { 110 | return ( 111 | 112 | {status === 'enabled' 113 | ? intl.formatMessage({ id: 'pages.system.parameter.form.status.enabled', defaultMessage: 'Enabled' }) 114 | : intl.formatMessage({ id: 'pages.system.parameter.form.status.disabled', defaultMessage: 'Disabled' })} 115 | 116 | ); 117 | }, 118 | } 119 | FormComponent: > 120 | 130 | -------------------------------------------------------------------------------- /tpls/default/api.go.tpl: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "{{.UtilImportPath}}" 5 | "{{.ModuleImportPath}}/biz" 6 | "{{.ModuleImportPath}}/schema" 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | {{$name := .Name}} 11 | 12 | {{with .Comment}}// {{.}}{{else}}// Defining the `{{$name}}` api.{{end}} 13 | type {{$name}} struct { 14 | {{$name}}BIZ *biz.{{$name}} 15 | } 16 | 17 | // @Tags {{$name}}API 18 | // @Security ApiKeyAuth 19 | // @Summary Query {{lowerSpace .Name}} list 20 | {{- if not .DisablePagination}} 21 | // @Param current query int true "pagination index" default(1) 22 | // @Param pageSize query int true "pagination size" default(10) 23 | {{- end}} 24 | {{- range .Fields}}{{$fieldType := .Type}} 25 | {{- with .Query}} 26 | {{- if .InQuery}} 27 | // @Param {{.FormTag}} query {{convSwaggerType $fieldType}} false "{{.Comment}}" 28 | {{- end}} 29 | {{- end}} 30 | {{- range .Queries}} 31 | {{- with .}} 32 | {{- if .InQuery}} 33 | // @Param {{.FormTag}} query {{convSwaggerType $fieldType}} false "{{.Comment}}" 34 | {{- end}} 35 | {{- end}} 36 | {{- end}} 37 | {{- end}} 38 | // @Success 200 {object} util.ResponseResult{data=[]schema.{{$name}}} 39 | // @Failure 401 {object} util.ResponseResult 40 | // @Failure 500 {object} util.ResponseResult 41 | // @Router /api/v1/{{if .FillRouterPrefix}}{{lower .Module}}/{{end}}{{lowerHyphensPlural .Name}} [get] 42 | func (a *{{$name}}) Query(c *gin.Context) { 43 | ctx := c.Request.Context() 44 | var params schema.{{$name}}QueryParam 45 | if err := util.ParseQuery(c, ¶ms); err != nil { 46 | util.ResError(c, err) 47 | return 48 | } 49 | 50 | result, err := a.{{$name}}BIZ.Query(ctx, params) 51 | if err != nil { 52 | util.ResError(c, err) 53 | return 54 | } 55 | util.ResPage(c, result.Data, result.PageResult) 56 | } 57 | 58 | // @Tags {{$name}}API 59 | // @Security ApiKeyAuth 60 | // @Summary Get {{lowerSpace .Name}} record by ID 61 | // @Param id path string true "unique id" 62 | // @Success 200 {object} util.ResponseResult{data=schema.{{$name}}} 63 | // @Failure 401 {object} util.ResponseResult 64 | // @Failure 500 {object} util.ResponseResult 65 | // @Router /api/v1/{{if .FillRouterPrefix}}{{lower .Module}}/{{end}}{{lowerHyphensPlural .Name}}/{id} [get] 66 | func (a *{{$name}}) Get(c *gin.Context) { 67 | ctx := c.Request.Context() 68 | item, err := a.{{$name}}BIZ.Get(ctx, c.Param("id")) 69 | if err != nil { 70 | util.ResError(c, err) 71 | return 72 | } 73 | util.ResSuccess(c, item) 74 | } 75 | 76 | // @Tags {{$name}}API 77 | // @Security ApiKeyAuth 78 | // @Summary Create {{lowerSpace .Name}} record 79 | // @Param body body schema.{{$name}}Form true "Request body" 80 | // @Success 200 {object} util.ResponseResult{data=schema.{{$name}}} 81 | // @Failure 400 {object} util.ResponseResult 82 | // @Failure 401 {object} util.ResponseResult 83 | // @Failure 500 {object} util.ResponseResult 84 | // @Router /api/v1/{{if .FillRouterPrefix}}{{lower .Module}}/{{end}}{{lowerHyphensPlural .Name}} [post] 85 | func (a *{{$name}}) Create(c *gin.Context) { 86 | ctx := c.Request.Context() 87 | item := new(schema.{{$name}}Form) 88 | if err := util.ParseJSON(c, item); err != nil { 89 | util.ResError(c, err) 90 | return 91 | } else if err := item.Validate(); err != nil { 92 | util.ResError(c, err) 93 | return 94 | } 95 | 96 | result, err := a.{{$name}}BIZ.Create(ctx, item) 97 | if err != nil { 98 | util.ResError(c, err) 99 | return 100 | } 101 | util.ResSuccess(c, result) 102 | } 103 | 104 | // @Tags {{$name}}API 105 | // @Security ApiKeyAuth 106 | // @Summary Update {{lowerSpace .Name}} record by ID 107 | // @Param id path string true "unique id" 108 | // @Param body body schema.{{$name}}Form true "Request body" 109 | // @Success 200 {object} util.ResponseResult 110 | // @Failure 400 {object} util.ResponseResult 111 | // @Failure 401 {object} util.ResponseResult 112 | // @Failure 500 {object} util.ResponseResult 113 | // @Router /api/v1/{{if .FillRouterPrefix}}{{lower .Module}}/{{end}}{{lowerHyphensPlural .Name}}/{id} [put] 114 | func (a *{{$name}}) Update(c *gin.Context) { 115 | ctx := c.Request.Context() 116 | item := new(schema.{{$name}}Form) 117 | if err := util.ParseJSON(c, item); err != nil { 118 | util.ResError(c, err) 119 | return 120 | } else if err := item.Validate(); err != nil { 121 | util.ResError(c, err) 122 | return 123 | } 124 | 125 | err := a.{{$name}}BIZ.Update(ctx, c.Param("id"), item) 126 | if err != nil { 127 | util.ResError(c, err) 128 | return 129 | } 130 | util.ResOK(c) 131 | } 132 | 133 | // @Tags {{$name}}API 134 | // @Security ApiKeyAuth 135 | // @Summary Delete {{lowerSpace .Name}} record by ID 136 | // @Param id path string true "unique id" 137 | // @Success 200 {object} util.ResponseResult 138 | // @Failure 401 {object} util.ResponseResult 139 | // @Failure 500 {object} util.ResponseResult 140 | // @Router /api/v1/{{if .FillRouterPrefix}}{{lower .Module}}/{{end}}{{lowerHyphensPlural .Name}}/{id} [delete] 141 | func (a *{{$name}}) Delete(c *gin.Context) { 142 | ctx := c.Request.Context() 143 | err := a.{{$name}}BIZ.Delete(ctx, c.Param("id")) 144 | if err != nil { 145 | util.ResError(c, err) 146 | return 147 | } 148 | util.ResOK(c) 149 | } 150 | -------------------------------------------------------------------------------- /tpls/react/pages.index.tsx.tpl: -------------------------------------------------------------------------------- 1 | {{- $name := .Name}} 2 | import { PageContainer } from '@ant-design/pro-components'; 3 | import React, { useRef, useReducer } from 'react'; 4 | import type { ProColumns, ActionType } from '@ant-design/pro-components'; 5 | import { ProTable{{if .Extra.IndexProComponentsImport}}, {{.Extra.IndexProComponentsImport}}{{end}} } from '@ant-design/pro-components'; 6 | import { Space, message{{if .Extra.IndexAntdImport}}, {{.Extra.IndexAntdImport}}{{end}}} from 'antd'; 7 | import { fetch{{$name}}, del{{$name}} } from '@/services/{{.Extra.ImportService}}'; 8 | import {{$name}}Modal from './components/SaveForm'; 9 | import { AddButton, EditIconButton, DelIconButton } from '@/components/Button'; 10 | 11 | enum ActionTypeEnum { 12 | ADD, 13 | EDIT, 14 | CANCEL, 15 | } 16 | 17 | interface Action { 18 | type: ActionTypeEnum; 19 | payload?: API.{{$name}}; 20 | } 21 | 22 | interface State { 23 | visible: boolean; 24 | title: string; 25 | id?: string; 26 | } 27 | 28 | const {{$name}}: React.FC = () => { 29 | const actionRef = useRef(); 30 | const addTitle = "{{.Extra.AddTitle}}"; 31 | const editTitle = "{{.Extra.EditTitle}}"; 32 | const delTip = "{{.Extra.DelTip}}"; 33 | 34 | const [state, dispatch] = useReducer( 35 | (pre: State, action: Action) => { 36 | switch (action.type) { 37 | case ActionTypeEnum.ADD: 38 | return { 39 | visible: true, 40 | title: addTitle, 41 | }; 42 | case ActionTypeEnum.EDIT: 43 | return { 44 | visible: true, 45 | title: editTitle, 46 | id: action.payload?.id, 47 | }; 48 | case ActionTypeEnum.CANCEL: 49 | return { 50 | visible: false, 51 | title: '', 52 | id: undefined, 53 | }; 54 | default: 55 | return pre; 56 | } 57 | }, 58 | { visible: false, title: '' }, 59 | ); 60 | 61 | const columns: ProColumns[] = [ 62 | {{- range .Fields}} 63 | {{- $fieldName := .Name}} 64 | {{- $fieldLabel :=.Extra.Label}} 65 | {{- if eq .Extra.InColumn "true"}} 66 | {{- if .Extra.ColumnComponent}} 67 | {{.Extra.ColumnComponent}}, 68 | {{- else}} 69 | { 70 | title: "{{$fieldLabel}}", 71 | dataIndex: '{{lowerUnderline $fieldName}}', 72 | {{- if .Extra.Ellipsis}} 73 | ellipsis: true, 74 | {{- end}} 75 | {{- if .Extra.Width}} 76 | width: {{.Extra.Width}}, 77 | {{- end}} 78 | {{- if .Extra.SearchKey}} 79 | key: '{{.Extra.SearchKey}}', 80 | {{- else}} 81 | search: false, 82 | {{- end}} 83 | {{- if .Extra.ValueType}} 84 | valueType: '{{.Extra.ValueType}}', 85 | {{- end}} 86 | }, 87 | {{- end}} 88 | {{- end}} 89 | {{- end}} 90 | { 91 | title: "{{.Extra.ActionText}}", 92 | valueType: 'option', 93 | key: 'option', 94 | width: 130, 95 | render: (_, record) => ( 96 | 97 | { 101 | dispatch({ type: ActionTypeEnum.EDIT, payload: record }); 102 | {{`}}`}} 103 | /> 104 | { 109 | const res = await del{{$name}}(record.id!); 110 | if (res.success) { 111 | message.success("{{.Extra.DeleteSuccessMessage}}"); 112 | actionRef.current?.reload(); 113 | } 114 | {{`}}`}} 115 | /> 116 | 117 | ), 118 | }, 119 | ]; 120 | 121 | return ( 122 | 123 | 124 | columns={columns} 125 | actionRef={actionRef} 126 | request={fetch{{$name}}} 127 | rowKey="id" 128 | cardBordered 129 | search={{`{{`}} 130 | labelWidth: 'auto', 131 | {{`}}`}} 132 | pagination={{`{{`}} pageSize: 10, showSizeChanger: true {{`}}`}} 133 | options={{`{{`}} 134 | density: true, 135 | fullScreen: true, 136 | reload: true, 137 | {{`}}`}} 138 | dateFormatter="string" 139 | toolBarRender={() => [ 140 | { 144 | dispatch({ type: ActionTypeEnum.ADD }); 145 | {{`}}`}} 146 | />, 147 | ]} 148 | /> 149 | <{{$name}}Modal 150 | visible={state.visible} 151 | title={state.title} 152 | id={state.id} 153 | onCancel={() => { 154 | dispatch({ type: ActionTypeEnum.CANCEL }); 155 | {{`}}`}} 156 | onSuccess={() => { 157 | dispatch({ type: ActionTypeEnum.CANCEL }); 158 | actionRef.current?.reload(); 159 | {{`}}`}} 160 | /> 161 | 162 | ); 163 | }; 164 | 165 | export default {{$name}}; 166 | -------------------------------------------------------------------------------- /internal/actions/remove.go: -------------------------------------------------------------------------------- 1 | package actions 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/gin-admin/gin-admin-cli/v10/internal/parser" 10 | "github.com/gin-admin/gin-admin-cli/v10/internal/schema" 11 | "github.com/gin-admin/gin-admin-cli/v10/internal/utils" 12 | "go.uber.org/zap" 13 | ) 14 | 15 | type RemoveConfig struct { 16 | Dir string 17 | ModuleName string 18 | ModulePath string 19 | WirePath string 20 | SwaggerPath string 21 | } 22 | 23 | func Remove(cfg RemoveConfig) *RemoveAction { 24 | return &RemoveAction{ 25 | logger: zap.S().Named("[REMOVE]"), 26 | cfg: &cfg, 27 | } 28 | } 29 | 30 | type RemoveAction struct { 31 | logger *zap.SugaredLogger 32 | cfg *RemoveConfig 33 | } 34 | 35 | func (a *RemoveAction) RunWithConfig(ctx context.Context, configFile string) error { 36 | var data []*schema.S 37 | switch filepath.Ext(configFile) { 38 | case ".json": 39 | if err := utils.ParseJSONFile(configFile, &data); err != nil { 40 | return err 41 | } 42 | case ".yaml", "yml": 43 | if err := utils.ParseYAMLFile(configFile, &data); err != nil { 44 | return err 45 | } 46 | default: 47 | return fmt.Errorf("unsupported config file type: %s", configFile) 48 | } 49 | 50 | var structs []string 51 | for _, item := range data { 52 | structs = append(structs, item.Name) 53 | } 54 | 55 | a.logger.Infof("Remove structs: %v", structs) 56 | return a.Run(ctx, structs) 57 | } 58 | 59 | func (a *RemoveAction) Run(ctx context.Context, structs []string) error { 60 | for _, name := range structs { 61 | for _, pkgName := range parser.StructPackages { 62 | err := a.modify(ctx, a.cfg.ModuleName, name, parser.StructPackageTplPaths[pkgName], nil, true) 63 | if err != nil { 64 | return err 65 | } 66 | } 67 | 68 | basicArgs := parser.BasicArgs{ 69 | Dir: a.cfg.Dir, 70 | ModuleName: a.cfg.ModuleName, 71 | ModulePath: a.cfg.ModulePath, 72 | StructName: name, 73 | Flag: parser.AstFlagRem, 74 | } 75 | moduleMainTplData, err := parser.ModifyModuleMainFile(ctx, basicArgs) 76 | if err != nil { 77 | a.logger.Errorf("Failed to modify module main file, err: %s, #struct %s", err, name) 78 | return err 79 | } 80 | 81 | err = a.modify(ctx, a.cfg.ModuleName, name, parser.FileForModuleMain, moduleMainTplData, false) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | moduleWireTplData, err := parser.ModifyModuleWireFile(ctx, basicArgs) 87 | if err != nil { 88 | a.logger.Errorf("Failed to modify module wire file, err: %s, #struct %s", err, name) 89 | return err 90 | } 91 | 92 | err = a.modify(ctx, a.cfg.ModuleName, name, parser.FileForModuleWire, moduleWireTplData, false) 93 | if err != nil { 94 | return err 95 | } 96 | } 97 | 98 | return a.execWireAndSwag(ctx) 99 | } 100 | 101 | func (a RemoveAction) getAbsPath(file string) (string, error) { 102 | modPath := a.cfg.ModulePath 103 | file = filepath.Join(a.cfg.Dir, modPath, file) 104 | fullPath, err := filepath.Abs(file) 105 | if err != nil { 106 | a.logger.Errorf("Failed to get abs path, err: %s, #file %s", err, file) 107 | return "", err 108 | } 109 | return fullPath, nil 110 | } 111 | 112 | func (a *RemoveAction) modify(_ context.Context, moduleName, structName, tpl string, data []byte, deleted bool) error { 113 | file, err := parser.ParseFilePathFromTpl(moduleName, structName, tpl) 114 | if err != nil { 115 | a.logger.Errorf("Failed to parse file path from tpl, err: %s, #struct %s, #tpl %s", err, structName, tpl) 116 | return err 117 | } 118 | 119 | file, err = a.getAbsPath(file) 120 | if err != nil { 121 | return err 122 | } 123 | 124 | exists, err := utils.ExistsFile(file) 125 | if err != nil { 126 | return err 127 | } 128 | 129 | if exists { 130 | if err := os.Remove(file); err != nil { 131 | a.logger.Errorf("Failed to remove file, err: %s, #file %s", err, file) 132 | return err 133 | } 134 | } 135 | 136 | if deleted { 137 | a.logger.Infof("Delete file: %s", file) 138 | return nil 139 | } 140 | 141 | if !exists { 142 | return nil 143 | } 144 | 145 | a.logger.Infof("Write file: %s", file) 146 | if err := utils.WriteFile(file, data); err != nil { 147 | a.logger.Errorf("Failed to write file, err: %s, #file %s", err, file) 148 | return err 149 | } 150 | 151 | if err := utils.ExecGoFormat(file); err != nil { 152 | a.logger.Errorf("Failed to exec go format, err: %s, #file %s", err, file) 153 | return nil 154 | } 155 | 156 | if err := utils.ExecGoImports(a.cfg.Dir, file); err != nil { 157 | a.logger.Errorf("Failed to exec go imports, err: %s, #file %s", err, file) 158 | return nil 159 | } 160 | return nil 161 | } 162 | 163 | func (a *RemoveAction) execWireAndSwag(_ context.Context) error { 164 | if p := a.cfg.WirePath; p != "" { 165 | if err := utils.ExecWireGen(a.cfg.Dir, p); err != nil { 166 | a.logger.Errorf("Failed to exec wire, err: %s, #wirePath %s", err, p) 167 | } 168 | } 169 | 170 | if p := a.cfg.SwaggerPath; p != "" { 171 | if err := utils.ExecSwagGen(a.cfg.Dir, "main.go", p); err != nil { 172 | a.logger.Errorf("Failed to exec swag, err: %s, #swaggerPath %s", err, p) 173 | } 174 | } 175 | 176 | return nil 177 | } 178 | -------------------------------------------------------------------------------- /tpls/ant-design-pro-v5/pages.index.tsx.tpl: -------------------------------------------------------------------------------- 1 | {{- $name := .Name}} 2 | {{- $lowerCamelName := lowerCamel .Name}} 3 | {{- $parentName := .Extra.ParentName}} 4 | import { PageContainer } from '@ant-design/pro-components'; 5 | import React, { useRef, useReducer } from 'react'; 6 | import { useIntl } from '@umijs/max'; 7 | import type { ProColumns, ActionType } from '@ant-design/pro-components'; 8 | import { ProTable{{if .Extra.IndexProComponentsImport}}, {{.Extra.IndexProComponentsImport}}{{end}} } from '@ant-design/pro-components'; 9 | import { Space, message{{if .Extra.IndexAntdImport}}, {{.Extra.IndexAntdImport}}{{end}} } from 'antd'; 10 | import { fetch{{$name}}, del{{$name}} } from '@/services/{{with $parentName}}{{.}}/{{end}}{{$lowerCamelName}}'; 11 | import {{$name}}Modal from './components/SaveForm'; 12 | import { AddButton, EditIconButton, DelIconButton } from '@/components/Button'; 13 | 14 | enum ActionTypeEnum { 15 | ADD, 16 | EDIT, 17 | CANCEL, 18 | } 19 | 20 | interface Action { 21 | type: ActionTypeEnum; 22 | payload?: API.{{$name}}; 23 | } 24 | 25 | interface State { 26 | visible: boolean; 27 | title: string; 28 | id?: string; 29 | } 30 | 31 | const {{$name}}: React.FC = () => { 32 | const intl = useIntl(); 33 | const actionRef = useRef(); 34 | const addTitle = intl.formatMessage({ id: 'pages.{{with $parentName}}{{.}}.{{end}}{{$lowerCamelName}}.add', defaultMessage: 'Add {{$name}}' }); 35 | const editTitle = intl.formatMessage({ id: 'pages.{{with $parentName}}{{.}}.{{end}}{{$lowerCamelName}}.edit', defaultMessage: 'Edit {{$name}}' }); 36 | const delTip = intl.formatMessage({ id: 'pages.{{with $parentName}}{{.}}.{{end}}{{$lowerCamelName}}.delTip', defaultMessage: 'Are you sure you want to delete this record?' }); 37 | 38 | const [state, dispatch] = useReducer( 39 | (pre: State, action: Action) => { 40 | switch (action.type) { 41 | case ActionTypeEnum.ADD: 42 | return { 43 | visible: true, 44 | title: addTitle, 45 | }; 46 | case ActionTypeEnum.EDIT: 47 | return { 48 | visible: true, 49 | title: editTitle, 50 | id: action.payload?.id, 51 | }; 52 | case ActionTypeEnum.CANCEL: 53 | return { 54 | visible: false, 55 | title: '', 56 | id: undefined, 57 | }; 58 | default: 59 | return pre; 60 | } 61 | }, 62 | { visible: false, title: '' }, 63 | ); 64 | 65 | const columns: ProColumns[] = [ 66 | {{- range .Fields}} 67 | {{- if .Extra.ColumnComponent}} 68 | {{.Extra.ColumnComponent}}, 69 | {{- end}} 70 | {{- end}} 71 | {{- if .Extra.IncludeCreatedAt}} 72 | { 73 | title: intl.formatMessage({ id: 'pages.table.column.created_at' }), 74 | dataIndex: 'created_at', 75 | valueType: 'dateTime', 76 | search: false, 77 | width: 160, 78 | }, 79 | {{- end}} 80 | {{- if .Extra.IncludeUpdatedAt}} 81 | { 82 | title: intl.formatMessage({ id: 'pages.table.column.updated_at' }), 83 | dataIndex: 'updated_at', 84 | valueType: 'dateTime', 85 | search: false, 86 | width: 160, 87 | }, 88 | {{- end}} 89 | { 90 | title: intl.formatMessage({ id: 'pages.table.column.operation' }), 91 | valueType: 'option', 92 | key: 'option', 93 | width: 130, 94 | render: (_, record) => ( 95 | 96 | { 100 | dispatch({ type: ActionTypeEnum.EDIT, payload: record }); 101 | {{`}}`}} 102 | /> 103 | { 108 | const res = await del{{$name}}(record.id!); 109 | if (res.success) { 110 | message.success(intl.formatMessage({ id: 'component.message.success.delete' })); 111 | actionRef.current?.reload(); 112 | } 113 | {{`}}`}} 114 | /> 115 | 116 | ), 117 | }, 118 | ]; 119 | 120 | return ( 121 | 122 | 123 | columns={columns} 124 | actionRef={actionRef} 125 | request={fetch{{$name}}} 126 | rowKey="id" 127 | cardBordered 128 | search={{`{{`}} 129 | labelWidth: 'auto', 130 | {{`}}`}} 131 | pagination={{`{{`}} defaultPageSize: 10, showSizeChanger: true {{`}}`}} 132 | options={{`{{`}} 133 | density: true, 134 | fullScreen: true, 135 | reload: true, 136 | {{`}}`}} 137 | dateFormatter="string" 138 | toolBarRender={() => [ 139 | { 143 | dispatch({ type: ActionTypeEnum.ADD }); 144 | {{`}}`}} 145 | />, 146 | ]} 147 | /> 148 | 149 | <{{$name}}Modal 150 | visible={state.visible} 151 | title={state.title} 152 | id={state.id} 153 | onCancel={() => { 154 | dispatch({ type: ActionTypeEnum.CANCEL }); 155 | {{`}}`}} 156 | onSuccess={() => { 157 | dispatch({ type: ActionTypeEnum.CANCEL }); 158 | actionRef.current?.reload(); 159 | {{`}}`}} 160 | /> 161 | 162 | ); 163 | }; 164 | 165 | export default {{$name}}; 166 | -------------------------------------------------------------------------------- /tpls/default/dal.go.tpl: -------------------------------------------------------------------------------- 1 | package dal 2 | 3 | import ( 4 | "context" 5 | 6 | "{{.UtilImportPath}}" 7 | "{{.ModuleImportPath}}/schema" 8 | "{{.RootImportPath}}/pkg/errors" 9 | "gorm.io/gorm" 10 | ) 11 | 12 | {{$name := .Name}} 13 | {{$includeCreatedAt := .Include.CreatedAt}} 14 | {{$includeStatus := .Include.Status}} 15 | {{$treeTpl := eq .TplType "tree"}} 16 | 17 | // Get {{lowerSpace .Name}} storage instance 18 | func Get{{$name}}DB(ctx context.Context, defDB *gorm.DB) *gorm.DB { 19 | return util.GetDB(ctx, defDB).Model(new(schema.{{$name}})) 20 | } 21 | 22 | {{with .Comment}}// {{.}}{{else}}// Defining the `{{$name}}` data access object.{{end}} 23 | type {{$name}} struct { 24 | DB *gorm.DB 25 | } 26 | 27 | // Query {{lowerSpacePlural .Name}} from the database based on the provided parameters and options. 28 | func (a *{{$name}}) Query(ctx context.Context, params schema.{{$name}}QueryParam, opts ...schema.{{$name}}QueryOptions) (*schema.{{$name}}QueryResult, error) { 29 | var opt schema.{{$name}}QueryOptions 30 | if len(opts) > 0 { 31 | opt = opts[0] 32 | } 33 | 34 | db := Get{{$name}}DB(ctx, a.DB) 35 | 36 | {{- if $treeTpl}} 37 | if v:= params.InIDs; len(v) > 0 { 38 | db = db.Where("id IN ?", v) 39 | } 40 | {{- end}} 41 | 42 | {{- range .Fields}}{{$type := .Type}}{{$fieldName := .Name}} 43 | {{- with .Query}} 44 | if v := params.{{.Name}}; {{with .IfCond}}{{.}}{{else}}{{convIfCond $type}}{{end}} { 45 | db = db.Where("{{lowerUnderline $fieldName}} {{.OP}} ?", {{if .Args}}{{raw .Args}}{{else}}{{if eq .OP "LIKE"}}"%"+v+"%"{{else}}v{{end}}{{end}}) 46 | } 47 | {{- end}} 48 | {{- range .Queries}} 49 | {{- with .}} 50 | if v := params.{{.Name}}; {{with .IfCond}}{{.}}{{else}}{{convIfCond $type}}{{end}} { 51 | db = db.Where("{{lowerUnderline $fieldName}} {{.OP}} ?", {{if .Args}}{{raw .Args}}{{else}}{{if eq .OP "LIKE"}}"%"+v+"%"{{else}}v{{end}}{{end}}) 52 | } 53 | {{- end}} 54 | {{- end}} 55 | {{- end}} 56 | 57 | if opt.Result != nil { 58 | pageResult, err := util.WrapPageQuery(ctx, db, params.PaginationParam, opt.QueryOptions, opt.Result) 59 | if err != nil { 60 | return nil, err 61 | } 62 | return &schema.{{$name}}QueryResult{ 63 | PageResult: pageResult, 64 | }, nil 65 | } 66 | 67 | var list schema.{{plural .Name}} 68 | pageResult, err := util.WrapPageQuery(ctx, db, params.PaginationParam, opt.QueryOptions, &list) 69 | if err != nil { 70 | return nil, errors.WithStack(err) 71 | } 72 | return &schema.{{$name}}QueryResult{ 73 | PageResult: pageResult, 74 | Data: list, 75 | }, nil 76 | } 77 | 78 | // Get the specified {{lowerSpace .Name}} from the database. 79 | func (a *{{$name}}) Get(ctx context.Context, id string, opts ...schema.{{$name}}QueryOptions) (*schema.{{$name}}, error) { 80 | var opt schema.{{$name}}QueryOptions 81 | if len(opts) > 0 { 82 | opt = opts[0] 83 | } 84 | 85 | item := new(schema.{{$name}}) 86 | ok, err := util.FindOne(ctx, Get{{$name}}DB(ctx, a.DB).Where("id=?", id), opt.QueryOptions, item) 87 | if err != nil { 88 | return nil, errors.WithStack(err) 89 | } else if !ok { 90 | return nil, nil 91 | } 92 | return item, nil 93 | } 94 | 95 | // Exists checks if the specified {{lowerSpace .Name}} exists in the database. 96 | func (a *{{$name}}) Exists(ctx context.Context, id string) (bool, error) { 97 | ok, err := util.Exists(ctx, Get{{$name}}DB(ctx, a.DB).Where("id=?", id)) 98 | return ok, errors.WithStack(err) 99 | } 100 | 101 | {{- range .Fields}} 102 | {{- if .Unique}} 103 | {{- if $treeTpl}} 104 | // Exist checks if the specified {{lowerSpace .Name}} exists in the database. 105 | func (a *{{$name}}) Exists{{.Name}}(ctx context.Context, parentID string, {{lowerCamel .Name}} string) (bool, error) { 106 | ok, err := util.Exists(ctx, Get{{$name}}DB(ctx, a.DB).Where("parent_id=? AND {{lowerUnderline .Name}}=?", parentID, {{lowerCamel .Name}})) 107 | return ok, errors.WithStack(err) 108 | } 109 | {{- else}} 110 | // Exist checks if the specified {{lowerSpace .Name}} exists in the database. 111 | func (a *{{$name}}) Exists{{.Name}}(ctx context.Context, {{lowerCamel .Name}} string) (bool, error) { 112 | ok, err := util.Exists(ctx, Get{{$name}}DB(ctx, a.DB).Where("{{lowerUnderline .Name}}=?", {{lowerCamel .Name}})) 113 | return ok, errors.WithStack(err) 114 | } 115 | {{- end}} 116 | {{- end}} 117 | {{- end}} 118 | 119 | // Create a new {{lowerSpace .Name}}. 120 | func (a *{{$name}}) Create(ctx context.Context, item *schema.{{$name}}) error { 121 | result := Get{{$name}}DB(ctx, a.DB).Create(item) 122 | return errors.WithStack(result.Error) 123 | } 124 | 125 | // Update the specified {{lowerSpace .Name}} in the database. 126 | func (a *{{$name}}) Update(ctx context.Context, item *schema.{{$name}}) error { 127 | result := Get{{$name}}DB(ctx, a.DB).Where("id=?", item.ID).Select("*"){{if $includeCreatedAt}}.Omit("created_at"){{end}}.Updates(item) 128 | return errors.WithStack(result.Error) 129 | } 130 | 131 | // Delete the specified {{lowerSpace .Name}} from the database. 132 | func (a *{{$name}}) Delete(ctx context.Context, id string) error { 133 | result := Get{{$name}}DB(ctx, a.DB).Where("id=?", id).Delete(new(schema.{{$name}})) 134 | return errors.WithStack(result.Error) 135 | } 136 | 137 | {{- if $treeTpl}} 138 | // Updates the parent path of the specified {{lowerSpace .Name}}. 139 | func (a *{{$name}}) UpdateParentPath(ctx context.Context, id, parentPath string) error { 140 | result := Get{{$name}}DB(ctx, a.DB).Where("id=?", id).Update("parent_path", parentPath) 141 | return errors.WithStack(result.Error) 142 | } 143 | 144 | {{- if $includeStatus}} 145 | // Updates the status of all {{lowerPlural .Name}} whose parent path starts with the provided parent path. 146 | func (a *{{$name}}) UpdateStatusByParentPath(ctx context.Context, parentPath, status string) error { 147 | result := Get{{$name}}DB(ctx, a.DB).Where("parent_path like ?", parentPath+"%").Update("status", status) 148 | return errors.WithStack(result.Error) 149 | } 150 | {{- end}} 151 | {{- end}} -------------------------------------------------------------------------------- /internal/utils/command.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "go/format" 6 | "os" 7 | "os/exec" 8 | "strings" 9 | 10 | "go.uber.org/zap" 11 | ) 12 | 13 | // Formats the given Go source code file 14 | func ExecGoFormat(name string) error { 15 | // read the contents of the file 16 | content, err := os.ReadFile(name) 17 | if err != nil { 18 | return fmt.Errorf("failed to reading file: %v", err) 19 | } 20 | 21 | // format the source code 22 | formatted, err := format.Source(content) 23 | if err != nil { 24 | return fmt.Errorf("failed to formatting file: %v", err) 25 | } 26 | 27 | // overwrite the existing file with the formatted code 28 | err = WriteFile(name, formatted) 29 | if err != nil { 30 | return fmt.Errorf("failed to writing formatted file: %v", err) 31 | } 32 | return nil 33 | } 34 | 35 | // Executes the goimports command on the given file 36 | func ExecGoImports(dir, name string) error { 37 | localPath, err := exec.LookPath("goimports") 38 | if err != nil { 39 | if err := ExecGoInstall(dir, "golang.org/x/tools/cmd/goimports@latest"); err != nil { 40 | return nil 41 | } 42 | } 43 | 44 | cmd := exec.Command(localPath, "-w", name) 45 | cmd.Dir = dir 46 | cmd.Stdout = os.Stdout 47 | cmd.Stderr = os.Stdout 48 | return cmd.Run() 49 | } 50 | 51 | func ExecGoInstall(dir, path string) error { 52 | localPath, err := exec.LookPath("go") 53 | if err != nil { 54 | zap.S().Warn("not found go command, please install go first") 55 | return nil 56 | } 57 | 58 | cmd := exec.Command(localPath, "install", path) 59 | cmd.Dir = dir 60 | cmd.Stdout = os.Stdout 61 | cmd.Stderr = os.Stdout 62 | return cmd.Run() 63 | } 64 | 65 | func ExecGoModTidy(dir string) error { 66 | localPath, err := exec.LookPath("go") 67 | if err != nil { 68 | zap.S().Warn("not found go command, please install go first") 69 | return nil 70 | } 71 | 72 | cmd := exec.Command(localPath, "mod", "tidy") 73 | cmd.Dir = dir 74 | cmd.Stdout = os.Stdout 75 | cmd.Stderr = os.Stdout 76 | return cmd.Run() 77 | } 78 | 79 | // Executes the wire command on the given file 80 | func ExecWireGen(dir, path string) error { 81 | localPath, err := exec.LookPath("wire") 82 | if err != nil { 83 | if err := ExecGoInstall(dir, "github.com/google/wire/cmd/wire@latest"); err != nil { 84 | return nil 85 | } 86 | } 87 | 88 | cmd := exec.Command(localPath, "gen", "./"+path) 89 | cmd.Dir = dir 90 | cmd.Stdout = os.Stdout 91 | cmd.Stderr = os.Stdout 92 | return cmd.Run() 93 | } 94 | 95 | // Executes the swag command on the given file 96 | func ExecSwagGen(dir, generalInfo, output string) error { 97 | localPath, err := exec.LookPath("swag") 98 | if err != nil { 99 | if err := ExecGoInstall(dir, "github.com/swaggo/swag/cmd/swag@latest"); err != nil { 100 | return nil 101 | } 102 | } 103 | 104 | fmt.Printf("swag init --parseDependency --generalInfo %s --output %s \n", generalInfo, output) 105 | cmd := exec.Command(localPath, "init", "--parseDependency", "--generalInfo", generalInfo, "--output", output) 106 | cmd.Dir = dir 107 | cmd.Stdout = os.Stdout 108 | cmd.Stderr = os.Stdout 109 | return cmd.Run() 110 | } 111 | 112 | func ExecGitInit(dir string) error { 113 | localPath, err := exec.LookPath("git") 114 | if err != nil { 115 | zap.S().Warn("not found git command, please install git first") 116 | return nil 117 | } 118 | 119 | cmd := exec.Command(localPath, "init") 120 | cmd.Dir = dir 121 | cmd.Stdout = os.Stdout 122 | cmd.Stderr = os.Stdout 123 | return cmd.Run() 124 | } 125 | 126 | func ExecGitClone(dir, url, branch, name string) error { 127 | localPath, err := exec.LookPath("git") 128 | if err != nil { 129 | zap.S().Warn("not found git command, please install git first") 130 | return nil 131 | } 132 | 133 | var args []string 134 | args = append(args, "clone") 135 | args = append(args, url) 136 | if branch != "" { 137 | args = append(args, "-b") 138 | args = append(args, branch) 139 | } 140 | if name != "" { 141 | args = append(args, name) 142 | } 143 | 144 | fmt.Printf("git %s \n", strings.Join(args, " ")) 145 | cmd := exec.Command(localPath, args...) 146 | cmd.Dir = dir 147 | cmd.Stdout = os.Stdout 148 | cmd.Stderr = os.Stdout 149 | return cmd.Run() 150 | } 151 | 152 | func ExecTree(dir string) error { 153 | localPath, err := exec.LookPath("tree") 154 | if err != nil { 155 | return nil 156 | } 157 | 158 | cmd := exec.Command(localPath, "-L", "4", "-I", ".git", "-I", "pkg", "--dirsfirst") 159 | cmd.Dir = dir 160 | cmd.Stdout = os.Stdout 161 | cmd.Stderr = os.Stdout 162 | return cmd.Run() 163 | } 164 | 165 | func GetDefaultProjectTree() string { 166 | return ` 167 | ├── cmd 168 | │   ├── start.go 169 | │   ├── stop.go 170 | │   └── version.go 171 | ├── configs 172 | │   ├── dev 173 | │   │   ├── logging.toml 174 | │   │   ├── middleware.toml 175 | │   │   └── server.toml 176 | │   ├── menu.json 177 | │   └── rbac_model.conf 178 | ├── internal 179 | │   ├── bootstrap 180 | │   │   ├── bootstrap.go 181 | │   │   ├── http.go 182 | │   │   └── logger.go 183 | │   ├── config 184 | │   │   ├── config.go 185 | │   │   ├── consts.go 186 | │   │   ├── middleware.go 187 | │   │   └── parse.go 188 | │   ├── mods 189 | │   │   ├── rbac 190 | │   │   │   ├── api 191 | │   │   │   ├── biz 192 | │   │   │   ├── dal 193 | │   │   │   ├── schema 194 | │   │   │   ├── casbin.go 195 | │   │   │   ├── main.go 196 | │   │   │   └── wire.go 197 | │   │   ├── sys 198 | │   │   │   ├── api 199 | │   │   │   ├── biz 200 | │   │   │   ├── dal 201 | │   │   │   ├── schema 202 | │   │   │   ├── main.go 203 | │   │   │   └── wire.go 204 | │   │   └── mods.go 205 | │   ├── utility 206 | │   │   └── prom 207 | │   │   └── prom.go 208 | │   └── wirex 209 | │   ├── injector.go 210 | │   ├── wire.go 211 | │   └── wire_gen.go 212 | ├── test 213 | │   ├── menu_test.go 214 | │   ├── role_test.go 215 | │   ├── test.go 216 | │   └── user_test.go 217 | ├── Dockerfile 218 | ├── Makefile 219 | ├── README.md 220 | ├── go.mod 221 | ├── go.sum 222 | └── main.go 223 | ` 224 | } 225 | -------------------------------------------------------------------------------- /internal/schema/struct.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/gin-admin/gin-admin-cli/v10/internal/utils" 8 | ) 9 | 10 | type S struct { 11 | RootImportPath string `yaml:"-" json:"-"` 12 | ModuleImportPath string `yaml:"-" json:"-"` 13 | UtilImportPath string `yaml:"-" json:"-"` 14 | Include struct { 15 | ID bool 16 | Status bool 17 | CreatedAt bool 18 | UpdatedAt bool 19 | Sequence bool 20 | } `yaml:"-" json:"-"` 21 | Module string `yaml:"module,omitempty" json:"module,omitempty"` 22 | Name string `yaml:"name,omitempty" json:"name,omitempty"` 23 | TableName string `yaml:"table_name,omitempty" json:"table_name,omitempty"` 24 | Comment string `yaml:"comment,omitempty" json:"comment,omitempty"` 25 | Outputs []string `yaml:"outputs,omitempty" json:"outputs,omitempty"` 26 | ForceWrite bool `yaml:"force_write,omitempty" json:"force_write,omitempty"` 27 | TplType string `yaml:"tpl_type,omitempty" json:"tpl_type,omitempty"` // crud/tree 28 | DisablePagination bool `yaml:"disable_pagination,omitempty" json:"disable_pagination,omitempty"` 29 | DisableDefaultFields bool `yaml:"disable_default_fields,omitempty" json:"disable_default_fields,omitempty"` 30 | FillGormCommit bool `yaml:"fill_gorm_commit,omitempty" json:"fill_gorm_commit,omitempty"` 31 | FillRouterPrefix bool `yaml:"fill_router_prefix,omitempty" json:"fill_router_prefix,omitempty"` 32 | Fields []*Field `yaml:"fields,omitempty" json:"fields,omitempty"` 33 | GenerateFE bool `yaml:"generate_fe,omitempty" json:"generate_fe,omitempty"` 34 | FETpl string `yaml:"fe_tpl,omitempty" json:"fe_tpl,omitempty"` // react/react-v5-i18n 35 | FEMapping map[string]string `yaml:"fe_mapping,omitempty" json:"fe_mapping,omitempty"` // tpl -> file 36 | Extra map[string]interface{} `yaml:"extra,omitempty" json:"extra,omitempty"` 37 | } 38 | 39 | func (a *S) Format() *S { 40 | if a.TplType != "" { 41 | a.TplType = strings.ToLower(a.TplType) 42 | } 43 | 44 | if !a.DisableDefaultFields { 45 | var fields []*Field 46 | fields = append(fields, &Field{ 47 | Name: "ID", 48 | Type: "string", 49 | GormTag: "size:20;primaryKey;", 50 | Comment: "Unique ID", 51 | }) 52 | fields = append(fields, a.Fields...) 53 | 54 | if a.TplType == "tree" { 55 | fields = append(fields, &Field{ 56 | Name: "ParentID", 57 | Type: "string", 58 | GormTag: "size:20;index;", 59 | Comment: "Parent ID", 60 | Query: &FieldQuery{}, 61 | Form: &FieldForm{}, 62 | }) 63 | fields = append(fields, &Field{ 64 | Name: "ParentPath", 65 | Type: "string", 66 | GormTag: "size:255;index;", 67 | Comment: "Parent path (split by .)", 68 | Query: &FieldQuery{ 69 | Name: "ParentPathPrefix", 70 | OP: "LIKE", 71 | Args: `v + "%"`, 72 | }, 73 | }) 74 | fields = append(fields, &Field{ 75 | Name: "Children", 76 | Type: fmt.Sprintf("*%s", utils.ToPlural(a.Name)), 77 | GormTag: "-", 78 | Comment: "Children nodes", 79 | }) 80 | } 81 | 82 | fields = append(fields, &Field{ 83 | Name: "CreatedAt", 84 | Type: "time.Time", 85 | GormTag: "index;", 86 | Comment: "Create time", 87 | Order: "DESC", 88 | }) 89 | fields = append(fields, &Field{ 90 | Name: "UpdatedAt", 91 | Type: "time.Time", 92 | GormTag: "index;", 93 | Comment: "Update time", 94 | }) 95 | a.Fields = fields 96 | } 97 | 98 | for i, item := range a.Fields { 99 | switch item.Name { 100 | case "ID": 101 | a.Include.ID = true 102 | case "Status": 103 | a.Include.Status = true 104 | case "CreatedAt": 105 | a.Include.CreatedAt = true 106 | case "UpdatedAt": 107 | a.Include.UpdatedAt = true 108 | case "Sequence": 109 | a.Include.Sequence = true 110 | } 111 | if a.FillGormCommit && item.Comment != "" { 112 | if len([]byte(item.GormTag)) > 0 && !strings.HasSuffix(item.GormTag, ";") { 113 | item.GormTag += ";" 114 | } 115 | item.GormTag += fmt.Sprintf("comment:%s;", item.Comment) 116 | } 117 | a.Fields[i] = item.Format() 118 | } 119 | 120 | return a 121 | } 122 | 123 | type Field struct { 124 | Name string `yaml:"name,omitempty" json:"name,omitempty"` 125 | Type string `yaml:"type,omitempty" json:"type,omitempty"` 126 | GormTag string `yaml:"gorm_tag,omitempty" json:"gorm_tag,omitempty"` 127 | JSONTag string `yaml:"json_tag,omitempty" json:"json_tag,omitempty"` 128 | CustomTag string `yaml:"custom_tag,omitempty" json:"custom_tag,omitempty"` 129 | Comment string `yaml:"comment,omitempty" json:"comment,omitempty"` 130 | Query *FieldQuery `yaml:"query,omitempty" json:"query,omitempty"` 131 | Queries []*FieldQuery `yaml:"queries,omitempty" json:"queries,omitempty"` 132 | Order string `yaml:"order,omitempty" json:"order,omitempty"` 133 | Form *FieldForm `yaml:"form,omitempty" json:"form,omitempty"` 134 | Unique bool `yaml:"unique,omitempty" json:"unique,omitempty"` 135 | Extra map[string]interface{} `yaml:"extra,omitempty" json:"extra,omitempty"` 136 | } 137 | 138 | func (a *Field) Format() *Field { 139 | if a.JSONTag != "" { 140 | if vv := strings.Split(a.JSONTag, ","); len(vv) > 1 { 141 | if vv[0] == "" { 142 | vv[0] = utils.ToLowerUnderlinedNamer(a.Name) 143 | a.JSONTag = strings.Join(vv, ",") 144 | } 145 | } 146 | } else { 147 | a.JSONTag = utils.ToLowerUnderlinedNamer(a.Name) 148 | } 149 | 150 | if a.Query != nil { 151 | if a.Query.Name == "" { 152 | a.Query.Name = a.Name 153 | } 154 | if a.Query.Comment == "" { 155 | a.Query.Comment = a.Comment 156 | } 157 | if a.Query.InQuery && a.Query.FormTag == "" { 158 | a.Query.FormTag = utils.ToLowerCamel(a.Name) 159 | } 160 | if a.Query.OP == "" { 161 | a.Query.OP = "=" 162 | } 163 | } 164 | 165 | if a.Form != nil { 166 | if a.Form.Name == "" { 167 | a.Form.Name = a.Name 168 | } 169 | if a.Form.JSONTag != "" { 170 | if vv := strings.Split(a.Form.JSONTag, ","); len(vv) > 1 { 171 | if vv[0] == "" { 172 | vv[0] = utils.ToLowerUnderlinedNamer(a.Name) 173 | a.Form.JSONTag = strings.Join(vv, ",") 174 | } 175 | } 176 | } else { 177 | a.Form.JSONTag = utils.ToLowerUnderlinedNamer(a.Name) 178 | } 179 | if a.Form.Comment == "" { 180 | a.Form.Comment = a.Comment 181 | } 182 | } 183 | return a 184 | } 185 | 186 | type FieldQuery struct { 187 | Name string `yaml:"name,omitempty" json:"name,omitempty"` 188 | InQuery bool `yaml:"in_query,omitempty" json:"in_query,omitempty"` 189 | FormTag string `yaml:"form_tag,omitempty" json:"form_tag,omitempty"` 190 | BindingTag string `yaml:"binding_tag,omitempty" json:"binding_tag,omitempty"` 191 | CustomTag string `yaml:"custom_tag,omitempty" json:"custom_tag,omitempty"` 192 | Comment string `yaml:"comment,omitempty" json:"comment,omitempty"` 193 | IfCond string `yaml:"cond,omitempty" json:"cond,omitempty"` 194 | OP string `yaml:"op,omitempty" json:"op,omitempty"` // LIKE/=//<=/>=/<> 195 | Args string `yaml:"args,omitempty" json:"args,omitempty"` // v + "%" 196 | } 197 | 198 | type FieldForm struct { 199 | Name string `yaml:"name,omitempty" json:"name,omitempty"` 200 | JSONTag string `yaml:"json_tag,omitempty" json:"json_tag,omitempty"` 201 | BindingTag string `yaml:"binding_tag,omitempty" json:"binding_tag,omitempty"` 202 | CustomTag string `yaml:"custom_tag,omitempty" json:"custom_tag,omitempty"` 203 | Comment string `yaml:"comment,omitempty" json:"comment,omitempty"` 204 | } 205 | -------------------------------------------------------------------------------- /internal/actions/new.go: -------------------------------------------------------------------------------- 1 | package actions 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "fmt" 7 | "io/fs" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | 12 | "github.com/gin-admin/gin-admin-cli/v10/internal/utils" 13 | "go.uber.org/zap" 14 | ) 15 | 16 | const ( 17 | defaultGitURL = "https://github.com/LyricTian/gin-admin.git" 18 | ) 19 | 20 | func New(cfg NewConfig) *NewAction { 21 | if cfg.AppName == "" { 22 | cfg.AppName = strings.ToLower(strings.ReplaceAll(cfg.Name, "-", "")) 23 | } 24 | if cfg.GitURL == "" { 25 | cfg.GitURL = defaultGitURL 26 | } 27 | if cfg.PkgName == "" { 28 | cfg.PkgName = cfg.Name 29 | } 30 | if cfg.Version == "" { 31 | cfg.Version = "v1.0.0" 32 | } 33 | if cfg.Description == "" { 34 | name := []byte(cfg.AppName) 35 | name[0] = name[0] - 32 36 | cfg.Description = fmt.Sprintf("%s API service", name) 37 | } 38 | 39 | return &NewAction{ 40 | logger: zap.S().Named("[NEW]"), 41 | cfg: &cfg, 42 | } 43 | } 44 | 45 | type NewConfig struct { 46 | Dir string 47 | Name string 48 | AppName string 49 | PkgName string 50 | Description string 51 | Version string 52 | GitURL string 53 | GitBranch string 54 | FeDir string 55 | FeName string 56 | FeGitURL string 57 | FeGitBranch string 58 | } 59 | 60 | type NewAction struct { 61 | logger *zap.SugaredLogger 62 | cfg *NewConfig 63 | } 64 | 65 | func (a *NewAction) Run(ctx context.Context) error { 66 | a.logger.Infof("Create project %s in %s", a.cfg.Name, a.cfg.Dir) 67 | projectDir := filepath.Join(a.cfg.Dir, a.cfg.Name) 68 | if exists, err := utils.ExistsFile(projectDir); err != nil { 69 | return err 70 | } else if exists { 71 | a.logger.Warnf("Project %s already exists", a.cfg.Name) 72 | return nil 73 | } 74 | _ = os.MkdirAll(a.cfg.Dir, os.ModePerm) 75 | 76 | if err := utils.ExecGitClone(a.cfg.Dir, a.cfg.GitURL, a.cfg.GitBranch, a.cfg.Name); err != nil { 77 | return err 78 | } 79 | 80 | cleanFiles := []string{".git", "CHANGELOG.md", "LICENSE", "README.md", "README_CN.md", "internal/swagger/v3", "internal/wirex/wire_gen.go", "swagger.jpeg"} 81 | for _, f := range cleanFiles { 82 | if err := os.RemoveAll(filepath.Join(projectDir, f)); err != nil { 83 | return err 84 | } 85 | } 86 | 87 | a.logger.Infof("Update project info...") 88 | oldModuleName, err := a.getModuleName(projectDir) 89 | if err != nil { 90 | return err 91 | } 92 | oldProjectInfo, err := a.getProjectInfo(projectDir) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | appName := a.cfg.AppName 98 | err = filepath.WalkDir(projectDir, func(path string, d fs.DirEntry, err error) error { 99 | if err != nil { 100 | return err 101 | } 102 | if d.IsDir() { 103 | return nil 104 | } 105 | 106 | info, err := d.Info() 107 | if err != nil { 108 | return err 109 | } 110 | 111 | name := d.Name() 112 | if name == "main.go" || name == "config.go" || 113 | name == "Makefile" || name == "Dockerfile" || name == ".gitignore" { 114 | f, err := os.ReadFile(path) 115 | if err != nil { 116 | return err 117 | } 118 | 119 | f = []byte(strings.ReplaceAll(string(f), oldProjectInfo.AppName, appName)) 120 | f = []byte(strings.ReplaceAll(string(f), oldProjectInfo.Version, a.cfg.Version)) 121 | f = []byte(strings.ReplaceAll(string(f), oldProjectInfo.Description, a.cfg.Description)) 122 | f = []byte(strings.ReplaceAll(string(f), oldModuleName, a.cfg.PkgName)) 123 | return os.WriteFile(path, f, info.Mode()) 124 | } 125 | 126 | if name == "go.mod" || strings.HasSuffix(name, ".go") { 127 | return utils.ReplaceFileContent(path, []byte(oldModuleName), []byte(a.cfg.PkgName), info.Mode()) 128 | } 129 | 130 | if strings.HasSuffix(name, ".toml") { 131 | return utils.ReplaceFileContent(path, []byte(oldProjectInfo.AppName), []byte(appName), info.Mode()) 132 | } 133 | 134 | return nil 135 | }) 136 | if err != nil { 137 | return err 138 | } 139 | 140 | err = utils.WriteFile(filepath.Join(projectDir, "README.md"), []byte(a.getReadme())) 141 | if err != nil { 142 | return err 143 | } 144 | 145 | a.logger.Infof("Generate wire and swagger files...") 146 | _ = utils.ExecGoModTidy(projectDir) 147 | _ = utils.ExecSwagGen(projectDir, "./main.go", "./internal/swagger") 148 | _ = utils.ExecWireGen(projectDir, "internal/wirex") 149 | _ = utils.ExecGitInit(projectDir) 150 | 151 | fmt.Println("🎉 Congratulations, your project has been created successfully.") 152 | fmt.Println("------------------------------------------------------------") 153 | fmt.Println(utils.GetDefaultProjectTree()) 154 | fmt.Println("------------------------------------------------------------") 155 | 156 | fmt.Println("🚀 You can execute the following commands to start the project:") 157 | fmt.Println("------------------------------------------------------------") 158 | fmt.Printf("cd %s\n", projectDir) 159 | fmt.Println("make start") 160 | fmt.Println("------------------------------------------------------------") 161 | 162 | if err := a.generateFE(ctx); err != nil { 163 | return err 164 | } 165 | 166 | return nil 167 | } 168 | 169 | func (a *NewAction) getModuleName(projectDir string) (string, error) { 170 | f, err := os.Open(filepath.Join(projectDir, "go.mod")) 171 | if err != nil { 172 | return "", err 173 | } 174 | defer f.Close() 175 | 176 | scanner := bufio.NewScanner(f) 177 | for scanner.Scan() { 178 | line := strings.TrimSpace(scanner.Text()) 179 | if strings.HasPrefix(line, "module") { 180 | return strings.TrimSpace(strings.TrimPrefix(line, "module")), nil 181 | } 182 | } 183 | return "", nil 184 | } 185 | 186 | type projectInfo struct { 187 | AppName string 188 | Description string 189 | Version string 190 | } 191 | 192 | func (a *NewAction) getProjectInfo(projectDir string) (*projectInfo, error) { 193 | var info projectInfo 194 | f, err := os.Open(filepath.Join(projectDir, "main.go")) 195 | if err != nil { 196 | return nil, err 197 | } 198 | defer f.Close() 199 | 200 | scanner := bufio.NewScanner(f) 201 | for scanner.Scan() { 202 | line := strings.TrimSpace(scanner.Text()) 203 | switch { 204 | case strings.HasPrefix(line, "// @title"): 205 | info.AppName = strings.TrimSpace(strings.TrimPrefix(line, "// @title")) 206 | case strings.HasPrefix(line, "// @version"): 207 | info.Version = strings.TrimSpace(strings.TrimPrefix(line, "// @version")) 208 | case strings.HasPrefix(line, "// @description"): 209 | info.Description = strings.TrimSpace(strings.TrimPrefix(line, "// @description")) 210 | } 211 | } 212 | return &info, nil 213 | } 214 | 215 | func (a *NewAction) getReadme() string { 216 | var sb strings.Builder 217 | sb.WriteString("# " + a.cfg.Name + "\n\n") 218 | sb.WriteString("> " + a.cfg.Description + "\n\n") 219 | 220 | sb.WriteString("## Quick Start\n\n") 221 | sb.WriteString("```bash\n") 222 | sb.WriteString("make start\n") 223 | sb.WriteString("```\n\n") 224 | 225 | sb.WriteString("## Build\n\n") 226 | sb.WriteString("```bash\n") 227 | sb.WriteString("make build\n") 228 | sb.WriteString("```\n\n") 229 | 230 | sb.WriteString("## Generate wire inject files\n\n") 231 | sb.WriteString("```bash\n") 232 | sb.WriteString("make wire\n") 233 | sb.WriteString("```\n\n") 234 | 235 | sb.WriteString("## Generate swagger documents\n\n") 236 | sb.WriteString("```bash\n") 237 | sb.WriteString("make swagger\n") 238 | sb.WriteString("```\n\n") 239 | 240 | return sb.String() 241 | } 242 | 243 | func (a *NewAction) generateFE(_ context.Context) error { 244 | if a.cfg.FeDir == "" { 245 | return nil 246 | } 247 | 248 | a.logger.Infof("Create frontend project %s in %s", a.cfg.FeName, a.cfg.FeDir) 249 | feDir, err := filepath.Abs(filepath.Join(a.cfg.FeDir, a.cfg.FeName)) 250 | if err != nil { 251 | return err 252 | } 253 | 254 | if exists, err := utils.ExistsFile(feDir); err != nil { 255 | return err 256 | } else if exists { 257 | a.logger.Warnf("Frontend project %s already exists", a.cfg.FeName) 258 | return nil 259 | } 260 | 261 | _ = os.MkdirAll(a.cfg.FeDir, os.ModePerm) 262 | err = utils.ExecGitClone(a.cfg.FeDir, a.cfg.FeGitURL, a.cfg.FeGitBranch, a.cfg.FeName) 263 | if err != nil { 264 | return err 265 | } 266 | 267 | cleanFiles := []string{".git", "LICENSE", "README.md", "demo.png"} 268 | for _, file := range cleanFiles { 269 | if err := os.RemoveAll(filepath.Join(feDir, file)); err != nil { 270 | return err 271 | } 272 | } 273 | 274 | err = utils.WriteFile(filepath.Join(feDir, "README.md"), []byte(a.getFeReadme())) 275 | if err != nil { 276 | return err 277 | } 278 | 279 | a.logger.Infof("🎉 Frontend project %s has been created successfully", a.cfg.FeName) 280 | fmt.Println("------------------------------------------------------------") 281 | fmt.Printf("Git repository: %s\n", a.cfg.FeGitURL) 282 | fmt.Printf("Branch: %s\n", a.cfg.FeGitBranch) 283 | fmt.Printf("Directory: %s\n", feDir) 284 | fmt.Println("------------------------------------------------------------") 285 | 286 | return nil 287 | } 288 | 289 | func (a *NewAction) getFeReadme() string { 290 | var sb strings.Builder 291 | sb.WriteString("# " + a.cfg.FeName + "\n\n") 292 | sb.WriteString("> " + a.cfg.Description + "\n\n") 293 | 294 | sb.WriteString("## Environment Prepare\n\n") 295 | sb.WriteString("> You can use [nvm](https://github.com/nvm-sh/nvm) to manage node version.\n\n") 296 | sb.WriteString("- Node.js v16.20.2\n\n") 297 | 298 | sb.WriteString("## Quick Start\n\n") 299 | 300 | sb.WriteString("### Install dependencies\n\n") 301 | sb.WriteString("```bash\n") 302 | sb.WriteString("npm install\n") 303 | sb.WriteString("```\n\n") 304 | 305 | sb.WriteString("### Start project\n\n") 306 | sb.WriteString("```bash\n") 307 | sb.WriteString("npm start\n") 308 | sb.WriteString("```\n\n") 309 | 310 | sb.WriteString("### Build project\n\n") 311 | sb.WriteString("```bash\n") 312 | sb.WriteString("npm run build\n") 313 | sb.WriteString("```\n\n") 314 | 315 | return sb.String() 316 | } 317 | -------------------------------------------------------------------------------- /tpls/default/biz.go.tpl: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "{{.UtilImportPath}}" 8 | "{{.ModuleImportPath}}/dal" 9 | "{{.ModuleImportPath}}/schema" 10 | "{{.RootImportPath}}/pkg/errors" 11 | ) 12 | 13 | {{$name := .Name}} 14 | {{$includeID := .Include.ID}} 15 | {{$includeCreatedAt := .Include.CreatedAt}} 16 | {{$includeUpdatedAt := .Include.UpdatedAt}} 17 | {{$includeStatus := .Include.Status}} 18 | {{$treeTpl := eq .TplType "tree"}} 19 | 20 | {{with .Comment}}// {{.}}{{else}}// Defining the `{{$name}}` business logic.{{end}} 21 | type {{$name}} struct { 22 | Trans *util.Trans 23 | {{$name}}DAL *dal.{{$name}} 24 | } 25 | 26 | // Query {{lowerSpacePlural .Name}} from the data access object based on the provided parameters and options. 27 | func (a *{{$name}}) Query(ctx context.Context, params schema.{{$name}}QueryParam) (*schema.{{$name}}QueryResult, error) { 28 | params.Pagination = {{if .DisablePagination}}false{{else}}true{{end}} 29 | 30 | result, err := a.{{$name}}DAL.Query(ctx, params, schema.{{$name}}QueryOptions{ 31 | QueryOptions: util.QueryOptions{ 32 | OrderFields: []util.OrderByParam{ 33 | {{- range .Fields}}{{$fieldName := .Name}} 34 | {{- if .Order}} 35 | {Field: "{{lowerUnderline $fieldName}}", Direction: {{if eq .Order "DESC"}}util.DESC{{else}}util.ASC{{end}}}, 36 | {{- end}} 37 | {{- end}} 38 | }, 39 | }, 40 | }) 41 | if err != nil { 42 | return nil, err 43 | } 44 | {{- if $treeTpl}} 45 | result.Data = result.Data.ToTree() 46 | sort.Sort(result.Data) 47 | {{- end}} 48 | return result, nil 49 | } 50 | 51 | {{- if $treeTpl}} 52 | func (a *{{$name}}) appendChildren(ctx context.Context, data schema.{{plural .Name}}) (schema.{{plural .Name}}, error) { 53 | if len(data) == 0 { 54 | return data, nil 55 | } 56 | 57 | existsInData := func(id string) bool { 58 | for _, item := range data { 59 | if item.ID == id { 60 | return true 61 | } 62 | } 63 | return false 64 | } 65 | 66 | for _, item := range data { 67 | childResult, err := a.{{$name}}DAL.Query(ctx, schema.{{$name}}QueryParam{ 68 | ParentPathPrefix: item.ParentPath + item.ID + util.TreePathDelimiter, 69 | }) 70 | if err != nil { 71 | return nil, err 72 | } 73 | for _, child := range childResult.Data { 74 | if existsInData(child.ID) { 75 | continue 76 | } 77 | data = append(data, child) 78 | } 79 | } 80 | 81 | parentIDs := data.SplitParentIDs() 82 | if len(parentIDs) > 0 { 83 | parentResult, err := a.{{$name}}DAL.Query(ctx, schema.{{$name}}QueryParam{ 84 | InIDs: parentIDs, 85 | }) 86 | if err != nil { 87 | return nil, err 88 | } 89 | for _, p := range parentResult.Data { 90 | if existsInData(p.ID) { 91 | continue 92 | } 93 | data = append(data, p) 94 | } 95 | sort.Sort(data) 96 | } 97 | 98 | return data, nil 99 | } 100 | {{- end}} 101 | 102 | // Get the specified {{lowerSpace .Name}} from the data access object. 103 | func (a *{{$name}}) Get(ctx context.Context, id string) (*schema.{{$name}}, error) { 104 | {{lowerCamel $name}}, err := a.{{$name}}DAL.Get(ctx, id) 105 | if err != nil { 106 | return nil, err 107 | } else if {{lowerCamel $name}} == nil { 108 | return nil, errors.NotFound("", "{{titleSpace $name}} not found") 109 | } 110 | return {{lowerCamel $name}}, nil 111 | } 112 | 113 | // Create a new {{lowerSpace .Name}} in the data access object. 114 | func (a *{{$name}}) Create(ctx context.Context, formItem *schema.{{$name}}Form) (*schema.{{$name}}, error) { 115 | {{lowerCamel $name}} := &schema.{{$name}}{ 116 | {{if $includeID}}ID: util.NewXID(),{{end}} 117 | {{if $includeCreatedAt}}CreatedAt: time.Now(),{{end}} 118 | } 119 | 120 | {{- range .Fields}} 121 | {{- if .Unique}} 122 | {{- if $treeTpl}} 123 | if exists,err := a.{{$name}}DAL.Exists{{.Name}}(ctx, formItem.ParentID, formItem.{{.Name}}); err != nil { 124 | return nil, err 125 | } else if exists { 126 | return nil, errors.BadRequest("", "{{.Name}} already exists") 127 | } 128 | {{- else}} 129 | if exists,err := a.{{$name}}DAL.Exists{{.Name}}(ctx, formItem.{{.Name}}); err != nil { 130 | return nil, err 131 | } else if exists { 132 | return nil, errors.BadRequest("", "{{.Name}} already exists") 133 | } 134 | {{- end}} 135 | {{- end}} 136 | {{- end}} 137 | 138 | {{- if $treeTpl}} 139 | if parentID := formItem.ParentID; parentID != "" { 140 | parent, err := a.{{$name}}DAL.Get(ctx, parentID) 141 | if err != nil { 142 | return nil, err 143 | } else if parent == nil { 144 | return nil, errors.NotFound("", "Parent not found") 145 | } 146 | {{lowerCamel $name}}.ParentPath = parent.ParentPath + parent.ID + util.TreePathDelimiter 147 | } 148 | {{- end}} 149 | 150 | if err := formItem.FillTo({{lowerCamel $name}}); err != nil { 151 | return nil, err 152 | } 153 | 154 | err := a.Trans.Exec(ctx, func(ctx context.Context) error { 155 | if err := a.{{$name}}DAL.Create(ctx, {{lowerCamel $name}}); err != nil { 156 | return err 157 | } 158 | return nil 159 | }) 160 | if err != nil { 161 | return nil, err 162 | } 163 | return {{lowerCamel $name}}, nil 164 | } 165 | 166 | // Update the specified {{lowerSpace .Name}} in the data access object. 167 | func (a *{{$name}}) Update(ctx context.Context, id string, formItem *schema.{{$name}}Form) error { 168 | {{lowerCamel $name}}, err := a.{{$name}}DAL.Get(ctx, id) 169 | if err != nil { 170 | return err 171 | } else if {{lowerCamel $name}} == nil { 172 | return errors.NotFound("", "{{titleSpace $name}} not found") 173 | } 174 | 175 | {{- range .Fields}} 176 | {{- if .Unique}} 177 | {{- if $treeTpl}} 178 | if {{lowerCamel $name}}.{{.Name}} != formItem.{{.Name}} { 179 | if exists,err := a.{{$name}}DAL.Exists{{.Name}}(ctx, formItem.ParentID, formItem.{{.Name}}); err != nil { 180 | return err 181 | } else if exists { 182 | return errors.BadRequest("", "{{.Name}} already exists") 183 | } 184 | } 185 | {{- else}} 186 | if {{lowerCamel $name}}.{{.Name}} != formItem.{{.Name}} { 187 | if exists,err := a.{{$name}}DAL.Exists{{.Name}}(ctx, formItem.{{.Name}}); err != nil { 188 | return err 189 | } else if exists { 190 | return errors.BadRequest("", "{{.Name}} already exists") 191 | } 192 | } 193 | {{- end}} 194 | {{- end}} 195 | {{- end}} 196 | 197 | {{- if $treeTpl}} 198 | oldParentPath := {{lowerCamel $name}}.ParentPath 199 | {{- if $includeStatus}} 200 | oldStatus := {{lowerCamel $name}}.Status 201 | {{- end}} 202 | var childData schema.{{plural .Name}} 203 | if {{lowerCamel $name}}.ParentID != formItem.ParentID { 204 | if parentID := formItem.ParentID; parentID != "" { 205 | parent, err := a.{{$name}}DAL.Get(ctx, parentID) 206 | if err != nil { 207 | return err 208 | } else if parent == nil { 209 | return errors.NotFound("", "Parent not found") 210 | } 211 | {{lowerCamel $name}}.ParentPath = parent.ParentPath + parent.ID + util.TreePathDelimiter 212 | } else { 213 | {{lowerCamel $name}}.ParentPath = "" 214 | } 215 | 216 | childResult, err := a.{{$name}}DAL.Query(ctx, schema.{{$name}}QueryParam{ 217 | ParentPathPrefix: oldParentPath + {{lowerCamel $name}}.ID + util.TreePathDelimiter, 218 | }, schema.{{$name}}QueryOptions{ 219 | QueryOptions: util.QueryOptions{ 220 | SelectFields: []string{"id", "parent_path"}, 221 | }, 222 | }) 223 | if err != nil { 224 | return err 225 | } 226 | childData = childResult.Data 227 | } 228 | {{- end}} 229 | 230 | if err := formItem.FillTo({{lowerCamel $name}}); err != nil { 231 | return err 232 | } 233 | {{if $includeUpdatedAt}}{{lowerCamel $name}}.UpdatedAt = time.Now(){{end}} 234 | 235 | return a.Trans.Exec(ctx, func(ctx context.Context) error { 236 | if err := a.{{$name}}DAL.Update(ctx, {{lowerCamel $name}}); err != nil { 237 | return err 238 | } 239 | 240 | {{- if $treeTpl}} 241 | {{- if $includeStatus}} 242 | if oldStatus != formItem.Status { 243 | opath := oldParentPath + {{lowerCamel $name}}.ID + util.TreePathDelimiter 244 | if err := a.{{$name}}DAL.UpdateStatusByParentPath(ctx, opath, formItem.Status); err != nil { 245 | return err 246 | } 247 | } 248 | {{- end}} 249 | 250 | for _, child := range childData { 251 | opath := oldParentPath + {{lowerCamel $name}}.ID + util.TreePathDelimiter 252 | npath := {{lowerCamel $name}}.ParentPath + {{lowerCamel $name}}.ID + util.TreePathDelimiter 253 | err := a.{{$name}}DAL.UpdateParentPath(ctx, child.ID, strings.Replace(child.ParentPath, opath, npath, 1)) 254 | if err != nil { 255 | return err 256 | } 257 | } 258 | {{- end}} 259 | return nil 260 | }) 261 | } 262 | 263 | // Delete the specified {{lowerSpace .Name}} from the data access object. 264 | func (a *{{$name}}) Delete(ctx context.Context, id string) error { 265 | {{- if $treeTpl}} 266 | {{lowerCamel $name}}, err := a.{{$name}}DAL.Get(ctx, id) 267 | if err != nil { 268 | return err 269 | } else if {{lowerCamel $name}} == nil { 270 | return errors.NotFound("", "{{titleSpace $name}} not found") 271 | } 272 | 273 | childResult, err := a.{{$name}}DAL.Query(ctx, schema.{{$name}}QueryParam{ 274 | ParentPathPrefix: {{lowerCamel $name}}.ParentPath + {{lowerCamel $name}}.ID + util.TreePathDelimiter, 275 | }, schema.{{$name}}QueryOptions{ 276 | QueryOptions: util.QueryOptions{ 277 | SelectFields: []string{"id"}, 278 | }, 279 | }) 280 | if err != nil { 281 | return err 282 | } 283 | {{- else}} 284 | exists, err := a.{{$name}}DAL.Exists(ctx, id) 285 | if err != nil { 286 | return err 287 | } else if !exists { 288 | return errors.NotFound("", "{{titleSpace $name}} not found") 289 | } 290 | {{- end}} 291 | 292 | return a.Trans.Exec(ctx, func(ctx context.Context) error { 293 | if err := a.{{$name}}DAL.Delete(ctx, id); err != nil { 294 | return err 295 | } 296 | {{- if $treeTpl}} 297 | for _, child := range childResult.Data { 298 | if err := a.{{$name}}DAL.Delete(ctx, child.ID); err != nil { 299 | return err 300 | } 301 | } 302 | {{- end}} 303 | return nil 304 | }) 305 | } 306 | -------------------------------------------------------------------------------- /internal/actions/generate.go: -------------------------------------------------------------------------------- 1 | package actions 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/gin-admin/gin-admin-cli/v10/internal/parser" 10 | "github.com/gin-admin/gin-admin-cli/v10/internal/schema" 11 | "github.com/gin-admin/gin-admin-cli/v10/internal/tfs" 12 | "github.com/gin-admin/gin-admin-cli/v10/internal/utils" 13 | "go.uber.org/zap" 14 | ) 15 | 16 | type GenerateConfig struct { 17 | Dir string 18 | TplType string 19 | Module string 20 | ModulePath string 21 | WirePath string 22 | SwaggerPath string 23 | FEDir string 24 | } 25 | 26 | func Generate(cfg GenerateConfig) *GenerateAction { 27 | return &GenerateAction{ 28 | logger: zap.S().Named("[GEN]"), 29 | cfg: &cfg, 30 | fs: tfs.Ins, 31 | rootImportPath: parser.GetRootImportPath(cfg.Dir), 32 | moduleImportPath: parser.GetModuleImportPath(cfg.Dir, cfg.ModulePath, cfg.Module), 33 | UtilImportPath: parser.GetUtilImportPath(cfg.Dir, cfg.ModulePath), 34 | } 35 | } 36 | 37 | type GenerateAction struct { 38 | logger *zap.SugaredLogger 39 | cfg *GenerateConfig 40 | fs tfs.FS 41 | rootImportPath string 42 | moduleImportPath string 43 | UtilImportPath string 44 | } 45 | 46 | // Run generate command 47 | func (a *GenerateAction) RunWithConfig(ctx context.Context, cfgName string) error { 48 | var parseFile = func(name string) ([]*schema.S, error) { 49 | var data []*schema.S 50 | switch filepath.Ext(name) { 51 | case ".json": 52 | if err := utils.ParseJSONFile(name, &data); err != nil { 53 | return nil, err 54 | } 55 | case ".yaml", ".yml": 56 | if err := utils.ParseYAMLFile(name, &data); err != nil { 57 | return nil, err 58 | } 59 | default: 60 | a.logger.Warnf("Ignore file %s, only support json/yaml/yml", name) 61 | } 62 | if len(data) == 0 { 63 | return nil, nil 64 | } 65 | return data, nil 66 | } 67 | 68 | if utils.IsDir(cfgName) { 69 | var data []*schema.S 70 | err := filepath.WalkDir(cfgName, func(path string, d os.DirEntry, err error) error { 71 | if err != nil { 72 | return err 73 | } 74 | if d.IsDir() { 75 | return nil 76 | } 77 | items, err := parseFile(path) 78 | if err != nil { 79 | return err 80 | } 81 | data = append(data, items...) 82 | return nil 83 | }) 84 | if err != nil { 85 | return err 86 | } 87 | return a.run(ctx, data) 88 | } 89 | 90 | data, err := parseFile(cfgName) 91 | if err != nil { 92 | return err 93 | } else if len(data) == 0 { 94 | a.logger.Warnf("No data found in file %s", cfgName) 95 | return nil 96 | } 97 | return a.run(ctx, data) 98 | } 99 | 100 | func (a *GenerateAction) RunWithStruct(ctx context.Context, s *schema.S) error { 101 | return a.run(ctx, []*schema.S{s}) 102 | } 103 | 104 | func (a *GenerateAction) run(ctx context.Context, data []*schema.S) error { 105 | moduleMap := make(map[string]bool) 106 | 107 | for _, d := range data { 108 | if d.Module == "" && a.cfg.Module == "" { 109 | return fmt.Errorf("Struct %s module is empty", d.Name) 110 | } 111 | 112 | if d.Module == "" { 113 | d.Module = a.cfg.Module 114 | } 115 | if !moduleMap[d.Module] { 116 | moduleMap[d.Module] = true 117 | } 118 | if err := a.generate(ctx, d); err != nil { 119 | return err 120 | } 121 | if d.GenerateFE { 122 | if err := a.generateFE(ctx, d); err != nil { 123 | return err 124 | } 125 | } 126 | } 127 | 128 | for module := range moduleMap { 129 | modsTplData, err := parser.ModifyModsFile(ctx, parser.BasicArgs{ 130 | Dir: a.cfg.Dir, 131 | ModuleName: module, 132 | ModulePath: a.cfg.ModulePath, 133 | Flag: parser.AstFlagGen, 134 | }) 135 | if err != nil { 136 | a.logger.Errorf("Failed to modify mods file, err: %s", err) 137 | return err 138 | } 139 | 140 | err = a.write(ctx, module, "", parser.FileForMods, modsTplData, false) 141 | if err != nil { 142 | return err 143 | } 144 | } 145 | 146 | return a.execWireAndSwag(ctx) 147 | } 148 | 149 | func (a *GenerateAction) getGoTplFile(tplName, tplType string) string { 150 | tplName = fmt.Sprintf("%s.go.tpl", tplName) 151 | if tplType == "" && a.cfg.TplType != "" { 152 | tplType = a.cfg.TplType 153 | } 154 | 155 | if tplType != "" { 156 | p := filepath.Join(tplType, tplName) 157 | if ok, _ := utils.ExistsFile(p); ok { 158 | return p 159 | } 160 | return filepath.Join("default", tplName) 161 | } 162 | return tplName 163 | } 164 | 165 | func (a GenerateAction) getAbsPath(file string) (string, error) { 166 | modPath := a.cfg.ModulePath 167 | file = filepath.Join(a.cfg.Dir, modPath, file) 168 | fullPath, err := filepath.Abs(file) 169 | if err != nil { 170 | a.logger.Errorf("Failed to get abs path, err: %s, #file %s", err, file) 171 | return "", err 172 | } 173 | return fullPath, nil 174 | } 175 | 176 | func (a *GenerateAction) write(_ context.Context, moduleName, structName, tpl string, data []byte, checkExists bool) error { 177 | file, err := parser.ParseFilePathFromTpl(moduleName, structName, tpl) 178 | if err != nil { 179 | a.logger.Errorf("Failed to parse file path from tpl, err: %s, #tpl %s", err, tpl) 180 | return err 181 | } 182 | 183 | file, err = a.getAbsPath(file) 184 | if err != nil { 185 | return err 186 | } 187 | 188 | exists, err := utils.ExistsFile(file) 189 | if err != nil { 190 | return err 191 | } 192 | if checkExists && exists { 193 | a.logger.Infof("File exists, skip, #file %s", file) 194 | return nil 195 | } 196 | 197 | a.logger.Infof("Write file: %s", file) 198 | if !exists { 199 | err = os.MkdirAll(filepath.Dir(file), os.ModePerm) 200 | if err != nil { 201 | a.logger.Errorf("Failed to create dir, err: %s, #dir %s", err, filepath.Dir(file)) 202 | return err 203 | } 204 | } 205 | 206 | if exists { 207 | if err := os.Remove(file); err != nil { 208 | a.logger.Errorf("Failed to remove file, err: %s, #file %s", err, file) 209 | return err 210 | } 211 | } 212 | 213 | if err := utils.WriteFile(file, data); err != nil { 214 | a.logger.Errorf("Failed to write file, err: %s, #file %s", err, file) 215 | return err 216 | } 217 | 218 | if err := utils.ExecGoFormat(file); err != nil { 219 | a.logger.Errorf("Failed to exec go format, err: %s, #file %s", err, file) 220 | return nil 221 | } 222 | 223 | if err := utils.ExecGoImports(a.cfg.Dir, file); err != nil { 224 | a.logger.Errorf("Failed to exec go imports, err: %s, #file %s", err, file) 225 | return nil 226 | } 227 | return nil 228 | } 229 | 230 | func (a *GenerateAction) generate(ctx context.Context, dataItem *schema.S) error { 231 | dataItem = dataItem.Format() 232 | dataItem.RootImportPath = a.rootImportPath 233 | dataItem.ModuleImportPath = a.moduleImportPath 234 | dataItem.UtilImportPath = a.UtilImportPath 235 | 236 | genPackages := parser.StructPackages 237 | if len(dataItem.Outputs) > 0 { 238 | genPackages = dataItem.Outputs 239 | } 240 | 241 | for _, pkgName := range genPackages { 242 | tplName := a.getGoTplFile(pkgName, dataItem.TplType) 243 | tplData, err := a.fs.ParseTpl(tplName, dataItem) 244 | if err != nil { 245 | a.logger.Errorf("Failed to parse tpl, err: %s, #struct %s, #tpl %s", err, dataItem.Name, tplName) 246 | return err 247 | } 248 | 249 | err = a.write(ctx, dataItem.Module, dataItem.Name, parser.StructPackageTplPaths[pkgName], tplData, !dataItem.ForceWrite) 250 | if err != nil { 251 | return err 252 | } 253 | } 254 | 255 | basicArgs := parser.BasicArgs{ 256 | Dir: a.cfg.Dir, 257 | ModuleName: dataItem.Module, 258 | ModulePath: a.cfg.ModulePath, 259 | StructName: dataItem.Name, 260 | GenPackages: genPackages, 261 | Flag: parser.AstFlagGen, 262 | FillRouterPrefix: dataItem.FillRouterPrefix, 263 | } 264 | moduleMainTplData, err := parser.ModifyModuleMainFile(ctx, basicArgs) 265 | if err != nil { 266 | a.logger.Errorf("Failed to modify module main file, err: %s, #struct %s", err, dataItem.Name) 267 | return err 268 | } 269 | 270 | err = a.write(ctx, dataItem.Module, dataItem.Name, parser.FileForModuleMain, moduleMainTplData, false) 271 | if err != nil { 272 | return err 273 | } 274 | 275 | moduleWireTplData, err := parser.ModifyModuleWireFile(ctx, basicArgs) 276 | if err != nil { 277 | a.logger.Errorf("Failed to modify module wire file, err: %s, #struct %s", err, dataItem.Name) 278 | return err 279 | } 280 | 281 | err = a.write(ctx, dataItem.Module, dataItem.Name, parser.FileForModuleWire, moduleWireTplData, false) 282 | if err != nil { 283 | return err 284 | } 285 | 286 | return nil 287 | } 288 | 289 | func (a *GenerateAction) execWireAndSwag(_ context.Context) error { 290 | if p := a.cfg.WirePath; p != "" { 291 | if err := utils.ExecWireGen(a.cfg.Dir, p); err != nil { 292 | a.logger.Errorf("Failed to exec wire, err: %s, #wirePath %s", err, p) 293 | } 294 | } 295 | 296 | if p := a.cfg.SwaggerPath; p != "" { 297 | if err := utils.ExecSwagGen(a.cfg.Dir, "main.go", p); err != nil { 298 | a.logger.Errorf("Failed to exec swag, err: %s, #swaggerPath %s", err, p) 299 | } 300 | } 301 | 302 | return nil 303 | } 304 | 305 | func (a *GenerateAction) generateFE(_ context.Context, dataItem *schema.S) error { 306 | for tpl, file := range dataItem.FEMapping { 307 | tplPath := filepath.Join(dataItem.FETpl, tpl) 308 | tplData, err := a.fs.ParseTpl(tplPath, dataItem) 309 | if err != nil { 310 | a.logger.Errorf("Failed to parse tpl, err: %s, #struct %s, #tpl %s", err, dataItem.Name, tplPath) 311 | return err 312 | } 313 | 314 | file, err := filepath.Abs(filepath.Join(a.cfg.FEDir, file)) 315 | if err != nil { 316 | return err 317 | } 318 | 319 | exists, err := utils.ExistsFile(file) 320 | if err != nil { 321 | return err 322 | } 323 | if exists { 324 | a.logger.Infof("File exists, skip, #file %s", file) 325 | continue 326 | } 327 | 328 | _ = os.MkdirAll(filepath.Dir(file), os.ModePerm) 329 | if err := utils.WriteFile(file, tplData); err != nil { 330 | a.logger.Errorf("Failed to write file, err: %s, #file %s", err, file) 331 | return err 332 | } 333 | a.logger.Info("Write file: ", file) 334 | } 335 | return nil 336 | } 337 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [GIN-Admin](https://github.com/LyricTian/gin-admin) efficiency assistant 2 | 3 | > A gin-admin efficiency assistant that provides project initialization, code generation, greatly improves work efficiency, and quickly completes the development of business logic. 4 | 5 | ## Dependencies 6 | 7 | - [Go](https://golang.org/) 1.19+ 8 | - [Wire](github.com/google/wire) `go install github.com/google/wire/cmd/wire@latest` 9 | - [Swag](github.com/swaggo/swag) `go install github.com/swaggo/swag/cmd/swag@latest` 10 | 11 | ## Quick start 12 | 13 | ### Get and install 14 | 15 | ```bash 16 | go install github.com/gin-admin/gin-admin-cli/v10@latest 17 | ``` 18 | 19 | ### Create a new project 20 | 21 | ```bash 22 | gin-admin-cli new -d ~/go/src --name testapp --desc 'A test API service based on golang.' --pkg 'github.com/xxx/testapp' 23 | ``` 24 | 25 | ### Quick generate a struct 26 | 27 | ```bash 28 | gin-admin-cli gen -d ~/go/src/testapp -m SYS --structs Dictionary --structs-comment "Dictionaries management" --structs-router-prefix 29 | ``` 30 | 31 | ### Use config file to generate struct 32 | 33 | > More examples can be found in the [examples directory](https://github.com/gin-admin/gin-admin-cli/tree/master/examples) 34 | 35 | Using `Dictionary` as an example, the configuration file is as follows `dictionary.yaml`: 36 | 37 | ```yaml 38 | - name: Dictionary 39 | comment: Dictionaries management 40 | disable_pagination: true 41 | fill_gorm_commit: true 42 | fill_router_prefix: true 43 | tpl_type: "tree" 44 | fields: 45 | - name: Code 46 | type: string 47 | comment: Code of dictionary (unique for same parent) 48 | gorm_tag: "size:32;" 49 | form: 50 | binding_tag: "required,max=32" 51 | - name: Name 52 | type: string 53 | comment: Display name of dictionary 54 | gorm_tag: "size:128;index" 55 | query: 56 | name: LikeName 57 | in_query: true 58 | form_tag: name 59 | op: LIKE 60 | form: 61 | binding_tag: "required,max=128" 62 | - name: Description 63 | type: string 64 | comment: Details about dictionary 65 | gorm_tag: "size:1024" 66 | form: {} 67 | - name: Sequence 68 | type: int 69 | comment: Sequence for sorting 70 | gorm_tag: "index;" 71 | order: DESC 72 | form: {} 73 | - name: Status 74 | type: string 75 | comment: Status of dictionary (disabled, enabled) 76 | gorm_tag: "size:20;index" 77 | query: {} 78 | form: 79 | binding_tag: "required,oneof=disabled enabled" 80 | ``` 81 | 82 | ```bash 83 | ./gin-admin-cli gen -d ~/go/src/testapp -m SYS -c dictionary.yaml 84 | ``` 85 | 86 | ### Use `ant-design-pro-v5` template to generate struct and UI 87 | 88 | ```yaml 89 | - name: Parameter 90 | comment: System parameter management 91 | generate_fe: true 92 | fe_tpl: "ant-design-pro-v5" 93 | extra: 94 | ParentName: system 95 | IndexAntdImport: "Tag" 96 | IndexProComponentsImport: "" 97 | FormAntdImport: "" 98 | FormProComponentsImport: "ProFormText, ProFormTextArea, ProFormSwitch" 99 | IncludeCreatedAt: true 100 | IncludeUpdatedAt: true 101 | fe_mapping: 102 | services.typings.d.ts.tpl: "src/services/system/parameter/typings.d.ts" 103 | services.index.ts.tpl: "src/services/system/parameter/index.ts" 104 | pages.components.form.tsx.tpl: "src/pages/system/Parameter/components/SaveForm.tsx" 105 | pages.index.tsx.tpl: "src/pages/system/Parameter/index.tsx" 106 | locales.en.page.ts.tpl: "src/locales/en-US/pages/system.parameter.ts" 107 | fields: 108 | - name: Name 109 | type: string 110 | comment: Name of parameter 111 | gorm_tag: "size:128;index" 112 | unique: true 113 | query: 114 | name: LikeName 115 | in_query: true 116 | form_tag: name 117 | op: LIKE 118 | form: 119 | binding_tag: "required,max=128" 120 | extra: 121 | ColumnComponent: > 122 | { 123 | title: intl.formatMessage({ id: 'pages.system.parameter.form.name' }), 124 | dataIndex: 'name', 125 | width: 160, 126 | key: 'name', // Query field name 127 | } 128 | FormComponent: > 129 | 142 | - name: Value 143 | type: string 144 | comment: Value of parameter 145 | gorm_tag: "size:1024;" 146 | form: 147 | binding_tag: "max=1024" 148 | extra: 149 | ColumnComponent: > 150 | { 151 | title: intl.formatMessage({ id: 'pages.system.parameter.form.value' }), 152 | dataIndex: 'value', 153 | width: 200, 154 | } 155 | FormComponent: > 156 | 163 | - name: Remark 164 | type: string 165 | comment: Remark of parameter 166 | gorm_tag: "size:255;" 167 | form: {} 168 | extra: 169 | ColumnComponent: > 170 | { 171 | title: intl.formatMessage({ id: 'pages.system.parameter.form.remark' }), 172 | dataIndex: 'remark', 173 | width: 180, 174 | } 175 | FormComponent: > 176 | 182 | - name: Status 183 | type: string 184 | comment: Status of parameter (enabled, disabled) 185 | gorm_tag: "size:20;index" 186 | query: 187 | in_query: true 188 | form: 189 | binding_tag: "required,oneof=enabled disabled" 190 | extra: 191 | ColumnComponent: > 192 | { 193 | title: intl.formatMessage({ id: 'pages.system.parameter.form.status' }), 194 | dataIndex: 'status', 195 | width: 130, 196 | search: false, 197 | render: (status) => { 198 | return ( 199 | 200 | {status === 'enabled' 201 | ? intl.formatMessage({ id: 'pages.system.parameter.form.status.enabled', defaultMessage: 'Enabled' }) 202 | : intl.formatMessage({ id: 'pages.system.parameter.form.status.disabled', defaultMessage: 'Disabled' })} 203 | 204 | ); 205 | }, 206 | } 207 | FormComponent: > 208 | 218 | ``` 219 | 220 | ```bash 221 | ./gin-admin-cli gen -d ~/go/src/testapp -m SYS -c parameter.yaml 222 | ``` 223 | 224 | ### Remove a struct from the module 225 | 226 | ```bash 227 | gin-admin-cli rm -d ~/go/src/testapp -m SYS -s Dictionary 228 | ``` 229 | 230 | ## Command help 231 | 232 | ### New command 233 | 234 | ```text 235 | NAME: 236 | gin-admin-cli new - Create a new project 237 | 238 | USAGE: 239 | gin-admin-cli new [command options] [arguments...] 240 | 241 | OPTIONS: 242 | --dir value, -d value The directory to generate the project (default: current directory) 243 | --name value The project name 244 | --app-name value The application name (default: project name) 245 | --desc value The project description 246 | --version value The project version (default: 1.0.0) 247 | --pkg value The project package name (default: project name) 248 | --git-url value Use git repository to initialize the project (default: https://github.com/LyricTian/gin-admin.git) 249 | --git-branch value Use git branch to initialize the project (default: main) 250 | --fe-dir value The frontend directory to generate the project (if empty, the frontend project will not be generated) 251 | --fe-name value The frontend project name (default: frontend) 252 | --fe-git-url value Use git repository to initialize the frontend project (default: https://github.com/gin-admin/gin-admin-frontend.git) 253 | --fe-git-branch value Use git branch to initialize the frontend project (default: main) 254 | --help, -h show help 255 | ``` 256 | 257 | ### Generate command 258 | 259 | ```text 260 | NAME: 261 | gin-admin-cli generate - Generate structs to the specified module, support config file 262 | 263 | USAGE: 264 | gin-admin-cli generate [command options] [arguments...] 265 | 266 | OPTIONS: 267 | --dir value, -d value The project directory to generate the struct 268 | --module value, -m value The module to generate the struct from (like: RBAC) 269 | --module-path value The module path to generate the struct from (default: internal/mods) 270 | --wire-path value The wire generate path to generate the struct from (default: internal/wirex) 271 | --swag-path value The swagger generate path to generate the struct from (default: internal/swagger) 272 | --config value, -c value The config file or directory to generate the struct from (JSON/YAML) 273 | --structs value The struct name to generate 274 | --structs-comment value Specify the struct comment 275 | --structs-router-prefix Use module name as router prefix (default: false) 276 | --structs-output value Specify the packages to generate the struct (default: schema,dal,biz,api) 277 | --tpl-path value The template path to generate the struct from (default use tpls) 278 | --tpl-type value The template type to generate the struct from (default: default) 279 | --fe-dir value The frontend project directory to generate the UI 280 | --help, -h show help 281 | ``` 282 | 283 | ### Remove command 284 | 285 | ```text 286 | NAME: 287 | gin-admin-cli remove - Remove structs from the module 288 | 289 | USAGE: 290 | gin-admin-cli remove [command options] [arguments...] 291 | 292 | OPTIONS: 293 | --dir value, -d value The directory to remove the struct from 294 | --module value, -m value The module to remove the struct from 295 | --module-path value The module path to remove the struct from (default: internal/mods) 296 | --structs value, -s value The struct to remove (multiple structs can be separated by a comma) 297 | --config value, -c value The config file to generate the struct from (JSON/YAML) 298 | --wire-path value The wire generate path to remove the struct from (default: internal/library/wirex) 299 | --swag-path value The swagger generate path to remove the struct from (default: internal/swagger) 300 | --help, -h show help 301 | ``` 302 | 303 | ## MIT License 304 | 305 | ```text 306 | Copyright (c) 2023 Lyric 307 | ``` 308 | -------------------------------------------------------------------------------- /internal/parser/visitor.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/parser" 7 | "go/printer" 8 | "go/token" 9 | "strings" 10 | ) 11 | 12 | type astModuleMainVisitor struct { 13 | fset *token.FileSet 14 | args BasicArgs 15 | } 16 | 17 | func (v *astModuleMainVisitor) Visit(node ast.Node) ast.Visitor { 18 | switch x := node.(type) { 19 | case *ast.GenDecl: 20 | if x.Tok == token.IMPORT && len(x.Specs) > 0 { 21 | v.modifyModuleImport(x) 22 | } 23 | case *ast.TypeSpec: 24 | if x.Name.Name == v.args.ModuleName { 25 | if xst, ok := x.Type.(*ast.StructType); ok { 26 | v.modifyStructField(xst) 27 | } 28 | } 29 | case *ast.FuncDecl: 30 | if x.Name.Name == "AutoMigrate" { 31 | v.modifyAutoMigrate(x) 32 | } else if x.Name.Name == "RegisterV1Routers" { 33 | v.modifyRegisterV1Routers(x) 34 | } 35 | } 36 | return v 37 | } 38 | 39 | func (v *astModuleMainVisitor) modifyModuleImport(x *ast.GenDecl) { 40 | if v.args.Flag&AstFlagGen != 0 { 41 | for _, pkgName := range []string{StructPackageAPI, StructPackageSchema} { 42 | findIndex := -1 43 | modulePath := GetModuleImportPath(v.args.Dir, v.args.ModulePath, v.args.ModuleName) + "/" + pkgName 44 | for i, spec := range x.Specs { 45 | if is, ok := spec.(*ast.ImportSpec); ok && 46 | is.Path.Value == fmt.Sprintf("\"%s\"", modulePath) { 47 | findIndex = i 48 | break 49 | } 50 | } 51 | 52 | if findIndex == -1 { 53 | x.Specs = append(x.Specs, &ast.ImportSpec{ 54 | Path: &ast.BasicLit{ 55 | Kind: token.STRING, 56 | Value: fmt.Sprintf("\"%s\"", modulePath), 57 | }, 58 | }) 59 | } 60 | } 61 | } 62 | } 63 | 64 | func (v *astModuleMainVisitor) modifyStructField(xst *ast.StructType) { 65 | findIndex := -1 66 | for i, field := range xst.Fields.List { 67 | starType, ok := field.Type.(*ast.StarExpr) 68 | if !ok { 69 | continue 70 | } 71 | selector, ok := starType.X.(*ast.SelectorExpr) 72 | if !ok { 73 | continue 74 | } 75 | if selector.Sel.Name == v.args.StructName && selector.X.(*ast.Ident).Name == StructPackageAPI { 76 | findIndex = i 77 | break 78 | } 79 | } 80 | 81 | if v.args.Flag&AstFlagGen != 0 { 82 | if findIndex != -1 { 83 | return 84 | } 85 | 86 | existsAPI := false 87 | for _, gpkg := range v.args.GenPackages { 88 | if gpkg == StructPackageAPI { 89 | existsAPI = true 90 | break 91 | } 92 | } 93 | 94 | if existsAPI { 95 | xst.Fields.List = append(xst.Fields.List, &ast.Field{ 96 | Names: []*ast.Ident{ 97 | {Name: GetStructAPIName(v.args.StructName)}, 98 | }, 99 | Type: &ast.StarExpr{ 100 | X: &ast.SelectorExpr{ 101 | X: ast.NewIdent(StructPackageAPI), 102 | Sel: ast.NewIdent(v.args.StructName), 103 | }, 104 | }, 105 | }) 106 | } 107 | } else if v.args.Flag&AstFlagRem != 0 { 108 | if findIndex != -1 { 109 | xst.Fields.List = append(xst.Fields.List[:findIndex], xst.Fields.List[findIndex+1:]...) 110 | } 111 | } 112 | } 113 | 114 | func (v *astModuleMainVisitor) modifyAutoMigrate(x *ast.FuncDecl) { 115 | if len(x.Body.List) == 0 { 116 | return 117 | } 118 | 119 | switch xst := x.Body.List[0].(type) { 120 | case *ast.ReturnStmt: 121 | if len(xst.Results) == 0 { 122 | return 123 | } 124 | 125 | result := xst.Results[0].(*ast.CallExpr) 126 | args := result.Args 127 | findIndex := -1 128 | for i, arg := range args { 129 | selector, ok := arg.(*ast.CallExpr).Args[0].(*ast.SelectorExpr) 130 | if !ok { 131 | continue 132 | } 133 | 134 | if selector.Sel.Name == v.args.StructName && selector.X.(*ast.Ident).Name == StructPackageSchema { 135 | findIndex = i 136 | break 137 | } 138 | } 139 | 140 | if v.args.Flag&AstFlagGen != 0 { 141 | if findIndex != -1 { 142 | return 143 | } 144 | args = append(args, &ast.CallExpr{ 145 | Fun: ast.NewIdent("new"), 146 | Args: []ast.Expr{ 147 | &ast.SelectorExpr{ 148 | X: ast.NewIdent(StructPackageSchema), 149 | Sel: ast.NewIdent(v.args.StructName), 150 | }, 151 | }, 152 | }) 153 | result.Args = args 154 | } else if v.args.Flag&AstFlagRem != 0 { 155 | if findIndex == -1 { 156 | return 157 | } 158 | args = append(args[:findIndex], args[findIndex+1:]...) 159 | result.Args = args 160 | } 161 | } 162 | } 163 | 164 | func (v *astModuleMainVisitor) modifyRegisterV1Routers(x *ast.FuncDecl) { 165 | if len(x.Body.List) == 0 { 166 | return 167 | } 168 | 169 | structRouterVarName := GetStructRouterVarName(v.args.StructName) 170 | findIndex := -1 171 | for i, list := range x.Body.List { 172 | if lt, ok := list.(*ast.AssignStmt); ok { 173 | if len(lt.Lhs) == 0 { 174 | continue 175 | } 176 | 177 | if lt.Lhs[0].(*ast.Ident).Name == structRouterVarName { 178 | findIndex = i 179 | break 180 | } 181 | } 182 | } 183 | 184 | if v.args.Flag&AstFlagGen != 0 { 185 | if findIndex != -1 { 186 | return 187 | } 188 | 189 | existsAPI := false 190 | for _, gpkg := range v.args.GenPackages { 191 | if gpkg == StructPackageAPI { 192 | existsAPI = true 193 | break 194 | } 195 | } 196 | if !existsAPI { 197 | return 198 | } 199 | 200 | assignStmt := &ast.AssignStmt{ 201 | Lhs: []ast.Expr{ 202 | &ast.Ident{ 203 | Name: structRouterVarName, 204 | Obj: ast.NewObj(ast.Var, structRouterVarName), 205 | }, 206 | }, 207 | Tok: token.DEFINE, 208 | Rhs: []ast.Expr{ 209 | &ast.CallExpr{ 210 | Fun: &ast.SelectorExpr{ 211 | X: ast.NewIdent("v1"), 212 | Sel: ast.NewIdent("Group"), 213 | }, 214 | Args: []ast.Expr{ 215 | &ast.BasicLit{ 216 | Kind: token.STRING, 217 | Value: fmt.Sprintf("\"%s\"", GetStructRouterGroupName(v.args.StructName)), 218 | }, 219 | }, 220 | }, 221 | }, 222 | } 223 | 224 | routes := [][]string{ 225 | {"GET", "\"\"", "Query"}, 226 | {"GET", "\":id\"", "Get"}, 227 | {"POST", "\"\"", "Create"}, 228 | {"PUT", "\":id\"", "Update"}, 229 | {"DELETE", "\":id\"", "Delete"}, 230 | } 231 | 232 | var blockList []ast.Stmt 233 | for _, r := range routes { 234 | blockList = append(blockList, &ast.ExprStmt{ 235 | X: &ast.CallExpr{ 236 | Fun: &ast.SelectorExpr{ 237 | X: ast.NewIdent(structRouterVarName), 238 | Sel: ast.NewIdent(r[0]), 239 | }, 240 | Args: []ast.Expr{ 241 | &ast.BasicLit{ 242 | Kind: token.STRING, 243 | Value: r[1], 244 | }, 245 | &ast.SelectorExpr{ 246 | X: &ast.SelectorExpr{ 247 | X: ast.NewIdent("a"), 248 | Sel: ast.NewIdent(GetStructAPIName(v.args.StructName)), 249 | }, 250 | Sel: ast.NewIdent(r[2]), 251 | }, 252 | }, 253 | }, 254 | }) 255 | } 256 | 257 | lastEle := x.Body.List[len(x.Body.List)-1] 258 | x.Body.List = append(x.Body.List[:len(x.Body.List)-1], 259 | assignStmt, 260 | &ast.BlockStmt{List: blockList}, 261 | lastEle, 262 | ) 263 | } else if v.args.Flag&AstFlagRem != 0 { 264 | if findIndex == -1 { 265 | return 266 | } 267 | x.Body.List = append(x.Body.List[:findIndex], x.Body.List[findIndex+2:]...) 268 | } 269 | } 270 | 271 | type astModuleWireVisitor struct { 272 | fset *token.FileSet 273 | args BasicArgs 274 | } 275 | 276 | func (v *astModuleWireVisitor) Visit(node ast.Node) ast.Visitor { 277 | switch x := node.(type) { 278 | case *ast.GenDecl: 279 | if x.Tok == token.IMPORT && len(x.Specs) > 0 { 280 | v.modifyModuleImport(x) 281 | } 282 | 283 | if x.Tok == token.VAR && len(x.Specs) > 0 { 284 | v.modifyNewSet(x) 285 | } 286 | } 287 | return v 288 | } 289 | 290 | func (v *astModuleWireVisitor) modifyModuleImport(x *ast.GenDecl) { 291 | if v.args.Flag&AstFlagGen != 0 { 292 | for _, pkgName := range []string{StructPackageAPI, StructPackageBIZ, StructPackageDAL} { 293 | findIndex := -1 294 | modulePath := GetModuleImportPath(v.args.Dir, v.args.ModulePath, v.args.ModuleName) + "/" + pkgName 295 | for i, spec := range x.Specs { 296 | if is, ok := spec.(*ast.ImportSpec); ok && 297 | is.Path.Value == fmt.Sprintf("\"%s\"", modulePath) { 298 | findIndex = i 299 | break 300 | } 301 | } 302 | 303 | if findIndex == -1 { 304 | x.Specs = append(x.Specs, &ast.ImportSpec{ 305 | Path: &ast.BasicLit{ 306 | Kind: token.STRING, 307 | Value: fmt.Sprintf("\"%s\"", modulePath), 308 | }, 309 | }) 310 | } 311 | } 312 | } 313 | } 314 | 315 | func (v *astModuleWireVisitor) modifyNewSet(x *ast.GenDecl) { 316 | vspec, ok := x.Specs[0].(*ast.ValueSpec) 317 | if !ok || len(vspec.Names) == 0 || 318 | len(vspec.Values) == 0 || vspec.Names[0].Name != "Set" { 319 | return 320 | } 321 | 322 | args := vspec.Values[0].(*ast.CallExpr).Args 323 | 324 | if v.args.Flag&AstFlagGen != 0 { 325 | genPackagesMap := make(map[string]bool) 326 | for _, p := range v.args.GenPackages { 327 | if p == StructPackageSchema { 328 | continue 329 | } 330 | genPackagesMap[p] = true 331 | } 332 | 333 | for _, arg := range args { 334 | if wireS, ok := arg.(*ast.CallExpr); ok && len(wireS.Args) > 0 { 335 | if newS, ok := wireS.Args[0].(*ast.CallExpr); ok && len(newS.Args) > 0 { 336 | if s, ok := newS.Args[0].(*ast.SelectorExpr); ok { 337 | if s.Sel.Name == v.args.StructName { 338 | name := s.X.(*ast.Ident).Name 339 | if _, ok := genPackagesMap[name]; ok { 340 | delete(genPackagesMap, name) 341 | continue 342 | } 343 | } 344 | } 345 | } 346 | } 347 | } 348 | 349 | for _, p := range v.args.GenPackages { 350 | if _, ok := genPackagesMap[p]; !ok { 351 | continue 352 | } 353 | 354 | arg := &ast.CallExpr{ 355 | Fun: &ast.SelectorExpr{ 356 | X: ast.NewIdent("wire"), 357 | Sel: ast.NewIdent("Struct"), 358 | }, 359 | Args: []ast.Expr{ 360 | &ast.CallExpr{ 361 | Fun: ast.NewIdent("new"), 362 | Args: []ast.Expr{ 363 | &ast.SelectorExpr{ 364 | X: ast.NewIdent(p), 365 | Sel: ast.NewIdent(v.args.StructName), 366 | }, 367 | }, 368 | }, 369 | &ast.BasicLit{ 370 | Kind: token.STRING, 371 | Value: "\"*\"", 372 | }, 373 | }, 374 | } 375 | args = append(args, arg) 376 | } 377 | vspec.Values[0].(*ast.CallExpr).Args = args 378 | } else if v.args.Flag&AstFlagRem != 0 { 379 | var newArgs []ast.Expr 380 | for _, arg := range args { 381 | if wireS, ok := arg.(*ast.CallExpr); ok && len(wireS.Args) > 0 { 382 | if newS, ok := wireS.Args[0].(*ast.CallExpr); ok && len(newS.Args) > 0 { 383 | if s, ok := newS.Args[0].(*ast.SelectorExpr); ok { 384 | if s.Sel.Name == v.args.StructName { 385 | continue 386 | } 387 | } 388 | } 389 | } 390 | newArgs = append(newArgs, arg) 391 | } 392 | vspec.Values[0].(*ast.CallExpr).Args = newArgs 393 | } 394 | } 395 | 396 | type astModsVisitor struct { 397 | fset *token.FileSet 398 | args BasicArgs 399 | } 400 | 401 | func (v *astModsVisitor) Visit(node ast.Node) ast.Visitor { 402 | switch x := node.(type) { 403 | case *ast.GenDecl: 404 | if x.Tok == token.IMPORT && len(x.Specs) > 0 { 405 | v.modifyModuleImport(x) 406 | } 407 | 408 | if x.Tok == token.VAR && len(x.Specs) > 0 { 409 | v.modifyWireSet(x) 410 | } 411 | case *ast.TypeSpec: 412 | if x.Name.Name == "Mods" { 413 | if xst, ok := x.Type.(*ast.StructType); ok { 414 | v.modifyStructField(xst) 415 | } 416 | } 417 | case *ast.FuncDecl: 418 | if x.Name.Name == "Init" { 419 | v.modifyFuncInit(x) 420 | } else if x.Name.Name == "RegisterRouters" { 421 | v.modifyFuncRegisterRouters(x) 422 | } else if x.Name.Name == "Release" { 423 | v.modifyFuncRelease(x) 424 | } 425 | } 426 | return v 427 | } 428 | 429 | func (v *astModsVisitor) modifyModuleImport(x *ast.GenDecl) { 430 | findIndex := -1 431 | modulePath := GetModuleImportPath(v.args.Dir, v.args.ModulePath, v.args.ModuleName) 432 | for i, spec := range x.Specs { 433 | if is, ok := spec.(*ast.ImportSpec); ok && 434 | is.Path.Value == fmt.Sprintf("\"%s\"", modulePath) { 435 | findIndex = i 436 | break 437 | } 438 | } 439 | 440 | if v.args.Flag&AstFlagGen != 0 { 441 | if findIndex == -1 { 442 | x.Specs = append(x.Specs, &ast.ImportSpec{ 443 | Path: &ast.BasicLit{ 444 | Kind: token.STRING, 445 | Value: fmt.Sprintf("\"%s\"", modulePath), 446 | }, 447 | }) 448 | } 449 | } else if v.args.Flag&AstFlagRem != 0 { 450 | if findIndex != -1 { 451 | x.Specs = append(x.Specs[:findIndex], x.Specs[findIndex+1:]...) 452 | } 453 | } 454 | } 455 | 456 | func (v *astModsVisitor) modifyWireSet(x *ast.GenDecl) { 457 | vspec, ok := x.Specs[0].(*ast.ValueSpec) 458 | if !ok || len(vspec.Names) == 0 || 459 | len(vspec.Values) == 0 || vspec.Names[0].Name != "Set" { 460 | return 461 | } 462 | 463 | args := vspec.Values[0].(*ast.CallExpr).Args 464 | findIndex := -1 465 | for i, arg := range args { 466 | if sel, ok := arg.(*ast.SelectorExpr); ok && sel.X.(*ast.Ident).Name == GetModuleImportName(v.args.ModuleName) { 467 | findIndex = i 468 | break 469 | } 470 | } 471 | 472 | if v.args.Flag&AstFlagGen != 0 { 473 | if findIndex != -1 { 474 | return 475 | } 476 | arg := &ast.SelectorExpr{ 477 | X: ast.NewIdent(GetModuleImportName(v.args.ModuleName)), 478 | Sel: ast.NewIdent("Set"), 479 | } 480 | args = append(args, arg) 481 | vspec.Values[0].(*ast.CallExpr).Args = args 482 | } else if v.args.Flag&AstFlagRem != 0 { 483 | if findIndex == -1 { 484 | return 485 | } 486 | args = append(args[:findIndex], args[findIndex+1:]...) 487 | vspec.Values[0].(*ast.CallExpr).Args = args 488 | } 489 | } 490 | 491 | func (v *astModsVisitor) modifyStructField(xst *ast.StructType) { 492 | findIndex := -1 493 | for i, field := range xst.Fields.List { 494 | starType, ok := field.Type.(*ast.StarExpr) 495 | if !ok { 496 | continue 497 | } 498 | selector, ok := starType.X.(*ast.SelectorExpr) 499 | if !ok { 500 | continue 501 | } 502 | if selector.Sel.Name == v.args.ModuleName { 503 | findIndex = i 504 | break 505 | } 506 | } 507 | 508 | if v.args.Flag&AstFlagGen != 0 { 509 | if findIndex != -1 { 510 | return 511 | } 512 | xst.Fields.List = append(xst.Fields.List, &ast.Field{ 513 | Names: []*ast.Ident{ 514 | {Name: v.args.ModuleName}, 515 | }, 516 | Type: &ast.StarExpr{ 517 | X: &ast.SelectorExpr{ 518 | X: ast.NewIdent(GetModuleImportName(v.args.ModuleName)), 519 | Sel: ast.NewIdent(v.args.ModuleName), 520 | }, 521 | }, 522 | }) 523 | } else if v.args.Flag&AstFlagRem != 0 { 524 | if findIndex != -1 { 525 | xst.Fields.List = append(xst.Fields.List[:findIndex], xst.Fields.List[findIndex+1:]...) 526 | } 527 | } 528 | } 529 | 530 | func (v *astModsVisitor) modifyFuncInit(x *ast.FuncDecl) { 531 | findIndex := -1 532 | list := x.Body.List 533 | for i, stmt := range list { 534 | if s, ok := stmt.(*ast.IfStmt); ok { 535 | var sb strings.Builder 536 | _ = printer.Fprint(&sb, v.fset, s.Init) 537 | if strings.Contains(sb.String(), fmt.Sprintf("%s.Init", v.args.ModuleName)) { 538 | findIndex = i 539 | break 540 | } 541 | } 542 | } 543 | if v.args.Flag&AstFlagGen != 0 { 544 | if findIndex == -1 { 545 | e, err := parser.ParseExpr(fmt.Sprintf("a.%s.Init(ctx)", v.args.ModuleName)) 546 | if err == nil { 547 | list = append(list[:len(list)-1], append([]ast.Stmt{&ast.IfStmt{ 548 | Init: &ast.AssignStmt{ 549 | Lhs: []ast.Expr{ 550 | ast.NewIdent("err"), 551 | }, 552 | Tok: token.DEFINE, 553 | Rhs: []ast.Expr{ 554 | e, 555 | }, 556 | }, 557 | Cond: &ast.BinaryExpr{ 558 | X: ast.NewIdent("err"), 559 | Op: token.NEQ, 560 | Y: ast.NewIdent("nil"), 561 | }, 562 | Body: &ast.BlockStmt{ 563 | List: []ast.Stmt{ 564 | &ast.ReturnStmt{ 565 | Results: []ast.Expr{ 566 | ast.NewIdent("err"), 567 | }, 568 | }, 569 | }, 570 | }, 571 | }}, list[len(list)-1])...) 572 | x.Body.List = list 573 | } 574 | } 575 | } else if v.args.Flag&AstFlagRem != 0 { 576 | if findIndex != -1 { 577 | list = append(list[:findIndex], list[findIndex+1:]...) 578 | x.Body.List = list 579 | } 580 | } 581 | } 582 | 583 | func (v *astModsVisitor) modifyFuncRegisterRouters(x *ast.FuncDecl) { 584 | findIndex := -1 585 | list := x.Body.List 586 | for i, stmt := range list { 587 | if s, ok := stmt.(*ast.IfStmt); ok { 588 | var sb strings.Builder 589 | printer.Fprint(&sb, v.fset, s.Init) 590 | if strings.Contains(sb.String(), fmt.Sprintf("%s.RegisterV1Routers", v.args.ModuleName)) { 591 | findIndex = i 592 | break 593 | } 594 | } 595 | } 596 | if v.args.Flag&AstFlagGen != 0 { 597 | if findIndex == -1 { 598 | e, err := parser.ParseExpr(fmt.Sprintf("a.%s.RegisterV1Routers(ctx, v1)", v.args.ModuleName)) 599 | if err == nil { 600 | list = append(list[:len(list)-1], append([]ast.Stmt{&ast.IfStmt{ 601 | Init: &ast.AssignStmt{ 602 | Lhs: []ast.Expr{ 603 | ast.NewIdent("err"), 604 | }, 605 | Tok: token.DEFINE, 606 | Rhs: []ast.Expr{ 607 | e, 608 | }, 609 | }, 610 | Cond: &ast.BinaryExpr{ 611 | X: ast.NewIdent("err"), 612 | Op: token.NEQ, 613 | Y: ast.NewIdent("nil"), 614 | }, 615 | Body: &ast.BlockStmt{ 616 | List: []ast.Stmt{ 617 | &ast.ReturnStmt{ 618 | Results: []ast.Expr{ 619 | ast.NewIdent("err"), 620 | }, 621 | }, 622 | }, 623 | }, 624 | }}, list[len(list)-1])...) 625 | x.Body.List = list 626 | } 627 | } 628 | } else if v.args.Flag&AstFlagRem != 0 { 629 | if findIndex != -1 { 630 | list = append(list[:findIndex], list[findIndex+1:]...) 631 | x.Body.List = list 632 | } 633 | } 634 | } 635 | 636 | func (v *astModsVisitor) modifyFuncRelease(x *ast.FuncDecl) { 637 | findIndex := -1 638 | list := x.Body.List 639 | for i, stmt := range list { 640 | if s, ok := stmt.(*ast.IfStmt); ok { 641 | var sb strings.Builder 642 | printer.Fprint(&sb, v.fset, s.Init) 643 | if strings.Contains(sb.String(), fmt.Sprintf("%s.Release", v.args.ModuleName)) { 644 | findIndex = i 645 | break 646 | } 647 | } 648 | } 649 | if v.args.Flag&AstFlagGen != 0 { 650 | if findIndex == -1 { 651 | e, err := parser.ParseExpr(fmt.Sprintf("a.%s.Release(ctx)", v.args.ModuleName)) 652 | if err == nil { 653 | list = append(list[:len(list)-1], append([]ast.Stmt{&ast.IfStmt{ 654 | Init: &ast.AssignStmt{ 655 | Lhs: []ast.Expr{ 656 | ast.NewIdent("err"), 657 | }, 658 | Tok: token.DEFINE, 659 | Rhs: []ast.Expr{ 660 | e, 661 | }, 662 | }, 663 | Cond: &ast.BinaryExpr{ 664 | X: ast.NewIdent("err"), 665 | Op: token.NEQ, 666 | Y: ast.NewIdent("nil"), 667 | }, 668 | Body: &ast.BlockStmt{ 669 | List: []ast.Stmt{ 670 | &ast.ReturnStmt{ 671 | Results: []ast.Expr{ 672 | ast.NewIdent("err"), 673 | }, 674 | }, 675 | }, 676 | }, 677 | }}, list[len(list)-1])...) 678 | x.Body.List = list 679 | } 680 | } 681 | } else if v.args.Flag&AstFlagRem != 0 { 682 | if findIndex != -1 { 683 | list = append(list[:findIndex], list[findIndex+1:]...) 684 | x.Body.List = list 685 | } 686 | } 687 | } 688 | --------------------------------------------------------------------------------