From a00dc2c6edf0c52b66407aa28c8bb96f0d2edbd8 Mon Sep 17 00:00:00 2001 From: Freeman Liu Date: Wed, 2 Oct 2024 01:21:43 +0800 Subject: [PATCH] Support --include-non-tpl and --output (#14) * Add --include-non-tpl * Add --output * Refactor --- cmd/handlebarsjs/main.go | 9 +-- pkg/cmd/generate/generate.go | 106 +++++++++++++++++++----------- pkg/cmd/generate/generate_test.go | 65 ++++++++++++++++++ pkg/cmd/init/init.go | 47 +++++++------ pkg/model/config.go | 1 + pkg/model/render.go | 20 ++++-- pkg/util/util.go | 61 ++++++++++++----- pkg/util/util_test.go | 6 +- 8 files changed, 220 insertions(+), 95 deletions(-) diff --git a/cmd/handlebarsjs/main.go b/cmd/handlebarsjs/main.go index 0feeb35..fc96a0c 100644 --- a/cmd/handlebarsjs/main.go +++ b/cmd/handlebarsjs/main.go @@ -2,11 +2,11 @@ package main import ( "fmt" + "github.com/DanielLiu1123/gencoder/pkg/util" "io" "log" "net/http" "os" - "path/filepath" "strings" ) @@ -62,11 +62,8 @@ func genHandlebarJS() { } func generateFile(filePath, content string) { - if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil { - log.Fatal(err) - } - err := os.WriteFile(filePath, []byte(content), 0644) + err := util.WriteFile(filePath, []byte(content)) if err != nil { - log.Fatal(err) + log.Fatal("Error writing file:", err) } } diff --git a/pkg/cmd/generate/generate.go b/pkg/cmd/generate/generate.go index 5797ad8..12ae0fb 100644 --- a/pkg/cmd/generate/generate.go +++ b/pkg/cmd/generate/generate.go @@ -17,10 +17,14 @@ import ( ) type generateOptions struct { - config string - importHelper string - commandLineProperties map[string]string - commandLineTemplates string + config string + importHelper string + includeNonTpl bool + + // Override config file gencoder.yaml + Templates string + Properties map[string]string // Add properties, will override properties in config file + output string } func NewCmdGenerate(globalOptions *model.GlobalOptions) *cobra.Command { @@ -45,7 +49,7 @@ func NewCmdGenerate(globalOptions *model.GlobalOptions) *cobra.Command { `, PreRun: func(cmd *cobra.Command, args []string) { validateArgs(args) - opt.commandLineProperties = parseProperties(props) + opt.Properties = parseProperties(props) if opt.importHelper != "" { registerCustomHelpers(opt.importHelper) } @@ -58,7 +62,9 @@ func NewCmdGenerate(globalOptions *model.GlobalOptions) *cobra.Command { c.Flags().StringVarP(&opt.config, "config", "f", globalOptions.Config, "Config file to use") c.Flags().StringVarP(&opt.importHelper, "import-helper", "i", "", "Import helper JavaScript file, can be URL ([http|https]://...) or file path") c.Flags().StringSliceVarP(&props, "properties", "p", []string{}, "Add properties, will override properties in config file, --properties=\"k1=v1\" --properties=\"k2=v2,k3=v3\"") - c.Flags().StringVarP(&opt.commandLineTemplates, "templates", "t", "", "Override templates directory, can be path or URL, e.g. https://github.com/DanielLiu1123/gencoder/tree/main/templates") + c.Flags().StringVarP(&opt.Templates, "templates", "t", "", "Override templates directory, can be path or URL, e.g. https://github.com/DanielLiu1123/gencoder/tree/main/templates") + c.Flags().BoolVarP(&opt.includeNonTpl, "include-non-tpl", "a", false, "Include non-template files in the 'templates' option") + c.Flags().StringVarP(&opt.output, "output", "o", "", "Output directory for generated files, default is the current directory") return c } @@ -121,51 +127,76 @@ func run(_ *cobra.Command, _ []string, opt *generateOptions, _ *model.GlobalOpti log.Fatal(err) } - templates, err := util.LoadTemplates(cfg, opt.commandLineTemplates) + mergeCmdOptionsToConfig(cfg, opt) + + files, err := util.LoadFiles(cfg) if err != nil { log.Fatal(err) } - registerPartialTemplates(templates) + registerPartials(files) - renderContexts := util.CollectRenderContexts(cfg, opt.commandLineProperties) + renderContexts := util.CollectRenderContexts(cfg, opt.Properties) + + if opt.includeNonTpl { + for _, f := range files { + generateForNormalFiles(cfg, f) + } + } if len(renderContexts) > 0 { - generateForAllContexts(cfg, templates, renderContexts) + generateForAllContexts(cfg, files, renderContexts) } else { - generateFromBoilerplate(cfg, templates, opt.commandLineProperties) + properties := mergeProperties(cfg.Properties, opt.Properties) + renderContext := &model.RenderContext{Properties: properties, Config: cfg} + for _, t := range files { + generateForTemplateFiles(cfg, t, renderContext) + } + } +} + +func mergeCmdOptionsToConfig(cfg *model.Config, opt *generateOptions) { + if opt.output != "" { + cfg.Output = opt.output + } + if opt.Templates != "" { + cfg.Templates = opt.Templates } } func loadConfig(opt *generateOptions) (*model.Config, error) { cfg, err := util.ReadConfig(opt.config) - if (err != nil && !errors.Is(err, os.ErrNotExist)) || (errors.Is(err, os.ErrNotExist) && opt.commandLineTemplates == "") { + if (err != nil && !errors.Is(err, os.ErrNotExist)) || (errors.Is(err, os.ErrNotExist) && opt.Templates == "") { return nil, err } return cfg, nil } -func registerPartialTemplates(templates []*model.Tpl) { - for _, t := range templates { - if t.GeneratedFileName == "" { - handlebars.RegisterPartial(t.TemplateName, t.Source) +func registerPartials(files []*model.File) { + for _, f := range files { + if f.Type == model.FileTypePartial { + handlebars.RegisterPartial(f.Name, string(f.Content)) } } } -func generateForAllContexts(cfg *model.Config, templates []*model.Tpl, renderContexts []*model.RenderContext) { +func generateForAllContexts(cfg *model.Config, files []*model.File, renderContexts []*model.RenderContext) { for _, ctx := range renderContexts { - for _, t := range templates { - generate(cfg, t, ctx) + for _, f := range files { + generateForTemplateFiles(cfg, f, ctx) } } } -func generateFromBoilerplate(cfg *model.Config, templates []*model.Tpl, commandLineProperties map[string]string) { - properties := mergeProperties(cfg.Properties, commandLineProperties) - renderContext := &model.RenderContext{Properties: properties, Config: cfg} - for _, t := range templates { - generate(cfg, t, renderContext) +func generateForNormalFiles(cfg *model.Config, f *model.File) { + if f.Type != model.FileTypeNormal { + return + } + + out := filepath.Join(cfg.Output, f.RelativePath) + + if _, err := os.Stat(out); err != nil { + createNewFile(out, f.Content) } } @@ -180,19 +211,20 @@ func mergeProperties(configProps map[string]string, cmdLineProps map[string]stri return merged } -func generate(cfg *model.Config, tpl *model.Tpl, ctx *model.RenderContext) { - if tpl.GeneratedFileName == "" { // partial template +func generateForTemplateFiles(cfg *model.Config, tpl *model.File, ctx *model.RenderContext) { + if tpl.Type != model.FileTypeTemplate { return } context := util.ToMap(ctx) newContent := handlebars.Render(tpl.Template, context) - fileName := getFileName(tpl.GeneratedFileName, context) + fileName := getFileName(tpl.Output, context) + out := filepath.Join(cfg.Output, fileName) - if _, err := os.Stat(fileName); err == nil { - updateExistingFile(cfg, fileName, newContent) + if _, err := os.Stat(out); err == nil { + updateExistingFile(cfg, out, newContent) } else { - createNewFile(fileName, newContent) + createNewFile(out, []byte(newContent)) } } @@ -203,19 +235,15 @@ func updateExistingFile(cfg *model.Config, fileName, newContent string) { } realContent := replaceBlocks(cfg, string(oldContent), newContent) - writeFile(fileName, realContent) -} - -func createNewFile(fileName, content string) { - dir := filepath.Dir(fileName) - if err := os.MkdirAll(dir, 0755); err != nil { + err = util.WriteFile(fileName, []byte(realContent)) + if err != nil { log.Fatal(err) } - writeFile(fileName, content) } -func writeFile(fileName, content string) { - if err := os.WriteFile(fileName, []byte(content), 0644); err != nil { +func createNewFile(fileName string, content []byte) { + err := util.WriteFile(fileName, content) + if err != nil { log.Fatal(err) } } diff --git a/pkg/cmd/generate/generate_test.go b/pkg/cmd/generate/generate_test.go index 6dbc0ac..9dad051 100644 --- a/pkg/cmd/generate/generate_test.go +++ b/pkg/cmd/generate/generate_test.go @@ -1,6 +1,9 @@ package generate import ( + "github.com/stretchr/testify/require" + "os" + "path/filepath" "testing" "github.com/DanielLiu1123/gencoder/pkg/model" @@ -217,3 +220,65 @@ new content 2 }) } } + +func TestNewCmdGenerate_whenUsingIncludeNonTpl_thenShouldGenerateNonTemplateFiles(t *testing.T) { + + // Create template directory + tplDir, err := os.MkdirTemp("", "tpl") + require.NoError(t, err) + defer os.RemoveAll(tplDir) + + _ = os.Chdir(tplDir) + + // Create non-template files + createNewFile(filepath.Join(tplDir, "templates/non-template1.txt"), []byte("This is a non-template file")) + createNewFile(filepath.Join(tplDir, "templates/foo/non-template2.txt"), []byte("This is a non-template file")) + // Create partial file + createNewFile(filepath.Join(tplDir, "templates/header.txt.hbs"), []byte("This is a header")) + // Create template file + createNewFile(filepath.Join(tplDir, "templates/test1.text.hbs"), []byte(`@gencoder.generated: test1.txt +{{> header.txt.hbs}} + +Hello, {{properties.name}}!`)) + createNewFile(filepath.Join(tplDir, "templates/test2.text.hbs"), []byte(`@gencoder.generated: foo/test2.txt +{{> header.txt.hbs}} + +Hello, {{properties.name}}!`)) + + // Create generated directory + generatedDir, err := os.MkdirTemp("", "generated") + require.NoError(t, err) + defer os.RemoveAll(generatedDir) + + _ = os.Chdir(generatedDir) + + cmd := NewCmdGenerate(&model.GlobalOptions{}) + + cmd.SetArgs([]string{"--templates", filepath.Join(tplDir, "templates"), "--include-non-tpl", "--properties", "name=World"}) + + err = cmd.Execute() + require.NoError(t, err) + + // Verify non-template files are generated + _, err = os.Stat(filepath.Join(generatedDir, "non-template1.txt")) + assert.NoError(t, err) + _, err = os.Stat(filepath.Join(generatedDir, "foo/non-template2.txt")) // keep directory structure + assert.NoError(t, err) + + // Verify partial files do not exist + _, err = os.Stat(filepath.Join(generatedDir, "header.txt.hbs")) + assert.Error(t, err) + + // Verify template files are generated + content, err := os.ReadFile(filepath.Join(generatedDir, "test1.txt")) + assert.NoError(t, err) + assert.Equal(t, `@gencoder.generated: test1.txt +This is a header +Hello, World!`, string(content)) + + content, err = os.ReadFile(filepath.Join(generatedDir, "foo/test2.txt")) // keep directory structure + assert.NoError(t, err) + assert.Equal(t, `@gencoder.generated: foo/test2.txt +This is a header +Hello, World!`, string(content)) +} diff --git a/pkg/cmd/init/init.go b/pkg/cmd/init/init.go index 45189f6..86d32e9 100644 --- a/pkg/cmd/init/init.go +++ b/pkg/cmd/init/init.go @@ -1,6 +1,7 @@ package init import ( + "github.com/DanielLiu1123/gencoder/pkg/util" "log" "os" "path/filepath" @@ -10,33 +11,40 @@ import ( ) type initOptions struct { + output string } func NewCmdInit(globalOptions *model.GlobalOptions) *cobra.Command { opt := &initOptions{} - return &cobra.Command{ + c := &cobra.Command{ Use: "init", Short: "Init basic configuration for gencoder", Example: ` # Init basic configuration for gencoder - $ gencoder init`, + $ gencoder init + + # Init basic configuration in a specific directory + $ gencoder init -o myproject`, Run: func(cmd *cobra.Command, args []string) { run(cmd, args, opt, globalOptions) }, } + + c.Flags().StringVarP(&opt.output, "output", "o", "", "Output directory, default to current directory") + + return c } -func run(_ *cobra.Command, _ []string, _ *initOptions, _ *model.GlobalOptions) { - initGencoderYaml() - initTemplatesDir() - initTemplates() +func run(_ *cobra.Command, _ []string, opt *initOptions, _ *model.GlobalOptions) { + initGencoderYaml(opt) + initTemplates(opt) log.Println("Init success! Please modify the gencoder.yaml and templates to fit your project needs.") log.Println() log.Println("Thank you for using gencoder!") } -func initGencoderYaml() { +func initGencoderYaml(opt *initOptions) { gencoderYaml := `templates: templates databases: - dsn: 'mysql://root:root@localhost:3306/testdb' @@ -45,19 +53,10 @@ databases: properties: package: 'com.example' ` - writeFileIfNotExists("gencoder.yaml", []byte(gencoderYaml)) + writeFileIfNotExists(filepath.Join(opt.output, "gencoder.yaml"), []byte(gencoderYaml)) } -func initTemplatesDir() { - if _, err := os.Stat("templates"); os.IsNotExist(err) { - err := os.Mkdir("templates", 0755) - if err != nil { - log.Fatal(err) - } - } -} - -func initTemplates() { +func initTemplates(opt *initOptions) { entityJava := `/** * @gencoder.generated: src/main/java/{{_replaceAll properties.package '.' '/'}}/{{_pascalCase table.name}}.java */ @@ -92,8 +91,6 @@ public record {{_pascalCase table.name}} ( } } ` - writeFileIfNotExists(filepath.Join("templates", "entity.java.hbs"), []byte(entityJava)) - javaTypePartial := `{{~#if (_match 'varchar\(\d+\)|char|tinytext|text|mediumtext|longtext' columnType)}}String {{~else if (_match 'bigint' columnType)}}Long {{~else if (_match 'int|integer|mediumint' columnType)}}Integer @@ -110,14 +107,16 @@ public record {{_pascalCase table.name}} ( {{~else if (_match 'enum.*' columnType)}}String {{~else}}Object {{~/if}}` - writeFileIfNotExists(filepath.Join("templates", "java_type.partial.hbs"), []byte(javaTypePartial)) + + writeFileIfNotExists(filepath.Join(opt.output, "templates", "entity.java.hbs"), []byte(entityJava)) + writeFileIfNotExists(filepath.Join(opt.output, "templates", "java_type.partial.hbs"), []byte(javaTypePartial)) } func writeFileIfNotExists(filename string, data []byte) { if _, err := os.Stat(filename); os.IsNotExist(err) { - err := os.WriteFile(filename, data, 0644) - if err != nil { - log.Fatal(err) + e := util.WriteFile(filename, data) + if e != nil { + log.Fatal(e) } } } diff --git a/pkg/model/config.go b/pkg/model/config.go index 2a3754a..bca32f0 100644 --- a/pkg/model/config.go +++ b/pkg/model/config.go @@ -6,6 +6,7 @@ type Config struct { BlockMarker BlockMarker `json:"blockMarker" yaml:"blockMarker"` Databases []*DatabaseConfig `json:"databases" yaml:"databases"` Properties map[string]string `json:"properties" yaml:"properties"` + Output string `json:"output" yaml:"output"` } type DatabaseConfig struct { diff --git a/pkg/model/render.go b/pkg/model/render.go index 8ca5f8b..c458864 100644 --- a/pkg/model/render.go +++ b/pkg/model/render.go @@ -12,9 +12,19 @@ type RenderContext struct { TableConfig *TableConfig `json:"tableConfig" yaml:"tableConfig"` } -type Tpl struct { - TemplateName string // template file name - GeneratedFileName string // generated file name, if empty, it's a partial template - Source string // template source code - Template goja.Value // compiled template +type FileType int + +const ( + FileTypeNormal FileType = iota + FileTypePartial + FileTypeTemplate +) + +type File struct { + Name string + RelativePath string + Content []byte + Type FileType + Output string // for Template FileType + Template goja.Value // for Template/Partial FileType } diff --git a/pkg/util/util.go b/pkg/util/util.go index f23d4b1..01b6e4c 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -36,12 +36,9 @@ func ReadConfig(configPath string) (*model.Config, error) { return &cfg, nil } -// LoadTemplates loads all templates from the given configuration and command line templates -func LoadTemplates(cfg *model.Config, commandLineTemplates string) ([]*model.Tpl, error) { +// LoadFiles loads all files from the given path +func LoadFiles(cfg *model.Config) ([]*model.File, error) { templatePath := cfg.GetTemplates() - if commandLineTemplates != "" { - templatePath = commandLineTemplates - } if isGitHubURL(templatePath) { var err error @@ -52,7 +49,7 @@ func LoadTemplates(cfg *model.Config, commandLineTemplates string) ([]*model.Tpl defer os.RemoveAll(templatePath) } - return loadTemplatesFromPath(templatePath, cfg) + return loadFilesFromPath(templatePath, cfg) } func isGitHubURL(url string) bool { @@ -81,7 +78,7 @@ func cloneGitHubRepo(url string) (string, error) { cloneURL := fmt.Sprintf("https://github.com/%s/%s.git", owner, repo) cmd := exec.Command("git", "clone", "--branch", branch, "--depth", "1", cloneURL, tmpDir) - cmd.Stdout = os.Stdout + cmd.Stdout = nil cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return "", fmt.Errorf("failed to clone repository: %v", err) @@ -93,10 +90,11 @@ func cloneGitHubRepo(url string) (string, error) { return tmpDir, nil } -func loadTemplatesFromPath(path string, cfg *model.Config) ([]*model.Tpl, error) { - var templates []*model.Tpl +func loadFilesFromPath(rootPath string, cfg *model.Config) ([]*model.File, error) { + + var templates []*model.File - err := filepath.WalkDir(path, func(path string, d fs.DirEntry, err error) error { + err := filepath.WalkDir(rootPath, func(path string, d fs.DirEntry, err error) error { if err != nil || d.IsDir() { return err } @@ -106,17 +104,31 @@ func loadTemplatesFromPath(path string, cfg *model.Config) ([]*model.Tpl, error) return err } - content := string(b) - template := handlebars.Compile(content) + var f model.File - t := &model.Tpl{ - TemplateName: d.Name(), - GeneratedFileName: getFileNameTemplate(content, cfg), - Source: content, - Template: template, + f.Name = d.Name() + rel, err := filepath.Rel(rootPath, path) + if err != nil { + return err + } + f.RelativePath = rel + f.Content = b + f.Type = model.FileTypeNormal + + // template or partial + if strings.HasSuffix(d.Name(), ".hbs") || strings.HasSuffix(d.Name(), ".mustache") { + content := string(b) + output := getFileNameTemplate(content, cfg) + if output != "" { + f.Type = model.FileTypeTemplate + f.Output = output + } else { + f.Type = model.FileTypePartial + } + f.Template = handlebars.Compile(content) } - templates = append(templates, t) + templates = append(templates, &f) return nil }) @@ -246,3 +258,16 @@ func createRenderContext(cfg *model.Config, dbCfg *model.DatabaseConfig, tbCfg * TableConfig: tbCfg, } } + +// WriteFile writes the content to the given file, creating directories if necessary +func WriteFile(filename string, content []byte) error { + dir := filepath.Dir(filename) + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + err := os.WriteFile(filename, content, 0644) + if err != nil { + return err + } + return nil +} diff --git a/pkg/util/util_test.go b/pkg/util/util_test.go index de6900d..f212693 100644 --- a/pkg/util/util_test.go +++ b/pkg/util/util_test.go @@ -59,11 +59,11 @@ package main OutputMarker: "@gencoder.generated:", } - templates, err := LoadTemplates(cfg, "") + templates, err := LoadFiles(cfg) assert.NoError(t, err) assert.Len(t, templates, 1) - assert.Equal(t, "test.go.hbs", templates[0].TemplateName) - assert.Equal(t, "{{table.name}}.go", templates[0].GeneratedFileName) + assert.Equal(t, "test.go.hbs", templates[0].Name) + assert.Equal(t, "{{table.name}}.go", templates[0].Output) } func TestCollectRenderContexts(t *testing.T) {