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..9a059cb 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 [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: ```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/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..66af3c5 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,124 @@ func getMigrationLoader() (*file.MigrationLoader, error) { } return file.NewMigrationLoader(getMigrationsDir(), template), nil } + +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 +} + +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) + } + + 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 +} 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)