Files
gormcol/cmd/main.go
T
2026-03-29 14:36:57 +02:00

183 lines
5.2 KiB
Go

package main
import (
"context"
"flag"
"fmt"
"log/slog"
"os"
"os/exec"
"strings"
"time"
"git.ma-al.com/goc_marek/gormcol"
"github.com/joho/godotenv"
"gorm.io/gorm"
)
// main is the entry point for the gormcol-gen CLI tool.
// It parses flags, loads configuration from .env, connects to the database,
// and generates GORM models with column descriptors.
func main() {
dsn := flag.String("dsn", "", "database DSN (e.g. user:pass@tcp(host:3306)/dbname)")
filter := flag.String("filter", "(ps_|b2b_).*", "regex to match table names")
all := flag.Bool("all", false, "generate all tables matching filter (shows confirmation)")
outDir := flag.String("out", "./app/model/dbmodel", "output directory for generated files")
pkgName := flag.String("pkg", "dbmodel", "Go package name for generated files")
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "gormcol-gen - generate GORM models with column descriptors\n\n")
fmt.Fprintf(os.Stderr, "Usage:\n")
fmt.Fprintf(os.Stderr, " gormcol-gen [options]\n\n")
fmt.Fprintf(os.Stderr, "DSN can be provided via --dsn flag or DSN env var (from .env)\n\n")
fmt.Fprintf(os.Stderr, "Flags:\n")
flag.PrintDefaults()
}
// Load .env file if present in current directory.
godotenv.Load()
flag.Parse()
// Get DSN from flag or environment variable.
dsnValue := *dsn
if dsnValue == "" {
dsnValue = os.Getenv("DSN")
}
if dsnValue == "" {
flag.Usage()
fmt.Fprintln(os.Stderr, "\nerror: --dsn or DSN env is required")
os.Exit(1)
}
// Connect to the database.
db, err := gormcol.ConnectDSN(dsnValue)
if err != nil {
slog.Error("failed to connect to database", "error", err)
os.Exit(1)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
cfg := gormcol.GenConfig{
OutputDir: *outDir,
PkgName: *pkgName,
TableFilter: *filter,
}
// Interactive mode (default): select tables using fzf.
// All mode: generate all tables matching filter with confirmation.
if *all {
if !confirmGenerateAll(db) {
fmt.Println("Aborted.")
os.Exit(0)
}
} else {
// Check if fzf is available for interactive mode.
if _, err := exec.LookPath("fzf"); err != nil {
fmt.Fprintln(os.Stderr, "error: fzf is required for interactive mode. Install from https://github.com/junegunn/fzf")
os.Exit(1)
}
selected, err := selectTablesInteractive(db)
if err != nil {
slog.Error("failed to select tables interactively", "error", err)
os.Exit(1)
}
if len(selected) == 0 {
fmt.Println("No tables selected, exiting.")
os.Exit(0)
}
cfg.SelectedTables = selected
}
// Generate models with column descriptors.
if err := gormcol.NewWithConfig(db, cfg).GenModels(ctx); err != nil {
slog.Error("failed to generate models", "error", err)
os.Exit(1)
}
}
// selectTablesInteractive displays all database tables in fzf for interactive selection.
// Returns a list of selected table names.
func selectTablesInteractive(db *gorm.DB) ([]string, error) {
type migratorWithGetTables interface {
GetTables() ([]string, error)
}
tableNames, err := db.Migrator().(migratorWithGetTables).GetTables()
if err != nil {
return nil, fmt.Errorf("failed to get tables: %w", err)
}
fmt.Printf("Found %d tables, launching fzf for selection...\n", len(tableNames))
selected, err := runFzf(tableNames)
if err != nil {
return nil, fmt.Errorf("fzf selection failed: %w", err)
}
return selected, nil
}
// confirmGenerateAll displays a confirmation prompt before generating all tables.
// Returns true if user confirms, false if cancelled or timeout.
func confirmGenerateAll(db *gorm.DB) bool {
type migratorWithGetTables interface {
GetTables() ([]string, error)
}
tableNames, err := db.Migrator().(migratorWithGetTables).GetTables()
if err != nil {
return false
}
if len(tableNames) == 0 {
fmt.Println("No tables in database.")
return false
}
// Display yellow warning message and wait for user input.
yellow := "\033[33m"
reset := "\033[0m"
msg := fmt.Sprintf("%sWARNING:%s Generate all %d tables? [Enter] confirm / [Esc] cancel", yellow, reset, len(tableNames))
script := fmt.Sprintf(`echo; echo -e '%s'; read -t 10 -n 1 -s key; if [[ -z "$key" ]]; then exit 0; elif [[ "$key" == $'\e' ]]; then exit 1; else exit 0; fi`, msg)
cmd := exec.Command("bash", "-c", script)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
return err == nil
}
// runFzf launches fzf for interactive table selection.
// Supports multi-select with Tab, fuzzy search, and keyboard navigation.
func runFzf(tableNames []string) ([]string, error) {
header := "Tab: toggle select | Enter: confirm | Esc: cancel"
cmd := exec.Command("fzf", "-m", "--height=50%", "--border", "--prompt", "Select tables> ", "--header", header)
cmd.Stdin = strings.NewReader(strings.Join(tableNames, "\n"))
out, err := cmd.Output()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
if exitErr.ExitCode() == 130 {
// User pressed Esc to cancel.
return []string{}, nil
}
}
return nil, fmt.Errorf("fzf failed: %w", err)
}
// Parse selected lines from fzf output.
selected := strings.Split(strings.TrimSpace(string(out)), "\n")
var result []string
for _, s := range selected {
s = strings.TrimSpace(s)
if s != "" {
result = append(result, s)
}
}
return result, nil
}