diff --git a/examples/models/enum/enum.go b/examples/models/enum/enum.go new file mode 100644 index 0000000..ad59f87 --- /dev/null +++ b/examples/models/enum/enum.go @@ -0,0 +1,3 @@ +package enum + +type Enum string diff --git a/examples/models/enum/enum/enum.go b/examples/models/enum/enum/enum.go new file mode 100644 index 0000000..ad59f87 --- /dev/null +++ b/examples/models/enum/enum/enum.go @@ -0,0 +1,3 @@ +package enum + +type Enum string diff --git a/examples/models/user.go b/examples/models/user.go index 35cae09..40aab05 100644 --- a/examples/models/user.go +++ b/examples/models/user.go @@ -4,6 +4,8 @@ import ( "database/sql" "time" + "gorm.io/cli/gorm/examples/models/enum" + enum2 "gorm.io/cli/gorm/examples/models/enum/enum" "gorm.io/cli/gorm/genconfig" "gorm.io/datatypes" "gorm.io/gorm" @@ -38,8 +40,14 @@ type User struct { IsAdult bool `gorm:"column:is_adult"` Profile string `gen:"json"` AwardTypes datatypes.JSONSlice[int] + TagTypes datatypes.JSONSlice[UserTagType] + Tag UserTagType + Enum enum.Enum + Enum2 enum2.Enum } +type UserTagType string + type Account struct { gorm.Model UserID sql.NullInt64 diff --git a/examples/output/models/user.go b/examples/output/models/user.go index 0960590..293c6c1 100644 --- a/examples/output/models/user.go +++ b/examples/output/models/user.go @@ -36,6 +36,8 @@ var User = struct { IsAdult field.Bool Profile examples.JSON AwardTypes field.Struct[datatypes.JSONSlice[int]] + TagTypes field.Struct[datatypes.JSONSlice[models.UserTagType]] + Tag field.Field[models.UserTagType] }{ ID: field.Number[uint]{}.WithColumn("id"), CreatedAt: field.Time{}.WithColumn("created_at"), @@ -60,6 +62,8 @@ var User = struct { IsAdult: field.Bool{}.WithColumn("is_adult"), Profile: examples.JSON{}.WithColumn("profile"), AwardTypes: field.Struct[datatypes.JSONSlice[int]]{}.WithName("AwardTypes"), + TagTypes: field.Struct[datatypes.JSONSlice[models.UserTagType]]{}.WithName("TagTypes"), + Tag: field.Field[models.UserTagType]{}.WithColumn("tag"), } var Account = struct { diff --git a/internal/gen/generator.go b/internal/gen/generator.go index f67eca0..2376cf5 100644 --- a/internal/gen/generator.go +++ b/internal/gen/generator.go @@ -433,13 +433,22 @@ func (f Field) Type() string { } // Check if type implements allowed interfaces + goType := strings.TrimPrefix(f.GoType, "*") + var ( - goType = strings.TrimPrefix(f.GoType, "*") - pkgIdx = strings.LastIndex(goType, ".") + pkgIdx int pkgName = f.file.Package typName = goType ) + // Find the last '.' before any generic bracket '[' + bracketIdx := strings.Index(goType, "[") + if bracketIdx != -1 { + pkgIdx = strings.LastIndex(goType[:bracketIdx], ".") + } else { + pkgIdx = strings.LastIndex(goType, ".") + } + if pkgIdx > 0 { pkgName, typName = goType[:pkgIdx], goType[pkgIdx+1:] } @@ -453,21 +462,88 @@ func (f Field) Type() string { return fmt.Sprintf("field.Number[%s]", goType) } + // Process generic type parameters to convert full paths to short names + goType = f.processGenericType(goType) + if typ := loadNamedType(f.file.goModDir, f.file.getFullImportPath(pkgName), typName); typ != nil { - if ImplementsAllowedInterfaces(typ) { // For interface-implementing types, use generic Field - return fmt.Sprintf("field.Field[%s]", filepath.Base(goType)) + if ImplementsAllowedInterfaces(typ) || IsUnderlyingComparable(typ) { + return fmt.Sprintf("field.Field[%s]", goType) } } // Check if this is a relation field based on its type if strings.HasPrefix(goType, "[]") { - elementType := filepath.Base(strings.TrimPrefix(goType, "[]")) + elementType := strings.TrimPrefix(goType, "[]") return fmt.Sprintf("field.Slice[%s]", elementType) } else if strings.Contains(goType, ".") { - return fmt.Sprintf("field.Struct[%s]", filepath.Base(goType)) + return fmt.Sprintf("field.Struct[%s]", goType) } - return fmt.Sprintf("field.Field[%s]", filepath.Base(goType)) + return fmt.Sprintf("field.Field[%s]", goType) +} + +// processGenericType converts full package paths in generic type parameters to short names +// and ensures required imports are added +// e.g., "datatypes.JSONSlice[gorm.io/cli/gorm/examples/models.UserTagType]" +// +// -> "datatypes.JSONSlice[models.UserTagType]" +func (f Field) processGenericType(goType string) string { + goType = strings.TrimSpace(goType) + + // Handle pointer types by recursively processing without the pointer prefix + if strings.HasPrefix(goType, "*") { + return f.processGenericType(goType[1:]) + } + + // Handle slice types by recursively processing the element type + if strings.HasPrefix(goType, "[]") { + return "[]" + f.processGenericType(goType[2:]) + } + + // Split the type into the main identifier and the generic arguments (separated by '[') + mainPart, argsPart, hasArgs := strings.Cut(goType, "[") + + // Resolve the package alias for the main type + shortMain := f.file.getImportAliasType(mainPart) + + // If generic arguments exist, process them recursively + if hasArgs { + // Remove the trailing closing bracket ']' from the arguments part + cleanArgs := strings.TrimSuffix(argsPart, "]") + + // Split the arguments string by comma, respecting nested brackets + // e.g., "TypeA, Map[K,V]" -> ["TypeA", "Map[K,V]"] + args := splitGenericArgs(cleanArgs) + + var simplifiedArgs []string + for _, arg := range args { + simplifiedArgs = append(simplifiedArgs, f.processGenericType(arg)) + } + + return shortMain + "[" + strings.Join(simplifiedArgs, ", ") + "]" + } + + return shortMain +} + +// getImportAliasType returns the import alias type string for a raw type string +// e.g., "datatypes2 gorm.io/datatypes.JSONSlice" -> "datatypes2.JSONSlice" +func (p *File) getImportAliasType(raw string) string { + lastDot := strings.LastIndex(raw, ".") + if lastDot == -1 { + return raw + } + + pathStr := raw[:lastDot] + typeName := raw[lastDot+1:] + + pkgName := filepath.Base(pathStr) + imp := p.getImport(pathStr) + if imp != nil { + pkgName = imp.Name + } + + return pkgName + "." + typeName } // Value returns the field value string with column name for template generation @@ -777,6 +853,15 @@ func (p *File) getFullImportPath(shortName string) string { return shortName } +func (p *File) getImport(path string) *Import { + for _, i := range p.Imports { + if i.Path == path { + return &i + } + } + return nil +} + // handleAnonymousEmbedding processes anonymous embedded fields and returns true if handled func (p *File) handleAnonymousEmbedding(field *ast.Field, pkgName string, s *Struct) bool { // Helper function to add fields from embedded struct diff --git a/internal/gen/generator_test.go b/internal/gen/generator_test.go index 8534990..5f81aa5 100644 --- a/internal/gen/generator_test.go +++ b/internal/gen/generator_test.go @@ -179,6 +179,10 @@ func TestProcessStructType(t *testing.T) { {Name: "IsAdult", DBName: "is_adult", GoType: "bool"}, {Name: "Profile", DBName: "profile", GoType: "string", NamedGoType: "json"}, {Name: "AwardTypes", DBName: "award_types", GoType: "datatypes.JSONSlice[int]"}, + {Name: "TagTypes", DBName: "tag_types", GoType: "datatypes.JSONSlice[UserTagType]"}, + {Name: "Tag", DBName: "tag", GoType: "UserTagType"}, + {Name: "Enum", DBName: "enum", GoType: "enum.Enum"}, // 添加 + {Name: "Enum2", DBName: "enum2", GoType: "enum2.Enum"}, }, } diff --git a/internal/gen/utils.go b/internal/gen/utils.go index 88dc6d4..ea736ac 100644 --- a/internal/gen/utils.go +++ b/internal/gen/utils.go @@ -73,6 +73,14 @@ func ImplementsAllowedInterfaces(typ types.Type) bool { return false } +func IsUnderlyingComparable(typ types.Type) bool { + underlying := typ.Underlying() + if _, ok := underlying.(*types.Struct); ok { + return false + } + return types.Comparable(underlying) +} + func findGoModDir(filename string) string { cmd := exec.Command("go", "env", "GOMOD") cmd.Dir = filepath.Dir(filename) @@ -202,3 +210,27 @@ func stripGeneric(s string) string { } return s } + +// splitGenericArgs splits a generic type argument string into individual arguments. +func splitGenericArgs(s string) []string { + var args []string + depth := 0 + start := 0 + for i, char := range s { + switch char { + case '[': + depth++ + case ']': + depth-- + case ',': + if depth == 0 { + args = append(args, s[start:i]) + start = i + 1 + } + } + } + if start < len(s) { + args = append(args, s[start:]) + } + return args +}