diff --git a/CHANGELOG.md b/CHANGELOG.md index 406039db..09a6d7e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Collation in `clause.OrderDef` is now a string not an expression and is always quoted - Calling `UpdateAll`, `DeleteAll` and `ReloadAll` on an empty model slice now returns nil without running any queries. - Generated files now end with `.bob.go` instead of `.go` and are always cleaned up before generating new files. Singleton templates are now required to have a `.bob.go.tpl` extension. +- The expected structure for templates have been changed: + - Previously, singleton templates should be kept in a `singleton` folder. Now, any template not inside a folder is considered a singleton template. + - Previoulsy, templates in the root folder are merged and run for each table. Now, this will happen to templates in the `table/` folder. + - Previoulsy, the entire file tree and every subdirectory is walked to find templates. Now only templates in the root folder and the `table/` folder are considered. ### Deprecated diff --git a/gen/bobgen-mysql/templates/models/bob_mysql_blocks.bob.go.tpl b/gen/bobgen-mysql/templates/models/bob_mysql_blocks.bob.go.tpl new file mode 100644 index 00000000..28a55199 --- /dev/null +++ b/gen/bobgen-mysql/templates/models/bob_mysql_blocks.bob.go.tpl @@ -0,0 +1,20 @@ +{{- define "helpers/where_variables"}} +{{$.Importer.Import (printf "github.com/stephenafamo/bob/dialect/%s/dialect" $.Dialect)}} +var ( + SelectWhere = Where[*dialect.SelectQuery]() + UpdateWhere = Where[*dialect.UpdateQuery]() + DeleteWhere = Where[*dialect.DeleteQuery]() +) +{{- end -}} + +{{define "unique_constraint_error_detection_method" -}} +{{$.Importer.Import "strings"}} +{{$.Importer.Import "mysqlDriver" "github.com/go-sql-driver/mysql"}} +func (e *UniqueConstraintError) Is(target error) bool { + err, ok := target.(*mysqlDriver.MySQLError) + if !ok { + return false + } + return err.Number == 1062 && strings.Contains(err.Message, e.s) +} +{{end -}} diff --git a/gen/bobgen-mysql/templates/models/singleton/bob_mysql_blocks.bob.go.tpl b/gen/bobgen-mysql/templates/models/singleton/bob_mysql_blocks.bob.go.tpl deleted file mode 100644 index 22767d99..00000000 --- a/gen/bobgen-mysql/templates/models/singleton/bob_mysql_blocks.bob.go.tpl +++ /dev/null @@ -1,54 +0,0 @@ -{{- define "helpers/where_variables"}} -{{$.Importer.Import (printf "github.com/stephenafamo/bob/dialect/%s/dialect" $.Dialect)}} -var ( - SelectWhere = Where[*dialect.SelectQuery]() - UpdateWhere = Where[*dialect.UpdateQuery]() - DeleteWhere = Where[*dialect.DeleteQuery]() -) -{{- end -}} - -{{define "unique_constraint_error_detection_method" -}} -{{$.Importer.Import "strings"}} -{{$.Importer.Import "mysqlDriver" "github.com/go-sql-driver/mysql"}} -func (e *UniqueConstraintError) Is(target error) bool { - err, ok := target.(*mysqlDriver.MySQLError) - if !ok { - return false - } - return err.Number == 1062 && strings.Contains(err.Message, e.s) -} -{{end -}} - - -{{define "one_update" -}} -{{$table := .Table}} -{{$tAlias := .Aliases.Table $table.Key -}} -{{$.Importer.Import (printf "github.com/stephenafamo/bob/dialect/%s/um" $.Dialect)}} -// Update uses an executor to update the {{$tAlias.UpSingular}} -func (o *{{$tAlias.UpSingular}}) Update(ctx context.Context, exec bob.Executor, s *{{$tAlias.UpSingular}}Setter) error { - _, err := {{$tAlias.UpPlural}}.Update(s.UpdateMod(), um.Where(o.pkEQ())).Exec(ctx, exec) - if err != nil { - return err - } - - s.Overwrite(o) - - return nil -} -{{- end}} - - -{{define "slice_update" -}} -{{$table := .Table}} -{{$tAlias := .Aliases.Table $table.Key -}} -func (o {{$tAlias.UpSingular}}Slice) UpdateAll(ctx context.Context, exec bob.Executor, vals {{$tAlias.UpSingular}}Setter) error { - _, err := {{$tAlias.UpPlural}}.Update(vals.UpdateMod(), o.UpdateMod()).Exec(ctx, exec) - - for i := range o { - vals.Overwrite(o[i]) - } - - return err -} -{{- end}} - diff --git a/gen/bobgen-mysql/templates/models/100_blocks.go.tpl b/gen/bobgen-mysql/templates/models/table/100_blocks.go.tpl similarity index 57% rename from gen/bobgen-mysql/templates/models/100_blocks.go.tpl rename to gen/bobgen-mysql/templates/models/table/100_blocks.go.tpl index 858c8b7d..49c34ac3 100644 --- a/gen/bobgen-mysql/templates/models/100_blocks.go.tpl +++ b/gen/bobgen-mysql/templates/models/table/100_blocks.go.tpl @@ -22,3 +22,34 @@ func (s {{$tAlias.UpSingular}}Setter) UpdateMod() bob.Mod[*dialect.UpdateQuery] return um.Set(s.Expressions("{{$table.Name}}")...) } {{- end}} + +{{define "one_update" -}} +{{$table := .Table}} +{{$tAlias := .Aliases.Table $table.Key -}} +{{$.Importer.Import (printf "github.com/stephenafamo/bob/dialect/%s/um" $.Dialect)}} +// Update uses an executor to update the {{$tAlias.UpSingular}} +func (o *{{$tAlias.UpSingular}}) Update(ctx context.Context, exec bob.Executor, s *{{$tAlias.UpSingular}}Setter) error { + _, err := {{$tAlias.UpPlural}}.Update(s.UpdateMod(), um.Where(o.pkEQ())).Exec(ctx, exec) + if err != nil { + return err + } + + s.Overwrite(o) + + return nil +} +{{- end}} + +{{define "slice_update" -}} +{{$table := .Table}} +{{$tAlias := .Aliases.Table $table.Key -}} +func (o {{$tAlias.UpSingular}}Slice) UpdateAll(ctx context.Context, exec bob.Executor, vals {{$tAlias.UpSingular}}Setter) error { + _, err := {{$tAlias.UpPlural}}.Update(vals.UpdateMod(), o.UpdateMod()).Exec(ctx, exec) + + for i := range o { + vals.Overwrite(o[i]) + } + + return err +} +{{- end}} diff --git a/gen/bobgen-psql/templates/models/singleton/bob_psql_blocks.bob.go.tpl b/gen/bobgen-psql/templates/models/bob_psql_blocks.bob.go.tpl similarity index 100% rename from gen/bobgen-psql/templates/models/singleton/bob_psql_blocks.bob.go.tpl rename to gen/bobgen-psql/templates/models/bob_psql_blocks.bob.go.tpl diff --git a/gen/bobgen-sqlite/templates/models/bob_sqlite_blocks.bob.go.tpl b/gen/bobgen-sqlite/templates/models/bob_sqlite_blocks.bob.go.tpl new file mode 100644 index 00000000..7bd5d6f7 --- /dev/null +++ b/gen/bobgen-sqlite/templates/models/bob_sqlite_blocks.bob.go.tpl @@ -0,0 +1,32 @@ +{{- define "helpers/join_variables"}} +var ( + SelectJoins = getJoins[*dialect.SelectQuery] + UpdateJoins = getJoins[*dialect.UpdateQuery] +) +{{end -}} + +{{define "unique_constraint_error_detection_method" -}} +func (e *UniqueConstraintError) Is(target error) bool { + {{if not (eq $.DriverName "modernc.org/sqlite" "github.com/mattn/go-sqlite3")}} + return false + {{else}} + {{$errType := ""}} + {{$codeGetter := ""}} + {{$.Importer.Import "strings"}} + {{if eq $.DriverName "modernc.org/sqlite"}} + {{$.Importer.Import "sqliteDriver" $.DriverName}} + {{$errType = "*sqliteDriver.Error"}} + {{$codeGetter = "Code()"}} + {{else}} + {{$.Importer.Import $.DriverName}} + {{$errType = "sqlite3.Error"}} + {{$codeGetter = "ExtendedCode"}} + {{end}} + err, ok := target.({{$errType}}) + if !ok { + return false + } + return err.{{$codeGetter}} == 2067 && strings.Contains(err.Error(), e.s) + {{end}} +} +{{end -}} diff --git a/gen/bobgen-sqlite/templates/models/singleton/bob_sqlite_blocks.bob.go.tpl b/gen/bobgen-sqlite/templates/models/table/100_blocks.go.tpl similarity index 52% rename from gen/bobgen-sqlite/templates/models/singleton/bob_sqlite_blocks.bob.go.tpl rename to gen/bobgen-sqlite/templates/models/table/100_blocks.go.tpl index c7dfb50d..01e2969a 100644 --- a/gen/bobgen-sqlite/templates/models/singleton/bob_sqlite_blocks.bob.go.tpl +++ b/gen/bobgen-sqlite/templates/models/table/100_blocks.go.tpl @@ -1,10 +1,3 @@ -{{- define "helpers/join_variables"}} -var ( - SelectJoins = getJoins[*dialect.SelectQuery] - UpdateJoins = getJoins[*dialect.UpdateQuery] -) -{{end -}} - {{define "setter_insert_mod" -}} {{$.Importer.Import "io"}} {{$.Importer.Import "github.com/stephenafamo/bob"}} @@ -33,29 +26,3 @@ func (s *{{$tAlias.UpSingular}}Setter) Apply(q *dialect.InsertQuery) { })) } {{- end}} - -{{define "unique_constraint_error_detection_method" -}} -func (e *UniqueConstraintError) Is(target error) bool { - {{if not (eq $.DriverName "modernc.org/sqlite" "github.com/mattn/go-sqlite3")}} - return false - {{else}} - {{$errType := ""}} - {{$codeGetter := ""}} - {{$.Importer.Import "strings"}} - {{if eq $.DriverName "modernc.org/sqlite"}} - {{$.Importer.Import "sqliteDriver" $.DriverName}} - {{$errType = "*sqliteDriver.Error"}} - {{$codeGetter = "Code()"}} - {{else}} - {{$.Importer.Import $.DriverName}} - {{$errType = "sqlite3.Error"}} - {{$codeGetter = "ExtendedCode"}} - {{end}} - err, ok := target.({{$errType}}) - if !ok { - return false - } - return err.{{$codeGetter}} == 2067 && strings.Contains(err.Error(), e.s) - {{end}} -} -{{end -}} diff --git a/gen/gen.go b/gen/gen.go index 28946864..c64ef506 100644 --- a/gen/gen.go +++ b/gen/gen.go @@ -174,24 +174,15 @@ func generate[T, C, I any](s *State[C], data *TemplateData[T, C, I], goVersion s // set the package name for this output data.PkgName = o.PkgName - templates, err := o.initTemplates(s.CustomTemplateFuncs, s.Config.NoTests) - if err != nil { + if err := o.initTemplates(s.CustomTemplateFuncs); err != nil { return fmt.Errorf("unable to initialize templates: %w", err) } - tplCount := 0 - if o.templates != nil { - tplCount += len(o.templates.Templates()) - } - if o.testTemplates != nil { - tplCount += len(o.testTemplates.Templates()) - } - if tplCount == 0 { + if o.numTemplates() == 0 { continue } - err = o.initOutFolders(templates, s.Config.Wipe) - if err != nil { + if err := o.initOutFolders(s.Config.Wipe); err != nil { return fmt.Errorf("unable to initialize the output folders: %w", err) } @@ -199,36 +190,19 @@ func generate[T, C, I any](s *State[C], data *TemplateData[T, C, I], goVersion s o.templateByteBuffer = templateByteBuffer o.templateHeaderByteBuffer = templateHeaderByteBuffer - if err := generateSingletonOutput(o, data, goVersion); err != nil { + if err := generateSingletonOutput(o, data, goVersion, s.Config.NoTests); err != nil { return fmt.Errorf("singleton template output: %w", err) } - if !s.Config.NoTests { - if err := generateSingletonTestOutput(o, data, goVersion); err != nil { - return fmt.Errorf("unable to generate singleton test template output: %w", err) - } - } - - var regularDirExtMap, testDirExtMap dirExtMap - regularDirExtMap = groupTemplates(o.templates) - if !s.Config.NoTests { - testDirExtMap = groupTemplates(o.testTemplates) - } + dirExtMap := groupTemplates(o.tableTemplates) for _, table := range data.Tables { data.Table = table // Generate the regular templates - if err := generateOutput(o, regularDirExtMap, data, goVersion); err != nil { + if err := generateOutput(o, dirExtMap, data, goVersion, s.Config.NoTests); err != nil { return fmt.Errorf("unable to generate output: %w", err) } - - // Generate the test templates - if !s.Config.NoTests { - if err := generateTestOutput(o, testDirExtMap, data, goVersion); err != nil { - return fmt.Errorf("unable to generate test output: %w", err) - } - } } } diff --git a/gen/output.go b/gen/output.go index deb703f6..299e1fef 100644 --- a/gen/output.go +++ b/gen/output.go @@ -7,10 +7,11 @@ import ( "fmt" "io" "io/fs" + "maps" "os" "path/filepath" "regexp" - "sort" + "slices" "strconv" "strings" "text/template" @@ -55,16 +56,20 @@ type Output struct { OutFolder string Templates []fs.FS - templates *templateList - testTemplates *templateList + singletonTemplates *template.Template + tableTemplates *template.Template // Scratch buffers used as staging area for preparing parsed template data templateByteBuffer *bytes.Buffer templateHeaderByteBuffer *bytes.Buffer } +func (o *Output) numTemplates() int { + return len(o.singletonTemplates.Templates()) + len(o.tableTemplates.Templates()) +} + // initOutFolders creates the folders that will hold the generated output. -func (o *Output) initOutFolders(lazyTemplates []lazyTemplate, wipe bool) error { +func (o *Output) initOutFolders(wipe bool) error { if wipe { if err := os.RemoveAll(o.OutFolder); err != nil { return fmt.Errorf("unable to wipe output folder: %w", err) @@ -93,36 +98,10 @@ func (o *Output) initOutFolders(lazyTemplates []lazyTemplate, wipe bool) error { } } - newDirs := make(map[string]struct{}) - for _, t := range lazyTemplates { - // js/00_struct.js.tpl - // js/singleton/00_struct.js.tpl - // we want the js part only - fragments := strings.Split(t.Name, string(os.PathSeparator)) - - // Throw away the filename - fragments = fragments[0 : len(fragments)-1] - if len(fragments) != 0 && fragments[len(fragments)-1] == "singleton" { - fragments = fragments[:len(fragments)-1] - } - - if len(fragments) == 0 { - continue - } - - newDirs[strings.Join(fragments, string(os.PathSeparator))] = struct{}{} - } - if err := os.MkdirAll(o.OutFolder, os.ModePerm); err != nil { return err } - for d := range newDirs { - if err := os.MkdirAll(filepath.Join(o.OutFolder, d), os.ModePerm); err != nil { - return err - } - } - return nil } @@ -138,122 +117,157 @@ func (o *Output) initOutFolders(lazyTemplates []lazyTemplate, wipe bool) error { // // Later, in order to properly look up imports the paths will // be forced back to linux style paths. -func (o *Output) initTemplates(funcs template.FuncMap, notests bool) ([]lazyTemplate, error) { - var err error - - templates := make(map[string]templateLoader) +func (o *Output) initTemplates(funcs template.FuncMap) error { if len(o.Templates) == 0 { - return nil, errors.New("No templates defined") + return errors.New("No templates defined") + } + + o.singletonTemplates = template.New("") + o.tableTemplates = template.New("") + + if err := addTemplates(o.singletonTemplates, o.Templates, funcs, ".", true); err != nil { + return fmt.Errorf("failed to add singleton templates: %w", err) + } + + if err := addTemplates(o.tableTemplates, o.Templates, funcs, "table", false); err != nil { + return fmt.Errorf("failed to add table templates: %w", err) } - for _, tempFS := range o.Templates { + return nil +} + +func addTemplates(tpl *template.Template, tempFSs []fs.FS, funcs template.FuncMap, dir string, singletons bool) error { + type details struct { + fs fs.FS + fullPath string + } + all := make(map[string]details) + + for _, tempFS := range tempFSs { if tempFS == nil { continue } - err := fs.WalkDir(tempFS, ".", func(path string, entry fs.DirEntry, err error) error { - if err != nil { - return fmt.Errorf("in walk err: %w", err) + + if dir != "" { + tempFS, _ = fs.Sub(tempFS, dir) + if tempFS == nil { + continue } + } + + entries, err := fs.ReadDir(tempFS, ".") + if err != nil { + return fmt.Errorf("failed to read dir %q: %w", dir, err) + } + for _, entry := range entries { if entry.IsDir() { - return nil + continue } name := entry.Name() - if filepath.Ext(name) == ".tpl" { - templates[normalizeSlashes(path)] = assetLoader{fs: tempFS, name: path} + ext := filepath.Ext(name) + if ext != ".tpl" { + continue } - return nil - }) - if err != nil { - return nil, fmt.Errorf("after walk err: %w", err) - } - } + if singletons { + ext2 := filepath.Ext(name[:len(name)-len(ext)]) + fNameWithoutExts := filepath.Base(name[:len(name)-len(ext)-len(ext2)]) + if !strings.HasSuffix(fNameWithoutExts, ".bob") && + !strings.HasSuffix(fNameWithoutExts, ".bob_test") { + panic(fmt.Sprintf("singleton file name must end with .bob or .bob_test: %s", name)) + } + } - // For stability, sort keys to traverse the map and turn it into a slice - keys := make([]string, 0, len(templates)) - for k := range templates { - keys = append(keys, k) + all[name] = details{ + fs: tempFS, + fullPath: filepath.Join(dir, name), + } + } } - sort.Strings(keys) - lazyTemplates := make([]lazyTemplate, 0, len(templates)) - for _, k := range keys { - lazyTemplates = append(lazyTemplates, lazyTemplate{ - Name: k, - Loader: templates[k], - }) - } + paths := slices.Collect(maps.Keys(all)) + slices.Sort(paths) - o.templates, err = loadTemplates(lazyTemplates, false, funcs) - if err != nil { - return nil, fmt.Errorf("loading templates: %w", err) - } + for _, path := range paths { + details := all[path] + content, err := fs.ReadFile(details.fs, path) + if err != nil { + return fmt.Errorf("failed to read template: %s: %w", details.fullPath, err) + } - if !notests { - o.testTemplates, err = loadTemplates(lazyTemplates, true, funcs) + err = loadTemplate(tpl, funcs, path, string(content)) if err != nil { - return nil, fmt.Errorf("loading test templates: %w", err) + return fmt.Errorf("failed to load template: %s: %w", details.fullPath, err) } } - return lazyTemplates, nil + return nil } type executeTemplateData[T, C, I any] struct { output *Output data *TemplateData[T, C, I] - templates *templateList + templates *template.Template dirExtensions dirExtMap - - isTest bool } // generateOutput builds the file output and sends it to outHandler for saving -func generateOutput[T, C, I any](o *Output, dirExts dirExtMap, data *TemplateData[T, C, I], goVersion string) error { - return executeTemplates(executeTemplateData[T, C, I]{ +func generateOutput[T, C, I any](o *Output, dirExts dirExtMap, data *TemplateData[T, C, I], goVersion string, noTests bool) error { + if err := executeTemplates(executeTemplateData[T, C, I]{ output: o, data: data, - templates: o.templates, + templates: o.tableTemplates, dirExtensions: dirExts, - }, goVersion) -} + }, goVersion, false); err != nil { + return fmt.Errorf("execute templates: %w", err) + } -// generateTestOutput builds the test file output and sends it to outHandler for saving -func generateTestOutput[T, C, I any](o *Output, dirExts dirExtMap, data *TemplateData[T, C, I], goVersion string) error { - return executeTemplates(executeTemplateData[T, C, I]{ + if noTests { + return nil + } + + if err := executeTemplates(executeTemplateData[T, C, I]{ output: o, data: data, - templates: o.testTemplates, - isTest: true, + templates: o.tableTemplates, dirExtensions: dirExts, - }, goVersion) + }, goVersion, true); err != nil { + return fmt.Errorf("execute test templates: %w", err) + } + + return nil } // generateSingletonOutput processes the templates that should only be run // one time. -func generateSingletonOutput[T, C, I any](o *Output, data *TemplateData[T, C, I], goVersion string) error { - return executeSingletonTemplates(executeTemplateData[T, C, I]{ +func generateSingletonOutput[T, C, I any](o *Output, data *TemplateData[T, C, I], goVersion string, noTests bool) error { + if err := executeSingletonTemplates(executeTemplateData[T, C, I]{ output: o, data: data, - templates: o.templates, - }, goVersion) -} + templates: o.singletonTemplates, + }, goVersion, false); err != nil { + return fmt.Errorf("execute singleton templates: %w", err) + } -// generateSingletonTestOutput processes the templates that should only be run -// one time. -func generateSingletonTestOutput[T, C, I any](o *Output, data *TemplateData[T, C, I], goVersion string) error { - return executeSingletonTemplates(executeTemplateData[T, C, I]{ + if noTests { + return nil + } + + if err := executeSingletonTemplates(executeTemplateData[T, C, I]{ output: o, data: data, - templates: o.testTemplates, - isTest: true, - }, goVersion) + templates: o.singletonTemplates, + }, goVersion, true); err != nil { + return fmt.Errorf("execute singleton test templates: %w", err) + } + + return nil } -func executeTemplates[T, C, I any](e executeTemplateData[T, C, I], goVersion string) error { +func executeTemplates[T, C, I any](e executeTemplateData[T, C, I], goVersion string, tests bool) error { for dir, dirExts := range e.dirExtensions { for ext, tplNames := range dirExts { headerOut := e.output.templateHeaderByteBuffer @@ -265,15 +279,26 @@ func executeTemplates[T, C, I any](e executeTemplateData[T, C, I], goVersion str prevLen := out.Len() e.data.ResetImports() + + matchingTemplates := 0 for _, tplName := range tplNames { - if err := executeTemplate(out, e.templates.Template, tplName, e.data); err != nil { + if tests != strings.Contains(tplName, "_test.go") { + continue + } + matchingTemplates++ + + if err := executeTemplate(out, e.templates, tplName, e.data); err != nil { return err } } + if matchingTemplates == 0 { + continue + } + fName := getOutputFilename(e.data.Table.Schema, e.data.Table.Name, isGo) fName += ".bob" - if e.isTest { + if tests { fName += "_test" } @@ -300,7 +325,7 @@ func executeTemplates[T, C, I any](e executeTemplateData[T, C, I], goVersion str if len(dir) != 0 { pkgName = filepath.Base(dir) } - if e.isTest { + if tests { pkgName = fmt.Sprintf("%s_test", pkgName) } version = goVersion @@ -318,21 +343,26 @@ func executeTemplates[T, C, I any](e executeTemplateData[T, C, I], goVersion str return nil } -func executeSingletonTemplates[T, C, I any](e executeTemplateData[T, C, I], goVersion string) error { +func executeSingletonTemplates[T, C, I any](e executeTemplateData[T, C, I], goVersion string, tests bool) error { headerOut := e.output.templateHeaderByteBuffer out := e.output.templateByteBuffer - for _, tplName := range e.templates.Templates() { - normalized, isSingleton, isGo := outputFilenameParts(tplName) - if !isSingleton { + for _, tpl := range e.templates.Templates() { + if !strings.HasSuffix(tpl.Name(), ".tpl") { continue } + if tests != strings.Contains(tpl.Name(), "_test.go") { + continue + } + + normalized, isGo := outputFilenameParts(tpl.Name()) + headerOut.Reset() out.Reset() prevLen := out.Len() e.data.ResetImports() - if err := executeTemplate(out, e.templates.Template, tplName, e.data); err != nil { + if err := executeTemplate(out, e.templates, tpl.Name(), e.data); err != nil { return err } @@ -525,50 +555,34 @@ func stringSliceToMap(slice []string) map[string]struct{} { // templates_test/js/hello.js.tpl // //nolint:nonamedreturns -func outputFilenameParts(filename string) (normalized string, isSingleton, isGo bool) { +func outputFilenameParts(filename string) (normalized string, isGo bool) { fragments := strings.Split(filename, string(os.PathSeparator)) - isSingleton = len(fragments) > 1 && fragments[len(fragments)-2] == "singleton" - var remainingFragments []string - for _, f := range fragments { - if f != "singleton" { - remainingFragments = append(remainingFragments, f) - } - } - - newFilename := remainingFragments[len(remainingFragments)-1] + newFilename := fragments[len(fragments)-1] newFilename = strings.TrimSuffix(newFilename, ".tpl") newFilename = rgxRemoveNumberedPrefix.ReplaceAllString(newFilename, "") ext := filepath.Ext(newFilename) isGo = ext == ".go" - remainingFragments[len(remainingFragments)-1] = newFilename - normalized = strings.Join(remainingFragments, string(os.PathSeparator)) - - if isSingleton { - fNameWithoutExt := newFilename[:len(newFilename)-len(ext)] - if !strings.HasSuffix(fNameWithoutExt, ".bob") && - !strings.HasSuffix(fNameWithoutExt, ".bob_test") { - panic(fmt.Sprintf("singleton file name must end with .bob or .bob_test: %s", filename)) - } - } + fragments[len(fragments)-1] = newFilename + normalized = strings.Join(fragments, string(os.PathSeparator)) - return normalized, isSingleton, isGo + return normalized, isGo } type dirExtMap map[string]map[string][]string // groupTemplates takes templates and groups them according to their output directory // and file extension. -func groupTemplates(templates *templateList) dirExtMap { +func groupTemplates(templates *template.Template) dirExtMap { tplNames := templates.Templates() dirs := make(map[string]map[string][]string) - for _, tplName := range tplNames { - normalized, isSingleton, _ := outputFilenameParts(tplName) - if isSingleton { + for _, tpl := range tplNames { + if !strings.HasSuffix(tpl.Name(), ".tpl") { continue } + normalized, _ := outputFilenameParts(tpl.Name()) dir := filepath.Dir(normalized) if dir == "." { dir = "" @@ -580,19 +594,17 @@ func groupTemplates(templates *templateList) dirExtMap { dirs[dir] = extensions } - ext := getLongExt(tplName) + ext := getLongExt(tpl.Name()) ext = strings.TrimSuffix(ext, ".tpl") slice := extensions[ext] - extensions[ext] = append(slice, tplName) + extensions[ext] = append(slice, tpl.Name()) } - return dirs -} + for _, exts := range dirs { + for _, tplNames := range exts { + slices.Sort(tplNames) + } + } -// normalizeSlashes takes a path that was made on linux or windows and converts it -// to a native path. -func normalizeSlashes(path string) string { - path = strings.ReplaceAll(path, `/`, string(os.PathSeparator)) - path = strings.ReplaceAll(path, `\`, string(os.PathSeparator)) - return path + return dirs } diff --git a/gen/output_test.go b/gen/output_test.go index ad14f843..8844d426 100644 --- a/gen/output_test.go +++ b/gen/output_test.go @@ -63,26 +63,20 @@ func TestOutputFilenameParts(t *testing.T) { tests := []struct { Filename string - Normalized string - IsSingleton bool - IsGo bool + Normalized string + IsGo bool }{ - {"00_struct.go.tpl", "struct.go", false, true}, - {"singleton/00_struct.bob.go.tpl", "struct.bob.go", true, true}, - {"notpkg/00_struct.go.tpl", "notpkg/struct.go", false, true}, - {"js/singleton/00_struct.bob.js.tpl", "js/struct.bob.js", true, false}, - {"js/00_struct.js.tpl", "js/struct.js", false, false}, + {"00_struct.go.tpl", "struct.go", true}, + {"notpkg/00_struct.go.tpl", "notpkg/struct.go", true}, + {"js/00_struct.js.tpl", "js/struct.js", false}, } for i, test := range tests { - normalized, isSingleton, isGo := outputFilenameParts(test.Filename) + normalized, isGo := outputFilenameParts(test.Filename) if normalized != test.Normalized { t.Errorf("%d) normalized wrong, want: %s, got: %s", i, test.Normalized, normalized) } - if isSingleton != test.IsSingleton { - t.Errorf("%d) isSingleton wrong, want: %t, got: %t", i, test.IsSingleton, isSingleton) - } if isGo != test.IsGo { t.Errorf("%d) isGo wrong, want: %t, got: %t", i, test.IsGo, isGo) } diff --git a/gen/templates.go b/gen/templates.go index 753eb961..f2d1eb46 100644 --- a/gen/templates.go +++ b/gen/templates.go @@ -2,10 +2,8 @@ package gen import ( "embed" - "encoding" "fmt" "io/fs" - "sort" "strings" "text/template" "unicode" @@ -121,102 +119,17 @@ func (t *TemplateData[T, C, I]) ResetImports() { t.Importer = make(Importer) } -type templateList struct { - *template.Template -} - -type templateNameList []string - -func (t templateNameList) Len() int { - return len(t) -} - -func (t templateNameList) Swap(k, j int) { - t[k], t[j] = t[j], t[k] -} - -func (t templateNameList) Less(k, j int) bool { - // Make sure "struct" goes to the front - if t[k] == "struct.tpl" { - return true - } - - res := strings.Compare(t[k], t[j]) - return res <= 0 -} - -// Templates returns the name of all the templates defined in the template list -func (t templateList) Templates() []string { - tplList := t.Template.Templates() - - if len(tplList) == 0 { - return nil - } - - ret := make([]string, 0, len(tplList)) - for _, tpl := range tplList { - if name := tpl.Name(); strings.HasSuffix(name, ".tpl") { - ret = append(ret, name) - } - } - - sort.Sort(templateNameList(ret)) - - return ret -} - -func loadTemplates(lazyTemplates []lazyTemplate, testTemplates bool, customFuncs template.FuncMap) (*templateList, error) { - tpl := template.New("") - - for _, t := range lazyTemplates { - isTest := strings.Contains(t.Name, "_test.go") - if testTemplates && !isTest || !testTemplates && isTest { - continue - } - - byt, err := t.Loader.Load() - if err != nil { - return nil, fmt.Errorf("failed to load template: %s: %w", t.Name, err) - } - - _, err = tpl.New(t.Name). - Funcs(sprig.GenericFuncMap()). - Funcs(templateFunctions). - Funcs(customFuncs). - Parse(string(byt)) - if err != nil { - return nil, fmt.Errorf("failed to parse template: %s: %w", t.Name, err) - } +func loadTemplate(tpl *template.Template, customFuncs template.FuncMap, name, content string) error { + _, err := tpl.New(name). + Funcs(sprig.GenericFuncMap()). + Funcs(templateFunctions). + Funcs(customFuncs). + Parse(content) + if err != nil { + return fmt.Errorf("failed to parse template: %s: %w", name, err) } - return &templateList{Template: tpl}, nil -} - -type lazyTemplate struct { - Name string `json:"name"` - Loader templateLoader `json:"loader"` -} - -type templateLoader interface { - encoding.TextMarshaler - Load() ([]byte, error) -} - -type assetLoader struct { - fs fs.FS - name string -} - -func (a assetLoader) Load() ([]byte, error) { - return fs.ReadFile(a.fs, string(a.name)) -} - -func (a assetLoader) MarshalText() ([]byte, error) { - return []byte(a.String()), nil -} - -func (a assetLoader) String() string { - return "asset:" + string(a.name) + return nil } // templateFunctions is a map of some helper functions that get passed into the diff --git a/gen/templates/factory/singleton/bobfactory_context.bob.go.tpl b/gen/templates/factory/bobfactory_context.bob.go.tpl similarity index 100% rename from gen/templates/factory/singleton/bobfactory_context.bob.go.tpl rename to gen/templates/factory/bobfactory_context.bob.go.tpl diff --git a/gen/templates/factory/singleton/bobfactory_enums.bob.go.tpl b/gen/templates/factory/bobfactory_enums.bob.go.tpl similarity index 100% rename from gen/templates/factory/singleton/bobfactory_enums.bob.go.tpl rename to gen/templates/factory/bobfactory_enums.bob.go.tpl diff --git a/gen/templates/factory/singleton/bobfactory_main.bob.go.tpl b/gen/templates/factory/bobfactory_main.bob.go.tpl similarity index 100% rename from gen/templates/factory/singleton/bobfactory_main.bob.go.tpl rename to gen/templates/factory/bobfactory_main.bob.go.tpl diff --git a/gen/templates/factory/singleton/bobfactory_random.bob.go.tpl b/gen/templates/factory/bobfactory_random.bob.go.tpl similarity index 100% rename from gen/templates/factory/singleton/bobfactory_random.bob.go.tpl rename to gen/templates/factory/bobfactory_random.bob.go.tpl diff --git a/gen/templates/factory/singleton/bobfactory_random_test.bob.go.tpl b/gen/templates/factory/bobfactory_random_test.bob.go.tpl similarity index 100% rename from gen/templates/factory/singleton/bobfactory_random_test.bob.go.tpl rename to gen/templates/factory/bobfactory_random_test.bob.go.tpl diff --git a/gen/templates/factory/01_types.go.tpl b/gen/templates/factory/table/01_types.go.tpl similarity index 100% rename from gen/templates/factory/01_types.go.tpl rename to gen/templates/factory/table/01_types.go.tpl diff --git a/gen/templates/factory/02_build.go.tpl b/gen/templates/factory/table/02_build.go.tpl similarity index 100% rename from gen/templates/factory/02_build.go.tpl rename to gen/templates/factory/table/02_build.go.tpl diff --git a/gen/templates/factory/03_create.go.tpl b/gen/templates/factory/table/03_create.go.tpl similarity index 100% rename from gen/templates/factory/03_create.go.tpl rename to gen/templates/factory/table/03_create.go.tpl diff --git a/gen/templates/factory/11_column_mods.go.tpl b/gen/templates/factory/table/11_column_mods.go.tpl similarity index 100% rename from gen/templates/factory/11_column_mods.go.tpl rename to gen/templates/factory/table/11_column_mods.go.tpl diff --git a/gen/templates/factory/12_rel_to_one_mods.go.tpl b/gen/templates/factory/table/12_rel_to_one_mods.go.tpl similarity index 100% rename from gen/templates/factory/12_rel_to_one_mods.go.tpl rename to gen/templates/factory/table/12_rel_to_one_mods.go.tpl diff --git a/gen/templates/factory/13_rel_to_many_mods.go.tpl b/gen/templates/factory/table/13_rel_to_many_mods.go.tpl similarity index 100% rename from gen/templates/factory/13_rel_to_many_mods.go.tpl rename to gen/templates/factory/table/13_rel_to_many_mods.go.tpl diff --git a/gen/templates/models/singleton/bob_enums.bob.go.tpl b/gen/templates/models/bob_enums.bob.go.tpl similarity index 100% rename from gen/templates/models/singleton/bob_enums.bob.go.tpl rename to gen/templates/models/bob_enums.bob.go.tpl diff --git a/gen/templates/models/singleton/bob_main.bob.go.tpl b/gen/templates/models/bob_main.bob.go.tpl similarity index 100% rename from gen/templates/models/singleton/bob_main.bob.go.tpl rename to gen/templates/models/bob_main.bob.go.tpl diff --git a/gen/templates/models/singleton/bob_main_test.bob.go.tpl b/gen/templates/models/bob_main_test.bob.go.tpl similarity index 100% rename from gen/templates/models/singleton/bob_main_test.bob.go.tpl rename to gen/templates/models/bob_main_test.bob.go.tpl diff --git a/gen/templates/models/01_types.go.tpl b/gen/templates/models/table/01_types.go.tpl similarity index 100% rename from gen/templates/models/01_types.go.tpl rename to gen/templates/models/table/01_types.go.tpl diff --git a/gen/templates/models/01_types_test.go.tpl b/gen/templates/models/table/01_types_test.go.tpl similarity index 100% rename from gen/templates/models/01_types_test.go.tpl rename to gen/templates/models/table/01_types_test.go.tpl diff --git a/gen/templates/models/02_setter.go.tpl b/gen/templates/models/table/02_setter.go.tpl similarity index 100% rename from gen/templates/models/02_setter.go.tpl rename to gen/templates/models/table/02_setter.go.tpl diff --git a/gen/templates/models/04_query.go.tpl b/gen/templates/models/table/04_query.go.tpl similarity index 100% rename from gen/templates/models/04_query.go.tpl rename to gen/templates/models/table/04_query.go.tpl diff --git a/gen/templates/models/05_one_methods.go.tpl b/gen/templates/models/table/05_one_methods.go.tpl similarity index 100% rename from gen/templates/models/05_one_methods.go.tpl rename to gen/templates/models/table/05_one_methods.go.tpl diff --git a/gen/templates/models/07_slice_methods.go.tpl b/gen/templates/models/table/07_slice_methods.go.tpl similarity index 100% rename from gen/templates/models/07_slice_methods.go.tpl rename to gen/templates/models/table/07_slice_methods.go.tpl diff --git a/gen/templates/models/09_rel_query.go.tpl b/gen/templates/models/table/09_rel_query.go.tpl similarity index 100% rename from gen/templates/models/09_rel_query.go.tpl rename to gen/templates/models/table/09_rel_query.go.tpl diff --git a/gen/templates/models/10_rel_load.go.tpl b/gen/templates/models/table/10_rel_load.go.tpl similarity index 100% rename from gen/templates/models/10_rel_load.go.tpl rename to gen/templates/models/table/10_rel_load.go.tpl diff --git a/gen/templates/models/11_rel_ops.go.tpl b/gen/templates/models/table/11_rel_ops.go.tpl similarity index 100% rename from gen/templates/models/11_rel_ops.go.tpl rename to gen/templates/models/table/11_rel_ops.go.tpl diff --git a/gen/templates_test.go b/gen/templates_test.go deleted file mode 100644 index 27736fbf..00000000 --- a/gen/templates_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package gen - -import ( - "sort" - "testing" - "text/template" -) - -func TestTemplateNameListSort(t *testing.T) { - t.Parallel() - - templs := templateNameList{ - "bob.tpl", - "all.tpl", - "struct.tpl", - "ttt.tpl", - } - - expected := []string{"bob.tpl", "all.tpl", "struct.tpl", "ttt.tpl"} - - for i, v := range templs { - if v != expected[i] { - t.Errorf("Order mismatch, expected: %s, got: %s", expected[i], v) - } - } - - expected = []string{"struct.tpl", "all.tpl", "bob.tpl", "ttt.tpl"} - - sort.Sort(templs) - - for i, v := range templs { - if v != expected[i] { - t.Errorf("Order mismatch, expected: %s, got: %s", expected[i], v) - } - } -} - -func TestTemplateList_Templates(t *testing.T) { - t.Parallel() - - tpl := template.New("") - tpl.New("wat.tpl").Parse("hello") - tpl.New("que.tpl").Parse("there") - tpl.New("not").Parse("hello") - - tplList := templateList{tpl} - foundWat, foundQue, foundNot := false, false, false - for _, n := range tplList.Templates() { - switch n { - case "wat.tpl": - foundWat = true - case "que.tpl": - foundQue = true - case "not": - foundNot = true - } - } - - if !foundWat { - t.Error("want wat") - } - if !foundQue { - t.Error("want que") - } - if foundNot { - t.Error("don't want not") - } -}