Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ main.go.txt
ast_dump.go.txt
.zed
gorm-schema
gorm-schema.exe
test.log
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ func main() {
}

rootCmd.AddCommand(
commands.RegisterCmd(),
commands.InitCmd(),
commands.CreateCmd(),
commands.GenerateCmd(),
Expand All @@ -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
Expand All @@ -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
```
Expand Down
5 changes: 3 additions & 2 deletions cmd/gorm-schema/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand All @@ -31,6 +31,7 @@ func main() {
}

rootCmd.AddCommand(
commands.RegisterCmd(),
commands.InitCmd(),
commands.CreateCmd(),
commands.GenerateCmd(),
Expand Down
14 changes: 7 additions & 7 deletions example/models/models_registry.go
Original file line number Diff line number Diff line change
@@ -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{},
}
Comment thread
beesaferoot marked this conversation as resolved.
"ApartmentContract": ApartmentContract{},
"ApartmentHighlight": ApartmentHighlight{},
"Estate": Estate{},
"Tenant": Tenant{},
"User": User{},
}
4 changes: 2 additions & 2 deletions example/user-project/cmd/migration/main.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -29,6 +28,7 @@ func main() {
}

rootCmd.AddCommand(
commands.RegisterCmd(),
commands.InitCmd(),
commands.CreateCmd(),
commands.GenerateCmd(),
Expand Down
38 changes: 38 additions & 0 deletions migration/commands/register.go
Original file line number Diff line number Diff line change
@@ -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

},
}
}
125 changes: 125 additions & 0 deletions migration/commands/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import (
"path/filepath"
"strings"

"go/ast"
"go/parser"
"go/token"

"gorm.io/driver/postgres"
"gorm.io/gorm"

Expand Down Expand Up @@ -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
}
7 changes: 7 additions & 0 deletions tests/migration/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down