diff --git a/cmd/gf/internal/cmd/cmd_z_unit_gen_service_test.go b/cmd/gf/internal/cmd/cmd_z_unit_gen_service_test.go index d01495b406..4491a3fcbc 100644 --- a/cmd/gf/internal/cmd/cmd_z_unit_gen_service_test.go +++ b/cmd/gf/internal/cmd/cmd_z_unit_gen_service_test.go @@ -57,12 +57,16 @@ func Test_Gen_Service_Default(t *testing.T) { t.AssertNil(err) t.Assert(files, []string{ dstFolder + filepath.FromSlash("/article.go"), + dstFolder + filepath.FromSlash("/delivery.go"), + dstFolder + filepath.FromSlash("/user.go"), }) // contents testPath := gtest.DataPath("genservice", "service") expectFiles := []string{ testPath + filepath.FromSlash("/article.go"), + testPath + filepath.FromSlash("/delivery.go"), + testPath + filepath.FromSlash("/user.go"), } for i := range files { t.Assert(gfile.GetContents(files[i]), gfile.GetContents(expectFiles[i])) diff --git a/cmd/gf/internal/cmd/genservice/genservice.go b/cmd/gf/internal/cmd/genservice/genservice.go index c15a70433c..5b9ae7efc0 100644 --- a/cmd/gf/internal/cmd/genservice/genservice.go +++ b/cmd/gf/internal/cmd/genservice/genservice.go @@ -10,7 +10,11 @@ import ( "context" "fmt" "path/filepath" + "sync" + "sync/atomic" + "github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog" + "github.com/gogf/gf/cmd/gf/v2/internal/utility/utils" "github.com/gogf/gf/v2/container/garray" "github.com/gogf/gf/v2/container/gmap" "github.com/gogf/gf/v2/container/gset" @@ -21,9 +25,6 @@ import ( "github.com/gogf/gf/v2/text/gstr" "github.com/gogf/gf/v2/util/gconv" "github.com/gogf/gf/v2/util/gtag" - - "github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog" - "github.com/gogf/gf/cmd/gf/v2/internal/utility/utils" ) const ( @@ -147,19 +148,22 @@ func (c CGenService) Service(ctx context.Context, in CGenServiceInput) (out *CGe } var ( - isDirty bool // Temp boolean. + isDirty atomic.Value // Temp boolean. files []string // Temp file array. - fileContent string // Temp file content for handling go file. initImportSrcPackages []string // Used for generating logic.go. inputPackages = in.Packages // Custom packages. dstPackageName = gstr.ToLower(gfile.Basename(in.DstFolder)) // Package name for generated go files. generatedDstFilePathSet = gset.NewStrSet() // All generated file path set. ) + isDirty.Store(false) + // The first level folders. srcFolderPaths, err := gfile.ScanDir(in.SrcFolder, "*", false) if err != nil { return nil, err } + // it will use goroutine to generate service files for each package. + var wg = sync.WaitGroup{} for _, srcFolderPath := range srcFolderPaths { if !gfile.IsDir(srcFolderPath) { continue @@ -173,111 +177,30 @@ func (c CGenService) Service(ctx context.Context, in CGenServiceInput) (out *CGe } // Parse single logic package folder. var ( - // StructName => FunctionDefinitions - srcPkgInterfaceMap = gmap.NewListMap() - srcImportedPackages = garray.NewSortedStrArray().SetUnique(true) - importAliasToPathMap = gmap.NewStrStrMap() // for conflict imports check. alias => import path(with `"`) - importPathToAliasMap = gmap.NewStrStrMap() // for conflict imports check. import path(with `"`) => alias - srcPackageName = gfile.Basename(srcFolderPath) - ok bool - dstFilePath = gfile.Join(in.DstFolder, + srcPackageName = gfile.Basename(srcFolderPath) + srcImportedPackages = garray.NewSortedStrArray().SetUnique(true) + srcStructFunctions = gmap.NewListMap() + dstFilePath = gfile.Join(in.DstFolder, c.getDstFileNameCase(srcPackageName, in.DstFileNameCase)+".go", ) - srcCodeCommentedMap = make(map[string]string) ) generatedDstFilePathSet.Add(dstFilePath) + // if it were to use goroutine, + // it would cause the order of the generated functions in the file to be disordered. for _, file := range files { - var packageItems []packageItem - fileContent = gfile.GetContents(file) - // Calculate code comments in source Go files. - err = c.calculateCodeCommented(in, fileContent, srcCodeCommentedMap) - if err != nil { - return nil, err - } - // remove all comments. - fileContent, err = gregex.ReplaceString(`(//.*)|((?s)/\*.*?\*/)`, "", fileContent) + pkgItems, funcItems, err := c.parseItemsInSrc(file) if err != nil { return nil, err } - // Calculate imported packages of source go files. - packageItems, err = c.calculateImportedPackages(fileContent) + // Calculate imported packages for service generating. + err = c.calculateImportedItems(in, pkgItems, funcItems, srcImportedPackages) if err != nil { return nil, err } - // try finding the conflicts imports between files. - for _, item := range packageItems { - var alias = item.Alias - if alias == "" { - alias = gfile.Basename(gstr.Trim(item.Path, `"`)) - } - - // ignore unused import paths, which do not exist in function definitions. - if !gregex.IsMatchString(fmt.Sprintf(`func .+?([^\w])%s(\.\w+).+?{`, alias), fileContent) { - mlog.Debugf(`ignore unused package: %s`, item.RawImport) - continue - } - - // find the exist alias with the same import path. - var existAlias = importPathToAliasMap.Get(item.Path) - if existAlias != "" { - fileContent, err = gregex.ReplaceStringFuncMatch( - fmt.Sprintf(`([^\w])%s(\.\w+)`, alias), fileContent, - func(match []string) string { - return match[1] + existAlias + match[2] - }, - ) - if err != nil { - return nil, err - } - continue - } - - // resolve alias conflicts. - var importPath = importAliasToPathMap.Get(alias) - if importPath == "" { - importAliasToPathMap.Set(alias, item.Path) - importPathToAliasMap.Set(item.Path, alias) - srcImportedPackages.Add(item.RawImport) - continue - } - if importPath != item.Path { - // update the conflicted alias for import path with suffix. - // eg: - // v1 -> v10 - // v11 -> v110 - for aliasIndex := 0; ; aliasIndex++ { - item.Alias = fmt.Sprintf(`%s%d`, alias, aliasIndex) - var existPathForAlias = importAliasToPathMap.Get(item.Alias) - if existPathForAlias != "" { - if existPathForAlias == item.Path { - break - } - continue - } - break - } - importPathToAliasMap.Set(item.Path, item.Alias) - importAliasToPathMap.Set(item.Alias, item.Path) - // reformat the import path with alias. - item.RawImport = fmt.Sprintf(`%s %s`, item.Alias, item.Path) - - // update the file content with new alias import. - fileContent, err = gregex.ReplaceStringFuncMatch( - fmt.Sprintf(`([^\w])%s(\.\w+)`, alias), fileContent, - func(match []string) string { - return match[1] + item.Alias + match[2] - }, - ) - if err != nil { - return nil, err - } - srcImportedPackages.Add(item.RawImport) - } - } // Calculate functions and interfaces for service generating. - err = c.calculateInterfaceFunctions(in, fileContent, srcPkgInterfaceMap) + err = c.calculateFuncItems(in, funcItems, srcStructFunctions) if err != nil { return nil, err } @@ -295,22 +218,28 @@ func (c CGenService) Service(ctx context.Context, in CGenServiceInput) (out *CGe ) continue } + // Generating service go file for single logic package. - if ok, err = c.generateServiceFile(generateServiceFilesInput{ + wg.Add(1) + go func(generateServiceFilesInput generateServiceFilesInput) { + defer wg.Done() + ok, err := c.generateServiceFile(generateServiceFilesInput) + if err != nil { + mlog.Printf(`error generating service file "%s": %v`, generateServiceFilesInput.DstFilePath, err) + } + if !isDirty.Load().(bool) && ok { + isDirty.Store(true) + } + }(generateServiceFilesInput{ CGenServiceInput: in, - SrcStructFunctions: srcPkgInterfaceMap, - SrcImportedPackages: srcImportedPackages.Slice(), SrcPackageName: srcPackageName, + SrcImportedPackages: srcImportedPackages.Slice(), + SrcStructFunctions: srcStructFunctions, DstPackageName: dstPackageName, DstFilePath: dstFilePath, - SrcCodeCommentedMap: srcCodeCommentedMap, - }); err != nil { - return - } - if ok { - isDirty = true - } + }) } + wg.Wait() if in.Clear { files, err = gfile.ScanDirFile(in.DstFolder, "*.go", false) @@ -320,7 +249,9 @@ func (c CGenService) Service(ctx context.Context, in CGenServiceInput) (out *CGe var relativeFilePath string for _, file := range files { relativeFilePath = gstr.SubStrFromR(file, in.DstFolder) - if !generatedDstFilePathSet.Contains(relativeFilePath) && utils.IsFileDoNotEdit(relativeFilePath) { + if !generatedDstFilePathSet.Contains(relativeFilePath) && + utils.IsFileDoNotEdit(relativeFilePath) { + mlog.Printf(`remove no longer used service file: %s`, relativeFilePath) if err = gfile.Remove(file); err != nil { return nil, err @@ -329,7 +260,7 @@ func (c CGenService) Service(ctx context.Context, in CGenServiceInput) (out *CGe } } - if isDirty { + if isDirty.Load().(bool) { // Generate initialization go file. if len(initImportSrcPackages) > 0 { if err = c.generateInitializationFile(in, initImportSrcPackages); err != nil { diff --git a/cmd/gf/internal/cmd/genservice/genservice_ast_parse.go b/cmd/gf/internal/cmd/genservice/genservice_ast_parse.go new file mode 100644 index 0000000000..6a7785ffa1 --- /dev/null +++ b/cmd/gf/internal/cmd/genservice/genservice_ast_parse.go @@ -0,0 +1,186 @@ +// 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 genservice + +import ( + "go/ast" + "go/parser" + "go/token" + + "github.com/gogf/gf/v2/os/gfile" +) + +type pkgItem struct { + Alias string `eg:"gdbas"` + Path string `eg:"github.com/gogf/gf/v2/database/gdb"` + RawImport string `eg:"gdbas github.com/gogf/gf/v2/database/gdb"` +} + +type funcItem struct { + Receiver string `eg:"sUser"` + MethodName string `eg:"GetList"` + Params []map[string]string `eg:"ctx: context.Context, cond: *SearchInput"` + Results []map[string]string `eg:"list: []*User, err: error"` + Comment string `eg:"Get user list"` +} + +// parseItemsInSrc parses the pkgItem and funcItem from the specified file. +// It can't skip the private methods. +// It can't skip the imported packages of import alias equal to `_`. +func (c CGenService) parseItemsInSrc(filePath string) (pkgItems []pkgItem, funcItems []funcItem, err error) { + var ( + fileContent = gfile.GetContents(filePath) + fileSet = token.NewFileSet() + ) + + node, err := parser.ParseFile(fileSet, "", fileContent, parser.ParseComments) + if err != nil { + return + } + + ast.Inspect(node, func(n ast.Node) bool { + switch x := n.(type) { + case *ast.ImportSpec: + // parse the imported packages. + pkgItems = append(pkgItems, c.parseImportPackages(x)) + + case *ast.FuncDecl: + // parse the function items. + if x.Recv == nil { + return true + } + + var funcName = x.Name.Name + funcItems = append(funcItems, funcItem{ + Receiver: c.parseFuncReceiverTypeName(x), + MethodName: funcName, + Params: c.parseFuncParams(x), + Results: c.parseFuncResults(x), + Comment: c.parseFuncComment(x), + }) + } + return true + }) + return +} + +// parseImportPackages retrieves the imported packages from the specified ast.ImportSpec. +func (c CGenService) parseImportPackages(node *ast.ImportSpec) (packages pkgItem) { + if node.Path == nil { + return + } + var ( + alias string + path = node.Path.Value + rawImport string + ) + if node.Name != nil { + alias = node.Name.Name + rawImport = alias + " " + path + } else { + rawImport = path + } + return pkgItem{ + Alias: alias, + Path: path, + RawImport: rawImport, + } +} + +// parseFuncReceiverTypeName retrieves the receiver type of the function. +// For example: +// +// func(s *sArticle) -> *sArticle +// func(s sArticle) -> sArticle +func (c CGenService) parseFuncReceiverTypeName(node *ast.FuncDecl) (receiverType string) { + if node.Recv == nil { + return "" + } + receiverType, err := c.astExprToString(node.Recv.List[0].Type) + if err != nil { + return "" + } + return +} + +// parseFuncParams retrieves the input parameters of the function. +// It returns the name and type of the input parameters. +// For example: +// +// []map[string]string{paramName:ctx paramType:context.Context, paramName:info paramType:struct{}} +func (c CGenService) parseFuncParams(node *ast.FuncDecl) (params []map[string]string) { + if node.Type.Params == nil { + return + } + for _, param := range node.Type.Params.List { + if param.Names == nil { + // No name for the return value. + resultType, err := c.astExprToString(param.Type) + if err != nil { + continue + } + params = append(params, map[string]string{ + "paramName": "", + "paramType": resultType, + }) + continue + } + for _, name := range param.Names { + paramType, err := c.astExprToString(param.Type) + if err != nil { + continue + } + params = append(params, map[string]string{ + "paramName": name.Name, + "paramType": paramType, + }) + } + } + return +} + +// parseFuncResults retrieves the output parameters of the function. +// It returns the name and type of the output parameters. +// For example: +// +// []map[string]string{resultName:list resultType:[]*User, resultName:err resultType:error} +// []map[string]string{resultName: "", resultType: error} +func (c CGenService) parseFuncResults(node *ast.FuncDecl) (results []map[string]string) { + if node.Type.Results == nil { + return + } + for _, result := range node.Type.Results.List { + if result.Names == nil { + // No name for the return value. + resultType, err := c.astExprToString(result.Type) + if err != nil { + continue + } + results = append(results, map[string]string{ + "resultName": "", + "resultType": resultType, + }) + continue + } + for _, name := range result.Names { + resultType, err := c.astExprToString(result.Type) + if err != nil { + continue + } + results = append(results, map[string]string{ + "resultName": name.Name, + "resultType": resultType, + }) + } + } + return +} + +// parseFuncComment retrieves the comment of the function. +func (c CGenService) parseFuncComment(node *ast.FuncDecl) string { + return c.astCommentToString(node.Doc) +} diff --git a/cmd/gf/internal/cmd/genservice/genservice_ast_utils.go b/cmd/gf/internal/cmd/genservice/genservice_ast_utils.go new file mode 100644 index 0000000000..8fc12e208e --- /dev/null +++ b/cmd/gf/internal/cmd/genservice/genservice_ast_utils.go @@ -0,0 +1,48 @@ +// 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 genservice + +import ( + "bytes" + "go/ast" + "go/format" + "go/token" + "strings" +) + +// exprToString converts ast.Expr to string. +// For example: +// +// ast.Expr -> "context.Context" +// ast.Expr -> "*v1.XxxReq" +// ast.Expr -> "error" +// ast.Expr -> "int" +func (c CGenService) astExprToString(expr ast.Expr) (string, error) { + var ( + buf bytes.Buffer + err error + ) + err = format.Node(&buf, token.NewFileSet(), expr) + if err != nil { + return "", err + } + return buf.String(), nil +} + +// astCommentToString returns the raw (original) text of the comment. +// It includes the comment markers (//, /*, and */). +// It adds a newline at the end of the comment. +func (c CGenService) astCommentToString(node *ast.CommentGroup) string { + if node == nil { + return "" + } + var b strings.Builder + for _, c := range node.List { + b.WriteString(c.Text + "\n") + } + return b.String() +} diff --git a/cmd/gf/internal/cmd/genservice/genservice_calculate.go b/cmd/gf/internal/cmd/genservice/genservice_calculate.go index 4312a6b399..53553a0403 100644 --- a/cmd/gf/internal/cmd/genservice/genservice_calculate.go +++ b/cmd/gf/internal/cmd/genservice/genservice_calculate.go @@ -8,168 +8,145 @@ package genservice import ( "fmt" - "go/parser" - "go/token" + "strings" + "github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog" "github.com/gogf/gf/v2/container/garray" "github.com/gogf/gf/v2/container/gmap" + "github.com/gogf/gf/v2/os/gfile" "github.com/gogf/gf/v2/text/gregex" "github.com/gogf/gf/v2/text/gstr" ) -type packageItem struct { - Alias string - Path string - RawImport string -} +func (c CGenService) calculateImportedItems( + in CGenServiceInput, + pkgItems []pkgItem, funcItems []funcItem, + srcImportedPackages *garray.SortedStrArray, +) (err error) { + // allFuncParamType saves all the param and result types of the functions. + var allFuncParamType strings.Builder -func (c CGenService) calculateImportedPackages(fileContent string) (packages []packageItem, err error) { - f, err := parser.ParseFile(token.NewFileSet(), "", fileContent, parser.ImportsOnly) - if err != nil { - return nil, err - } - packages = make([]packageItem, 0) - for _, s := range f.Imports { - if s.Path != nil { - if s.Name != nil { - // If it has alias, and it is not `_`. - if pkgAlias := s.Name.String(); pkgAlias != "_" { - packages = append(packages, packageItem{ - Alias: pkgAlias, - Path: s.Path.Value, - RawImport: pkgAlias + " " + s.Path.Value, - }) - } - } else { - // no alias - packages = append(packages, packageItem{ - Alias: "", - Path: s.Path.Value, - RawImport: s.Path.Value, - }) - } + for _, item := range funcItems { + for _, param := range item.Params { + allFuncParamType.WriteString(param["paramType"] + ",") + } + for _, result := range item.Results { + allFuncParamType.WriteString(result["resultType"] + ",") } } - return packages, nil -} -func (c CGenService) calculateCodeCommented(in CGenServiceInput, fileContent string, srcCodeCommentedMap map[string]string) error { - matches, err := gregex.MatchAllString(`((((//.*)|(/\*[\s\S]*?\*/))\s)+)func \((.+?)\) ([\s\S]+?) {`, fileContent) - if err != nil { - return err - } - for _, match := range matches { - var ( - structName string - structMatch []string - funcReceiver = gstr.Trim(match[1+5]) - receiverArray = gstr.SplitAndTrim(funcReceiver, " ") - functionHead = gstr.Trim(gstr.Replace(match[2+5], "\n", "")) - commentedInfo = "" - ) - if len(receiverArray) > 1 { - structName = receiverArray[1] - } else if len(receiverArray) == 1 { - structName = receiverArray[0] - } - structName = gstr.Trim(structName, "*") - - // Case of: - // Xxx(\n ctx context.Context, req *v1.XxxReq,\n) -> Xxx(ctx context.Context, req *v1.XxxReq) - functionHead = gstr.Replace(functionHead, `,)`, `)`) - functionHead, _ = gregex.ReplaceString(`\(\s+`, `(`, functionHead) - functionHead, _ = gregex.ReplaceString(`\s{2,}`, ` `, functionHead) - if !gstr.IsLetterUpper(functionHead[0]) { + for _, item := range pkgItems { + alias := item.Alias + + // If the alias is _, it means that the package is not generated. + if alias == "_" { + mlog.Debugf(`ignore anonymous package: %s`, item.RawImport) continue } - // Match and pick the struct name from receiver. - if structMatch, err = gregex.MatchString(in.StPattern, structName); err != nil { - return err + // If the alias is empty, it will use the package name as the alias. + if alias == "" { + alias = gfile.Basename(gstr.Trim(item.Path, `"`)) } - if len(structMatch) < 1 { + if !gstr.Contains(allFuncParamType.String(), alias) { + mlog.Debugf(`ignore unused package: %s`, item.RawImport) continue } - structName = gstr.CaseCamel(structMatch[1]) - - commentedInfo = match[1] - if len(commentedInfo) > 0 { - srcCodeCommentedMap[fmt.Sprintf("%s-%s", structName, functionHead)] = commentedInfo - } + srcImportedPackages.Add(item.RawImport) } return nil } -func (c CGenService) calculateInterfaceFunctions( - in CGenServiceInput, fileContent string, srcPkgInterfaceMap *gmap.ListMap, +func (c CGenService) calculateFuncItems( + in CGenServiceInput, + funcItems []funcItem, + srcPkgInterfaceMap *gmap.ListMap, ) (err error) { - var ( - matches [][]string - srcPkgInterfaceFuncArray *garray.StrArray - ) - // calculate struct name and its functions according function definitions. - matches, err = gregex.MatchAllString(`func \((.+?)\) ([\s\S]+?) {`, fileContent) - if err != nil { - return err - } - for _, match := range matches { + var srcPkgInterfaceFunc []map[string]string + + for _, item := range funcItems { var ( - structName string - structMatch []string - funcReceiver = gstr.Trim(match[1]) - receiverArray = gstr.SplitAndTrim(funcReceiver, " ") - functionHead = gstr.Trim(gstr.Replace(match[2], "\n", "")) + // eg: "sArticle" + receiverName string + receiverMatch []string + + // eg: "GetList(ctx context.Context, req *v1.ArticleListReq) (list []*v1.Article, err error)" + funcHead string ) - if len(receiverArray) > 1 { - structName = receiverArray[1] - } else if len(receiverArray) == 1 { - structName = receiverArray[0] - } - structName = gstr.Trim(structName, "*") - - // Case of: - // Xxx(\n ctx context.Context, req *v1.XxxReq,\n) -> Xxx(ctx context.Context, req *v1.XxxReq) - functionHead = gstr.Replace(functionHead, `,)`, `)`) - functionHead, _ = gregex.ReplaceString(`\(\s+`, `(`, functionHead) - functionHead, _ = gregex.ReplaceString(`\s{2,}`, ` `, functionHead) - if !gstr.IsLetterUpper(functionHead[0]) { + + // handle the receiver name. + if item.Receiver == "" { continue } + receiverName = item.Receiver + receiverName = gstr.Trim(receiverName, "*") // Match and pick the struct name from receiver. - if structMatch, err = gregex.MatchString(in.StPattern, structName); err != nil { + if receiverMatch, err = gregex.MatchString(in.StPattern, receiverName); err != nil { return err } - if len(structMatch) < 1 { + if len(receiverMatch) < 1 { continue } - structName = gstr.CaseCamel(structMatch[1]) - if !srcPkgInterfaceMap.Contains(structName) { - srcPkgInterfaceFuncArray = garray.NewStrArray() - srcPkgInterfaceMap.Set(structName, srcPkgInterfaceFuncArray) + receiverName = gstr.CaseCamel(receiverMatch[1]) + + // check if the func name is public. + if !gstr.IsLetterUpper(item.MethodName[0]) { + continue + } + + if !srcPkgInterfaceMap.Contains(receiverName) { + srcPkgInterfaceFunc = make([]map[string]string, 0) + srcPkgInterfaceMap.Set(receiverName, srcPkgInterfaceFunc) } else { - srcPkgInterfaceFuncArray = srcPkgInterfaceMap.Get(structName).(*garray.StrArray) + srcPkgInterfaceFunc = srcPkgInterfaceMap.Get(receiverName).([]map[string]string) } - srcPkgInterfaceFuncArray.Append(functionHead) - } - // calculate struct name according type definitions. - matches, err = gregex.MatchAllString(`type (.+) struct\s*{`, fileContent) - if err != nil { - return err + + // make the func head. + paramsStr := c.tidyParam(item.Params) + resultsStr := c.tidyResult(item.Results) + funcHead = fmt.Sprintf("%s(%s) (%s)", item.MethodName, paramsStr, resultsStr) + + srcPkgInterfaceFunc = append(srcPkgInterfaceFunc, map[string]string{ + "funcHead": funcHead, + "funcComment": item.Comment, + }) + srcPkgInterfaceMap.Set(receiverName, srcPkgInterfaceFunc) } - for _, match := range matches { - var ( - structName string - structMatch []string - ) - if structMatch, err = gregex.MatchString(in.StPattern, match[1]); err != nil { - return err + return nil +} + +// tidyParam tidies the input parameters. +// For example: +// +// []map[string]string{paramName:ctx paramType:context.Context, paramName:info paramType:struct{}} +// -> ctx context.Context, info struct{} +func (c CGenService) tidyParam(paramSlice []map[string]string) (paramStr string) { + for i, param := range paramSlice { + if i > 0 { + paramStr += ", " } - if len(structMatch) < 1 { - continue + paramStr += fmt.Sprintf("%s %s", param["paramName"], param["paramType"]) + } + return +} + +// tidyResult tidies the output parameters. +// For example: +// +// []map[string]string{resultName:list resultType:[]*User, resultName:err resultType:error} +// -> list []*User, err error +// +// []map[string]string{resultName: "", resultType: error} +// -> error +func (c CGenService) tidyResult(resultSlice []map[string]string) (resultStr string) { + for i, result := range resultSlice { + if i > 0 { + resultStr += ", " } - structName = gstr.CaseCamel(structMatch[1]) - if !srcPkgInterfaceMap.Contains(structName) { - srcPkgInterfaceMap.Set(structName, garray.NewStrArray()) + if result["resultName"] != "" { + resultStr += fmt.Sprintf("%s %s", result["resultName"], result["resultType"]) + } else { + resultStr += result["resultType"] } } - return nil + return } diff --git a/cmd/gf/internal/cmd/genservice/genservice_generate.go b/cmd/gf/internal/cmd/genservice/genservice_generate.go index 4753de7e84..a11d54e7f8 100644 --- a/cmd/gf/internal/cmd/genservice/genservice_generate.go +++ b/cmd/gf/internal/cmd/genservice/genservice_generate.go @@ -7,112 +7,34 @@ package genservice import ( + "bytes" "fmt" - "github.com/gogf/gf/v2/container/garray" + "github.com/gogf/gf/cmd/gf/v2/internal/consts" + "github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog" + "github.com/gogf/gf/cmd/gf/v2/internal/utility/utils" "github.com/gogf/gf/v2/container/gmap" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gfile" - "github.com/gogf/gf/v2/text/gregex" "github.com/gogf/gf/v2/text/gstr" - - "github.com/gogf/gf/cmd/gf/v2/internal/consts" - "github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog" - "github.com/gogf/gf/cmd/gf/v2/internal/utility/utils" ) type generateServiceFilesInput struct { CGenServiceInput - DstFilePath string // Absolute file path for generated service go file. - SrcStructFunctions *gmap.ListMap - SrcImportedPackages []string SrcPackageName string + SrcImportedPackages []string + SrcStructFunctions *gmap.ListMap DstPackageName string - SrcCodeCommentedMap map[string]string + DstFilePath string // Absolute file path for generated service go file. } func (c CGenService) generateServiceFile(in generateServiceFilesInput) (ok bool, err error) { - var ( - generatedContent string - allFuncArray = garray.NewStrArray() // Used for check whether interface dirty, going to change file content. - importedPackagesContent = fmt.Sprintf( - "import (\n%s\n)", gstr.Join(in.SrcImportedPackages, "\n"), - ) - ) - generatedContent += gstr.ReplaceByMap(consts.TemplateGenServiceContentHead, g.MapStrStr{ - "{Imports}": importedPackagesContent, - "{PackageName}": in.DstPackageName, - }) - - // Type definitions. - generatedContent += "type(" - generatedContent += "\n" - in.SrcStructFunctions.Iterator(func(key, value interface{}) bool { - structName, funcArray := key.(string), value.(*garray.StrArray) - allFuncArray.Append(funcArray.Slice()...) - // Add comments to a method. - for index, funcName := range funcArray.Slice() { - if commentedInfo, exist := in.SrcCodeCommentedMap[fmt.Sprintf("%s-%s", structName, funcName)]; exist { - funcName = commentedInfo + funcName - _ = funcArray.Set(index, funcName) - } - } - generatedContent += gstr.Trim(gstr.ReplaceByMap(consts.TemplateGenServiceContentInterface, g.MapStrStr{ - "{InterfaceName}": "I" + structName, - "{FuncDefinition}": funcArray.Join("\n\t"), - })) - generatedContent += "\n" - return true - }) - generatedContent += ")" - generatedContent += "\n" - - // Generating variable and register definitions. - var ( - variableContent string - generatingInterfaceCheck string - ) - // Variable definitions. - in.SrcStructFunctions.Iterator(func(key, value interface{}) bool { - structName := key.(string) - generatingInterfaceCheck = fmt.Sprintf(`[^\w\d]+%s.I%s[^\w\d]`, in.DstPackageName, structName) - if gregex.IsMatchString(generatingInterfaceCheck, generatedContent) { - return true - } - variableContent += gstr.Trim(gstr.ReplaceByMap(consts.TemplateGenServiceContentVariable, g.MapStrStr{ - "{StructName}": structName, - "{InterfaceName}": "I" + structName, - })) - variableContent += "\n" - return true - }) - if variableContent != "" { - generatedContent += "var(" - generatedContent += "\n" - generatedContent += variableContent - generatedContent += ")" - generatedContent += "\n" - } - // Variable register function definitions. - in.SrcStructFunctions.Iterator(func(key, value interface{}) bool { - structName := key.(string) - generatingInterfaceCheck = fmt.Sprintf(`[^\w\d]+%s.I%s[^\w\d]`, in.DstPackageName, structName) - if gregex.IsMatchString(generatingInterfaceCheck, generatedContent) { - return true - } - generatedContent += gstr.Trim(gstr.ReplaceByMap(consts.TemplateGenServiceContentRegister, g.MapStrStr{ - "{StructName}": structName, - "{InterfaceName}": "I" + structName, - })) - generatedContent += "\n\n" - return true - }) - - // Replace empty braces that have new line. - generatedContent, _ = gregex.ReplaceString(`{[\s\t]+}`, `{}`, generatedContent) + var generatedContent bytes.Buffer - // Remove package name calls of `dstPackageName` in produced codes. - generatedContent, _ = gregex.ReplaceString(fmt.Sprintf(`\*{0,1}%s\.`, in.DstPackageName), ``, generatedContent) + c.generatePackageImports(&generatedContent, in.DstPackageName, in.SrcImportedPackages) + c.generateType(&generatedContent, in.SrcStructFunctions, in.DstPackageName) + c.generateVar(&generatedContent, in.SrcStructFunctions) + c.generateFunc(&generatedContent, in.SrcStructFunctions) // Write file content to disk. if gfile.Exists(in.DstFilePath) { @@ -120,59 +42,14 @@ func (c CGenService) generateServiceFile(in generateServiceFilesInput) (ok bool, mlog.Printf(`ignore file as it is manually maintained: %s`, in.DstFilePath) return false, nil } - if !c.isToGenerateServiceGoFile(in.DstPackageName, in.DstFilePath, allFuncArray) { - mlog.Printf(`not dirty, ignore generating service go file: %s`, in.DstFilePath) - return false, nil - } } mlog.Printf(`generating service go file: %s`, in.DstFilePath) - if err = gfile.PutContents(in.DstFilePath, generatedContent); err != nil { + if err = gfile.PutBytes(in.DstFilePath, generatedContent.Bytes()); err != nil { return true, err } return true, nil } -// isToGenerateServiceGoFile checks and returns whether the service content dirty. -func (c CGenService) isToGenerateServiceGoFile(dstPackageName, filePath string, funcArray *garray.StrArray) bool { - var ( - err error - fileContent = gfile.GetContents(filePath) - generatedFuncArray = garray.NewSortedStrArrayFrom(funcArray.Slice()) - contentFuncArray = garray.NewSortedStrArray() - ) - if fileContent == "" { - return true - } - // remove all comments. - fileContent, err = gregex.ReplaceString(`(//.*)|((?s)/\*.*?\*/)`, "", fileContent) - if err != nil { - panic(err) - return false - } - matches, _ := gregex.MatchAllString(`\s+interface\s+{([\s\S]+?)}`, fileContent) - for _, match := range matches { - contentFuncArray.Append(gstr.SplitAndTrim(match[1], "\n")...) - } - if generatedFuncArray.Len() != contentFuncArray.Len() { - mlog.Debugf( - `dirty, generatedFuncArray.Len()[%d] != contentFuncArray.Len()[%d]`, - generatedFuncArray.Len(), contentFuncArray.Len(), - ) - return true - } - var funcDefinition string - for i := 0; i < generatedFuncArray.Len(); i++ { - funcDefinition, _ = gregex.ReplaceString( - fmt.Sprintf(`\*{0,1}%s\.`, dstPackageName), ``, generatedFuncArray.At(i), - ) - if funcDefinition != contentFuncArray.At(i) { - mlog.Debugf(`dirty, %s != %s`, funcDefinition, contentFuncArray.At(i)) - return true - } - } - return false -} - // generateInitializationFile generates `logic.go`. func (c CGenService) generateInitializationFile(in CGenServiceInput, importSrcPackages []string) (err error) { var ( diff --git a/cmd/gf/internal/cmd/genservice/genservice_generate_template.go b/cmd/gf/internal/cmd/genservice/genservice_generate_template.go new file mode 100644 index 0000000000..664ce699ee --- /dev/null +++ b/cmd/gf/internal/cmd/genservice/genservice_generate_template.go @@ -0,0 +1,104 @@ +// 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 genservice + +import ( + "bytes" + "fmt" + + "github.com/gogf/gf/cmd/gf/v2/internal/consts" + "github.com/gogf/gf/v2/container/gmap" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/text/gregex" + "github.com/gogf/gf/v2/text/gstr" +) + +func (c CGenService) generatePackageImports(generatedContent *bytes.Buffer, packageName string, imports []string) { + generatedContent.WriteString(gstr.ReplaceByMap(consts.TemplateGenServiceContentHead, g.MapStrStr{ + "{PackageName}": packageName, + "{Imports}": fmt.Sprintf( + "import (\n%s\n)", gstr.Join(imports, "\n"), + ), + })) +} + +// generateType type definitions. +// See: const.TemplateGenServiceContentInterface +func (c CGenService) generateType(generatedContent *bytes.Buffer, srcStructFunctions *gmap.ListMap, dstPackageName string) { + generatedContent.WriteString("type(") + generatedContent.WriteString("\n") + + srcStructFunctions.Iterator(func(key, value interface{}) bool { + var ( + funcContents = make([]string, 0) + funcContent string + ) + structName, funcSlice := key.(string), value.([]map[string]string) + // Generating interface content. + for _, funcInfo := range funcSlice { + // Remove package name calls of `dstPackageName` in produced codes. + funcHead, _ := gregex.ReplaceString( + fmt.Sprintf(`\*{0,1}%s\.`, dstPackageName), + ``, funcInfo["funcHead"], + ) + funcContent = funcInfo["funcComment"] + funcHead + funcContents = append(funcContents, funcContent) + } + + // funcContents to string. + generatedContent.WriteString( + gstr.Trim(gstr.ReplaceByMap(consts.TemplateGenServiceContentInterface, g.MapStrStr{ + "{InterfaceName}": "I" + structName, + "{FuncDefinition}": gstr.Join(funcContents, "\n\t"), + })), + ) + generatedContent.WriteString("\n") + return true + }) + + generatedContent.WriteString(")") + generatedContent.WriteString("\n") +} + +// generateVar variable definitions. +// See: const.TemplateGenServiceContentVariable +func (c CGenService) generateVar(generatedContent *bytes.Buffer, srcStructFunctions *gmap.ListMap) { + // Generating variable and register definitions. + var variableContent string + + srcStructFunctions.Iterator(func(key, value interface{}) bool { + structName := key.(string) + variableContent += gstr.Trim(gstr.ReplaceByMap(consts.TemplateGenServiceContentVariable, g.MapStrStr{ + "{StructName}": structName, + "{InterfaceName}": "I" + structName, + })) + variableContent += "\n" + return true + }) + if variableContent != "" { + generatedContent.WriteString("var(") + generatedContent.WriteString("\n") + generatedContent.WriteString(variableContent) + generatedContent.WriteString(")") + generatedContent.WriteString("\n") + } +} + +// generateFunc function definitions. +// See: const.TemplateGenServiceContentRegister +func (c CGenService) generateFunc(generatedContent *bytes.Buffer, srcStructFunctions *gmap.ListMap) { + // Variable register function definitions. + srcStructFunctions.Iterator(func(key, value interface{}) bool { + structName := key.(string) + generatedContent.WriteString(gstr.Trim(gstr.ReplaceByMap(consts.TemplateGenServiceContentRegister, g.MapStrStr{ + "{StructName}": structName, + "{InterfaceName}": "I" + structName, + }))) + generatedContent.WriteString("\n\n") + return true + }) +} diff --git a/cmd/gf/internal/cmd/testdata/genservice/logic/article/article.go b/cmd/gf/internal/cmd/testdata/genservice/logic/article/article.go index 26e695a8c0..57b6a31a6e 100644 --- a/cmd/gf/internal/cmd/testdata/genservice/logic/article/article.go +++ b/cmd/gf/internal/cmd/testdata/genservice/logic/article/article.go @@ -8,8 +8,11 @@ package article import ( "context" + "go/ast" + t "time" "github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genservice/service" + gdbalias "github.com/gogf/gf/v2/database/gdb" ) type sArticle struct { @@ -30,5 +33,15 @@ func (s *sArticle) Get(ctx context.Context, id uint) (info struct{}, err error) * @author oldme */ func (s *sArticle) Create(ctx context.Context, info struct{}) (id uint, err error) { + // Use time package to test alias import. + t.Now() return id, err } + +func (s *sArticle) A1o2(ctx context.Context, str string, a, b *ast.GoStmt) error { + return nil +} + +func (s *sArticle) B_2(ctx context.Context, db gdbalias.Raw) (err error) { + return nil +} diff --git a/cmd/gf/internal/cmd/testdata/genservice/logic/article/article_extra.go b/cmd/gf/internal/cmd/testdata/genservice/logic/article/article_extra.go new file mode 100644 index 0000000000..165747407d --- /dev/null +++ b/cmd/gf/internal/cmd/testdata/genservice/logic/article/article_extra.go @@ -0,0 +1,75 @@ +// Copyright GoFrame 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 article + +// import ( +// "context" +// +// "github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genservice/service" +// ) +import ( + "context" + + // This is a random comment + gdbas "github.com/gogf/gf/v2/database/gdb" + /** + * + */ + _ "github.com/gogf/gf/v2/os/gfile" +) + +// T1 random comment +func (s sArticle) T1(ctx context.Context, id, id2 uint) (gdb gdbas.Model, err error) { + g := gdbas.Model{} + return g, err +} + +// I'm a random comment + +// t2 random comment +func (s *sArticle) t2(ctx context.Context) (err error) { + /** + * random comment + * i (1). func (s *sArticle) t2(ctx context.Context) (err error) { /** 1883 + * + */ + _ = func(ctx2 context.Context) {} + return nil +} + +// T3 +/** + * random comment @*4213hHY1&%##%>< ? , . / +func (s *sArticle) T4(i interface{}) interface{} { + return nil +} + +/** + * func (s *sArticle) T4(i interface{}) interface{} { + * return nil + * } + */ diff --git a/cmd/gf/internal/cmd/testdata/genservice/logic/delivery/delivery_app.go b/cmd/gf/internal/cmd/testdata/genservice/logic/delivery/delivery_app.go new file mode 100644 index 0000000000..300260cc05 --- /dev/null +++ b/cmd/gf/internal/cmd/testdata/genservice/logic/delivery/delivery_app.go @@ -0,0 +1,38 @@ +// Copyright GoFrame 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 delivery + +import ( + "context" + + "github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genservice/service" +) + +type sDeliveryApp struct{} + +func NewDeliveryApp() *sDeliveryApp { + return &sDeliveryApp{} +} + +func (s *sDeliveryApp) Create(ctx context.Context) (i service.IDeliveryCluster, err error) { + return +} + +func (s *sDeliveryApp) GetList(ctx context.Context, i service.IDeliveryCluster) (err error) { + service.Article().Get(ctx, 1) + return +} + +func (s *sDeliveryApp) GetOne(ctx context.Context) (err error) { + return +} + +func (s *sDeliveryApp) Delete(ctx context.Context) (err error) { + return +} + +func (s *sDeliveryApp) AA(ctx context.Context) (err error) { return } diff --git a/cmd/gf/internal/cmd/testdata/genservice/logic/delivery/delivery_cluster.go b/cmd/gf/internal/cmd/testdata/genservice/logic/delivery/delivery_cluster.go new file mode 100644 index 0000000000..4be21e7754 --- /dev/null +++ b/cmd/gf/internal/cmd/testdata/genservice/logic/delivery/delivery_cluster.go @@ -0,0 +1,32 @@ +// Copyright GoFrame 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 delivery + +import ( + "context" + + gdbas "github.com/gogf/gf/v2/database/gdb" +) + +type sDeliveryCluster struct{} + +func NewDeliveryCluster() *sDeliveryCluster { + return &sDeliveryCluster{} +} + +// Create 自动创建Cluster及Project. +func (s *sDeliveryCluster) Create(ctx context.Context) (err error, gdb gdbas.Model) { + return +} + +func (s *sDeliveryCluster) Delete(ctx context.Context) (err error) { + return +} + +func (s *sDeliveryCluster) GetList(ctx context.Context) (err error) { + return +} diff --git a/cmd/gf/internal/cmd/testdata/genservice/logic/logic_expect.go b/cmd/gf/internal/cmd/testdata/genservice/logic/logic_expect.go index c7c3f52e9c..6efcac5e82 100644 --- a/cmd/gf/internal/cmd/testdata/genservice/logic/logic_expect.go +++ b/cmd/gf/internal/cmd/testdata/genservice/logic/logic_expect.go @@ -6,4 +6,6 @@ package logic import ( _ "github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genservice/logic/article" + _ "github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genservice/logic/delivery" + _ "github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genservice/logic/user" ) diff --git a/cmd/gf/internal/cmd/testdata/genservice/logic/user/user.go b/cmd/gf/internal/cmd/testdata/genservice/logic/user/user.go new file mode 100644 index 0000000000..a95e22740e --- /dev/null +++ b/cmd/gf/internal/cmd/testdata/genservice/logic/user/user.go @@ -0,0 +1,49 @@ +// Copyright GoFrame 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 user + +import ( + "context" + + "github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/genservice/service" +) + +func init() { + service.RegisterUser(New()) +} + +type sUser struct { +} + +func New() *sUser { + return &sUser{} +} + +// Create creates a new user. +func (s *sUser) Create(ctx context.Context, name string) (id int, err error) { + return 0, nil +} + +// GetOne retrieves user by id. +func (s *sUser) GetOne(ctx context.Context, id int) (name string, err error) { + return "", nil +} + +// GetList retrieves user list. +func (s *sUser) GetList(ctx context.Context) (names []string, err error) { + return nil, nil +} + +// Update updates user by id. +func (s *sUser) Update(ctx context.Context, id int) (name string, err error) { + return "", nil +} + +// Delete deletes user by id. +func (s *sUser) Delete(ctx context.Context, id int) (err error) { + return nil +} diff --git a/cmd/gf/internal/cmd/testdata/genservice/service/article.go b/cmd/gf/internal/cmd/testdata/genservice/service/article.go index 765a1c8a5a..0d10924634 100644 --- a/cmd/gf/internal/cmd/testdata/genservice/service/article.go +++ b/cmd/gf/internal/cmd/testdata/genservice/service/article.go @@ -7,6 +7,10 @@ package service import ( "context" + "go/ast" + + gdbalias "github.com/gogf/gf/v2/database/gdb" + gdbas "github.com/gogf/gf/v2/database/gdb" ) type ( @@ -19,6 +23,22 @@ type ( * @author oldme */ Create(ctx context.Context, info struct{}) (id uint, err error) + A1o2(ctx context.Context, str string, a *ast.GoStmt, b *ast.GoStmt) error + B_2(ctx context.Context, db gdbalias.Raw) (err error) + // T1 random comment + T1(ctx context.Context, id uint, id2 uint) (gdb gdbas.Model, err error) + // T3 + /** + * random comment @*4213hHY1&%##%>< ? , . / + T4(i interface{}) interface{} } ) diff --git a/cmd/gf/internal/cmd/testdata/genservice/service/delivery.go b/cmd/gf/internal/cmd/testdata/genservice/service/delivery.go new file mode 100644 index 0000000000..842ed88dd2 --- /dev/null +++ b/cmd/gf/internal/cmd/testdata/genservice/service/delivery.go @@ -0,0 +1,55 @@ +// ================================================================================ +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// You can delete these comments if you wish manually maintain this interface file. +// ================================================================================ + +package service + +import ( + "context" + + gdbas "github.com/gogf/gf/v2/database/gdb" +) + +type ( + IDeliveryApp interface { + Create(ctx context.Context) (i IDeliveryCluster, err error) + GetList(ctx context.Context, i IDeliveryCluster) (err error) + GetOne(ctx context.Context) (err error) + Delete(ctx context.Context) (err error) + AA(ctx context.Context) (err error) + } + IDeliveryCluster interface { + // Create 自动创建Cluster及Project. + Create(ctx context.Context) (err error, gdb gdbas.Model) + Delete(ctx context.Context) (err error) + GetList(ctx context.Context) (err error) + } +) + +var ( + localDeliveryApp IDeliveryApp + localDeliveryCluster IDeliveryCluster +) + +func DeliveryApp() IDeliveryApp { + if localDeliveryApp == nil { + panic("implement not found for interface IDeliveryApp, forgot register?") + } + return localDeliveryApp +} + +func RegisterDeliveryApp(i IDeliveryApp) { + localDeliveryApp = i +} + +func DeliveryCluster() IDeliveryCluster { + if localDeliveryCluster == nil { + panic("implement not found for interface IDeliveryCluster, forgot register?") + } + return localDeliveryCluster +} + +func RegisterDeliveryCluster(i IDeliveryCluster) { + localDeliveryCluster = i +} diff --git a/cmd/gf/internal/cmd/testdata/genservice/service/user.go b/cmd/gf/internal/cmd/testdata/genservice/service/user.go new file mode 100644 index 0000000000..731ee87c50 --- /dev/null +++ b/cmd/gf/internal/cmd/testdata/genservice/service/user.go @@ -0,0 +1,40 @@ +// ================================================================================ +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// You can delete these comments if you wish manually maintain this interface file. +// ================================================================================ + +package service + +import ( + "context" +) + +type ( + IUser interface { + // Create creates a new user. + Create(ctx context.Context, name string) (id int, err error) + // GetOne retrieves user by id. + GetOne(ctx context.Context, id int) (name string, err error) + // GetList retrieves user list. + GetList(ctx context.Context) (names []string, err error) + // Update updates user by id. + Update(ctx context.Context, id int) (name string, err error) + // Delete deletes user by id. + Delete(ctx context.Context, id int) (err error) + } +) + +var ( + localUser IUser +) + +func User() IUser { + if localUser == nil { + panic("implement not found for interface IUser, forgot register?") + } + return localUser +} + +func RegisterUser(i IUser) { + localUser = i +}