genpbentity.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. // Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved.
  2. //
  3. // This Source Code Form is subject to the terms of the MIT License.
  4. // If a copy of the MIT was not distributed with this file,
  5. // You can obtain one at https://github.com/gogf/gf.
  6. package genpbentity
  7. import (
  8. "bytes"
  9. "context"
  10. "fmt"
  11. "strings"
  12. "github.com/gogf/gf/cmd/gf/v2/internal/consts"
  13. "github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog"
  14. "github.com/gogf/gf/v2/database/gdb"
  15. "github.com/gogf/gf/v2/frame/g"
  16. "github.com/gogf/gf/v2/os/gctx"
  17. "github.com/gogf/gf/v2/os/gfile"
  18. "github.com/gogf/gf/v2/os/gtime"
  19. "github.com/gogf/gf/v2/text/gregex"
  20. "github.com/gogf/gf/v2/text/gstr"
  21. "github.com/gogf/gf/v2/util/gconv"
  22. "github.com/gogf/gf/v2/util/gtag"
  23. "github.com/olekukonko/tablewriter"
  24. )
  25. type (
  26. CGenPbEntity struct{}
  27. CGenPbEntityInput struct {
  28. g.Meta `name:"pbentity" config:"{CGenPbEntityConfig}" brief:"{CGenPbEntityBrief}" eg:"{CGenPbEntityEg}" ad:"{CGenPbEntityAd}"`
  29. Path string `name:"path" short:"p" brief:"{CGenPbEntityBriefPath}" d:"manifest/protobuf/pbentity"`
  30. Package string `name:"package" short:"k" brief:"{CGenPbEntityBriefPackage}"`
  31. Link string `name:"link" short:"l" brief:"{CGenPbEntityBriefLink}"`
  32. Tables string `name:"tables" short:"t" brief:"{CGenPbEntityBriefTables}"`
  33. Prefix string `name:"prefix" short:"f" brief:"{CGenPbEntityBriefPrefix}"`
  34. RemovePrefix string `name:"removePrefix" short:"r" brief:"{CGenPbEntityBriefRemovePrefix}"`
  35. NameCase string `name:"nameCase" short:"n" brief:"{CGenPbEntityBriefNameCase}" d:"Camel"`
  36. JsonCase string `name:"jsonCase" short:"j" brief:"{CGenPbEntityBriefJsonCase}" d:"CamelLower"`
  37. Option string `name:"option" short:"o" brief:"{CGenPbEntityBriefOption}"`
  38. }
  39. CGenPbEntityOutput struct{}
  40. CGenPbEntityInternalInput struct {
  41. CGenPbEntityInput
  42. DB gdb.DB
  43. TableName string // TableName specifies the table name of the table.
  44. NewTableName string // NewTableName specifies the prefix-stripped name of the table.
  45. }
  46. )
  47. const (
  48. defaultPackageSuffix = `api/pbentity`
  49. CGenPbEntityConfig = `gfcli.gen.pbentity`
  50. CGenPbEntityBrief = `generate entity message files in protobuf3 format`
  51. CGenPbEntityEg = `
  52. gf gen pbentity
  53. gf gen pbentity -l "mysql:root:12345678@tcp(127.0.0.1:3306)/test"
  54. gf gen pbentity -p ./protocol/demos/entity -t user,user_detail,user_login
  55. gf gen pbentity -r user_ -k github.com/gogf/gf/example/protobuf
  56. gf gen pbentity -r user_
  57. `
  58. CGenPbEntityAd = `
  59. CONFIGURATION SUPPORT
  60. Options are also supported by configuration file.
  61. It's suggested using configuration file instead of command line arguments making producing.
  62. The configuration node name is "gf.gen.pbentity", which also supports multiple databases, for example(config.yaml):
  63. gfcli:
  64. gen:
  65. - pbentity:
  66. link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test"
  67. path: "protocol/demos/entity"
  68. tables: "order,products"
  69. package: "demos"
  70. - pbentity:
  71. link: "mysql:root:12345678@tcp(127.0.0.1:3306)/primary"
  72. path: "protocol/demos/entity"
  73. prefix: "primary_"
  74. tables: "user, userDetail"
  75. package: "demos"
  76. option: |
  77. option go_package = "protobuf/demos";
  78. option java_package = "protobuf/demos";
  79. option php_namespace = "protobuf/demos";
  80. `
  81. CGenPbEntityBriefPath = `directory path for generated files storing`
  82. CGenPbEntityBriefPackage = `package path for all entity proto files`
  83. CGenPbEntityBriefLink = `database configuration, the same as the ORM configuration of GoFrame`
  84. CGenPbEntityBriefTables = `generate models only for given tables, multiple table names separated with ','`
  85. CGenPbEntityBriefPrefix = `add specified prefix for all entity names and entity proto files`
  86. CGenPbEntityBriefRemovePrefix = `remove specified prefix of the table, multiple prefix separated with ','`
  87. CGenPbEntityBriefOption = `extra protobuf options`
  88. CGenPbEntityBriefGroup = `
  89. specifying the configuration group name of database for generated ORM instance,
  90. it's not necessary and the default value is "default"
  91. `
  92. CGenPbEntityBriefNameCase = `
  93. case for message attribute names, default is "Camel":
  94. | Case | Example |
  95. |---------------- |--------------------|
  96. | Camel | AnyKindOfString |
  97. | CamelLower | anyKindOfString | default
  98. | Snake | any_kind_of_string |
  99. | SnakeScreaming | ANY_KIND_OF_STRING |
  100. | SnakeFirstUpper | rgb_code_md5 |
  101. | Kebab | any-kind-of-string |
  102. | KebabScreaming | ANY-KIND-OF-STRING |
  103. `
  104. CGenPbEntityBriefJsonCase = `
  105. case for message json tag, cases are the same as "nameCase", default "CamelLower".
  106. set it to "none" to ignore json tag generating.
  107. `
  108. )
  109. func init() {
  110. gtag.Sets(g.MapStrStr{
  111. `CGenPbEntityConfig`: CGenPbEntityConfig,
  112. `CGenPbEntityBrief`: CGenPbEntityBrief,
  113. `CGenPbEntityEg`: CGenPbEntityEg,
  114. `CGenPbEntityAd`: CGenPbEntityAd,
  115. `CGenPbEntityBriefPath`: CGenPbEntityBriefPath,
  116. `CGenPbEntityBriefPackage`: CGenPbEntityBriefPackage,
  117. `CGenPbEntityBriefLink`: CGenPbEntityBriefLink,
  118. `CGenPbEntityBriefTables`: CGenPbEntityBriefTables,
  119. `CGenPbEntityBriefPrefix`: CGenPbEntityBriefPrefix,
  120. `CGenPbEntityBriefRemovePrefix`: CGenPbEntityBriefRemovePrefix,
  121. `CGenPbEntityBriefGroup`: CGenPbEntityBriefGroup,
  122. `CGenPbEntityBriefNameCase`: CGenPbEntityBriefNameCase,
  123. `CGenPbEntityBriefJsonCase`: CGenPbEntityBriefJsonCase,
  124. `CGenPbEntityBriefOption`: CGenPbEntityBriefOption,
  125. })
  126. }
  127. func (c CGenPbEntity) PbEntity(ctx context.Context, in CGenPbEntityInput) (out *CGenPbEntityOutput, err error) {
  128. var (
  129. config = g.Cfg()
  130. )
  131. if config.Available(ctx) {
  132. v := config.MustGet(ctx, CGenPbEntityConfig)
  133. if v.IsSlice() {
  134. for i := 0; i < len(v.Interfaces()); i++ {
  135. doGenPbEntityForArray(ctx, i, in)
  136. }
  137. } else {
  138. doGenPbEntityForArray(ctx, -1, in)
  139. }
  140. } else {
  141. doGenPbEntityForArray(ctx, -1, in)
  142. }
  143. mlog.Print("done!")
  144. return
  145. }
  146. func doGenPbEntityForArray(ctx context.Context, index int, in CGenPbEntityInput) {
  147. var (
  148. err error
  149. db gdb.DB
  150. )
  151. if index >= 0 {
  152. err = g.Cfg().MustGet(
  153. ctx,
  154. fmt.Sprintf(`%s.%d`, CGenPbEntityConfig, index),
  155. ).Scan(&in)
  156. if err != nil {
  157. mlog.Fatalf(`invalid configuration of "%s": %+v`, CGenPbEntityConfig, err)
  158. }
  159. }
  160. if in.Package == "" {
  161. mlog.Debug(`package parameter is empty, trying calculating the package path using go.mod`)
  162. if !gfile.Exists("go.mod") {
  163. mlog.Fatal("go.mod does not exist in current working directory")
  164. }
  165. var (
  166. modName string
  167. goModContent = gfile.GetContents("go.mod")
  168. match, _ = gregex.MatchString(`^module\s+(.+)\s*`, goModContent)
  169. )
  170. if len(match) > 1 {
  171. modName = gstr.Trim(match[1])
  172. in.Package = modName + "/" + defaultPackageSuffix
  173. } else {
  174. mlog.Fatal("module name does not found in go.mod")
  175. }
  176. }
  177. removePrefixArray := gstr.SplitAndTrim(in.RemovePrefix, ",")
  178. // It uses user passed database configuration.
  179. if in.Link != "" {
  180. var (
  181. tempGroup = gtime.TimestampNanoStr()
  182. match, _ = gregex.MatchString(`([a-z]+):(.+)`, in.Link)
  183. )
  184. if len(match) == 3 {
  185. gdb.AddConfigNode(tempGroup, gdb.ConfigNode{
  186. Type: gstr.Trim(match[1]),
  187. Link: gstr.Trim(match[2]),
  188. })
  189. db, _ = gdb.Instance(tempGroup)
  190. }
  191. } else {
  192. db = g.DB()
  193. }
  194. if db == nil {
  195. mlog.Fatal("database initialization failed")
  196. }
  197. tableNames := ([]string)(nil)
  198. if in.Tables != "" {
  199. tableNames = gstr.SplitAndTrim(in.Tables, ",")
  200. } else {
  201. tableNames, err = db.Tables(context.TODO())
  202. if err != nil {
  203. mlog.Fatalf("fetching tables failed: \n %v", err)
  204. }
  205. }
  206. for _, tableName := range tableNames {
  207. newTableName := tableName
  208. for _, v := range removePrefixArray {
  209. newTableName = gstr.TrimLeftStr(newTableName, v, 1)
  210. }
  211. generatePbEntityContentFile(ctx, CGenPbEntityInternalInput{
  212. CGenPbEntityInput: in,
  213. DB: db,
  214. TableName: tableName,
  215. NewTableName: newTableName,
  216. })
  217. }
  218. }
  219. // generatePbEntityContentFile generates the protobuf files for given table.
  220. func generatePbEntityContentFile(ctx context.Context, in CGenPbEntityInternalInput) {
  221. fieldMap, err := in.DB.TableFields(ctx, in.TableName)
  222. if err != nil {
  223. mlog.Fatalf("fetching tables fields failed for table '%s':\n%v", in.TableName, err)
  224. }
  225. // Change the `newTableName` if `Prefix` is given.
  226. newTableName := in.Prefix + in.NewTableName
  227. var (
  228. imports string
  229. tableNameCamelCase = gstr.CaseCamel(newTableName)
  230. tableNameSnakeCase = gstr.CaseSnake(newTableName)
  231. entityMessageDefine = generateEntityMessageDefinition(tableNameCamelCase, fieldMap, in)
  232. fileName = gstr.Trim(tableNameSnakeCase, "-_.")
  233. path = gfile.Join(in.Path, fileName+".proto")
  234. )
  235. if gstr.Contains(entityMessageDefine, "google.protobuf.Timestamp") {
  236. imports = `import "google/protobuf/timestamp.proto";`
  237. }
  238. entityContent := gstr.ReplaceByMap(getTplPbEntityContent(""), g.MapStrStr{
  239. "{Imports}": imports,
  240. "{PackageName}": gfile.Basename(in.Package),
  241. "{GoPackage}": in.Package,
  242. "{OptionContent}": in.Option,
  243. "{EntityMessage}": entityMessageDefine,
  244. })
  245. if err := gfile.PutContents(path, strings.TrimSpace(entityContent)); err != nil {
  246. mlog.Fatalf("writing content to '%s' failed: %v", path, err)
  247. } else {
  248. mlog.Print("generated:", path)
  249. }
  250. }
  251. // generateEntityMessageDefinition generates and returns the message definition for specified table.
  252. func generateEntityMessageDefinition(entityName string, fieldMap map[string]*gdb.TableField, in CGenPbEntityInternalInput) string {
  253. var (
  254. buffer = bytes.NewBuffer(nil)
  255. array = make([][]string, len(fieldMap))
  256. names = sortFieldKeyForPbEntity(fieldMap)
  257. )
  258. for index, name := range names {
  259. array[index] = generateMessageFieldForPbEntity(index+1, fieldMap[name], in)
  260. }
  261. tw := tablewriter.NewWriter(buffer)
  262. tw.SetBorder(false)
  263. tw.SetRowLine(false)
  264. tw.SetAutoWrapText(false)
  265. tw.SetColumnSeparator("")
  266. tw.AppendBulk(array)
  267. tw.Render()
  268. stContent := buffer.String()
  269. // Let's do this hack of table writer for indent!
  270. stContent = gstr.Replace(stContent, " #", "")
  271. buffer.Reset()
  272. buffer.WriteString(fmt.Sprintf("message %s {\n", entityName))
  273. buffer.WriteString(stContent)
  274. buffer.WriteString("}")
  275. return buffer.String()
  276. }
  277. // generateMessageFieldForPbEntity generates and returns the message definition for specified field.
  278. func generateMessageFieldForPbEntity(index int, field *gdb.TableField, in CGenPbEntityInternalInput) []string {
  279. var (
  280. typeName string
  281. comment string
  282. jsonTagStr string
  283. err error
  284. ctx = gctx.GetInitCtx()
  285. )
  286. typeName, err = in.DB.CheckLocalTypeForField(ctx, field.Type, nil)
  287. if err != nil {
  288. panic(err)
  289. }
  290. var typeMapping = map[string]string{
  291. gdb.LocalTypeString: "string",
  292. gdb.LocalTypeDate: "google.protobuf.Timestamp",
  293. gdb.LocalTypeDatetime: "google.protobuf.Timestamp",
  294. gdb.LocalTypeInt: "int32",
  295. gdb.LocalTypeUint: "uint32",
  296. gdb.LocalTypeInt64: "int64",
  297. gdb.LocalTypeUint64: "uint64",
  298. gdb.LocalTypeIntSlice: "repeated int32",
  299. gdb.LocalTypeInt64Slice: "repeated int64",
  300. gdb.LocalTypeUint64Slice: "repeated uint64",
  301. gdb.LocalTypeInt64Bytes: "repeated int64",
  302. gdb.LocalTypeUint64Bytes: "repeated uint64",
  303. gdb.LocalTypeFloat32: "float",
  304. gdb.LocalTypeFloat64: "double",
  305. gdb.LocalTypeBytes: "bytes",
  306. gdb.LocalTypeBool: "bool",
  307. gdb.LocalTypeJson: "string",
  308. gdb.LocalTypeJsonb: "string",
  309. }
  310. typeName = typeMapping[typeName]
  311. if typeName == "" {
  312. typeName = "string"
  313. }
  314. comment = gstr.ReplaceByArray(field.Comment, g.SliceStr{
  315. "\n", " ",
  316. "\r", " ",
  317. })
  318. comment = gstr.Trim(comment)
  319. comment = gstr.Replace(comment, `\n`, " ")
  320. comment, _ = gregex.ReplaceString(`\s{2,}`, ` `, comment)
  321. if jsonTagName := formatCase(field.Name, in.JsonCase); jsonTagName != "" {
  322. // beautiful indent.
  323. if index < 10 {
  324. // 3 spaces
  325. jsonTagStr = " " + jsonTagStr
  326. } else if index < 100 {
  327. // 2 spaces
  328. jsonTagStr = " " + jsonTagStr
  329. } else {
  330. // 1 spaces
  331. jsonTagStr = " " + jsonTagStr
  332. }
  333. }
  334. return []string{
  335. " #" + typeName,
  336. " #" + formatCase(field.Name, in.NameCase),
  337. " #= " + gconv.String(index) + jsonTagStr + ";",
  338. " #" + fmt.Sprintf(`// %s`, comment),
  339. }
  340. }
  341. func getTplPbEntityContent(tplEntityPath string) string {
  342. if tplEntityPath != "" {
  343. return gfile.GetContents(tplEntityPath)
  344. }
  345. return consts.TemplatePbEntityMessageContent
  346. }
  347. // formatCase call gstr.Case* function to convert the s to specified case.
  348. func formatCase(str, caseStr string) string {
  349. switch gstr.ToLower(caseStr) {
  350. case gstr.ToLower("Camel"):
  351. return gstr.CaseCamel(str)
  352. case gstr.ToLower("CamelLower"):
  353. return gstr.CaseCamelLower(str)
  354. case gstr.ToLower("Kebab"):
  355. return gstr.CaseKebab(str)
  356. case gstr.ToLower("KebabScreaming"):
  357. return gstr.CaseKebabScreaming(str)
  358. case gstr.ToLower("Snake"):
  359. return gstr.CaseSnake(str)
  360. case gstr.ToLower("SnakeFirstUpper"):
  361. return gstr.CaseSnakeFirstUpper(str)
  362. case gstr.ToLower("SnakeScreaming"):
  363. return gstr.CaseSnakeScreaming(str)
  364. case "none":
  365. return ""
  366. }
  367. return str
  368. }
  369. func sortFieldKeyForPbEntity(fieldMap map[string]*gdb.TableField) []string {
  370. names := make(map[int]string)
  371. for _, field := range fieldMap {
  372. names[field.Index] = field.Name
  373. }
  374. var (
  375. result = make([]string, len(names))
  376. i = 0
  377. j = 0
  378. )
  379. for {
  380. if len(names) == 0 {
  381. break
  382. }
  383. if val, ok := names[i]; ok {
  384. result[j] = val
  385. j++
  386. delete(names, i)
  387. }
  388. i++
  389. }
  390. return result
  391. }