genservice.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  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 genservice
  7. import (
  8. "context"
  9. "fmt"
  10. "github.com/gogf/gf/v2/container/garray"
  11. "github.com/gogf/gf/v2/container/gmap"
  12. "github.com/gogf/gf/v2/container/gset"
  13. "github.com/gogf/gf/v2/frame/g"
  14. "github.com/gogf/gf/v2/os/gfile"
  15. "github.com/gogf/gf/v2/os/gtime"
  16. "github.com/gogf/gf/v2/text/gregex"
  17. "github.com/gogf/gf/v2/text/gstr"
  18. "github.com/gogf/gf/v2/util/gconv"
  19. "github.com/gogf/gf/v2/util/gtag"
  20. "github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog"
  21. "github.com/gogf/gf/cmd/gf/v2/internal/utility/utils"
  22. )
  23. const (
  24. CGenServiceConfig = `gfcli.gen.service`
  25. CGenServiceUsage = `gf gen service [OPTION]`
  26. CGenServiceBrief = `parse struct and associated functions from packages to generate service go file`
  27. CGenServiceEg = `
  28. gf gen service
  29. gf gen service -f Snake
  30. `
  31. CGenServiceBriefSrcFolder = `source folder path to be parsed. default: internal/logic`
  32. CGenServiceBriefDstFolder = `destination folder path storing automatically generated go files. default: internal/service`
  33. CGenServiceBriefFileNameCase = `
  34. destination file name storing automatically generated go files, cases are as follows:
  35. | Case | Example |
  36. |---------------- |--------------------|
  37. | Lower | anykindofstring |
  38. | Camel | AnyKindOfString |
  39. | CamelLower | anyKindOfString |
  40. | Snake | any_kind_of_string | default
  41. | SnakeScreaming | ANY_KIND_OF_STRING |
  42. | SnakeFirstUpper | rgb_code_md5 |
  43. | Kebab | any-kind-of-string |
  44. | KebabScreaming | ANY-KIND-OF-STRING |
  45. `
  46. CGenServiceBriefWatchFile = `used in file watcher, it re-generates all service go files only if given file is under srcFolder`
  47. CGenServiceBriefStPattern = `regular expression matching struct name for generating service. default: ^s([A-Z]\\\\w+)$`
  48. CGenServiceBriefPackages = `produce go files only for given source packages(source folders)`
  49. CGenServiceBriefImportPrefix = `custom import prefix to calculate import path for generated importing go file of logic`
  50. CGenServiceBriefClear = `delete all generated go files that are not used any further`
  51. )
  52. func init() {
  53. gtag.Sets(g.MapStrStr{
  54. `CGenServiceConfig`: CGenServiceConfig,
  55. `CGenServiceUsage`: CGenServiceUsage,
  56. `CGenServiceBrief`: CGenServiceBrief,
  57. `CGenServiceEg`: CGenServiceEg,
  58. `CGenServiceBriefSrcFolder`: CGenServiceBriefSrcFolder,
  59. `CGenServiceBriefDstFolder`: CGenServiceBriefDstFolder,
  60. `CGenServiceBriefFileNameCase`: CGenServiceBriefFileNameCase,
  61. `CGenServiceBriefWatchFile`: CGenServiceBriefWatchFile,
  62. `CGenServiceBriefStPattern`: CGenServiceBriefStPattern,
  63. `CGenServiceBriefPackages`: CGenServiceBriefPackages,
  64. `CGenServiceBriefImportPrefix`: CGenServiceBriefImportPrefix,
  65. `CGenServiceBriefClear`: CGenServiceBriefClear,
  66. })
  67. }
  68. type (
  69. CGenService struct{}
  70. CGenServiceInput struct {
  71. g.Meta `name:"service" config:"{CGenServiceConfig}" usage:"{CGenServiceUsage}" brief:"{CGenServiceBrief}" eg:"{CGenServiceEg}"`
  72. SrcFolder string `short:"s" name:"srcFolder" brief:"{CGenServiceBriefSrcFolder}" d:"internal/logic"`
  73. DstFolder string `short:"d" name:"dstFolder" brief:"{CGenServiceBriefDstFolder}" d:"internal/service"`
  74. DstFileNameCase string `short:"f" name:"dstFileNameCase" brief:"{CGenServiceBriefFileNameCase}" d:"Snake"`
  75. WatchFile string `short:"w" name:"watchFile" brief:"{CGenServiceBriefWatchFile}"`
  76. StPattern string `short:"a" name:"stPattern" brief:"{CGenServiceBriefStPattern}" d:"^s([A-Z]\\w+)$"`
  77. Packages []string `short:"p" name:"packages" brief:"{CGenServiceBriefPackages}"`
  78. ImportPrefix string `short:"i" name:"importPrefix" brief:"{CGenServiceBriefImportPrefix}"`
  79. Clear bool `short:"l" name:"clear" brief:"{CGenServiceBriefClear}" orphan:"true"`
  80. }
  81. CGenServiceOutput struct{}
  82. )
  83. const (
  84. genServiceFileLockSeconds = 10
  85. )
  86. func (c CGenService) Service(ctx context.Context, in CGenServiceInput) (out *CGenServiceOutput, err error) {
  87. in.SrcFolder = gstr.TrimRight(in.SrcFolder, `\/`)
  88. in.SrcFolder = gstr.Replace(in.SrcFolder, "\\", "/")
  89. in.WatchFile = gstr.TrimRight(in.WatchFile, `\/`)
  90. in.WatchFile = gstr.Replace(in.WatchFile, "\\", "/")
  91. // Watch file handling.
  92. if in.WatchFile != "" {
  93. // File lock to avoid multiple processes.
  94. var (
  95. flockFilePath = gfile.Temp("gf.cli.gen.service.lock")
  96. flockContent = gfile.GetContents(flockFilePath)
  97. )
  98. if flockContent != "" {
  99. if gtime.Timestamp()-gconv.Int64(flockContent) < genServiceFileLockSeconds {
  100. // If another "gen service" process is running, it just exits.
  101. mlog.Debug(`another "gen service" process is running, exit`)
  102. return
  103. }
  104. }
  105. defer gfile.Remove(flockFilePath)
  106. _ = gfile.PutContents(flockFilePath, gtime.TimestampStr())
  107. // It works only if given WatchFile is in SrcFolder.
  108. var (
  109. watchFileDir = gfile.Dir(in.WatchFile)
  110. srcFolderDir = gfile.Dir(watchFileDir)
  111. )
  112. mlog.Debug("watchFileDir:", watchFileDir)
  113. mlog.Debug("logicFolderDir:", srcFolderDir)
  114. if !gstr.HasSuffix(gstr.Replace(srcFolderDir, `\`, `/`), in.SrcFolder) {
  115. mlog.Printf(`ignore watch file "%s", not in source path "%s"`, in.WatchFile, in.SrcFolder)
  116. return
  117. }
  118. var newWorkingDir = gfile.Dir(gfile.Dir(srcFolderDir))
  119. if err = gfile.Chdir(newWorkingDir); err != nil {
  120. mlog.Fatalf(`%+v`, err)
  121. }
  122. mlog.Debug("Chdir:", newWorkingDir)
  123. in.WatchFile = ""
  124. in.Packages = []string{gfile.Basename(watchFileDir)}
  125. return c.Service(ctx, in)
  126. }
  127. if !gfile.Exists(in.SrcFolder) {
  128. mlog.Fatalf(`source folder path "%s" does not exist`, in.SrcFolder)
  129. }
  130. if in.ImportPrefix == "" {
  131. in.ImportPrefix = utils.GetImportPath(in.SrcFolder)
  132. }
  133. var (
  134. isDirty bool // Temp boolean.
  135. files []string // Temp file array.
  136. fileContent string // Temp file content for handling go file.
  137. initImportSrcPackages []string // Used for generating logic.go.
  138. inputPackages = in.Packages // Custom packages.
  139. dstPackageName = gstr.ToLower(gfile.Basename(in.DstFolder)) // Package name for generated go files.
  140. generatedDstFilePathSet = gset.NewStrSet() // All generated file path set.
  141. )
  142. // The first level folders.
  143. srcFolderPaths, err := gfile.ScanDir(in.SrcFolder, "*", false)
  144. if err != nil {
  145. return nil, err
  146. }
  147. for _, srcFolderPath := range srcFolderPaths {
  148. if !gfile.IsDir(srcFolderPath) {
  149. continue
  150. }
  151. // Only retrieve sub files, no recursively.
  152. if files, err = gfile.ScanDir(srcFolderPath, "*.go", false); err != nil {
  153. return nil, err
  154. }
  155. if len(files) == 0 {
  156. continue
  157. }
  158. // Parse single logic package folder.
  159. var (
  160. // StructName => FunctionDefinitions
  161. srcPkgInterfaceMap = make(map[string]*garray.StrArray)
  162. srcImportedPackages = garray.NewSortedStrArray().SetUnique(true)
  163. importAliasToPathMap = gmap.NewStrStrMap() // for conflict imports check. alias => import path(with `"`)
  164. importPathToAliasMap = gmap.NewStrStrMap() // for conflict imports check. import path(with `"`) => alias
  165. srcPackageName = gfile.Basename(srcFolderPath)
  166. ok bool
  167. dstFilePath = gfile.Join(in.DstFolder,
  168. c.getDstFileNameCase(srcPackageName, in.DstFileNameCase)+".go",
  169. )
  170. srcCodeCommentedMap = make(map[string]string)
  171. )
  172. generatedDstFilePathSet.Add(dstFilePath)
  173. for _, file := range files {
  174. var packageItems []packageItem
  175. fileContent = gfile.GetContents(file)
  176. // Calculate code comments in source Go files.
  177. err = c.calculateCodeCommented(in, fileContent, srcCodeCommentedMap)
  178. if err != nil {
  179. return nil, err
  180. }
  181. // remove all comments.
  182. fileContent, err = gregex.ReplaceString(`/[/|\*](.*)`, "", fileContent)
  183. if err != nil {
  184. return nil, err
  185. }
  186. // Calculate imported packages of source go files.
  187. packageItems, err = c.calculateImportedPackages(fileContent)
  188. if err != nil {
  189. return nil, err
  190. }
  191. // try finding the conflicts imports between files.
  192. for _, item := range packageItems {
  193. var alias = item.Alias
  194. if alias == "" {
  195. alias = gfile.Basename(gstr.Trim(item.Path, `"`))
  196. }
  197. // ignore unused import paths, which do not exist in function definitions.
  198. if !gregex.IsMatchString(fmt.Sprintf(`func .+?([^\w])%s(\.\w+).+?{`, alias), fileContent) {
  199. mlog.Debugf(`ignore unused package: %s`, item.RawImport)
  200. continue
  201. }
  202. // find the exist alias with the same import path.
  203. var existAlias = importPathToAliasMap.Get(item.Path)
  204. if existAlias != "" {
  205. fileContent, err = gregex.ReplaceStringFuncMatch(
  206. fmt.Sprintf(`([^\w])%s(\.\w+)`, alias), fileContent,
  207. func(match []string) string {
  208. return match[1] + existAlias + match[2]
  209. },
  210. )
  211. if err != nil {
  212. return nil, err
  213. }
  214. continue
  215. }
  216. // resolve alias conflicts.
  217. var importPath = importAliasToPathMap.Get(alias)
  218. if importPath == "" {
  219. importAliasToPathMap.Set(alias, item.Path)
  220. importPathToAliasMap.Set(item.Path, alias)
  221. srcImportedPackages.Add(item.RawImport)
  222. continue
  223. }
  224. if importPath != item.Path {
  225. // update the conflicted alias for import path with suffix.
  226. // eg:
  227. // v1 -> v10
  228. // v11 -> v110
  229. for aliasIndex := 0; ; aliasIndex++ {
  230. item.Alias = fmt.Sprintf(`%s%d`, alias, aliasIndex)
  231. var existPathForAlias = importAliasToPathMap.Get(item.Alias)
  232. if existPathForAlias != "" {
  233. if existPathForAlias == item.Path {
  234. break
  235. }
  236. continue
  237. }
  238. break
  239. }
  240. importPathToAliasMap.Set(item.Path, item.Alias)
  241. importAliasToPathMap.Set(item.Alias, item.Path)
  242. // reformat the import path with alias.
  243. item.RawImport = fmt.Sprintf(`%s %s`, item.Alias, item.Path)
  244. // update the file content with new alias import.
  245. fileContent, err = gregex.ReplaceStringFuncMatch(
  246. fmt.Sprintf(`([^\w])%s(\.\w+)`, alias), fileContent,
  247. func(match []string) string {
  248. return match[1] + item.Alias + match[2]
  249. },
  250. )
  251. if err != nil {
  252. return nil, err
  253. }
  254. srcImportedPackages.Add(item.RawImport)
  255. }
  256. }
  257. // Calculate functions and interfaces for service generating.
  258. err = c.calculateInterfaceFunctions(in, fileContent, srcPkgInterfaceMap)
  259. if err != nil {
  260. return nil, err
  261. }
  262. }
  263. initImportSrcPackages = append(
  264. initImportSrcPackages,
  265. fmt.Sprintf(`%s/%s`, in.ImportPrefix, srcPackageName),
  266. )
  267. // Ignore source packages if input packages given.
  268. if len(inputPackages) > 0 && !gstr.InArray(inputPackages, srcPackageName) {
  269. mlog.Debugf(
  270. `ignore source package "%s" as it is not in desired packages: %+v`,
  271. srcPackageName, inputPackages,
  272. )
  273. continue
  274. }
  275. // Generating service go file for single logic package.
  276. if ok, err = c.generateServiceFile(generateServiceFilesInput{
  277. CGenServiceInput: in,
  278. SrcStructFunctions: srcPkgInterfaceMap,
  279. SrcImportedPackages: srcImportedPackages.Slice(),
  280. SrcPackageName: srcPackageName,
  281. DstPackageName: dstPackageName,
  282. DstFilePath: dstFilePath,
  283. SrcCodeCommentedMap: srcCodeCommentedMap,
  284. }); err != nil {
  285. return
  286. }
  287. if ok {
  288. isDirty = true
  289. }
  290. }
  291. if in.Clear {
  292. files, err = gfile.ScanDirFile(in.DstFolder, "*.go", false)
  293. if err != nil {
  294. return nil, err
  295. }
  296. var relativeFilePath string
  297. for _, file := range files {
  298. relativeFilePath = gstr.SubStrFromR(file, in.DstFolder)
  299. if !generatedDstFilePathSet.Contains(relativeFilePath) && utils.IsFileDoNotEdit(relativeFilePath) {
  300. mlog.Printf(`remove no longer used service file: %s`, relativeFilePath)
  301. if err = gfile.Remove(file); err != nil {
  302. return nil, err
  303. }
  304. }
  305. }
  306. }
  307. if isDirty {
  308. // Generate initialization go file.
  309. if len(initImportSrcPackages) > 0 {
  310. if err = c.generateInitializationFile(in, initImportSrcPackages); err != nil {
  311. return
  312. }
  313. }
  314. // Replace v1 to v2 for GoFrame.
  315. if err = utils.ReplaceGeneratedContentGFV2(in.DstFolder); err != nil {
  316. return nil, err
  317. }
  318. mlog.Printf(`gofmt go files in "%s"`, in.DstFolder)
  319. utils.GoFmt(in.DstFolder)
  320. }
  321. // auto update main.go.
  322. if err = c.checkAndUpdateMain(in.SrcFolder); err != nil {
  323. return nil, err
  324. }
  325. mlog.Print(`done!`)
  326. return
  327. }
  328. func (c CGenService) checkAndUpdateMain(srcFolder string) (err error) {
  329. var (
  330. logicPackageName = gstr.ToLower(gfile.Basename(srcFolder))
  331. logicFilePath = gfile.Join(srcFolder, logicPackageName+".go")
  332. importPath = utils.GetImportPath(logicFilePath)
  333. importStr = fmt.Sprintf(`_ "%s"`, importPath)
  334. mainFilePath = gfile.Join(gfile.Dir(gfile.Dir(gfile.Dir(logicFilePath))), "main.go")
  335. mainFileContent = gfile.GetContents(mainFilePath)
  336. )
  337. // No main content found.
  338. if mainFileContent == "" {
  339. return nil
  340. }
  341. if gstr.Contains(mainFileContent, importStr) {
  342. return nil
  343. }
  344. match, err := gregex.MatchString(`import \(([\s\S]+?)\)`, mainFileContent)
  345. if err != nil {
  346. return err
  347. }
  348. // No match.
  349. if len(match) < 2 {
  350. return nil
  351. }
  352. lines := garray.NewStrArrayFrom(gstr.Split(match[1], "\n"))
  353. for i, line := range lines.Slice() {
  354. line = gstr.Trim(line)
  355. if len(line) == 0 {
  356. continue
  357. }
  358. if line[0] == '_' {
  359. continue
  360. }
  361. // Insert the logic import into imports.
  362. if err = lines.InsertBefore(i, fmt.Sprintf("\t%s\n\n", importStr)); err != nil {
  363. return err
  364. }
  365. break
  366. }
  367. mainFileContent, err = gregex.ReplaceString(
  368. `import \(([\s\S]+?)\)`,
  369. fmt.Sprintf(`import (%s)`, lines.Join("\n")),
  370. mainFileContent,
  371. )
  372. if err != nil {
  373. return err
  374. }
  375. mlog.Print(`update main.go`)
  376. err = gfile.PutContents(mainFilePath, mainFileContent)
  377. utils.GoFmt(mainFilePath)
  378. return
  379. }