From dc9172cc7c31b496f27157fbbbb79b0666ee5419 Mon Sep 17 00:00:00 2001 From: Hein Date: Sat, 28 Feb 2026 19:32:19 +0200 Subject: [PATCH] feat(templ): add support for --from-list flag and related tests --- cmd/relspec/templ.go | 17 +++- cmd/relspec/templ_from_list_test.go | 134 ++++++++++++++++++++++++++++ cmd/relspec/testhelpers_test.go | 39 ++++++++ 3 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 cmd/relspec/templ_from_list_test.go diff --git a/cmd/relspec/templ.go b/cmd/relspec/templ.go index 3697b37..b2ad631 100644 --- a/cmd/relspec/templ.go +++ b/cmd/relspec/templ.go @@ -15,6 +15,7 @@ var ( templSourceType string templSourcePath string templSourceConn string + templFromList []string templTemplatePath string templOutputPath string templSchemaFilter string @@ -78,8 +79,9 @@ Examples: func init() { templCmd.Flags().StringVar(&templSourceType, "from", "", "Source format (dbml, pgsql, json, etc.)") - templCmd.Flags().StringVar(&templSourcePath, "from-path", "", "Source file path (for file-based sources)") + templCmd.Flags().StringVar(&templSourcePath, "from-path", "", "Source file path (for file-based sources, mutually exclusive with --from-list)") templCmd.Flags().StringVar(&templSourceConn, "from-conn", "", "Source connection string (for database sources)") + templCmd.Flags().StringSliceVar(&templFromList, "from-list", nil, "Comma-separated list of source file paths to read and merge (mutually exclusive with --from-path)") templCmd.Flags().StringVar(&templTemplatePath, "template", "", "Template file path (required)") templCmd.Flags().StringVar(&templOutputPath, "output", "", "Output path (file or directory, empty for stdout)") templCmd.Flags().StringVar(&templSchemaFilter, "schema", "", "Filter to specific schema") @@ -95,9 +97,20 @@ func runTempl(cmd *cobra.Command, args []string) error { fmt.Fprintf(os.Stderr, "=== RelSpec Template Execution ===\n") fmt.Fprintf(os.Stderr, "Started at: %s\n\n", getCurrentTimestamp()) + // Validate mutually exclusive flags + if templSourcePath != "" && len(templFromList) > 0 { + return fmt.Errorf("--from-path and --from-list are mutually exclusive") + } + // Read database using the same function as convert fmt.Fprintf(os.Stderr, "Reading from %s...\n", templSourceType) - db, err := readDatabaseForConvert(templSourceType, templSourcePath, templSourceConn) + var db *models.Database + var err error + if len(templFromList) > 0 { + db, err = readDatabaseListForConvert(templSourceType, templFromList) + } else { + db, err = readDatabaseForConvert(templSourceType, templSourcePath, templSourceConn) + } if err != nil { return fmt.Errorf("failed to read source: %w", err) } diff --git a/cmd/relspec/templ_from_list_test.go b/cmd/relspec/templ_from_list_test.go new file mode 100644 index 0000000..d01754e --- /dev/null +++ b/cmd/relspec/templ_from_list_test.go @@ -0,0 +1,134 @@ +package main + +import ( + "os" + "path/filepath" + "testing" +) + +// writeTestTemplate writes a minimal Go text template file. +func writeTestTemplate(t *testing.T, path string) { + t.Helper() + content := []byte(`{{.Name}}`) + if err := os.WriteFile(path, content, 0644); err != nil { + t.Fatalf("failed to write template file %s: %v", path, err) + } +} + +func TestRunTempl_FromListMutuallyExclusiveWithFromPath(t *testing.T) { + saved := saveTemplState() + defer restoreTemplState(saved) + + dir := t.TempDir() + file := filepath.Join(dir, "schema.json") + tmpl := filepath.Join(dir, "tmpl.tmpl") + writeTestJSON(t, file, []string{"users"}) + writeTestTemplate(t, tmpl) + + templSourceType = "json" + templSourcePath = file + templFromList = []string{file} + templTemplatePath = tmpl + templOutputPath = "" + templMode = "database" + templFilenamePattern = "{{.Name}}.txt" + + err := runTempl(nil, nil) + if err == nil { + t.Error("expected error when --from-path and --from-list are both set") + } +} + +func TestRunTempl_FromListSingleFile(t *testing.T) { + saved := saveTemplState() + defer restoreTemplState(saved) + + dir := t.TempDir() + file := filepath.Join(dir, "schema.json") + tmpl := filepath.Join(dir, "tmpl.tmpl") + outFile := filepath.Join(dir, "output.txt") + writeTestJSON(t, file, []string{"users"}) + writeTestTemplate(t, tmpl) + + templSourceType = "json" + templSourcePath = "" + templSourceConn = "" + templFromList = []string{file} + templTemplatePath = tmpl + templOutputPath = outFile + templSchemaFilter = "" + templMode = "database" + templFilenamePattern = "{{.Name}}.txt" + + if err := runTempl(nil, nil); err != nil { + t.Fatalf("runTempl() error = %v", err) + } + if _, err := os.Stat(outFile); os.IsNotExist(err) { + t.Error("expected output file to be created") + } +} + +func TestRunTempl_FromListMultipleFiles(t *testing.T) { + saved := saveTemplState() + defer restoreTemplState(saved) + + dir := t.TempDir() + file1 := filepath.Join(dir, "users.json") + file2 := filepath.Join(dir, "posts.json") + tmpl := filepath.Join(dir, "tmpl.tmpl") + outFile := filepath.Join(dir, "output.txt") + writeTestJSON(t, file1, []string{"users"}) + writeTestJSON(t, file2, []string{"posts"}) + writeTestTemplate(t, tmpl) + + templSourceType = "json" + templSourcePath = "" + templSourceConn = "" + templFromList = []string{file1, file2} + templTemplatePath = tmpl + templOutputPath = outFile + templSchemaFilter = "" + templMode = "database" + templFilenamePattern = "{{.Name}}.txt" + + if err := runTempl(nil, nil); err != nil { + t.Fatalf("runTempl() error = %v", err) + } + if _, err := os.Stat(outFile); os.IsNotExist(err) { + t.Error("expected output file to be created") + } +} + +func TestRunTempl_FromListPathWithSpaces(t *testing.T) { + saved := saveTemplState() + defer restoreTemplState(saved) + + spacedDir := filepath.Join(t.TempDir(), "my schema files") + if err := os.MkdirAll(spacedDir, 0755); err != nil { + t.Fatal(err) + } + file1 := filepath.Join(spacedDir, "users schema.json") + file2 := filepath.Join(spacedDir, "posts schema.json") + tmpl := filepath.Join(spacedDir, "my template.tmpl") + outFile := filepath.Join(spacedDir, "output file.txt") + writeTestJSON(t, file1, []string{"users"}) + writeTestJSON(t, file2, []string{"posts"}) + writeTestTemplate(t, tmpl) + + templSourceType = "json" + templSourcePath = "" + templSourceConn = "" + templFromList = []string{file1, file2} + templTemplatePath = tmpl + templOutputPath = outFile + templSchemaFilter = "" + templMode = "database" + templFilenamePattern = "{{.Name}}.txt" + + if err := runTempl(nil, nil); err != nil { + t.Fatalf("runTempl() with spaced paths error = %v", err) + } + if _, err := os.Stat(outFile); os.IsNotExist(err) { + t.Error("expected output file to be created") + } +} diff --git a/cmd/relspec/testhelpers_test.go b/cmd/relspec/testhelpers_test.go index 5916e3b..fa79bee 100644 --- a/cmd/relspec/testhelpers_test.go +++ b/cmd/relspec/testhelpers_test.go @@ -110,6 +110,45 @@ func restoreConvertState(s convertState) { convertFlattenSchema = s.flattenSchema } +// templState captures and restores all templ global vars. +type templState struct { + sourceType string + sourcePath string + sourceConn string + fromList []string + templatePath string + outputPath string + schemaFilter string + mode string + filenamePattern string +} + +func saveTemplState() templState { + return templState{ + sourceType: templSourceType, + sourcePath: templSourcePath, + sourceConn: templSourceConn, + fromList: templFromList, + templatePath: templTemplatePath, + outputPath: templOutputPath, + schemaFilter: templSchemaFilter, + mode: templMode, + filenamePattern: templFilenamePattern, + } +} + +func restoreTemplState(s templState) { + templSourceType = s.sourceType + templSourcePath = s.sourcePath + templSourceConn = s.sourceConn + templFromList = s.fromList + templTemplatePath = s.templatePath + templOutputPath = s.outputPath + templSchemaFilter = s.schemaFilter + templMode = s.mode + templFilenamePattern = s.filenamePattern +} + // mergeState captures and restores all merge global vars. type mergeState struct { targetType string