├── .gitignore ├── README.md ├── cmd └── repogen │ ├── helpers.go │ ├── main.go │ └── repository.go ├── go.mod ├── go.sum ├── user.go └── user_gen.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GopherconRussia2020 2 | 3 | Исходный код из доклада "Как писать генераторы на Go?" для конференции GopherconRussia2020 4 | 5 | #### Видео 6 | https://www.youtube.com/watch?v=ZKI6RfdqZGk 7 | 8 | #### Слайды 9 | https://docs.google.com/presentation/d/1cSdlIKUT8Ywyf0MTx8yWW1mgVw-sWS9uNPZowV-fjwU/edit?usp=sharing 10 | -------------------------------------------------------------------------------- /cmd/repogen/helpers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "go/ast" 6 | "go/printer" 7 | "go/token" 8 | "log" 9 | ) 10 | 11 | func expr2string(expr ast.Expr) string { 12 | var buf bytes.Buffer 13 | err := printer.Fprint(&buf, token.NewFileSet(), expr) 14 | if err != nil { 15 | log.Fatalf("error print expression to string: %v", err) 16 | } 17 | return buf.String() 18 | } 19 | 20 | 21 | -------------------------------------------------------------------------------- /cmd/repogen/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "go/ast" 5 | "go/parser" 6 | "go/printer" 7 | "go/token" 8 | "log" 9 | "os" 10 | "strings" 11 | 12 | "golang.org/x/tools/go/ast/inspector" 13 | ) 14 | 15 | type generateTask interface { 16 | Generate(file *ast.File) error 17 | } 18 | 19 | func main() { 20 | path := os.Getenv("GOFILE") 21 | if path == "" { 22 | log.Fatal("GOFILE env variable must be set") 23 | } 24 | 25 | astInFile, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.ParseComments) 26 | if err != nil { 27 | log.Fatalf("parse file: %v", err) 28 | } 29 | 30 | i := inspector.New([]*ast.File{astInFile}) 31 | iFilter := []ast.Node{ 32 | &ast.GenDecl{}, 33 | } 34 | 35 | var tasks []generateTask 36 | 37 | i.Nodes(iFilter, func(node ast.Node, push bool) (proceed bool) { 38 | genDecl := node.(*ast.GenDecl) 39 | if genDecl.Doc == nil { 40 | return false 41 | } 42 | 43 | typeSpec, ok := genDecl.Specs[0].(*ast.TypeSpec) 44 | if !ok { 45 | return false 46 | } 47 | 48 | structType, ok := typeSpec.Type.(*ast.StructType) 49 | if !ok { 50 | return false 51 | } 52 | 53 | for _, comment := range genDecl.Doc.List { 54 | switch comment.Text { 55 | case "//repogen:entity": 56 | tasks = append(tasks, repositoryGenerator{ 57 | typeSpec: typeSpec, 58 | structType: structType, 59 | }) 60 | } 61 | } 62 | 63 | return false 64 | }) 65 | 66 | astOutFile := &ast.File{ 67 | Name: astInFile.Name, 68 | } 69 | 70 | for _, g := range tasks { 71 | err = g.Generate(astOutFile) 72 | if err != nil { 73 | log.Fatalf("generate: %v", err) 74 | } 75 | } 76 | 77 | outFile, err := os.Create(strings.TrimSuffix(path, ".go") + "_gen.go") 78 | if err != nil { 79 | log.Fatalf("create file: %v", err) 80 | } 81 | 82 | err = printer.Fprint(outFile, token.NewFileSet(), astOutFile) 83 | if err != nil { 84 | log.Fatalf("print file: %v", err) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /cmd/repogen/repository.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "go/ast" 7 | "go/parser" 8 | "go/token" 9 | "strings" 10 | "text/template" 11 | 12 | "github.com/iancoleman/strcase" 13 | ) 14 | 15 | var repositoryTemplate = template.Must(template.New("").Parse(` 16 | package main 17 | 18 | import ( 19 | "github.com/jinzhu/gorm" 20 | ) 21 | 22 | type {{ .EntityName }}Repository struct { 23 | db *gorm.DB 24 | } 25 | 26 | func New{{ .EntityName }}Repository(db *gorm.DB) {{ .EntityName }}Repository { 27 | return {{ .EntityName }}Repository{ db: db } 28 | } 29 | 30 | func (r {{ .EntityName }}Repository) Get({{ .PrimaryName }} {{ .PrimaryType }}) (*{{ .EntityName }}, error) { 31 | entity := new({{ .EntityName }}) 32 | err := r.db.Limit(1).Where("{{ .PrimarySQLName }} = ?", {{ .PrimaryName }}).Find(entity).Error 33 | return entity, err 34 | } 35 | 36 | func (r {{ .EntityName }}Repository) Create(entity *{{ .EntityName }}) error { 37 | return r.db.Create(entity).Error 38 | } 39 | 40 | func (r {{ .EntityName }}Repository) Update(entity *{{ .EntityName }}) error { 41 | return r.db.Model(entity).Update(entity).Error 42 | } 43 | 44 | func (r {{ .EntityName }}Repository) Delete(entity *{{ .EntityName }}) error { 45 | return r.db.Delete(entity).Error 46 | } 47 | `)) 48 | 49 | type repositoryGenerator struct { 50 | typeSpec *ast.TypeSpec 51 | structType *ast.StructType 52 | } 53 | 54 | func (r repositoryGenerator) Generate(file *ast.File) error { 55 | primary, err := r.primaryField() 56 | if err != nil { 57 | return err 58 | } 59 | 60 | type templateParams struct { 61 | EntityName string 62 | PrimaryType string 63 | PrimaryName string 64 | PrimarySQLName string 65 | } 66 | 67 | params := templateParams{ 68 | EntityName: r.typeSpec.Name.Name, 69 | PrimaryName: strcase.ToLowerCamel(primary.Names[0].Name), 70 | PrimarySQLName: strcase.ToSnake(primary.Names[0].Name), 71 | PrimaryType: expr2string(primary.Type), 72 | } 73 | 74 | var buf bytes.Buffer 75 | err = repositoryTemplate.Execute(&buf, params) 76 | if err != nil { 77 | return fmt.Errorf("execute template: %v", err) 78 | } 79 | 80 | templateAst, err := parser.ParseFile(token.NewFileSet(), "", buf.Bytes(), parser.ParseComments) 81 | if err != nil { 82 | return fmt.Errorf("parse template: %v", err) 83 | } 84 | 85 | for _, decl := range templateAst.Decls { 86 | file.Decls = append(file.Decls, decl) 87 | } 88 | 89 | return nil 90 | } 91 | 92 | func (r repositoryGenerator) primaryField() (*ast.Field, error) { 93 | for _, field := range r.structType.Fields.List { 94 | if !strings.Contains(field.Tag.Value, "primary") { 95 | continue 96 | } 97 | 98 | return field, nil 99 | } 100 | 101 | return nil, fmt.Errorf("has no primary field") 102 | } 103 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module gophercon2020 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334 7 | github.com/jinzhu/gorm v1.9.16 8 | golang.org/x/tools v0.0.0-20200723000907-a7c6fd066f6d 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= 2 | github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= 3 | github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM= 4 | github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= 5 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= 6 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= 7 | github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= 8 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 9 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= 10 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 11 | github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334 h1:VHgatEHNcBFEB7inlalqfNqw65aNkM1lGX2yt3NmbS8= 12 | github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE= 13 | github.com/jinzhu/gorm v1.9.15 h1:OdR1qFvtXktlxk73XFYMiYn9ywzTwytqe4QkuMRqc38= 14 | github.com/jinzhu/gorm v1.9.15/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs= 15 | github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs= 16 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 17 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 18 | github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M= 19 | github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 20 | github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4= 21 | github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 22 | github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA= 23 | github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= 24 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 25 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 26 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 27 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 28 | golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 29 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= 30 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 31 | golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= 32 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 33 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 34 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 35 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 36 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 37 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 38 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 39 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 40 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 41 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 42 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 43 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 44 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 45 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 46 | golang.org/x/tools v0.0.0-20200723000907-a7c6fd066f6d h1:7k9BKfwmdbykG6l5ztniTrH0TP25yel8O7l26/yovMU= 47 | golang.org/x/tools v0.0.0-20200723000907-a7c6fd066f6d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 48 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 49 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 50 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 51 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 52 | -------------------------------------------------------------------------------- /user.go: -------------------------------------------------------------------------------- 1 | package gophercon2020 2 | 3 | //go:generate repogen 4 | 5 | //repogen:entity 6 | type User struct { 7 | ID uint `gorm:"primary_key"` 8 | Email string 9 | PasswordHash string 10 | } 11 | 12 | -------------------------------------------------------------------------------- /user_gen.go: -------------------------------------------------------------------------------- 1 | package gophercon2020 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | ) 6 | 7 | type UserRepository struct{ db *gorm.DB } 8 | 9 | func NewUserRepository(db *gorm.DB) UserRepository { 10 | return UserRepository{db: db} 11 | } 12 | func (r UserRepository) Get(id uint) (*User, error) { 13 | entity := new(User) 14 | err := r.db.Limit(1).Where("id = ?", id).Find(entity).Error 15 | return entity, err 16 | } 17 | func (r UserRepository) Create(entity *User) error { 18 | return r.db.Create(entity).Error 19 | } 20 | func (r UserRepository) Update(entity *User) error { 21 | return r.db.Model(entity).Update(entity).Error 22 | } 23 | func (r UserRepository) Delete(entity *User) error { 24 | return r.db.Delete(entity).Error 25 | } 26 | --------------------------------------------------------------------------------