/* * Copyright 2022 CloudWeGo Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package generator import ( "bytes" "fmt" "io/ioutil" "path/filepath" "strings" "text/template" "golib.gaore.com/GaoreGo/gr_hz/util" "golib.gaore.com/GaoreGo/gr_hz/util/logs" ) type FilePathRenderInfo struct { MasterIDLName string // master IDL name GenPackage string // master IDL generate code package HandlerDir string // handler generate dir ModelDir string // model generate dir RouterDir string // router generate dir ProjectDir string // projectDir GoModule string // go module ServiceName string // service name, changed as services are traversed MethodName string // method name, changed as methods are traversed HandlerGenPath string // "api.gen_path" value } type IDLPackageRenderInfo struct { FilePathRenderInfo ServiceInfos *HttpPackage } type CustomizedFileForMethod struct { *HttpMethod FilePath string FilePackage string ServiceInfo *Service // service info for this method IDLPackageInfo *IDLPackageRenderInfo // IDL info for this service } type CustomizedFileForService struct { *Service FilePath string FilePackage string IDLPackageInfo *IDLPackageRenderInfo // IDL info for this service } type CustomizedFileForIDL struct { *IDLPackageRenderInfo FilePath string FilePackage string } // todo: 1. how to import other file, if the other file name is a template // genCustomizedFile generate customized file template func (pkgGen *HttpPackageGenerator) genCustomizedFile(pkg *HttpPackage) error { filePathRenderInfo := FilePathRenderInfo{ MasterIDLName: pkg.IdlName, GenPackage: pkg.Package, HandlerDir: pkgGen.HandlerDir, ModelDir: pkgGen.ModelDir, RouterDir: pkgGen.RouterDir, ProjectDir: pkgGen.OutputDir, GoModule: pkgGen.ProjPackage, // methodName & serviceName will change as traverse } idlPackageRenderInfo := IDLPackageRenderInfo{ FilePathRenderInfo: filePathRenderInfo, ServiceInfos: pkg, } for _, tplInfo := range pkgGen.tplsInfo { // the default template has been automatically generated by the tool, so skip if tplInfo.Default { continue } // loop generate file if tplInfo.LoopService || tplInfo.LoopMethod { loopMethod := tplInfo.LoopMethod loopService := tplInfo.LoopService if loopService && !loopMethod { // only loop service for _, service := range idlPackageRenderInfo.ServiceInfos.Services { filePathRenderInfo.ServiceName = service.Name err := pkgGen.genLoopService(tplInfo, filePathRenderInfo, service, &idlPackageRenderInfo) if err != nil { return err } } } else { // loop service & method, because if loop method, the service must be looped for _, service := range idlPackageRenderInfo.ServiceInfos.Services { for _, method := range service.Methods { filePathRenderInfo.ServiceName = service.Name filePathRenderInfo.MethodName = method.Name filePathRenderInfo.HandlerGenPath = method.OutputDir err := pkgGen.genLoopMethod(tplInfo, filePathRenderInfo, method, service, &idlPackageRenderInfo) if err != nil { return err } } } } } else { // generate customized file single err := pkgGen.genSingleCustomizedFile(tplInfo, filePathRenderInfo, idlPackageRenderInfo) if err != nil { return err } } } return nil } // renderFilePath used to render file path template to get real file path func renderFilePath(tplInfo *Template, filePathRenderInfo FilePathRenderInfo) (string, error) { tpl, err := template.New(tplInfo.Path).Funcs(funcMap).Parse(tplInfo.Path) if err != nil { return "", fmt.Errorf("parse file path template(%s) failed, err: %v", tplInfo.Path, err) } filePath := bytes.NewBuffer(nil) err = tpl.Execute(filePath, filePathRenderInfo) if err != nil { return "", fmt.Errorf("render file path template(%s) failed, err: %v", tplInfo.Path, err) } return filePath.String(), nil } func renderInsertKey(tplInfo *Template, data interface{}) (string, error) { tpl, err := template.New(tplInfo.UpdateBehavior.InsertKey).Funcs(funcMap).Parse(tplInfo.UpdateBehavior.InsertKey) if err != nil { return "", fmt.Errorf("parse insert key template(%s) failed, err: %v", tplInfo.UpdateBehavior.InsertKey, err) } insertKey := bytes.NewBuffer(nil) err = tpl.Execute(insertKey, data) if err != nil { return "", fmt.Errorf("render insert key template(%s) failed, err: %v", tplInfo.UpdateBehavior.InsertKey, err) } return insertKey.String(), nil } // renderImportTpl will render import template // it will return the []string, like blow: // ["import", alias "import", import] // other format will be error func renderImportTpl(tplInfo *Template, data interface{}) ([]string, error) { var importList []string for _, impt := range tplInfo.UpdateBehavior.ImportTpl { tpl, err := template.New(impt).Funcs(funcMap).Parse(impt) if err != nil { return nil, fmt.Errorf("parse import template(%s) failed, err: %v", impt, err) } imptContent := bytes.NewBuffer(nil) err = tpl.Execute(imptContent, data) if err != nil { return nil, fmt.Errorf("render import template(%s) failed, err: %v", impt, err) } importList = append(importList, imptContent.String()) } var ret []string for _, impts := range importList { // 'import render result' may have multiple imports if strings.Contains(impts, "\n") { for _, impt := range strings.Split(impts, "\n") { ret = append(ret, impt) } } else { ret = append(ret, impts) } } return ret, nil } // renderAppendContent used to render append content for 'update' command func renderAppendContent(tplInfo *Template, renderInfo interface{}) (string, error) { tpl, err := template.New(tplInfo.Path).Funcs(funcMap).Parse(tplInfo.UpdateBehavior.AppendTpl) if err != nil { return "", fmt.Errorf("parse append content template(%s) failed, err: %v", tplInfo.Path, err) } appendContent := bytes.NewBuffer(nil) err = tpl.Execute(appendContent, renderInfo) if err != nil { return "", fmt.Errorf("render append content template(%s) failed, err: %v", tplInfo.Path, err) } return appendContent.String(), nil } // appendUpdateFile used to append content to file for 'update' command func appendUpdateFile(tplInfo *Template, renderInfo interface{}, fileContent []byte) ([]byte, error) { // render insert content appendContent, err := renderAppendContent(tplInfo, renderInfo) if err != nil { return []byte(""), err } buf := bytes.NewBuffer(nil) _, err = buf.Write(fileContent) if err != nil { return []byte(""), fmt.Errorf("write file(%s) failed, err: %v", tplInfo.Path, err) } // "\r\n" && "\n" has the same suffix if !bytes.HasSuffix(buf.Bytes(), []byte("\n")) { _, err = buf.WriteString("\n") if err != nil { return []byte(""), fmt.Errorf("write file(%s) line break failed, err: %v", tplInfo.Path, err) } } _, err = buf.WriteString(appendContent) if err != nil { return []byte(""), fmt.Errorf("append file(%s) failed, err: %v", tplInfo.Path, err) } return buf.Bytes(), nil } func getInsertImportContent(tplInfo *Template, renderInfo interface{}, fileContent []byte) ([][2]string, error) { importContent, err := renderImportTpl(tplInfo, renderInfo) if err != nil { return nil, err } var imptSlice [][2]string for _, impt := range importContent { // import has to format // 1. alias "import" // 2. "import" // 3. import (can not contain '"') impt = strings.TrimSpace(impt) if !strings.Contains(impt, "\"") { // 3. import (can not contain '"') if bytes.Contains(fileContent, []byte(impt)) { continue } imptSlice = append(imptSlice, [2]string{"", impt}) } else { if !strings.HasSuffix(impt, "\"") { return nil, fmt.Errorf("import can not has suffix \"\"\", for file: %s", tplInfo.Path) } if strings.HasPrefix(impt, "\"") { // 2. "import" if bytes.Contains(fileContent, []byte(impt[1:len(impt)-1])) { continue } imptSlice = append(imptSlice, [2]string{"", impt[1 : len(impt)-1]}) } else { // 3. alias "import" idx := strings.Index(impt, "\n") if idx == -1 { return nil, fmt.Errorf("error import format for file: %s", tplInfo.Path) } if bytes.Contains(fileContent, []byte(impt[idx+1:len(impt)-1])) { continue } imptSlice = append(imptSlice, [2]string{impt[:idx], impt[idx+1 : len(impt)-1]}) } } } return imptSlice, nil } // genLoopService used to generate files by 'service' func (pkgGen *HttpPackageGenerator) genLoopService(tplInfo *Template, filePathRenderInfo FilePathRenderInfo, service *Service, idlPackageRenderInfo *IDLPackageRenderInfo) error { filePath, err := renderFilePath(tplInfo, filePathRenderInfo) if err != nil { return err } // determine if a custom file exists exist, err := util.PathExist(filePath) if err != nil { return fmt.Errorf("judge file(%s) exists failed, err: %v", filePath, err) } if !exist { // create file data := CustomizedFileForService{ Service: service, FilePath: filePath, FilePackage: util.SplitPackage(filepath.Dir(filePath), ""), IDLPackageInfo: idlPackageRenderInfo, } err = pkgGen.TemplateGenerator.Generate(data, tplInfo.Path, filePath, false) if err != nil { return err } } else { switch tplInfo.UpdateBehavior.Type { case Skip: // do nothing logs.Infof("do not update file '%s', because the update behavior is 'Unchanged'", filePath) case Cover: // re-generate logs.Infof("re-generate file '%s', because the update behavior is 'Regenerate'", filePath) data := CustomizedFileForService{ Service: service, FilePath: filePath, FilePackage: util.SplitPackage(filepath.Dir(filePath), ""), IDLPackageInfo: idlPackageRenderInfo, } err := pkgGen.TemplateGenerator.Generate(data, tplInfo.Path, filePath, false) if err != nil { return err } case Append: // todo: append logic need to be optimized for method fileContent, err := ioutil.ReadFile(filePath) if err != nil { return err } var appendContent []byte // get insert content if tplInfo.UpdateBehavior.AppendKey == "method" { for _, method := range service.Methods { data := CustomizedFileForMethod{ HttpMethod: method, FilePath: filePath, FilePackage: util.SplitPackage(filepath.Dir(filePath), ""), ServiceInfo: service, IDLPackageInfo: idlPackageRenderInfo, } insertKey, err := renderInsertKey(tplInfo, data) if err != nil { return err } if bytes.Contains(fileContent, []byte(insertKey)) { continue } imptSlice, err := getInsertImportContent(tplInfo, data, fileContent) if err != nil { return err } // insert new import to the fileContent for _, impt := range imptSlice { if bytes.Contains(fileContent, []byte(impt[1])) { continue } fileContent, err = util.AddImportForContent(fileContent, impt[0], impt[1]) // insert import error do not influence the generated file if err != nil { logs.Warnf("can not add import(%s) for file(%s), err: %v\n", impt[1], filePath, err) } } appendContent, err = appendUpdateFile(tplInfo, data, appendContent) if err != nil { return err } } if len(tplInfo.UpdateBehavior.AppendLocation) == 0 { // default, append to end of file buf := bytes.NewBuffer(nil) _, err = buf.Write(fileContent) if err != nil { return fmt.Errorf("write file(%s) failed, err: %v", tplInfo.Path, err) } _, err = buf.Write(appendContent) if err != nil { return fmt.Errorf("append file(%s) failed, err: %v", tplInfo.Path, err) } logs.Infof("append content for file '%s', because the update behavior is 'Append' and appendKey is 'method'", filePath) pkgGen.files = append(pkgGen.files, File{filePath, buf.String(), false, ""}) } else { // 'append location', append new content after 'append location' part := bytes.Split(fileContent, []byte(tplInfo.UpdateBehavior.AppendLocation)) if len(part) == 0 { return fmt.Errorf("can not find append location '%s' for file '%s'\n", tplInfo.UpdateBehavior.AppendLocation, filePath) } if len(part) != 2 { return fmt.Errorf("do not support multiple append location '%s' for file '%s'\n", tplInfo.UpdateBehavior.AppendLocation, filePath) } buf := bytes.NewBuffer(nil) err = writeBytes(buf, part[0], []byte(tplInfo.UpdateBehavior.AppendLocation), appendContent, part[1]) if err != nil { return fmt.Errorf("write file(%s) failed, err: %v", tplInfo.Path, err) } logs.Infof("append content for file '%s', because the update behavior is 'Append' and appendKey is 'method'", filePath) pkgGen.files = append(pkgGen.files, File{filePath, buf.String(), false, ""}) } } else { logs.Warnf("Loop 'service' field for '%s' only append content by appendKey for 'method', so cannot append content", filePath) } default: // do nothing logs.Warnf("unknown update behavior, do nothing") } } return nil } // genLoopMethod used to generate files by 'method' func (pkgGen *HttpPackageGenerator) genLoopMethod(tplInfo *Template, filePathRenderInfo FilePathRenderInfo, method *HttpMethod, service *Service, idlPackageRenderInfo *IDLPackageRenderInfo) error { filePath, err := renderFilePath(tplInfo, filePathRenderInfo) if err != nil { return err } // determine if a custom file exists exist, err := util.PathExist(filePath) if err != nil { return fmt.Errorf("judge file(%s) exists failed, err: %v", filePath, err) } if !exist { // create file data := CustomizedFileForMethod{ HttpMethod: method, FilePath: filePath, FilePackage: util.SplitPackage(filepath.Dir(filePath), ""), ServiceInfo: service, IDLPackageInfo: idlPackageRenderInfo, } err := pkgGen.TemplateGenerator.Generate(data, tplInfo.Path, filePath, false) if err != nil { return err } } else { switch tplInfo.UpdateBehavior.Type { case Skip: // do nothing logs.Infof("do not update file '%s', because the update behavior is 'Unchanged'", filePath) case Cover: // re-generate logs.Infof("re-generate file '%s', because the update behavior is 'Regenerate'", filePath) data := CustomizedFileForMethod{ HttpMethod: method, FilePath: filePath, FilePackage: util.SplitPackage(filepath.Dir(filePath), ""), ServiceInfo: service, IDLPackageInfo: idlPackageRenderInfo, } err := pkgGen.TemplateGenerator.Generate(data, tplInfo.Path, filePath, false) if err != nil { return err } case Append: // for loop method, no need to append something; so do nothing logs.Warnf("do not append content for file '%s', because the update behavior is 'Append' and loop 'method' have no need to append content", filePath) default: // do nothing logs.Warnf("unknown update behavior, do nothing") } } return nil } // genSingleCustomizedFile used to generate file by 'master IDL' func (pkgGen *HttpPackageGenerator) genSingleCustomizedFile(tplInfo *Template, filePathRenderInfo FilePathRenderInfo, idlPackageRenderInfo IDLPackageRenderInfo) error { // generate file single filePath, err := renderFilePath(tplInfo, filePathRenderInfo) if err != nil { return err } // determine if a custom file exists exist, err := util.PathExist(filePath) if err != nil { return fmt.Errorf("judge file(%s) exists failed, err: %v", filePath, err) } if !exist { // create file data := CustomizedFileForIDL{ IDLPackageRenderInfo: &idlPackageRenderInfo, FilePath: filePath, FilePackage: util.SplitPackage(filepath.Dir(filePath), ""), } err := pkgGen.TemplateGenerator.Generate(data, tplInfo.Path, filePath, false) if err != nil { return err } } else { switch tplInfo.UpdateBehavior.Type { case Skip: // do nothing logs.Infof("do not update file '%s', because the update behavior is 'Unchanged'", filePath) case Cover: // re-generate logs.Infof("re-generate file '%s', because the update behavior is 'Regenerate'", filePath) data := CustomizedFileForIDL{ IDLPackageRenderInfo: &idlPackageRenderInfo, FilePath: filePath, FilePackage: util.SplitPackage(filepath.Dir(filePath), ""), } err := pkgGen.TemplateGenerator.Generate(data, tplInfo.Path, filePath, false) if err != nil { return err } case Append: // todo: append logic need to be optimized for single file fileContent, err := ioutil.ReadFile(filePath) if err != nil { return err } if tplInfo.UpdateBehavior.AppendKey == "method" { var appendContent []byte for _, service := range idlPackageRenderInfo.ServiceInfos.Services { for _, method := range service.Methods { data := CustomizedFileForMethod{ HttpMethod: method, FilePath: filePath, FilePackage: util.SplitPackage(filepath.Dir(filePath), ""), ServiceInfo: service, IDLPackageInfo: &idlPackageRenderInfo, } insertKey, err := renderInsertKey(tplInfo, data) if err != nil { return err } if bytes.Contains(fileContent, []byte(insertKey)) { continue } imptSlice, err := getInsertImportContent(tplInfo, data, fileContent) if err != nil { return err } for _, impt := range imptSlice { if bytes.Contains(fileContent, []byte(impt[1])) { continue } fileContent, err = util.AddImportForContent(fileContent, impt[0], impt[1]) if err != nil { logs.Warnf("can not add import(%s) for file(%s)\n", impt[1], filePath) } } appendContent, err = appendUpdateFile(tplInfo, data, appendContent) if err != nil { return err } } } if len(tplInfo.UpdateBehavior.AppendLocation) == 0 { // default, append to end of file buf := bytes.NewBuffer(nil) _, err = buf.Write(fileContent) if err != nil { return fmt.Errorf("write file(%s) failed, err: %v", tplInfo.Path, err) } _, err = buf.Write(appendContent) if err != nil { return fmt.Errorf("append file(%s) failed, err: %v", tplInfo.Path, err) } logs.Infof("append content for file '%s', because the update behavior is 'Append' and appendKey is 'method'", filePath) pkgGen.files = append(pkgGen.files, File{filePath, buf.String(), false, ""}) } else { // 'append location', append new content after 'append location' part := bytes.Split(fileContent, []byte(tplInfo.UpdateBehavior.AppendLocation)) if len(part) == 0 { return fmt.Errorf("can not find append location '%s' for file '%s'\n", tplInfo.UpdateBehavior.AppendLocation, filePath) } if len(part) != 2 { return fmt.Errorf("do not support multiple append location '%s' for file '%s'\n", tplInfo.UpdateBehavior.AppendLocation, filePath) } buf := bytes.NewBuffer(nil) err = writeBytes(buf, part[0], []byte(tplInfo.UpdateBehavior.AppendLocation), appendContent, part[1]) if err != nil { return fmt.Errorf("write file(%s) failed, err: %v", tplInfo.Path, err) } logs.Infof("append content for file '%s', because the update behavior is 'Append' and appendKey is 'method'", filePath) pkgGen.files = append(pkgGen.files, File{filePath, buf.String(), false, ""}) } } else if tplInfo.UpdateBehavior.AppendKey == "service" { var appendContent []byte for _, service := range idlPackageRenderInfo.ServiceInfos.Services { data := CustomizedFileForService{ Service: service, FilePath: filePath, FilePackage: util.SplitPackage(filepath.Dir(filePath), ""), IDLPackageInfo: &idlPackageRenderInfo, } insertKey, err := renderInsertKey(tplInfo, data) if err != nil { return err } if bytes.Contains(fileContent, []byte(insertKey)) { continue } imptSlice, err := getInsertImportContent(tplInfo, data, fileContent) if err != nil { return err } for _, impt := range imptSlice { if bytes.Contains(fileContent, []byte(impt[1])) { continue } fileContent, err = util.AddImportForContent(fileContent, impt[0], impt[1]) if err != nil { logs.Warnf("can not add import(%s) for file(%s)\n", impt[1], filePath) } } appendContent, err = appendUpdateFile(tplInfo, data, appendContent) if err != nil { return err } } if len(tplInfo.UpdateBehavior.AppendLocation) == 0 { // default, append to end of file buf := bytes.NewBuffer(nil) _, err = buf.Write(fileContent) if err != nil { return fmt.Errorf("write file(%s) failed, err: %v", tplInfo.Path, err) } _, err = buf.Write(appendContent) if err != nil { return fmt.Errorf("append file(%s) failed, err: %v", tplInfo.Path, err) } logs.Infof("append content for file '%s', because the update behavior is 'Append' and appendKey is 'service'", filePath) pkgGen.files = append(pkgGen.files, File{filePath, buf.String(), false, ""}) } else { // 'append location', append new content after 'append location' part := bytes.Split(fileContent, []byte(tplInfo.UpdateBehavior.AppendLocation)) if len(part) == 0 { return fmt.Errorf("can not find append location '%s' for file '%s'\n", tplInfo.UpdateBehavior.AppendLocation, filePath) } if len(part) != 2 { return fmt.Errorf("do not support multiple append location '%s' for file '%s'\n", tplInfo.UpdateBehavior.AppendLocation, filePath) } buf := bytes.NewBuffer(nil) err = writeBytes(buf, part[0], []byte(tplInfo.UpdateBehavior.AppendLocation), appendContent, part[1]) if err != nil { return fmt.Errorf("write file(%s) failed, err: %v", tplInfo.Path, err) } logs.Infof("append content for file '%s', because the update behavior is 'Append' and appendKey is 'service'", filePath) pkgGen.files = append(pkgGen.files, File{filePath, buf.String(), false, ""}) } } else { // add append content to the file directly data := CustomizedFileForIDL{ IDLPackageRenderInfo: &idlPackageRenderInfo, FilePath: filePath, FilePackage: util.SplitPackage(filepath.Dir(filePath), ""), } file, err := appendUpdateFile(tplInfo, data, fileContent) if err != nil { return err } pkgGen.files = append(pkgGen.files, File{filePath, string(file), false, ""}) } default: // do nothing logs.Warnf("unknown update behavior, do nothing") } } return nil } func writeBytes(buf *bytes.Buffer, bytes ...[]byte) error { for _, b := range bytes { _, err := buf.Write(b) if err != nil { return err } } return nil }