| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419 |
- // Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved.
- //
- // This Source Code Form is subject to the terms of the MIT License.
- // If a copy of the MIT was not distributed with this file,
- // You can obtain one at https://github.com/gogf/gf.
- package genpbentity
- import (
- "bytes"
- "context"
- "fmt"
- "strings"
- "github.com/gogf/gf/cmd/gf/v2/internal/consts"
- "github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog"
- "github.com/gogf/gf/v2/database/gdb"
- "github.com/gogf/gf/v2/frame/g"
- "github.com/gogf/gf/v2/os/gctx"
- "github.com/gogf/gf/v2/os/gfile"
- "github.com/gogf/gf/v2/os/gtime"
- "github.com/gogf/gf/v2/text/gregex"
- "github.com/gogf/gf/v2/text/gstr"
- "github.com/gogf/gf/v2/util/gconv"
- "github.com/gogf/gf/v2/util/gtag"
- "github.com/olekukonko/tablewriter"
- )
- type (
- CGenPbEntity struct{}
- CGenPbEntityInput struct {
- g.Meta `name:"pbentity" config:"{CGenPbEntityConfig}" brief:"{CGenPbEntityBrief}" eg:"{CGenPbEntityEg}" ad:"{CGenPbEntityAd}"`
- Path string `name:"path" short:"p" brief:"{CGenPbEntityBriefPath}" d:"manifest/protobuf/pbentity"`
- Package string `name:"package" short:"k" brief:"{CGenPbEntityBriefPackage}"`
- Link string `name:"link" short:"l" brief:"{CGenPbEntityBriefLink}"`
- Tables string `name:"tables" short:"t" brief:"{CGenPbEntityBriefTables}"`
- Prefix string `name:"prefix" short:"f" brief:"{CGenPbEntityBriefPrefix}"`
- RemovePrefix string `name:"removePrefix" short:"r" brief:"{CGenPbEntityBriefRemovePrefix}"`
- NameCase string `name:"nameCase" short:"n" brief:"{CGenPbEntityBriefNameCase}" d:"Camel"`
- JsonCase string `name:"jsonCase" short:"j" brief:"{CGenPbEntityBriefJsonCase}" d:"CamelLower"`
- Option string `name:"option" short:"o" brief:"{CGenPbEntityBriefOption}"`
- }
- CGenPbEntityOutput struct{}
- CGenPbEntityInternalInput struct {
- CGenPbEntityInput
- DB gdb.DB
- TableName string // TableName specifies the table name of the table.
- NewTableName string // NewTableName specifies the prefix-stripped name of the table.
- }
- )
- const (
- defaultPackageSuffix = `api/pbentity`
- CGenPbEntityConfig = `gfcli.gen.pbentity`
- CGenPbEntityBrief = `generate entity message files in protobuf3 format`
- CGenPbEntityEg = `
- gf gen pbentity
- gf gen pbentity -l "mysql:root:12345678@tcp(127.0.0.1:3306)/test"
- gf gen pbentity -p ./protocol/demos/entity -t user,user_detail,user_login
- gf gen pbentity -r user_ -k github.com/gogf/gf/example/protobuf
- gf gen pbentity -r user_
- `
- CGenPbEntityAd = `
- CONFIGURATION SUPPORT
- Options are also supported by configuration file.
- It's suggested using configuration file instead of command line arguments making producing.
- The configuration node name is "gf.gen.pbentity", which also supports multiple databases, for example(config.yaml):
- gfcli:
- gen:
- - pbentity:
- link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test"
- path: "protocol/demos/entity"
- tables: "order,products"
- package: "demos"
- - pbentity:
- link: "mysql:root:12345678@tcp(127.0.0.1:3306)/primary"
- path: "protocol/demos/entity"
- prefix: "primary_"
- tables: "user, userDetail"
- package: "demos"
- option: |
- option go_package = "protobuf/demos";
- option java_package = "protobuf/demos";
- option php_namespace = "protobuf/demos";
- `
- CGenPbEntityBriefPath = `directory path for generated files storing`
- CGenPbEntityBriefPackage = `package path for all entity proto files`
- CGenPbEntityBriefLink = `database configuration, the same as the ORM configuration of GoFrame`
- CGenPbEntityBriefTables = `generate models only for given tables, multiple table names separated with ','`
- CGenPbEntityBriefPrefix = `add specified prefix for all entity names and entity proto files`
- CGenPbEntityBriefRemovePrefix = `remove specified prefix of the table, multiple prefix separated with ','`
- CGenPbEntityBriefOption = `extra protobuf options`
- CGenPbEntityBriefGroup = `
- specifying the configuration group name of database for generated ORM instance,
- it's not necessary and the default value is "default"
- `
- CGenPbEntityBriefNameCase = `
- case for message attribute names, default is "Camel":
- | Case | Example |
- |---------------- |--------------------|
- | Camel | AnyKindOfString |
- | CamelLower | anyKindOfString | default
- | Snake | any_kind_of_string |
- | SnakeScreaming | ANY_KIND_OF_STRING |
- | SnakeFirstUpper | rgb_code_md5 |
- | Kebab | any-kind-of-string |
- | KebabScreaming | ANY-KIND-OF-STRING |
- `
- CGenPbEntityBriefJsonCase = `
- case for message json tag, cases are the same as "nameCase", default "CamelLower".
- set it to "none" to ignore json tag generating.
- `
- )
- func init() {
- gtag.Sets(g.MapStrStr{
- `CGenPbEntityConfig`: CGenPbEntityConfig,
- `CGenPbEntityBrief`: CGenPbEntityBrief,
- `CGenPbEntityEg`: CGenPbEntityEg,
- `CGenPbEntityAd`: CGenPbEntityAd,
- `CGenPbEntityBriefPath`: CGenPbEntityBriefPath,
- `CGenPbEntityBriefPackage`: CGenPbEntityBriefPackage,
- `CGenPbEntityBriefLink`: CGenPbEntityBriefLink,
- `CGenPbEntityBriefTables`: CGenPbEntityBriefTables,
- `CGenPbEntityBriefPrefix`: CGenPbEntityBriefPrefix,
- `CGenPbEntityBriefRemovePrefix`: CGenPbEntityBriefRemovePrefix,
- `CGenPbEntityBriefGroup`: CGenPbEntityBriefGroup,
- `CGenPbEntityBriefNameCase`: CGenPbEntityBriefNameCase,
- `CGenPbEntityBriefJsonCase`: CGenPbEntityBriefJsonCase,
- `CGenPbEntityBriefOption`: CGenPbEntityBriefOption,
- })
- }
- func (c CGenPbEntity) PbEntity(ctx context.Context, in CGenPbEntityInput) (out *CGenPbEntityOutput, err error) {
- var (
- config = g.Cfg()
- )
- if config.Available(ctx) {
- v := config.MustGet(ctx, CGenPbEntityConfig)
- if v.IsSlice() {
- for i := 0; i < len(v.Interfaces()); i++ {
- doGenPbEntityForArray(ctx, i, in)
- }
- } else {
- doGenPbEntityForArray(ctx, -1, in)
- }
- } else {
- doGenPbEntityForArray(ctx, -1, in)
- }
- mlog.Print("done!")
- return
- }
- func doGenPbEntityForArray(ctx context.Context, index int, in CGenPbEntityInput) {
- var (
- err error
- db gdb.DB
- )
- if index >= 0 {
- err = g.Cfg().MustGet(
- ctx,
- fmt.Sprintf(`%s.%d`, CGenPbEntityConfig, index),
- ).Scan(&in)
- if err != nil {
- mlog.Fatalf(`invalid configuration of "%s": %+v`, CGenPbEntityConfig, err)
- }
- }
- if in.Package == "" {
- mlog.Debug(`package parameter is empty, trying calculating the package path using go.mod`)
- if !gfile.Exists("go.mod") {
- mlog.Fatal("go.mod does not exist in current working directory")
- }
- var (
- modName string
- goModContent = gfile.GetContents("go.mod")
- match, _ = gregex.MatchString(`^module\s+(.+)\s*`, goModContent)
- )
- if len(match) > 1 {
- modName = gstr.Trim(match[1])
- in.Package = modName + "/" + defaultPackageSuffix
- } else {
- mlog.Fatal("module name does not found in go.mod")
- }
- }
- removePrefixArray := gstr.SplitAndTrim(in.RemovePrefix, ",")
- // It uses user passed database configuration.
- if in.Link != "" {
- var (
- tempGroup = gtime.TimestampNanoStr()
- match, _ = gregex.MatchString(`([a-z]+):(.+)`, in.Link)
- )
- if len(match) == 3 {
- gdb.AddConfigNode(tempGroup, gdb.ConfigNode{
- Type: gstr.Trim(match[1]),
- Link: gstr.Trim(match[2]),
- })
- db, _ = gdb.Instance(tempGroup)
- }
- } else {
- db = g.DB()
- }
- if db == nil {
- mlog.Fatal("database initialization failed")
- }
- tableNames := ([]string)(nil)
- if in.Tables != "" {
- tableNames = gstr.SplitAndTrim(in.Tables, ",")
- } else {
- tableNames, err = db.Tables(context.TODO())
- if err != nil {
- mlog.Fatalf("fetching tables failed: \n %v", err)
- }
- }
- for _, tableName := range tableNames {
- newTableName := tableName
- for _, v := range removePrefixArray {
- newTableName = gstr.TrimLeftStr(newTableName, v, 1)
- }
- generatePbEntityContentFile(ctx, CGenPbEntityInternalInput{
- CGenPbEntityInput: in,
- DB: db,
- TableName: tableName,
- NewTableName: newTableName,
- })
- }
- }
- // generatePbEntityContentFile generates the protobuf files for given table.
- func generatePbEntityContentFile(ctx context.Context, in CGenPbEntityInternalInput) {
- fieldMap, err := in.DB.TableFields(ctx, in.TableName)
- if err != nil {
- mlog.Fatalf("fetching tables fields failed for table '%s':\n%v", in.TableName, err)
- }
- // Change the `newTableName` if `Prefix` is given.
- newTableName := in.Prefix + in.NewTableName
- var (
- imports string
- tableNameCamelCase = gstr.CaseCamel(newTableName)
- tableNameSnakeCase = gstr.CaseSnake(newTableName)
- entityMessageDefine = generateEntityMessageDefinition(tableNameCamelCase, fieldMap, in)
- fileName = gstr.Trim(tableNameSnakeCase, "-_.")
- path = gfile.Join(in.Path, fileName+".proto")
- )
- if gstr.Contains(entityMessageDefine, "google.protobuf.Timestamp") {
- imports = `import "google/protobuf/timestamp.proto";`
- }
- entityContent := gstr.ReplaceByMap(getTplPbEntityContent(""), g.MapStrStr{
- "{Imports}": imports,
- "{PackageName}": gfile.Basename(in.Package),
- "{GoPackage}": in.Package,
- "{OptionContent}": in.Option,
- "{EntityMessage}": entityMessageDefine,
- })
- if err := gfile.PutContents(path, strings.TrimSpace(entityContent)); err != nil {
- mlog.Fatalf("writing content to '%s' failed: %v", path, err)
- } else {
- mlog.Print("generated:", path)
- }
- }
- // generateEntityMessageDefinition generates and returns the message definition for specified table.
- func generateEntityMessageDefinition(entityName string, fieldMap map[string]*gdb.TableField, in CGenPbEntityInternalInput) string {
- var (
- buffer = bytes.NewBuffer(nil)
- array = make([][]string, len(fieldMap))
- names = sortFieldKeyForPbEntity(fieldMap)
- )
- for index, name := range names {
- array[index] = generateMessageFieldForPbEntity(index+1, fieldMap[name], in)
- }
- tw := tablewriter.NewWriter(buffer)
- tw.SetBorder(false)
- tw.SetRowLine(false)
- tw.SetAutoWrapText(false)
- tw.SetColumnSeparator("")
- tw.AppendBulk(array)
- tw.Render()
- stContent := buffer.String()
- // Let's do this hack of table writer for indent!
- stContent = gstr.Replace(stContent, " #", "")
- buffer.Reset()
- buffer.WriteString(fmt.Sprintf("message %s {\n", entityName))
- buffer.WriteString(stContent)
- buffer.WriteString("}")
- return buffer.String()
- }
- // generateMessageFieldForPbEntity generates and returns the message definition for specified field.
- func generateMessageFieldForPbEntity(index int, field *gdb.TableField, in CGenPbEntityInternalInput) []string {
- var (
- typeName string
- comment string
- jsonTagStr string
- err error
- ctx = gctx.GetInitCtx()
- )
- typeName, err = in.DB.CheckLocalTypeForField(ctx, field.Type, nil)
- if err != nil {
- panic(err)
- }
- var typeMapping = map[string]string{
- gdb.LocalTypeString: "string",
- gdb.LocalTypeDate: "google.protobuf.Timestamp",
- gdb.LocalTypeDatetime: "google.protobuf.Timestamp",
- gdb.LocalTypeInt: "int32",
- gdb.LocalTypeUint: "uint32",
- gdb.LocalTypeInt64: "int64",
- gdb.LocalTypeUint64: "uint64",
- gdb.LocalTypeIntSlice: "repeated int32",
- gdb.LocalTypeInt64Slice: "repeated int64",
- gdb.LocalTypeUint64Slice: "repeated uint64",
- gdb.LocalTypeInt64Bytes: "repeated int64",
- gdb.LocalTypeUint64Bytes: "repeated uint64",
- gdb.LocalTypeFloat32: "float",
- gdb.LocalTypeFloat64: "double",
- gdb.LocalTypeBytes: "bytes",
- gdb.LocalTypeBool: "bool",
- gdb.LocalTypeJson: "string",
- gdb.LocalTypeJsonb: "string",
- }
- typeName = typeMapping[typeName]
- if typeName == "" {
- typeName = "string"
- }
- comment = gstr.ReplaceByArray(field.Comment, g.SliceStr{
- "\n", " ",
- "\r", " ",
- })
- comment = gstr.Trim(comment)
- comment = gstr.Replace(comment, `\n`, " ")
- comment, _ = gregex.ReplaceString(`\s{2,}`, ` `, comment)
- if jsonTagName := formatCase(field.Name, in.JsonCase); jsonTagName != "" {
- // beautiful indent.
- if index < 10 {
- // 3 spaces
- jsonTagStr = " " + jsonTagStr
- } else if index < 100 {
- // 2 spaces
- jsonTagStr = " " + jsonTagStr
- } else {
- // 1 spaces
- jsonTagStr = " " + jsonTagStr
- }
- }
- return []string{
- " #" + typeName,
- " #" + formatCase(field.Name, in.NameCase),
- " #= " + gconv.String(index) + jsonTagStr + ";",
- " #" + fmt.Sprintf(`// %s`, comment),
- }
- }
- func getTplPbEntityContent(tplEntityPath string) string {
- if tplEntityPath != "" {
- return gfile.GetContents(tplEntityPath)
- }
- return consts.TemplatePbEntityMessageContent
- }
- // formatCase call gstr.Case* function to convert the s to specified case.
- func formatCase(str, caseStr string) string {
- switch gstr.ToLower(caseStr) {
- case gstr.ToLower("Camel"):
- return gstr.CaseCamel(str)
- case gstr.ToLower("CamelLower"):
- return gstr.CaseCamelLower(str)
- case gstr.ToLower("Kebab"):
- return gstr.CaseKebab(str)
- case gstr.ToLower("KebabScreaming"):
- return gstr.CaseKebabScreaming(str)
- case gstr.ToLower("Snake"):
- return gstr.CaseSnake(str)
- case gstr.ToLower("SnakeFirstUpper"):
- return gstr.CaseSnakeFirstUpper(str)
- case gstr.ToLower("SnakeScreaming"):
- return gstr.CaseSnakeScreaming(str)
- case "none":
- return ""
- }
- return str
- }
- func sortFieldKeyForPbEntity(fieldMap map[string]*gdb.TableField) []string {
- names := make(map[int]string)
- for _, field := range fieldMap {
- names[field.Index] = field.Name
- }
- var (
- result = make([]string, len(names))
- i = 0
- j = 0
- )
- for {
- if len(names) == 0 {
- break
- }
- if val, ok := names[i]; ok {
- result[j] = val
- j++
- delete(names, i)
- }
- i++
- }
- return result
- }
|