github.com/cloudwego/kitex@v0.9.0/tool/internal_pkg/generator/custom_template.go (about)

     1  // Copyright 2021 CloudWeGo Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //   http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package generator
    16  
    17  import (
    18  	"fmt"
    19  	"io/ioutil"
    20  	"os"
    21  	"path"
    22  	"path/filepath"
    23  	"strings"
    24  
    25  	"gopkg.in/yaml.v3"
    26  
    27  	"github.com/cloudwego/kitex/tool/internal_pkg/log"
    28  	"github.com/cloudwego/kitex/tool/internal_pkg/util"
    29  )
    30  
    31  var DefaultDelimiters = [2]string{"{{", "}}"}
    32  
    33  type updateType string
    34  
    35  const (
    36  	skip              updateType = "skip"
    37  	cover             updateType = "cover"
    38  	incrementalUpdate updateType = "append"
    39  )
    40  
    41  type Update struct {
    42  	// update type: skip / cover / append. Default is skip.
    43  	// If `LoopMethod` is true, only Type field is effect and no append behavior.
    44  	Type string `yaml:"type,omitempty"`
    45  	// Match key in append type. If the rendered key exists in the file, the method will be skipped.
    46  	Key string `yaml:"key,omitempty"`
    47  	// Append template. Use it to render append content.
    48  	AppendTpl string `yaml:"append_tpl,omitempty"`
    49  	// Append import template. Use it to render import content to append.
    50  	ImportTpl []string `yaml:"import_tpl,omitempty"`
    51  }
    52  
    53  type Template struct {
    54  	// The generated path and its filename. For example: biz/test.go
    55  	// will generate test.go in biz directory.
    56  	Path string `yaml:"path,omitempty"`
    57  	// Render template content, currently only supports go template syntax
    58  	Body string `yaml:"body,omitempty"`
    59  	// define update behavior
    60  	UpdateBehavior *Update `yaml:"update_behavior,omitempty"`
    61  	// If set this field, kitex will generate file by cycle. For example:
    62  	// test_a/test_b/{{ .Name}}_test.go
    63  	LoopMethod bool `yaml:"loop_method,omitempty"`
    64  	// If both set this field and combine-service, kitex will generate service by cycle.
    65  	LoopService bool `yaml:"loop_service,omitempty"`
    66  }
    67  
    68  type customGenerator struct {
    69  	fs       []*File
    70  	pkg      *PackageInfo
    71  	basePath string
    72  }
    73  
    74  func NewCustomGenerator(pkg *PackageInfo, basePath string) *customGenerator {
    75  	return &customGenerator{
    76  		pkg:      pkg,
    77  		basePath: basePath,
    78  	}
    79  }
    80  
    81  func (c *customGenerator) loopGenerate(tpl *Template) error {
    82  	tmp := c.pkg.Methods
    83  	defer func() {
    84  		c.pkg.Methods = tmp
    85  	}()
    86  	m := c.pkg.AllMethods()
    87  	for _, method := range m {
    88  		c.pkg.Methods = []*MethodInfo{method}
    89  		pathTask := &Task{
    90  			Text: tpl.Path,
    91  		}
    92  		renderPath, err := pathTask.RenderString(c.pkg)
    93  		if err != nil {
    94  			return err
    95  		}
    96  		filePath := filepath.Join(c.basePath, renderPath)
    97  		// update
    98  		if util.Exists(filePath) && updateType(tpl.UpdateBehavior.Type) == skip {
    99  			continue
   100  		}
   101  		task := &Task{
   102  			Name: path.Base(renderPath),
   103  			Path: filePath,
   104  			Text: tpl.Body,
   105  		}
   106  		f, err := task.Render(c.pkg)
   107  		if err != nil {
   108  			return err
   109  		}
   110  		c.fs = append(c.fs, f)
   111  	}
   112  	return nil
   113  }
   114  
   115  func (c *customGenerator) commonGenerate(tpl *Template) error {
   116  	// Use all services including base service.
   117  	tmp := c.pkg.Methods
   118  	defer func() {
   119  		c.pkg.Methods = tmp
   120  	}()
   121  	c.pkg.Methods = c.pkg.AllMethods()
   122  
   123  	pathTask := &Task{
   124  		Text: tpl.Path,
   125  	}
   126  	renderPath, err := pathTask.RenderString(c.pkg)
   127  	if err != nil {
   128  		return err
   129  	}
   130  	filePath := filepath.Join(c.basePath, renderPath)
   131  	update := util.Exists(filePath)
   132  	if update && updateType(tpl.UpdateBehavior.Type) == skip {
   133  		log.Infof("skip generate file %s", tpl.Path)
   134  		return nil
   135  	}
   136  	var f *File
   137  	if update && updateType(tpl.UpdateBehavior.Type) == incrementalUpdate {
   138  		cc := &commonCompleter{
   139  			path:   filePath,
   140  			pkg:    c.pkg,
   141  			update: tpl.UpdateBehavior,
   142  		}
   143  		f, err = cc.Complete()
   144  		if err != nil {
   145  			return err
   146  		}
   147  	} else {
   148  		// just create dir
   149  		if tpl.Path[len(tpl.Path)-1] == '/' {
   150  			os.MkdirAll(filePath, 0o755)
   151  			return nil
   152  		}
   153  
   154  		task := &Task{
   155  			Name: path.Base(tpl.Path),
   156  			Path: filePath,
   157  			Text: tpl.Body,
   158  		}
   159  
   160  		f, err = task.Render(c.pkg)
   161  		if err != nil {
   162  			return err
   163  		}
   164  	}
   165  
   166  	c.fs = append(c.fs, f)
   167  	return nil
   168  }
   169  
   170  func (g *generator) GenerateCustomPackage(pkg *PackageInfo) (fs []*File, err error) {
   171  	g.updatePackageInfo(pkg)
   172  
   173  	g.setImports(HandlerFileName, pkg)
   174  	t, err := readTemplates(g.TemplateDir)
   175  	if err != nil {
   176  		return nil, err
   177  	}
   178  	for _, tpl := range t {
   179  		if tpl.LoopService && g.CombineService {
   180  			svrInfo, cs := pkg.ServiceInfo, pkg.CombineServices
   181  
   182  			for i := range cs {
   183  				pkg.ServiceInfo = cs[i]
   184  				f, err := renderFile(pkg, g.OutputPath, tpl)
   185  				if err != nil {
   186  					return nil, err
   187  				}
   188  				fs = append(fs, f...)
   189  			}
   190  			pkg.ServiceInfo, pkg.CombineServices = svrInfo, cs
   191  		} else {
   192  			f, err := renderFile(pkg, g.OutputPath, tpl)
   193  			if err != nil {
   194  				return nil, err
   195  			}
   196  			fs = append(fs, f...)
   197  		}
   198  	}
   199  	return fs, nil
   200  }
   201  
   202  func renderFile(pkg *PackageInfo, outputPath string, tpl *Template) (fs []*File, err error) {
   203  	cg := NewCustomGenerator(pkg, outputPath)
   204  	// special handling Methods field
   205  	if tpl.LoopMethod {
   206  		err = cg.loopGenerate(tpl)
   207  	} else {
   208  		err = cg.commonGenerate(tpl)
   209  	}
   210  	if err == errNoNewMethod {
   211  		err = nil
   212  	}
   213  	return cg.fs, err
   214  }
   215  
   216  func readTemplates(dir string) ([]*Template, error) {
   217  	files, _ := ioutil.ReadDir(dir)
   218  	var ts []*Template
   219  	for _, f := range files {
   220  		// filter dir and non-yaml files
   221  		if f.Name() != ExtensionFilename && !f.IsDir() && (strings.HasSuffix(f.Name(), "yaml") || strings.HasSuffix(f.Name(), "yml")) {
   222  			p := filepath.Join(dir, f.Name())
   223  			tplData, err := ioutil.ReadFile(p)
   224  			if err != nil {
   225  				return nil, fmt.Errorf("read layout config from  %s failed, err: %v", p, err.Error())
   226  			}
   227  			t := &Template{
   228  				UpdateBehavior: &Update{Type: string(skip)},
   229  			}
   230  			if err = yaml.Unmarshal(tplData, t); err != nil {
   231  				return nil, fmt.Errorf("%s: unmarshal layout config failed, err: %s", f.Name(), err.Error())
   232  			}
   233  			ts = append(ts, t)
   234  		}
   235  	}
   236  
   237  	return ts, nil
   238  }