2024-04-30 19:30:09 +08:00
/ *
* 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"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"text/template"
2024-04-30 20:39:07 +08:00
"std.gaore.com/Gaore-Go/gr_hz/meta"
"std.gaore.com/Gaore-Go/gr_hz/util"
"std.gaore.com/Gaore-Go/gr_hz/util/logs"
2024-04-30 19:30:09 +08:00
)
var DefaultDelimiters = [ 2 ] string { "{{" , "}}" }
type TemplateConfig struct {
Layouts [ ] Template ` yaml:"layouts" `
}
const (
Skip = "skip"
Cover = "cover"
Append = "append"
)
type Template struct {
Default bool // Is it the default template
Path string ` yaml:"path" ` // The generated path and its filename, such as biz/handler/ping.go
Delims [ 2 ] string ` yaml:"delims" ` // Template Action Instruction Identifier, default: "{{}}"
Body string ` yaml:"body" ` // Render template, currently only supports go template syntax
Disable bool ` yaml:"disable" ` // Disable generating file, used to disable default package template
LoopMethod bool ` yaml:"loop_method" ` // Loop generate files based on "method"
LoopService bool ` yaml:"loop_service" ` // Loop generate files based on "service"
UpdateBehavior UpdateBehavior ` yaml:"update_behavior" ` // Update command behavior; 0:unchanged, 1:regenerate, 2:append
}
type UpdateBehavior struct {
Type string ` yaml:"type" ` // Update behavior type: skip/cover/append
// the following variables are used for append update
AppendKey string ` yaml:"append_key" ` // Append content based in key; for example: 'method'/'service'
InsertKey string ` yaml:"insert_key" ` // Insert content by "insert_key"
AppendTpl string ` yaml:"append_content_tpl" ` // Append content if UpdateBehavior is "append"
ImportTpl [ ] string ` yaml:"import_tpl" ` // Import insert template
AppendLocation string ` yaml:"append_location" ` // AppendLocation specifies the location of append, the default is the end of the file
}
// TemplateGenerator contains information about the output template
type TemplateGenerator struct {
OutputDir string
Config * TemplateConfig
Excludes [ ] string
tpls map [ string ] * template . Template // "template name" -> "Template", it is used get the "parsed template" directly
tplsInfo map [ string ] * Template // "template name" -> "template info", it is used to get the original "template information"
dirs map [ string ] bool
isPackageTpl bool
files [ ] File
excludedFiles map [ string ] * File
}
func ( tg * TemplateGenerator ) Init ( ) error {
if tg . Config == nil {
return errors . New ( "config not set yet" )
}
if tg . tpls == nil {
tg . tpls = make ( map [ string ] * template . Template , len ( tg . Config . Layouts ) )
}
if tg . tplsInfo == nil {
tg . tplsInfo = make ( map [ string ] * Template , len ( tg . Config . Layouts ) )
}
if tg . dirs == nil {
tg . dirs = make ( map [ string ] bool )
}
for _ , l := range tg . Config . Layouts {
if tg . isPackageTpl && IsDefaultPackageTpl ( l . Path ) {
continue
}
// check if is a directory
var noFile bool
if strings . HasSuffix ( l . Path , string ( filepath . Separator ) ) {
noFile = true
}
path := l . Path
if filepath . IsAbs ( path ) {
return fmt . Errorf ( "absolute template path '%s' is not allowed" , path )
}
dir := filepath . Dir ( path )
isExist , err := util . PathExist ( filepath . Join ( tg . OutputDir , dir ) )
if err != nil {
return fmt . Errorf ( "check directory '%s' failed, err: %v" , dir , err . Error ( ) )
}
if isExist {
tg . dirs [ dir ] = true
} else {
tg . dirs [ dir ] = false
}
if noFile {
continue
}
// parse templates
if _ , ok := tg . tpls [ path ] ; ok {
continue
}
err = tg . loadLayout ( l , path , false )
if err != nil {
return err
}
}
excludes := make ( map [ string ] * File , len ( tg . Excludes ) )
for _ , f := range tg . Excludes {
excludes [ f ] = & File { }
}
tg . excludedFiles = excludes
return nil
}
func ( tg * TemplateGenerator ) loadLayout ( layout Template , tplName string , isDefaultTpl bool ) error {
delims := DefaultDelimiters
if layout . Delims [ 0 ] != "" && layout . Delims [ 1 ] != "" {
delims = layout . Delims
}
// insert template funcs
tpl := template . New ( tplName ) . Funcs ( funcMap )
tpl = tpl . Delims ( delims [ 0 ] , delims [ 1 ] )
var err error
if tpl , err = tpl . Parse ( layout . Body ) ; err != nil {
return fmt . Errorf ( "parse template '%s' failed, err: %v" , tplName , err . Error ( ) )
}
layout . Default = isDefaultTpl
tg . tpls [ tplName ] = tpl
tg . tplsInfo [ tplName ] = & layout
return nil
}
func ( tg * TemplateGenerator ) Generate ( input interface { } , tplName , filepath string , noRepeat bool ) error {
// check if "*" (global scope) data exists, and stores it to all
var all map [ string ] interface { }
if data , ok := input . ( map [ string ] interface { } ) ; ok {
ad , ok := data [ "*" ]
if ok {
all = ad . ( map [ string ] interface { } )
}
if all == nil {
all = map [ string ] interface { } { }
}
all [ "hzVersion" ] = meta . Version
}
file := bytes . NewBuffer ( nil )
if tplName != "" {
tpl := tg . tpls [ tplName ]
if tpl == nil {
return fmt . Errorf ( "tpl %s not found" , tplName )
}
if err := tpl . Execute ( file , input ) ; err != nil {
return fmt . Errorf ( "render template '%s' failed, err: %v" , tplName , err . Error ( ) )
}
in := File { filepath , string ( file . Bytes ( ) ) , noRepeat , tplName }
tg . files = append ( tg . files , in )
return nil
}
for path , tpl := range tg . tpls {
file . Reset ( )
var fd interface { }
// search and merge rendering data
if data , ok := input . ( map [ string ] interface { } ) ; ok {
td := map [ string ] interface { } { }
tmp , ok := data [ path ]
if ok {
td = tmp . ( map [ string ] interface { } )
}
for k , v := range all {
td [ k ] = v
}
fd = td
} else {
fd = input
}
if err := tpl . Execute ( file , fd ) ; err != nil {
return fmt . Errorf ( "render template '%s' failed, err: %v" , path , err . Error ( ) )
}
in := File { path , string ( file . Bytes ( ) ) , noRepeat , tpl . Name ( ) }
tg . files = append ( tg . files , in )
}
return nil
}
func ( tg * TemplateGenerator ) Persist ( ) error {
files := tg . files
outPath := tg . OutputDir
if ! filepath . IsAbs ( outPath ) {
outPath , _ = filepath . Abs ( outPath )
}
for _ , data := range files {
// check for -E flags
if _ , ok := tg . excludedFiles [ filepath . Join ( data . Path ) ] ; ok {
continue
}
// lint file
if err := data . Lint ( ) ; err != nil {
return err
}
// create rendered file
abPath := filepath . Join ( outPath , data . Path )
abDir := filepath . Dir ( abPath )
isExist , err := util . PathExist ( abDir )
if err != nil {
return fmt . Errorf ( "check directory '%s' failed, err: %v" , abDir , err . Error ( ) )
}
if ! isExist {
if err := os . MkdirAll ( abDir , os . FileMode ( 0 o744 ) ) ; err != nil {
return fmt . Errorf ( "mkdir %s failed, err: %v" , abDir , err . Error ( ) )
}
}
err = func ( ) error {
file , err := os . OpenFile ( abPath , os . O_CREATE | os . O_TRUNC | os . O_RDWR , os . FileMode ( 0 o755 ) )
defer file . Close ( )
if err != nil {
return fmt . Errorf ( "open file '%s' failed, err: %v" , abPath , err . Error ( ) )
}
if _ , err = file . WriteString ( data . Content ) ; err != nil {
return fmt . Errorf ( "write file '%s' failed, err: %v" , abPath , err . Error ( ) )
}
return nil
} ( )
if err != nil {
return err
}
}
tg . files = tg . files [ : 0 ]
return nil
}
func ( tg * TemplateGenerator ) GetFormatAndExcludedFiles ( ) ( [ ] File , error ) {
var files [ ] File
outPath := tg . OutputDir
if ! filepath . IsAbs ( outPath ) {
outPath , _ = filepath . Abs ( outPath )
}
for _ , data := range tg . Files ( ) {
if _ , ok := tg . excludedFiles [ filepath . Join ( data . Path ) ] ; ok {
continue
}
// check repeat files
logs . Infof ( "Write %s" , data . Path )
isExist , err := util . PathExist ( filepath . Join ( data . Path ) )
if err != nil {
return nil , fmt . Errorf ( "check file '%s' failed, err: %v" , data . Path , err . Error ( ) )
}
if isExist && data . NoRepeat {
if data . FileTplName == handlerTplName {
logs . Warnf ( "Handler file(%s) has been generated.\n If you want to re-generate it, please copy and delete the file to prevent the already written code from being deleted." , data . Path )
} else if data . FileTplName == routerTplName {
logs . Warnf ( "Router file(%s) has been generated.\n If you want to re-generate it, please delete the file." , data . Path )
} else {
logs . Warnf ( "file '%s' already exists, so drop the generated file" , data . Path )
}
continue
}
// lint file
if err := data . Lint ( ) ; err != nil {
logs . Warnf ( "Lint file: %s failed:\n %s\n" , data . Path , data . Content )
}
files = append ( files , data )
}
return files , nil
}
func ( tg * TemplateGenerator ) Files ( ) [ ] File {
return tg . files
}
func ( tg * TemplateGenerator ) Degenerate ( ) error {
outPath := tg . OutputDir
if ! filepath . IsAbs ( outPath ) {
outPath , _ = filepath . Abs ( outPath )
}
for path := range tg . tpls {
abPath := filepath . Join ( outPath , path )
if err := os . RemoveAll ( abPath ) ; err != nil {
return fmt . Errorf ( "remove file '%s' failed, err: %v" , path , err . Error ( ) )
}
}
for dir , exist := range tg . dirs {
if ! exist {
abDir := filepath . Join ( outPath , dir )
if err := os . RemoveAll ( abDir ) ; err != nil {
return fmt . Errorf ( "remove directory '%s' failed, err: %v" , dir , err . Error ( ) )
}
}
}
return nil
}