gr_hz/generator/custom_files.go
2024-04-30 20:22:44 +08:00

658 lines
23 KiB
Go

/*
* 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"
"gr_hz/util"
"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
}