Skip to content
Draft
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
8 changes: 5 additions & 3 deletions internal/cmd/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type Options struct {
useContext bool
useValueModifier bool
config string
targetDir string
}

func (o *Options) packageDir() string {
Expand Down Expand Up @@ -45,11 +46,12 @@ func (o *Options) buildGenOptions() []gen.OptionFunc {
type GeneratorType string

const (
GeneratorTypeYo GeneratorType = "yo"
GeneratorTypeEnt GeneratorType = "ent"
GeneratorTypeYo GeneratorType = "yo"
GeneratorTypeEnt GeneratorType = "ent"
GeneratorTypeStructs GeneratorType = "structs"
)

var generatorTypes = []GeneratorType{GeneratorTypeYo, GeneratorTypeEnt}
var generatorTypes = []GeneratorType{GeneratorTypeYo, GeneratorTypeEnt, GeneratorTypeStructs}

func (t GeneratorType) Validate() error {
if t == "" {
Expand Down
11 changes: 9 additions & 2 deletions internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/earlgray283/fixgen/internal/config"
"github.com/earlgray283/fixgen/internal/gen"
gen_ent "github.com/earlgray283/fixgen/internal/gen/ent"
gen_structs "github.com/earlgray283/fixgen/internal/gen/structs"
gen_yo "github.com/earlgray283/fixgen/internal/gen/yo"
"github.com/spf13/cobra"
)
Expand All @@ -32,8 +33,11 @@ func NewCommand() *cobra.Command {
if err := generatorType.Validate(); err != nil {
return fmt.Errorf("%+w", err)
}
if generatorType == GeneratorTypeStructs && opts.targetDir == "" {
return errors.New("`--target-dir` is required for structs generator")
}

generator, err := loadGenerator(generatorType, ".")
generator, err := loadGenerator(generatorType, ".", opts.targetDir)
if err != nil {
return fmt.Errorf("failed to load generator: %+w", err)
}
Expand Down Expand Up @@ -71,16 +75,19 @@ func NewCommand() *cobra.Command {
fs.BoolVar(&opts.useContext, "use-context", false, "add context.Context argument for the generated functions")
fs.BoolVar(&opts.useValueModifier, "use-value-modifier", false, "use value modifier for the generated functions")
fs.StringVarP(&opts.config, "config", "c", "fixgen.yaml", "config file path")
fs.StringVar(&opts.targetDir, "target-dir", "", "target directory for the generated files")

return cmd
}

func loadGenerator(typ GeneratorType, workDir string) (gen.Generator, error) {
func loadGenerator(typ GeneratorType, workDir, targetDir string) (gen.Generator, error) {
switch typ {
case GeneratorTypeEnt:
return gen_ent.NewGenerator(workDir)
case GeneratorTypeYo:
return gen_yo.NewGenerator(workDir)
case GeneratorTypeStructs:
return gen_structs.NewGenerator(workDir, targetDir)
default:
return nil, fmt.Errorf("unrecognized generator type: %s", typ)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/gen/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func GenerateWithFormat[G Generator](g G, c *config.Config, opts ...OptionFunc)
for _, f := range files {
content, err := Format(append(header, f.Content...))
if err != nil {
return nil, fmt.Errorf("failed to Format: %+w", err)
return nil, fmt.Errorf("failed to Format(%s): %+w", f.Name, err)
}
f.Content = content
}
Expand Down
8 changes: 8 additions & 0 deletions internal/gen/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ type option struct {
packageName string // default: "fixture"
useContext bool
useValueModifier bool
targetDir string
}

func defaultOption() *option {
return &option{
packageName: "fixture",
useContext: false,
useValueModifier: false,
targetDir: "",
}
}

Expand Down Expand Up @@ -39,3 +41,9 @@ func UseValueModifier() OptionFunc {
o.useValueModifier = true
}
}

func TargetDir(targetDir string) OptionFunc {
return func(o *option) {
o.targetDir = targetDir
}
}
95 changes: 95 additions & 0 deletions internal/gen/structs/structs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package datastore

import (
"fmt"
"maps"
"path/filepath"
"strings"

"github.com/earlgray283/fixgen/internal/caseconv"
"github.com/earlgray283/fixgen/internal/gen"
"github.com/earlgray283/fixgen/internal/load"
"github.com/earlgray283/fixgen/internal/templates"
)

type Generator struct {
packagePath string
dirPath string
filepaths []string
}

var _ gen.Generator = (*Generator)(nil)

func NewGenerator(workDir, packageDirPath string) (*Generator, error) {
goModulePath, err := gen.LoadGoModulePath(workDir)
if err != nil {
return nil, fmt.Errorf("failed to load go module path: %+w", err)
}

filepaths, err := gen.ReadDir(packageDirPath)
if err != nil {
return nil, fmt.Errorf("failed to read dir: %+w", err)
}

rel, err := filepath.Rel(".", packageDirPath)
if err != nil {
return nil, fmt.Errorf("failed to get relative path: %+w", err)
}

return &Generator{
packagePath: strings.Join([]string{goModulePath, rel}, "/"),
dirPath: packageDirPath,
filepaths: filepaths,
}, nil
}

// Generate implements gen.Generator.
func (g *Generator) Generate(structInfos []*load.StructInfo, data map[string]any) ([]*gen.File, error) {
files := make([]*gen.File, 0, len(structInfos)+1)

content, err := templates.Execute(templates.TmplStructsCommonFile, nil)
if err != nil {
return nil, err
}
files = append(files, &gen.File{
Name: "structs_common",
Content: content,
})

for _, si := range structInfos {
file, err := g.execute(si, data)
if err != nil {
return nil, err
}
files = append(files, file)
}

return files, nil
}

func (g *Generator) execute(si *load.StructInfo, data map[string]any) (*gen.File, error) {
newData := map[string]any{
"TableName": si.Name,
"Fields": si.Fields,
}
maps.Copy(newData, data)

content, err := templates.Execute(templates.TmplStructsFile, newData)
if err != nil {
return nil, err
}

return &gen.File{
Name: caseconv.ConvertPascalToSnake(si.Name),
Content: content,
}, nil
}

// PackageInfo implements gen.Generator.
func (g *Generator) PackageInfo() *gen.PackageInfo {
return &gen.PackageInfo{
PackagePath: g.packagePath,
PackageAlias: "structs_gen",
PackageLocation: g.dirPath,
}
}
13 changes: 11 additions & 2 deletions internal/gen/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,18 @@ func findAndReadDir(rootDir string, keyFunc func(d fs.DirEntry) bool) (string, [
}
dirPath := filepath.Dir(keyPath)

filepaths, err := ReadDir(dirPath)
if err != nil {
return "", nil, fmt.Errorf("failed to read dir: %+w", err)
}

return dirPath, filepaths, nil
}

func ReadDir(dirPath string) ([]string, error) {
entries, err := os.ReadDir(dirPath)
if err != nil {
return "", nil, err
return nil, err
}

filepaths := make([]string, 0, len(entries))
Expand All @@ -41,7 +50,7 @@ func findAndReadDir(rootDir string, keyFunc func(d fs.DirEntry) bool) (string, [
filepaths = append(filepaths, filepath.Join(dirPath, e.Name()))
}

return dirPath, filepaths, nil
return filepaths, nil
}

func findByKey(rootDir string, keyFunc func(d fs.DirEntry) bool) (string, error) {
Expand Down
35 changes: 35 additions & 0 deletions internal/templates/structs.go.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
func Create{{.TableName}}({{ if .UseContext}}ctx context.Context,{{ end }} t *testing.T, db Inserter[*structs_gen.{{ .TableName }}], m {{ if not .UseValueModifier }}*{{ end }}structs_gen.{{.TableName}}, opts ...func(*structs_gen.{{ .TableName }})) *structs_gen.{{.TableName}} {
t.Helper()

tbl := &structs_gen.{{.TableName}} {
{{ range $i, $f := .Fields -}}
{{ if $f.DefaultValue }} {{ $f.Name }}: {{ $f.DefaultValue }}, {{ if $f.IsOverwritten }} // {{ $f.Name }} is overwritten {{ end }}
{{ else if $f.Type.IsSlice }} // {{ $f.Name }} is slice
{{ else -}} // {{ $f.Name }} is unknown
{{ end }}
{{- end -}}
}

{{ range $i, $f := .Fields -}}
{{ if $f.MustOverwrite -}} tbl.{{ $f.Name }} = m.{{ $f.Name }} // must overwrite
{{ else -}}
if
{{ if $f.IsModifiedCond -}} {{ $f.IsModifiedCond }}
{{- else if $f.Type.IsSlice -}} len(m.{{ $f.Name }}) > 0
{{- else -}} isModified(m.{{ $f.Name }})
{{- end -}} {
tbl.{{ $f.Name }} = m.{{ $f.Name }}
}
{{ end -}}
{{- end -}}
for _, opt := range opts {
opt(tbl)
}

m, err := db.Insert({{ if .UseContext }}ctx{{ else }}context.Background(){{ end}}, tbl)
if err != nil {
t.Fatal(err)
}

return m
}
3 changes: 3 additions & 0 deletions internal/templates/structs_common.go.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
type Inserter[M any] interface {
Insert(ctx context.Context, m M) (M, error)
}
14 changes: 9 additions & 5 deletions internal/templates/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,22 @@ import (

var (
//go:embed *
embedFs embed.FS
TmplEntFile *template.Template
TmplYoFile *template.Template
TmplCommonFile *template.Template
TmplHeaderFile *template.Template
embedFs embed.FS
TmplEntFile *template.Template
TmplYoFile *template.Template
TmplCommonFile *template.Template
TmplHeaderFile *template.Template
TmplStructsFile *template.Template
TmplStructsCommonFile *template.Template
)

func init() {
TmplEntFile = parseFS("ent.go.tmpl")
TmplYoFile = parseFS("yo.go.tmpl")
TmplCommonFile = parseFS("common.go.tmpl")
TmplHeaderFile = parseFS("header.go.tmpl")
TmplStructsFile = parseFS("structs.go.tmpl")
TmplStructsCommonFile = parseFS("structs_common.go.tmpl")
}

func parseFS(pattern string) *template.Template {
Expand Down
5 changes: 4 additions & 1 deletion test/fixgen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/earlgray283/fixgen/internal/config"
"github.com/earlgray283/fixgen/internal/gen"
gen_ent "github.com/earlgray283/fixgen/internal/gen/ent"
gen_structs "github.com/earlgray283/fixgen/internal/gen/structs"
gen_yo "github.com/earlgray283/fixgen/internal/gen/yo"
)

Expand Down Expand Up @@ -50,7 +51,7 @@ func Test_GoldenTest(t *testing.T) {
}
}

generators := []string{"yo", "ent"}
generators := []string{"yo", "ent", "structs"}

for _, typ := range generators {
testDir := filepath.Join(wd, typ, "test")
Expand Down Expand Up @@ -131,6 +132,8 @@ func mustNewGenerator(t *testing.T, typ string) gen.Generator {
g, err = gen_yo.NewGenerator(".")
case "ent":
g, err = gen_ent.NewGenerator(".")
case "structs":
g, err = gen_structs.NewGenerator(".", "models")
default:
t.Fatalf("unrecognized generator type `%s`", typ)
}
Expand Down
7 changes: 7 additions & 0 deletions test/structs/test/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module structs

go 1.24.1

require github.com/samber/lo v1.49.1

require golang.org/x/text v0.21.0 // indirect
12 changes: 12 additions & 0 deletions test/structs/test/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
13 changes: 13 additions & 0 deletions test/structs/test/models/todo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package models

import "time"

type Todo struct {
ID int64
Title string
Description string
Tags []string
CreatedAt time.Time
UpdatedAt *time.Time
DoneAt *time.Time
}
12 changes: 12 additions & 0 deletions test/structs/test/models/user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package models

import "time"

type User struct {
ID int64
Name string
IconURL string
UserType int64
CreatedAt time.Time
UpdatedAt *time.Time
}
16 changes: 16 additions & 0 deletions test/structs/test/testdata-context/fixgen.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
structs:
Todo:
fields:
Title:
overwrite: true
User:
fields:
IconURL:
expr: fmt.Sprintf("http://example.com/%d", 123456)
Name:
value: Taro Yamada
UserType:
value: 1
isModifiedCond: m.UserType != 1
imports:
- package: fmt
8 changes: 8 additions & 0 deletions test/structs/test/testdata-context/goldie-common.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading