From 9177618abfa194f45a3ff756a92c1153f5cd44ee Mon Sep 17 00:00:00 2001 From: DivineUX23 Date: Fri, 11 Jul 2025 18:44:21 +0100 Subject: [PATCH 1/2] feat: Add register command for auto-generating model registry --- .gitignore | 1 + README.md | 14 ++- cmd/gorm-schema/main.go | 5 +- example/models/estate.go | 24 ++++ example/models/models_registry.go | 14 +-- example/user-project/cmd/migration/main.go | 4 +- migration/commands/register.go | 38 ++++++ migration/commands/utils.go | 129 +++++++++++++++++++++ 8 files changed, 216 insertions(+), 13 deletions(-) create mode 100644 migration/commands/register.go diff --git a/.gitignore b/.gitignore index 41c5fb2..5711014 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ main.go.txt ast_dump.go.txt .zed gorm-schema +gorm-schema.exe test.log \ No newline at end of file diff --git a/README.md b/README.md index 5ad9e5f..3b8cd09 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ func main() { } rootCmd.AddCommand( + commands.RegisterCmd(), commands.InitCmd(), commands.CreateCmd(), commands.GenerateCmd(), @@ -78,9 +79,17 @@ func main() { } ``` -### 4. Create your model registry -Create `models/models_registry.go` in your project: +### 4. Generate your model registry + +Use the register command to automatically scan your models directory (e.g., models/) and generate a models_registry.go file. + + +```bash +go run cmd/migration/main.go register +``` + +This command creates a standard Go file that you can review and even edit if needed. It will look something like this: ```go package models @@ -97,6 +106,7 @@ var ModelTypeRegistry = map[string]interface{}{ ```bash go run cmd/migration/main.go init +go run cmd/migration/main.go register [path/to/models] go run cmd/migration/main.go generate init_db go run cmd/migration/main.go up ``` diff --git a/cmd/gorm-schema/main.go b/cmd/gorm-schema/main.go index 54dc371..7d08cbc 100644 --- a/cmd/gorm-schema/main.go +++ b/cmd/gorm-schema/main.go @@ -7,9 +7,9 @@ import ( "github.com/joho/godotenv" "github.com/spf13/cobra" - "github.com/beesaferoot/gorm-schema/migration/commands" - "github.com/beesaferoot/gorm-schema/migration" "github.com/beesaferoot/gorm-schema/example/models" + "github.com/beesaferoot/gorm-schema/migration" + "github.com/beesaferoot/gorm-schema/migration/commands" ) type MyModelRegistry struct{} @@ -31,6 +31,7 @@ func main() { } rootCmd.AddCommand( + commands.RegisterCmd(), commands.InitCmd(), commands.CreateCmd(), commands.GenerateCmd(), diff --git a/example/models/estate.go b/example/models/estate.go index 5c1eea6..62f2f30 100644 --- a/example/models/estate.go +++ b/example/models/estate.go @@ -14,3 +14,27 @@ type Estate struct { UserManagerID uint UserManager *User `gorm:"foreignKey:UserManagerID"` } + +type Estate1 struct { + gorm.Model + Name string + Address string + City string + State string + Country string + IsDeleted bool + UserManagerID uint + UserManager *User `gorm:"foreignKey:UserManagerID"` +} + +type Estate2 struct { + gorm.Model + Name string + Address string + City string + State string + Country string + IsDeleted bool + UserManagerID uint + UserManager *User `gorm:"foreignKey:UserManagerID"` +} diff --git a/example/models/models_registry.go b/example/models/models_registry.go index a56d3d0..47b6740 100644 --- a/example/models/models_registry.go +++ b/example/models/models_registry.go @@ -1,11 +1,11 @@ package models var ModelTypeRegistry = map[string]interface{}{ - "Apartment": Apartment{}, + "Apartment": Apartment{}, "ApartmentBookingPrice": ApartmentBookingPrice{}, - "ApartmentContract": ApartmentContract{}, - "ApartmentHighlight": ApartmentHighlight{}, - "Estate": Estate{}, - "Tenant": Tenant{}, - "User": User{}, -} + "ApartmentContract": ApartmentContract{}, + "ApartmentHighlight": ApartmentHighlight{}, + "Estate": Estate{}, + "Tenant": Tenant{}, + "User": User{}, +} diff --git a/example/user-project/cmd/migration/main.go b/example/user-project/cmd/migration/main.go index 9627562..e737844 100644 --- a/example/user-project/cmd/migration/main.go +++ b/example/user-project/cmd/migration/main.go @@ -1,13 +1,12 @@ package main import ( - "github.com/beesaferoot/gorm-schema/example/user-project/models" // User's models package - CHANGE THIS "github.com/beesaferoot/gorm-schema/migration" "github.com/beesaferoot/gorm-schema/migration/commands" - "github.com/spf13/cobra" "github.com/joho/godotenv" + "github.com/spf13/cobra" ) // Simple registry implementation @@ -29,6 +28,7 @@ func main() { } rootCmd.AddCommand( + commands.RegisterCmd(), commands.InitCmd(), commands.CreateCmd(), commands.GenerateCmd(), diff --git a/migration/commands/register.go b/migration/commands/register.go new file mode 100644 index 0000000..de54b7f --- /dev/null +++ b/migration/commands/register.go @@ -0,0 +1,38 @@ +package commands + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +func RegisterCmd() *cobra.Command { + return &cobra.Command{ + Use: "register [path]", + Short: "Generates model registry file", + Long: `Scans the given path for Go files containing GORM models (structs embedding gorm.Model) and generates a models_registry.go file. If no path is provided, it defaults to the 'models' directory.`, + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + + var pathToValidate string + if len(args) > 0 { + pathToValidate = args[0] + } + + validatedPath, err := validateModelPath(pathToValidate) + if err != nil { + return fmt.Errorf("failed to validate model path: %w", err) + } + + //create model_register.go file: + ModelRegistry, err := createModelRegisterFile(validatedPath) + if err != nil { + return fmt.Errorf("failed to create model registry file: %w", err) + } + + fmt.Printf("Successfully generated model registry: %s\n", ModelRegistry) + return nil + + }, + } +} diff --git a/migration/commands/utils.go b/migration/commands/utils.go index 422aa13..d0a4b8f 100644 --- a/migration/commands/utils.go +++ b/migration/commands/utils.go @@ -6,6 +6,10 @@ import ( "path/filepath" "strings" + "go/ast" + "go/parser" + "go/token" + "gorm.io/driver/postgres" "gorm.io/gorm" @@ -67,3 +71,128 @@ func getMigrationLoader() (*file.MigrationLoader, error) { } return file.NewMigrationLoader(getMigrationsDir(), template), nil } + +// validate models path +func validateModelPath(path string) (string, error) { + if path == "" { + path = "models" + } + + cleanpath := filepath.Clean(path) + + absPath, err := filepath.Abs(cleanpath) + if err != nil { + return "", fmt.Errorf("invalid model path: %w", err) + } + + wd, err := os.Getwd() + if err != nil { + return "", fmt.Errorf("failed to get working directory: %w", err) + } + + if !strings.HasPrefix(absPath, wd) { + return "", fmt.Errorf("model path must be within working directory") + } + + return absPath, nil +} + +// Create model registry +func createModelRegisterFile(dirPath string) (string, error) { + filePath := filepath.Join(dirPath, "models_registry.go") + + packageName := filepath.Base(dirPath) + allModels, err := getModels(dirPath) + + if err != nil { + return "", err + } + + content := fmt.Sprintf(`package %s + +var ModelTypeRegistry = map[string]interface{}{ + %s +} `, packageName, allModels) + + if err := os.WriteFile(filePath, []byte(content), 0644); err != nil { + return "", fmt.Errorf("failed to create model registry file: %w", err) + } + + //defer file.Close() + + return filePath, nil +} + +func getModels(dirPath string) (string, error) { + var allModels []string + + files, err := os.ReadDir(dirPath) + if err != nil { + return "", fmt.Errorf("failed to read directory: %w", err) + } + + for _, file := range files { + if file.IsDir() || !strings.HasSuffix(file.Name(), ".go") || file.Name() == "models_registry.go" { + continue + } + filePath := filepath.Join(dirPath, file.Name()) + modelNames, err := modelPerser(filePath) + + if err != nil { + fmt.Printf("Warning: could not parse models from %s: %v\n", file.Name(), err) + continue + } + allModels = append(allModels, modelNames...) + } + + var contentBuilder strings.Builder + for _, name := range allModels { + contentBuilder.WriteString(fmt.Sprintf("\t\"%s\": %s{},\n", name, name)) + } + return contentBuilder.String(), nil +} + +func modelPerser(file string) ([]string, error) { + var modelNames []string + + fset := token.NewFileSet() + + node, err := parser.ParseFile(fset, file, nil, 0) + if err != nil { + return nil, fmt.Errorf("failed to parse file: %w", err) + } + + ast.Inspect(node, func(n ast.Node) bool { + genDecl, ok := n.(*ast.GenDecl) + + if !ok || genDecl.Tok != token.TYPE { + return true + } + + for _, spec := range genDecl.Specs { + typeSpec, ok := spec.(*ast.TypeSpec) + + if !ok { + continue + } + + structType, ok := typeSpec.Type.(*ast.StructType) + if !ok { + continue + } + + for _, field := range structType.Fields.List { + if len(field.Names) == 0 { + if selfExpr, ok := field.Type.(*ast.SelectorExpr); ok { + if indent, ok := selfExpr.X.(*ast.Ident); ok && indent.Name == "gorm" && selfExpr.Sel.Name == "Model" { + modelNames = append(modelNames, typeSpec.Name.Name) + break + } + } + } + } + } + return true + }) + return modelNames, nil +} From ca00e18fcd70417578be313c5b4222e6d7c62564 Mon Sep 17 00:00:00 2001 From: DivineUX23 Date: Fri, 11 Jul 2025 21:32:26 +0100 Subject: [PATCH 2/2] Fix: remove uncessary code & comment, updated README.md and implement registry test --- README.md | 2 +- example/models/estate.go | 24 ------------------------ migration/commands/utils.go | 4 ---- tests/migration/commands_test.go | 7 +++++++ 4 files changed, 8 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 3b8cd09..9a059cb 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ Use the register command to automatically scan your models directory (e.g., mode ```bash -go run cmd/migration/main.go register +go run cmd/migration/main.go register [path/to/models] ``` This command creates a standard Go file that you can review and even edit if needed. It will look something like this: diff --git a/example/models/estate.go b/example/models/estate.go index 62f2f30..5c1eea6 100644 --- a/example/models/estate.go +++ b/example/models/estate.go @@ -14,27 +14,3 @@ type Estate struct { UserManagerID uint UserManager *User `gorm:"foreignKey:UserManagerID"` } - -type Estate1 struct { - gorm.Model - Name string - Address string - City string - State string - Country string - IsDeleted bool - UserManagerID uint - UserManager *User `gorm:"foreignKey:UserManagerID"` -} - -type Estate2 struct { - gorm.Model - Name string - Address string - City string - State string - Country string - IsDeleted bool - UserManagerID uint - UserManager *User `gorm:"foreignKey:UserManagerID"` -} diff --git a/migration/commands/utils.go b/migration/commands/utils.go index d0a4b8f..66af3c5 100644 --- a/migration/commands/utils.go +++ b/migration/commands/utils.go @@ -72,7 +72,6 @@ func getMigrationLoader() (*file.MigrationLoader, error) { return file.NewMigrationLoader(getMigrationsDir(), template), nil } -// validate models path func validateModelPath(path string) (string, error) { if path == "" { path = "models" @@ -97,7 +96,6 @@ func validateModelPath(path string) (string, error) { return absPath, nil } -// Create model registry func createModelRegisterFile(dirPath string) (string, error) { filePath := filepath.Join(dirPath, "models_registry.go") @@ -118,8 +116,6 @@ var ModelTypeRegistry = map[string]interface{}{ return "", fmt.Errorf("failed to create model registry file: %w", err) } - //defer file.Close() - return filePath, nil } diff --git a/tests/migration/commands_test.go b/tests/migration/commands_test.go index 2d48256..f79eaa1 100644 --- a/tests/migration/commands_test.go +++ b/tests/migration/commands_test.go @@ -13,6 +13,13 @@ import ( "github.com/beesaferoot/gorm-schema/migration/commands" ) +func TestRegisterCmd(t *testing.T) { + cmd := commands.RegisterCmd() + assert.Equal(t, "register [path]", cmd.Use) + assert.Equal(t, "Generates model registry file", cmd.Short) + assert.Equal(t, `Scans the given path for Go files containing GORM models (structs embedding gorm.Model) and generates a models_registry.go file. If no path is provided, it defaults to the 'models' directory.`, cmd.Long) +} + func TestInitCmd(t *testing.T) { cmd := commands.InitCmd() assert.Equal(t, "init", cmd.Use)