diff --git a/internal/gen/generator.go b/internal/gen/generator.go index f67eca0..7ce2f78 100644 --- a/internal/gen/generator.go +++ b/internal/gen/generator.go @@ -677,6 +677,11 @@ func (p *File) processStructType(typeSpec *ast.TypeSpec, data *ast.StructType, p fieldTag, _ = strconv.Unquote(field.Tag.Value) } + // GORM maps no column for these, so a helper would reference nothing. + if isIgnoredByGORM(fieldTag) { + continue + } + s.Fields = append(s.Fields, Field{ Name: n.Name, DBName: generateDBName(n.Name, fieldTag), diff --git a/internal/gen/generator_support_test.go b/internal/gen/generator_support_test.go new file mode 100644 index 0000000..6c485f7 --- /dev/null +++ b/internal/gen/generator_support_test.go @@ -0,0 +1,80 @@ +package gen + +import ( + "io/fs" + "os" + "path/filepath" + "regexp" + "strings" + "testing" +) + +// generateFromSources writes files into a throwaway module, runs the generator over it and +// returns the concatenated generated code. Keys are file names relative to the module root. +func generateFromSources(t *testing.T, files map[string]string) string { + t.Helper() + + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module example.com/sample\n\ngo 1.21\n"), 0o644); err != nil { + t.Fatalf("write go.mod: %v", err) + } + for name, content := range files { + path := filepath.Join(dir, name) + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatalf("create directory for %s: %v", name, err) + } + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + t.Fatalf("write %s: %v", name, err) + } + } + + out := filepath.Join(dir, "out") + g := &Generator{Files: map[string]*File{}, outPath: out} + if err := g.Process(dir); err != nil { + t.Fatalf("Process: %v", err) + } + if err := g.Gen(); err != nil { + t.Fatalf("Gen: %v", err) + } + return readGeneratedTree(t, out) +} + +// readGeneratedTree concatenates every generated file under dir, the nested packages included. +// readAllGeneratedGoFiles reads the top level only, which is enough for a fixture of one package +// but silently drops the output of one that spans several. +func readGeneratedTree(t *testing.T, dir string) string { + t.Helper() + + var b strings.Builder + err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() || !strings.HasSuffix(path, ".go") { + return nil + } + content, err := os.ReadFile(path) + if err != nil { + return err + } + b.WriteString(string(content)) + b.WriteString("\n\n") + + return nil + }) + if err != nil { + t.Fatalf("read generated tree %s: %v", dir, err) + } + if b.Len() == 0 { + t.Fatalf("no .go files under %s", dir) + } + + return b.String() +} + +// containsField reports whether the generated code declares a helper named name with the given +// type, tolerating the column alignment gofmt applies to struct fields. +func containsField(content, name, typ string) bool { + re := regexp.MustCompile(`(?m)^\s*` + regexp.QuoteMeta(name) + `\s+` + regexp.QuoteMeta(typ) + `\s*$`) + return re.MatchString(content) +} diff --git a/internal/gen/ignored_fields_test.go b/internal/gen/ignored_fields_test.go new file mode 100644 index 0000000..274cb3d --- /dev/null +++ b/internal/gen/ignored_fields_test.go @@ -0,0 +1,26 @@ +package gen + +import ( + "strings" + "testing" +) + +func TestIgnoredFieldsGetNoHelpers(t *testing.T) { + content := generateFromSources(t, map[string]string{ + "model.go": "package sample\n\ntype Article struct {\n" + + "\tID uint\n" + + "\tDraft string `gorm:\"-\"`\n" + + "\tCache string `gorm:\"-:all\"`\n" + + "\tLegacy string `gorm:\"-:migration\"`\n" + + "}\n", + }) + + for _, absent := range []string{"Draft", "Cache"} { + if strings.Contains(content, absent) { + t.Errorf("field %s is ignored by GORM and must not get a helper, got:\n%s", absent, content) + } + } + if !containsField(content, "Legacy", "field.String") { + t.Errorf("field Legacy only skips migrations and must keep its helper, got:\n%s", content) + } +} diff --git a/internal/gen/utils.go b/internal/gen/utils.go index 88dc6d4..bbcd7b9 100644 --- a/internal/gen/utils.go +++ b/internal/gen/utils.go @@ -149,6 +149,18 @@ func loadNamedStructType(modRoot, pkgPath, name string) (*ast.StructType, error) return nil, fmt.Errorf("struct %s not found in package %s", name, pkgPath) } +// isIgnoredByGORM reports whether the gorm tag removes the field from the schema. "-" and +// "-:all" map no column; "-:migration" only skips migrations and still maps one. +func isIgnoredByGORM(fieldTag string) bool { + settings := schema.ParseTagSetting(reflect.StructTag(fieldTag).Get("gorm"), ";") + v, ok := settings["-"] + if !ok { + return false + } + v = strings.ToLower(strings.TrimSpace(v)) + return v == "-" || v == "all" +} + // generateDBName generates database column name using GORM's NamingStrategy and COLUMN tag. func generateDBName(fieldName, gormTag string) string { tagSettings := schema.ParseTagSetting(reflect.StructTag(gormTag).Get("gorm"), ";")