├── .golangci.yml ├── main.go ├── Makefile ├── generators ├── validate │ ├── options.go │ ├── generator_test.output │ ├── generator_test.go │ ├── template.go │ ├── generator.go │ ├── README.md │ └── model.go ├── search │ ├── options.go │ ├── generator_test.go │ ├── template.go │ ├── generator.go │ ├── model.go │ ├── generator_test.output │ └── README.md ├── named │ ├── generator.go │ ├── generator_test.go │ ├── template.go │ ├── generator_test.output │ └── README.md ├── model │ ├── options.go │ ├── generator_test.go │ ├── template.go │ ├── generator_test.output │ ├── generator.go │ ├── README.md │ └── model.go └── base │ └── base.go ├── lib ├── genna_test.go ├── database.go ├── genna.go ├── store_test.go └── store.go ├── util ├── index.go ├── set.go ├── formatter.go ├── tags.go ├── set_test.go ├── util.go ├── index_test.go ├── tags_test.go ├── util_test.go ├── texts.go └── texts_test.go ├── go.mod ├── .gitignore ├── cmd └── root.go ├── LICENSE ├── README.md ├── model ├── relation.go ├── column.go ├── entity.go ├── custom_types.go ├── column_test.go ├── relation_test.go ├── custom_types_test.go ├── entity_test.go ├── types.go └── types_test.go ├── test_db.sql └── go.sum /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable: 3 | - gocritic -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/dizzyfool/genna/cmd" 5 | ) 6 | 7 | func main() { 8 | cmd.Execute() 9 | } 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: lint test 2 | 3 | lint: 4 | @golangci-lint run ./... 5 | 6 | test: 7 | psql postgres://genna:genna@localhost:5432/genna?sslmode=disable -f test_db.sql 8 | go test ./... -------------------------------------------------------------------------------- /generators/validate/options.go: -------------------------------------------------------------------------------- 1 | package validate 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/dizzyfool/genna/generators/base" 7 | "github.com/dizzyfool/genna/util" 8 | ) 9 | 10 | // Options for generator 11 | type Options struct { 12 | base.Options 13 | 14 | // Package sets package name for model 15 | // Works only with SchemaPackage = false 16 | Package string 17 | 18 | // Do not replace primary key name to ID 19 | KeepPK bool 20 | } 21 | 22 | // Def fills default values of an options 23 | func (o *Options) Def() { 24 | o.Options.Def() 25 | 26 | if strings.Trim(o.Package, " ") == "" { 27 | o.Package = util.DefaultPackage 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/genna_test.go: -------------------------------------------------------------------------------- 1 | package genna 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func prepareReq() (url string, logger *log.Logger) { 10 | logger = log.New(os.Stderr, "", log.LstdFlags) 11 | url = `postgres://genna:genna@localhost:5432/genna?sslmode=disable` 12 | 13 | return 14 | } 15 | 16 | func TestGenna_Read(t *testing.T) { 17 | genna := New(prepareReq()) 18 | 19 | t.Run("Should read DB", func(t *testing.T) { 20 | entities, err := genna.Read([]string{"public.*"}, true, false, 9, nil) 21 | if err != nil { 22 | t.Errorf("Genna.Read error %v", err) 23 | return 24 | } 25 | 26 | if ln := len(entities); ln != 3 { 27 | t.Errorf("len(entities) = %v, want %v", ln, 3) 28 | return 29 | } 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /generators/search/options.go: -------------------------------------------------------------------------------- 1 | package search 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/dizzyfool/genna/generators/base" 7 | "github.com/dizzyfool/genna/util" 8 | ) 9 | 10 | // Options for generator 11 | type Options struct { 12 | base.Options 13 | 14 | // Package sets package name for model 15 | // Works only with SchemaPackage = false 16 | Package string 17 | 18 | // Do not replace primary key name to ID 19 | KeepPK bool 20 | 21 | // Do not generate alias tag 22 | NoAlias bool 23 | 24 | // Strict types in filters 25 | Relaxed bool 26 | 27 | // Add json tag to models 28 | AddJSONTag bool 29 | } 30 | 31 | // Def fills default values of an options 32 | func (o *Options) Def() { 33 | o.Options.Def() 34 | 35 | if strings.Trim(o.Package, " ") == "" { 36 | o.Package = util.DefaultPackage 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /util/index.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Index stores unique strings 8 | type Index struct { 9 | index map[string]struct{} 10 | } 11 | 12 | // NewIndex creates Index 13 | func NewIndex() Index { 14 | return Index{ 15 | index: map[string]struct{}{}, 16 | } 17 | } 18 | 19 | // Available checks if string already exists 20 | func (i *Index) Available(s string) bool { 21 | _, ok := i.index[s] 22 | return !ok 23 | } 24 | 25 | // Add ads string to index 26 | func (i *Index) Add(s string) { 27 | i.index[s] = struct{}{} 28 | } 29 | 30 | // GetNext get next available string if given already exists 31 | func (i *Index) GetNext(s string) string { 32 | if i.Available(s) { 33 | return s 34 | } 35 | 36 | suffix := 1 37 | for { 38 | next := fmt.Sprintf("%s%d", s, suffix) 39 | if i.Available(next) { 40 | return next 41 | } 42 | suffix++ 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dizzyfool/genna 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/fatih/camelcase v1.0.0 7 | github.com/go-pg/pg/v10 v10.10.5 8 | github.com/jinzhu/inflection v1.0.0 9 | github.com/spf13/cobra v1.1.3 10 | ) 11 | 12 | require ( 13 | github.com/go-pg/zerochecker v0.2.0 // indirect 14 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 15 | github.com/spf13/pflag v1.0.5 // indirect 16 | github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect 17 | github.com/vmihailenco/bufpool v0.1.11 // indirect 18 | github.com/vmihailenco/msgpack/v5 v5.3.1 // indirect 19 | github.com/vmihailenco/tagparser v0.1.2 // indirect 20 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 21 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect 22 | golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7 // indirect 23 | mellium.im/sasl v0.2.1 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /util/set.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | // Set stores only unique strings 4 | type Set struct { 5 | elements []string 6 | index map[string]struct{} 7 | } 8 | 9 | // NewSet creates Set 10 | func NewSet() Set { 11 | return Set{ 12 | elements: []string{}, 13 | index: map[string]struct{}{}, 14 | } 15 | } 16 | 17 | // Add adds element to set 18 | // return false if element already exists 19 | func (s *Set) Add(element string) bool { 20 | if s.Exists(element) { 21 | return false 22 | } 23 | 24 | s.elements = append(s.elements, element) 25 | s.index[element] = struct{}{} 26 | 27 | return true 28 | } 29 | 30 | // Exists checks if element exists 31 | func (s *Set) Exists(element string) bool { 32 | _, ok := s.index[element] 33 | return ok 34 | } 35 | 36 | // Elements return all elements from set 37 | func (s *Set) Elements() []string { 38 | return s.elements 39 | } 40 | 41 | // Len gets elements count 42 | func (s *Set) Len() int { 43 | return len(s.elements) 44 | } 45 | -------------------------------------------------------------------------------- /generators/named/generator.go: -------------------------------------------------------------------------------- 1 | package named 2 | 3 | import ( 4 | "github.com/dizzyfool/genna/generators/base" 5 | "github.com/dizzyfool/genna/generators/model" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | // CreateCommand creates generator command 11 | func CreateCommand() *cobra.Command { 12 | return base.CreateCommand("model-named", "Basic go-pg model generator with named structures", New()) 13 | } 14 | 15 | // Generator represents basic named generator 16 | type Generator struct { 17 | *model.Basic 18 | } 19 | 20 | // New creates basic generator 21 | func New() *Generator { 22 | return &Generator{ 23 | Basic: model.New(), 24 | } 25 | } 26 | 27 | // Generate runs whole generation process 28 | func (g *Generator) Generate() error { 29 | options := g.Options() 30 | return base.NewGenerator(options.URL). 31 | Generate( 32 | options.Tables, 33 | options.FollowFKs, 34 | options.UseSQLNulls, 35 | options.Output, 36 | Template, 37 | g.Packer(), 38 | options.GoPgVer, 39 | options.CustomTypes, 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Go template 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.exe~ 6 | *.dll 7 | *.so 8 | *.dylib 9 | 10 | # Test binary, build with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | ### JetBrains template 17 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 18 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 19 | 20 | # JetBrains stuff 21 | .idea/ 22 | 23 | # CMake 24 | cmake-build-debug/ 25 | cmake-build-release/ 26 | 27 | # File-based project format 28 | *.iws 29 | 30 | # IntelliJ 31 | out/ 32 | 33 | # mpeltonen/sbt-idea plugin 34 | .idea_modules/ 35 | 36 | # JIRA plugin 37 | atlassian-ide-plugin.xml 38 | 39 | # Crashlytics plugin (for Android Studio and IntelliJ) 40 | com_crashlytics_export_strings.xml 41 | crashlytics.properties 42 | crashlytics-build.properties 43 | fabric.properties 44 | 45 | /vendor/ 46 | -------------------------------------------------------------------------------- /generators/model/options.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/dizzyfool/genna/generators/base" 7 | "github.com/dizzyfool/genna/util" 8 | ) 9 | 10 | // Options for generator 11 | type Options struct { 12 | base.Options 13 | 14 | // Package sets package name for model 15 | // Works only with SchemaPackage = false 16 | Package string 17 | 18 | // Do not replace primary key name to ID 19 | KeepPK bool 20 | 21 | // Soft delete column 22 | SoftDelete string 23 | 24 | // use sql.Null... instead of pointers 25 | UseSQLNulls bool 26 | 27 | // Do not generate alias tag 28 | NoAlias bool 29 | 30 | // Do not generate discard_unknown_columns tag 31 | NoDiscard bool 32 | 33 | // Override type for json/jsonb 34 | JSONTypes map[string]string 35 | 36 | // Add json tag to models 37 | AddJSONTag bool 38 | } 39 | 40 | // Def fills default values of an options 41 | func (o *Options) Def() { 42 | o.Options.Def() 43 | 44 | if strings.Trim(o.Package, " ") == "" { 45 | o.Package = util.DefaultPackage 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /generators/validate/generator_test.output: -------------------------------------------------------------------------------- 1 | //nolint 2 | //lint:file-ignore U1000 ignore unused code, it's generated 3 | package model 4 | 5 | import ( 6 | "unicode/utf8" 7 | ) 8 | 9 | const ( 10 | ErrEmptyValue = "empty" 11 | ErrMaxLength = "len" 12 | ErrWrongValue = "value" 13 | ) 14 | 15 | func (m User) Validate() (errors map[string]string, valid bool) { 16 | errors = map[string]string{} 17 | 18 | if utf8.RuneCountInString(m.Email) > 64 { 19 | errors[Columns.User.Email] = ErrMaxLength 20 | } 21 | 22 | if m.Name != nil && utf8.RuneCountInString(*m.Name) > 128 { 23 | errors[Columns.User.Name] = ErrMaxLength 24 | } 25 | 26 | if m.CountryID != nil && *m.CountryID == 0 { 27 | errors[Columns.User.CountryID] = ErrEmptyValue 28 | } 29 | 30 | return errors, len(errors) == 0 31 | } 32 | 33 | func (m GeoCountry) Validate() (errors map[string]string, valid bool) { 34 | errors = map[string]string{} 35 | 36 | if utf8.RuneCountInString(m.Code) > 3 { 37 | errors[Columns.GeoCountry.Code] = ErrMaxLength 38 | } 39 | 40 | return errors, len(errors) == 0 41 | } 42 | -------------------------------------------------------------------------------- /generators/named/generator_test.go: -------------------------------------------------------------------------------- 1 | package named 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path" 7 | "runtime" 8 | "testing" 9 | ) 10 | 11 | func TestGenerator_Generate(t *testing.T) { 12 | generator := New() 13 | options := generator.Options() 14 | 15 | options.Def() 16 | options.URL = `postgres://genna:genna@localhost:5432/genna?sslmode=disable` 17 | options.Output = path.Join(os.TempDir(), "model_test.go") 18 | options.FollowFKs = true 19 | 20 | generator.SetOptions(options) 21 | 22 | if err := generator.Generate(); err != nil { 23 | t.Errorf("generate error = %v", err) 24 | return 25 | } 26 | 27 | generated, err := ioutil.ReadFile(options.Output) 28 | if err != nil { 29 | t.Errorf("file not generated = %v", err) 30 | } 31 | 32 | _, filename, _, _ := runtime.Caller(0) 33 | check, err := ioutil.ReadFile(path.Join(path.Dir(filename), "generator_test.output")) 34 | if err != nil { 35 | t.Errorf("check file not found = %v", err) 36 | } 37 | 38 | if string(generated) != string(check) { 39 | t.Errorf("generated does not match with check") 40 | return 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /util/formatter.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "go/format" 6 | "os" 7 | "path" 8 | ) 9 | 10 | // FmtAndSave formats go code and saves file 11 | // if formatting failed it still saves file but return error also 12 | func FmtAndSave(unformatted []byte, filename string) (bool, error) { 13 | // formatting by go-fmt 14 | content, fmtErr := format.Source(unformatted) 15 | if fmtErr != nil { 16 | // saving file even if there is fmt errors 17 | content = unformatted 18 | } 19 | 20 | file, err := File(filename) 21 | if err != nil { 22 | return false, fmt.Errorf("open model file error: %w", err) 23 | } 24 | 25 | if _, err := file.Write(content); err != nil { 26 | return false, fmt.Errorf("writing content to file error: %w", err) 27 | } 28 | 29 | return true, fmtErr 30 | } 31 | 32 | // File creates file 33 | func File(filename string) (*os.File, error) { 34 | directory := path.Dir(filename) 35 | 36 | if err := os.MkdirAll(directory, os.ModePerm); err != nil { 37 | return nil, err 38 | } 39 | 40 | return os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) 41 | } 42 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/dizzyfool/genna/generators/model" 7 | "github.com/dizzyfool/genna/generators/named" 8 | "github.com/dizzyfool/genna/generators/search" 9 | "github.com/dizzyfool/genna/generators/validate" 10 | 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var root = &cobra.Command{ 15 | Use: "genna", 16 | Short: "Genna is model generator for go-pg package", 17 | Long: `This application is a tool to generate the needed files 18 | to quickly create a models for go-pg https://github.com/go-pg/pg`, 19 | Version: "1.2.0", 20 | Run: func(cmd *cobra.Command, args []string) { 21 | if err := cmd.Help(); err != nil { 22 | panic("help not found") 23 | } 24 | }, 25 | FParseErrWhitelist: cobra.FParseErrWhitelist{ 26 | UnknownFlags: true, 27 | }, 28 | } 29 | 30 | func init() { 31 | root.AddCommand( 32 | model.CreateCommand(), 33 | search.CreateCommand(), 34 | validate.CreateCommand(), 35 | named.CreateCommand(), 36 | ) 37 | } 38 | 39 | // Execute runs root cmd 40 | func Execute() { 41 | if err := root.Execute(); err != nil { 42 | os.Exit(1) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /util/tags.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // Annotation is a simple helper used to build tags for structs 9 | type Annotation struct { 10 | tags []tag 11 | } 12 | 13 | type tag struct { 14 | name string 15 | values []string 16 | } 17 | 18 | // NewAnnotation creates annotation 19 | func NewAnnotation() *Annotation { 20 | return &Annotation{} 21 | } 22 | 23 | // AddTag ads a tag if not exists, appends a value otherwise 24 | func (a *Annotation) AddTag(name string, value string) *Annotation { 25 | for i, tag := range a.tags { 26 | if tag.name == name { 27 | a.tags[i].values = append(a.tags[i].values, value) 28 | return a 29 | } 30 | } 31 | a.tags = append(a.tags, tag{name, []string{value}}) 32 | return a 33 | } 34 | 35 | func (a *Annotation) Len() int { 36 | return len(a.tags) 37 | } 38 | 39 | // String prints valid tag 40 | func (a *Annotation) String() string { 41 | result := make([]string, 0) 42 | for _, tag := range a.tags { 43 | result = append(result, fmt.Sprintf(`%s:"%s"`, tag.name, strings.Join(tag.values, ","))) 44 | } 45 | 46 | return strings.Join(result, " ") 47 | } 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Andrei Simonov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Genna - cli tool for generating go-pg models 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/dizzyfool/genna)](https://goreportcard.com/report/github.com/dizzyfool/genna) 4 | 5 | 6 | Requirements: 7 | - [go-pg](https://github.com/go-pg/pg) 8 | - your PostgreSQL database 9 | 10 | ### Idea 11 | 12 | In most of the cases go-pg models represent database's tables and relations. Genna's main goal is to prepare those models by reading detailed information about PostrgeSQL database. The result should be several files with ready to use structs. 13 | 14 | ### Usage 15 | 16 | 1. Install `go get github.com/dizzyfool/genna` 17 | 1. Read though help `genna -h` 18 | 19 | Currently genna support 3 generators: 20 | - [model](generators/model/README.md), that generates basic go-pg model 21 | - [model-named](generators/named/README.md), same as basic but with named structs for columns and tables (author: [@Dionid](https://github.com/Dionid)) 22 | - [search](generators/search/README.md), that generates search structs for basic model 23 | - [validation](generators/validate/README.md), that generates validate functions for basic model 24 | 25 | Examples located in each generator 26 | -------------------------------------------------------------------------------- /model/relation.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/dizzyfool/genna/util" 7 | ) 8 | 9 | // Relation stores relation 10 | type Relation struct { 11 | FKFields []string 12 | GoName string 13 | 14 | TargetPGName string 15 | TargetPGSchema string 16 | TargetPGFullName string 17 | 18 | TargetEntity *Entity 19 | 20 | GoType string 21 | } 22 | 23 | // NewRelation creates relation from pg info 24 | func NewRelation(sourceColumns []string, targetSchema, targetTable string) Relation { 25 | names := make([]string, len(sourceColumns)) 26 | for i, name := range sourceColumns { 27 | names[i] = util.ReplaceSuffix(util.ColumnName(name), util.ID, "") 28 | } 29 | 30 | typ := util.EntityName(targetTable) 31 | if targetSchema != util.PublicSchema { 32 | typ = util.CamelCased(targetSchema) + typ 33 | } 34 | 35 | return Relation{ 36 | FKFields: sourceColumns, 37 | GoName: strings.Join(names, ""), 38 | 39 | TargetPGName: targetTable, 40 | TargetPGSchema: targetSchema, 41 | TargetPGFullName: util.JoinF(targetSchema, targetTable), 42 | 43 | GoType: typ, 44 | } 45 | } 46 | 47 | func (r *Relation) AddEntity(entity *Entity) { 48 | r.TargetEntity = entity 49 | } 50 | -------------------------------------------------------------------------------- /generators/validate/generator_test.go: -------------------------------------------------------------------------------- 1 | package validate 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path" 7 | "runtime" 8 | "testing" 9 | 10 | "github.com/dizzyfool/genna/model" 11 | ) 12 | 13 | func TestGenerator_Generate(t *testing.T) { 14 | generator := New() 15 | 16 | generator.options.Def() 17 | generator.options.URL = `postgres://genna:genna@localhost:5432/genna?sslmode=disable` 18 | generator.options.Output = path.Join(os.TempDir(), "validate_test.go") 19 | generator.options.FollowFKs = true 20 | generator.options.CustomTypes.Add(model.TypePGUuid, "uuid.UUID", "github.com/google/uuid") 21 | 22 | if err := generator.Generate(); err != nil { 23 | t.Errorf("generate error = %v", err) 24 | return 25 | } 26 | 27 | generated, err := ioutil.ReadFile(generator.options.Output) 28 | if err != nil { 29 | t.Errorf("file not generated = %v", err) 30 | } 31 | 32 | _, filename, _, _ := runtime.Caller(0) 33 | check, err := ioutil.ReadFile(path.Join(path.Dir(filename), "generator_test.output")) 34 | if err != nil { 35 | t.Errorf("check file not found = %v", err) 36 | } 37 | 38 | if string(generated) != string(check) { 39 | t.Errorf("generated does not match with check") 40 | return 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test_db.sql: -------------------------------------------------------------------------------- 1 | drop schema if exists "public" cascade; 2 | drop schema if exists "geo" cascade; 3 | 4 | create schema "public"; 5 | 6 | create extension if not exists "uuid-ossp"; 7 | 8 | create table "projects" 9 | ( 10 | "projectId" uuid not null default uuid_generate_v4(), 11 | "code" uuid, 12 | "name" text not null, 13 | 14 | primary key ("projectId") 15 | ); 16 | 17 | create table "users" 18 | ( 19 | "userId" serial not null, 20 | "email" varchar(64) not null, 21 | "activated" bool not null default false, 22 | "name" varchar(128), 23 | "countryId" integer, 24 | "avatar" bytea not null, 25 | "avatarAlt" bytea, 26 | "apiKeys" bytea[], 27 | "loggedAt" timestamp, 28 | 29 | primary key ("userId") 30 | ); 31 | 32 | create schema "geo"; 33 | 34 | create table geo."countries" 35 | ( 36 | "countryId" serial not null, 37 | "code" varchar(3) not null, 38 | "coords" integer[], 39 | 40 | primary key ("countryId") 41 | ); 42 | 43 | alter table "users" 44 | add constraint "fk_user_country" 45 | foreign key ("countryId") 46 | references geo."countries" ("countryId") on update restrict on delete restrict; -------------------------------------------------------------------------------- /generators/search/generator_test.go: -------------------------------------------------------------------------------- 1 | package search 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path" 7 | "runtime" 8 | "testing" 9 | 10 | "github.com/dizzyfool/genna/model" 11 | ) 12 | 13 | func TestGenerator_Generate(t *testing.T) { 14 | generator := New() 15 | 16 | generator.options.Def() 17 | generator.options.URL = `postgres://genna:genna@localhost:5432/genna?sslmode=disable` 18 | generator.options.Output = path.Join(os.TempDir(), "search_test.go") 19 | generator.options.FollowFKs = true 20 | generator.options.CustomTypes.Add(model.TypePGUuid, "uuid.UUID", "github.com/google/uuid") 21 | //generator.options.AddJSONTag = true 22 | 23 | if err := generator.Generate(); err != nil { 24 | t.Errorf("generate error = %v", err) 25 | return 26 | } 27 | 28 | generated, err := ioutil.ReadFile(generator.options.Output) 29 | if err != nil { 30 | t.Errorf("file not generated = %v", err) 31 | } 32 | 33 | _, filename, _, _ := runtime.Caller(0) 34 | check, err := ioutil.ReadFile(path.Join(path.Dir(filename), "generator_test.output")) 35 | if err != nil { 36 | t.Errorf("check file not found = %v", err) 37 | } 38 | 39 | if string(generated) != string(check) { 40 | t.Errorf("generated does not match with check") 41 | return 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /generators/model/generator_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path" 7 | "runtime" 8 | "testing" 9 | 10 | "github.com/dizzyfool/genna/model" 11 | ) 12 | 13 | func TestGenerator_Generate(t *testing.T) { 14 | generator := New() 15 | 16 | generator.options.Def() 17 | generator.options.URL = `postgres://genna:genna@localhost:5432/genna?sslmode=disable` 18 | generator.options.Output = path.Join(os.TempDir(), "model_test.go") 19 | generator.options.FollowFKs = true 20 | generator.options.CustomTypes.Add(model.TypePGUuid, "uuid.UUID", "github.com/google/uuid") 21 | generator.options.GoPgVer = 10 22 | //generator.options.AddJSONTag = true 23 | 24 | if err := generator.Generate(); err != nil { 25 | t.Errorf("generate error = %v", err) 26 | return 27 | } 28 | 29 | generated, err := ioutil.ReadFile(generator.options.Output) 30 | if err != nil { 31 | t.Errorf("file not generated = %v", err) 32 | } 33 | 34 | _, filename, _, _ := runtime.Caller(0) 35 | check, err := ioutil.ReadFile(path.Join(path.Dir(filename), "generator_test.output")) 36 | if err != nil { 37 | t.Errorf("check file not found = %v", err) 38 | } 39 | 40 | if string(generated) != string(check) { 41 | t.Errorf("generated does not match with check") 42 | return 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /util/set_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestSet_Add(t *testing.T) { 8 | tests := []struct { 9 | name string 10 | old string 11 | new string 12 | want bool 13 | }{ 14 | { 15 | name: "Should add new element", 16 | old: "old", 17 | new: "new", 18 | want: true, 19 | }, 20 | { 21 | name: "Should not add old element", 22 | old: "old", 23 | new: "old", 24 | want: false, 25 | }, 26 | } 27 | for _, tt := range tests { 28 | t.Run(tt.name, func(t *testing.T) { 29 | s := NewSet() 30 | s.Add(tt.old) 31 | if got := s.Add(tt.new); got != tt.want { 32 | t.Errorf("Set.Add() = %v, want %v", got, tt.want) 33 | } 34 | }) 35 | } 36 | } 37 | 38 | func TestSet_Elements(t *testing.T) { 39 | tests := []struct { 40 | name string 41 | old string 42 | new string 43 | want []string 44 | }{ 45 | { 46 | name: "Should get 2 elements", 47 | old: "old", 48 | new: "new", 49 | want: []string{"old", "new"}, 50 | }, 51 | { 52 | name: "Should get 1 element", 53 | old: "old", 54 | new: "old", 55 | want: []string{"old"}, 56 | }, 57 | } 58 | for _, tt := range tests { 59 | t.Run(tt.name, func(t *testing.T) { 60 | s := NewSet() 61 | s.Add(tt.old) 62 | s.Add(tt.new) 63 | if got := s.Elements(); len(got) != len(tt.want) { 64 | t.Errorf("Set.Add() = %v, want %v", got, tt.want) 65 | } 66 | }) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /generators/named/template.go: -------------------------------------------------------------------------------- 1 | package named 2 | 3 | const Template = `//nolint 4 | //lint:file-ignore U1000 ignore unused code, it's generated 5 | package {{.Package}}{{if .HasImports}} 6 | 7 | import ({{range .Imports}} 8 | "{{.}}"{{end}} 9 | ){{end}} 10 | 11 | {{range .Entities}} 12 | type Columns{{.GoName}} struct{ 13 | {{range $i, $e := .Columns}}{{if $i}}, {{end}}{{.GoName}}{{end}} string{{if .HasRelations}} 14 | {{range $i, $e := .Relations}}{{if $i}}, {{end}}{{.GoName}}{{end}} string{{end}} 15 | } 16 | {{end}} 17 | type ColumnsSt struct { {{range .Entities}} 18 | {{.GoName}} Columns{{.GoName}}{{end}} 19 | } 20 | var Columns = ColumnsSt{ {{range .Entities}} 21 | {{.GoName}}: Columns{{.GoName}}{ {{range .Columns}} 22 | {{.GoName}}: "{{.PGName}}",{{end}}{{if .HasRelations}} 23 | {{range .Relations}} 24 | {{.GoName}}: "{{.GoName}}",{{end}}{{end}} 25 | },{{end}} 26 | } 27 | {{range .Entities}} 28 | type Table{{.GoName}} struct { 29 | Name{{if not .NoAlias}}, Alias{{end}} string 30 | } 31 | {{end}} 32 | type TablesSt struct { {{range .Entities}} 33 | {{.GoName}} Table{{.GoName}}{{end}} 34 | } 35 | var Tables = TablesSt { {{range .Entities}} 36 | {{.GoName}}: Table{{.GoName}}{ 37 | Name: "{{.PGFullName}}"{{if not .NoAlias}}, 38 | Alias: "{{.Alias}}",{{end}} 39 | },{{end}} 40 | } 41 | {{range $model := .Entities}} 42 | type {{.GoName}} struct { 43 | tableName struct{} {{.Tag}} 44 | {{range .Columns}} 45 | {{.GoName}} {{.Type}} {{.Tag}} {{.Comment}}{{end}}{{if .HasRelations}} 46 | {{range .Relations}} 47 | {{.GoName}} *{{.GoType}} {{.Tag}} {{.Comment}}{{end}}{{end}} 48 | } 49 | {{end}} 50 | ` 51 | -------------------------------------------------------------------------------- /generators/model/template.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | const Template = `//nolint 4 | //lint:file-ignore U1000 ignore unused code, it's generated 5 | package {{.Package}}{{if .HasImports}} 6 | 7 | import ({{range .Imports}} 8 | "{{.}}"{{end}} 9 | ){{end}} 10 | 11 | var Columns = struct { {{range .Entities}} 12 | {{.GoName}} struct{ 13 | {{range $i, $e := .Columns}}{{if $i}}, {{end}}{{.GoName}}{{end}} string{{if .HasRelations}} 14 | 15 | {{range $i, $e := .Relations}}{{if $i}}, {{end}}{{.GoName}}{{end}} string{{end}} 16 | }{{end}} 17 | }{ {{range .Entities}} 18 | {{.GoName}}: struct { 19 | {{range $i, $e := .Columns}}{{if $i}}, {{end}}{{.GoName}}{{end}} string{{if .HasRelations}} 20 | 21 | {{range $i, $e := .Relations}}{{if $i}}, {{end}}{{.GoName}}{{end}} string{{end}} 22 | }{ {{range .Columns}} 23 | {{.GoName}}: "{{.PGName}}",{{end}}{{if .HasRelations}} 24 | {{range .Relations}} 25 | {{.GoName}}: "{{.GoName}}",{{end}}{{end}} 26 | },{{end}} 27 | } 28 | 29 | var Tables = struct { {{range .Entities}} 30 | {{.GoName}} struct { 31 | Name{{if not .NoAlias }}, Alias{{end}} string 32 | }{{end}} 33 | }{ {{range .Entities}} 34 | {{.GoName}}: struct { 35 | Name{{if not .NoAlias}}, Alias{{end}} string 36 | }{ 37 | Name: "{{.PGFullName}}",{{if not .NoAlias}} 38 | Alias: "{{.Alias}}",{{end}} 39 | },{{end}} 40 | } 41 | {{range $model := .Entities}} 42 | type {{.GoName}} struct { 43 | tableName struct{} {{.Tag}} 44 | {{range .Columns}} 45 | {{.GoName}} {{.Type}} {{.Tag}} {{.Comment}}{{end}}{{if .HasRelations}} 46 | {{range .Relations}} 47 | {{.GoName}} *{{.GoType}} {{.Tag}} {{.Comment}}{{end}}{{end}} 48 | } 49 | {{end}} 50 | ` 51 | -------------------------------------------------------------------------------- /util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | const ( 9 | // PublicSchema is a default postgresql schema 10 | PublicSchema = "public" 11 | 12 | // DefaultPackage is a default package name 13 | DefaultPackage = "model" 14 | 15 | // DefaultAlias is a default alias for model 16 | DefaultAlias = "t" 17 | ) 18 | 19 | // Split splits full table name in schema and table name 20 | func Split(s string) (string, string) { 21 | d := strings.Split(s, ".") 22 | if len(d) < 2 { 23 | return PublicSchema, s 24 | } 25 | 26 | return d[0], d[1] 27 | } 28 | 29 | // Join joins table name and schema to full name 30 | func Join(schema, table string) string { 31 | return schema + "." + table 32 | } 33 | 34 | // JoinF joins table name and schema to full name filtering public 35 | func JoinF(schema, table string) string { 36 | if schema == PublicSchema { 37 | return table 38 | } 39 | 40 | return Join(schema, table) 41 | } 42 | 43 | // Quoted quotes entity name if needed 44 | func Quoted(fullName string, escape bool) string { 45 | if !HasUpper(fullName) { 46 | return fullName 47 | } 48 | 49 | pattern := `"%s"` 50 | if escape { 51 | pattern = `\"%s\"` 52 | } 53 | 54 | d := strings.Split(fullName, ".") 55 | if len(d) < 2 { 56 | return fmt.Sprintf(pattern, fullName) 57 | } 58 | 59 | return Join(fmt.Sprintf(pattern, d[0]), fmt.Sprintf(pattern, d[1])) 60 | } 61 | 62 | // Schemas get schemas from table names 63 | func Schemas(tables []string) (schemas []string) { 64 | index := map[string]struct{}{} 65 | for _, t := range tables { 66 | schema, _ := Split(t) 67 | if _, ok := index[schema]; !ok { 68 | index[schema] = struct{}{} 69 | schemas = append(schemas, schema) 70 | } 71 | } 72 | 73 | return 74 | } 75 | -------------------------------------------------------------------------------- /lib/database.go: -------------------------------------------------------------------------------- 1 | package genna 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "time" 8 | 9 | "github.com/go-pg/pg/v10" 10 | "github.com/go-pg/pg/v10/orm" 11 | ) 12 | 13 | // queryLogger helper struct for query logging 14 | type queryLogger struct { 15 | logger log.Logger 16 | } 17 | 18 | // newQueryLogger creates new helper struct for query logging 19 | func newQueryLogger(logger log.Logger) queryLogger { 20 | return queryLogger{logger: logger} 21 | } 22 | 23 | // BeforeQuery stores start time in custom data array 24 | func (ql queryLogger) BeforeQuery(ctx context.Context, event *pg.QueryEvent) (context.Context, error) { 25 | event.Stash = make(map[interface{}]interface{}) 26 | event.Stash["startedAt"] = time.Now() 27 | 28 | return ctx, nil 29 | } 30 | 31 | // AfterQuery calculates execution time and print it with formatted query 32 | func (ql queryLogger) AfterQuery(ctx context.Context, event *pg.QueryEvent) error { 33 | query, err := event.FormattedQuery() 34 | if err != nil { 35 | ql.logger.Printf("formatted query error: %s", err) 36 | } 37 | 38 | var since time.Duration 39 | if event.Stash != nil { 40 | if v, ok := event.Stash["startedAt"]; ok { 41 | if startAt, ok := v.(time.Time); ok { 42 | since = time.Since(startAt) 43 | } 44 | } 45 | } 46 | 47 | ql.logger.Printf("query: %s, duration: %d", query, since) 48 | return nil 49 | } 50 | 51 | // newDatabase creates database connection 52 | func newDatabase(url string, logger *log.Logger) (orm.DB, error) { 53 | options, err := pg.ParseURL(url) 54 | if err != nil { 55 | return nil, fmt.Errorf("parsing connection url error: %w", err) 56 | } 57 | 58 | client := pg.Connect(options) 59 | 60 | if logger != nil { 61 | client.AddQueryHook(newQueryLogger(*logger)) 62 | } 63 | 64 | return client, nil 65 | } 66 | -------------------------------------------------------------------------------- /util/index_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestIndex_Available(t *testing.T) { 8 | tests := []struct { 9 | name string 10 | old string 11 | new string 12 | want bool 13 | }{ 14 | { 15 | name: "Should check for available", 16 | old: "old", 17 | new: "new", 18 | want: true, 19 | }, 20 | { 21 | name: "Should check for not available", 22 | old: "old", 23 | new: "old", 24 | want: false, 25 | }, 26 | } 27 | for _, tt := range tests { 28 | t.Run(tt.name, func(t *testing.T) { 29 | i := NewIndex() 30 | i.Add(tt.old) 31 | if got := i.Available(tt.new); got != tt.want { 32 | t.Errorf("Index.Available() = %v, want %v", got, tt.want) 33 | } 34 | }) 35 | } 36 | } 37 | 38 | func TestIndex_GetNext(t *testing.T) { 39 | tests := []struct { 40 | name string 41 | index map[string]struct{} 42 | s string 43 | want string 44 | }{ 45 | { 46 | name: "Should get same if available", 47 | index: map[string]struct{}{"old": {}}, 48 | s: "new", 49 | want: "new", 50 | }, 51 | { 52 | name: "Should get next", 53 | index: map[string]struct{}{"old": {}}, 54 | s: "old", 55 | want: "old1", 56 | }, 57 | { 58 | name: "Should get second", 59 | index: map[string]struct{}{"old": {}, "old1": {}}, 60 | s: "old", 61 | want: "old2", 62 | }, 63 | { 64 | name: "Should check for existing", 65 | index: map[string]struct{}{"old": {}, "old1": {}}, 66 | s: "old1", 67 | want: "old11", 68 | }, 69 | } 70 | for _, tt := range tests { 71 | t.Run(tt.name, func(t *testing.T) { 72 | i := NewIndex() 73 | i.index = tt.index 74 | if got := i.GetNext(tt.s); got != tt.want { 75 | t.Errorf("Index.GetNext() = %v, want %v", got, tt.want) 76 | } 77 | }) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /generators/validate/template.go: -------------------------------------------------------------------------------- 1 | package validate 2 | 3 | const Template = `//nolint 4 | //lint:file-ignore U1000 ignore unused code, it's generated 5 | package {{.Package}}{{if .HasImports}} 6 | 7 | import ({{range .Imports}} 8 | "{{.}}"{{end}} 9 | ){{end}} 10 | 11 | const ( 12 | ErrEmptyValue = "empty" 13 | ErrMaxLength = "len" 14 | ErrWrongValue = "value" 15 | ) 16 | 17 | {{range $model := .Entities}} 18 | func (m {{.GoName}}) Validate() (errors map[string]string, valid bool) { 19 | errors = map[string]string{} 20 | 21 | {{range .Columns}} 22 | {{if eq .Check "nil" }} 23 | if m.{{.GoName}} == nil { 24 | errors[Columns.{{$model.GoName}}.{{.GoName}}] = ErrEmptyValue 25 | } 26 | {{else if eq .Check "zero"}} 27 | if m.{{.GoName}} == 0 { 28 | errors[Columns.{{$model.GoName}}.{{.GoName}}] = ErrEmptyValue 29 | } 30 | {{else if eq .Check "pzero"}} 31 | if m.{{.GoName}} != nil && *m.{{.GoName}} == 0 { 32 | errors[Columns.{{$model.GoName}}.{{.GoName}}] = ErrEmptyValue 33 | } 34 | {{else if eq .Check "len"}} 35 | if utf8.RuneCountInString(m.{{.GoName}}) > {{.MaxLen}} { 36 | errors[Columns.{{$model.GoName}}.{{.GoName}}] = ErrMaxLength 37 | } 38 | {{else if eq .Check "plen"}} 39 | if m.{{.GoName}} != nil && utf8.RuneCountInString(*m.{{.GoName}}) > {{.MaxLen}} { 40 | errors[Columns.{{$model.GoName}}.{{.GoName}}] = ErrMaxLength 41 | } 42 | {{else if eq .Check "enum"}} 43 | switch m.{{.GoName}} { 44 | case {{.Enum}}: 45 | default: 46 | errors[Columns.{{$model.GoName}}.{{.GoName}}] = ErrWrongValue 47 | } 48 | {{else if eq .Check "penum"}} 49 | if m.{{.GoName}} != nil { 50 | switch *m.{{.GoName}} { 51 | case {{.Enum}}: 52 | default: 53 | errors[Columns.{{$model.GoName}}.{{.GoName}}] = ErrWrongValue 54 | } 55 | } 56 | {{end}} 57 | {{end}} 58 | 59 | return errors, len(errors) == 0 60 | } 61 | {{end}} 62 | ` 63 | -------------------------------------------------------------------------------- /model/column.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/dizzyfool/genna/util" 5 | ) 6 | 7 | // Column stores information about column 8 | type Column struct { 9 | GoName string 10 | PGName string 11 | 12 | Type string 13 | 14 | GoType string 15 | PGType string 16 | 17 | Default string 18 | HasDefault bool 19 | Nullable bool 20 | 21 | IsArray bool 22 | Dimensions int 23 | 24 | IsPK bool 25 | IsFK bool 26 | Relation *Relation 27 | 28 | Import string 29 | 30 | MaxLen int 31 | Values []string 32 | } 33 | 34 | // NewColumn creates Column from pg info 35 | func NewColumn(pgName string, pgType, defaultValue string, hasDefault, nullable, sqlNulls, array bool, dims int, pk, fk bool, len int, values []string, goPGVer int, customTypes CustomTypeMapping) Column { 36 | var ( 37 | err error 38 | ok bool 39 | ) 40 | 41 | column := Column{ 42 | PGName: pgName, 43 | PGType: pgType, 44 | Nullable: nullable, 45 | IsArray: array, 46 | Dimensions: dims, 47 | IsPK: pk, 48 | IsFK: fk, 49 | MaxLen: len, 50 | Values: values, 51 | Default: defaultValue, 52 | HasDefault: hasDefault, 53 | GoName: util.ColumnName(pgName), 54 | } 55 | 56 | if customTypes == nil { 57 | customTypes = CustomTypeMapping{} 58 | } 59 | 60 | if column.GoType, ok = customTypes.GoType(pgType); !ok || column.GoType == "" { 61 | if column.GoType, err = GoType(pgType); err != nil { 62 | column.GoType = "interface{}" 63 | } 64 | } 65 | 66 | switch { 67 | case column.IsArray: 68 | column.Type, err = GoSlice(pgType, dims) 69 | case column.Nullable: 70 | column.Type, err = GoNullable(pgType, sqlNulls, customTypes) 71 | default: 72 | column.Type = column.GoType 73 | } 74 | 75 | if err != nil { 76 | column.Type = column.GoType 77 | } 78 | 79 | if column.Import, ok = customTypes.GoImport(pgType); !ok { 80 | column.Import = GoImport(pgType, nullable, sqlNulls, goPGVer) 81 | } 82 | 83 | return column 84 | } 85 | 86 | // AddRelation adds relation to column. Should be used if FK 87 | func (c *Column) AddRelation(relation *Relation) { 88 | c.Relation = relation 89 | } 90 | -------------------------------------------------------------------------------- /util/tags_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestAnnotation_AddTag(t *testing.T) { 8 | type fields struct { 9 | tags []tag 10 | } 11 | type args struct { 12 | name string 13 | value string 14 | } 15 | tests := []struct { 16 | name string 17 | fields fields 18 | args args 19 | want int 20 | }{ 21 | { 22 | name: "Should add new tag", 23 | fields: fields{[]tag{}}, 24 | args: args{"tag", "value"}, 25 | want: 1, 26 | }, 27 | { 28 | name: "Should append to existing tag", 29 | fields: fields{[]tag{{"tag", []string{"value1"}}}}, 30 | args: args{"tag", "value2"}, 31 | want: 1, 32 | }, 33 | } 34 | for _, tt := range tests { 35 | t.Run(tt.name, func(t *testing.T) { 36 | a := NewAnnotation() 37 | a.tags = tt.fields.tags 38 | 39 | a.AddTag(tt.args.name, tt.args.value) 40 | if ln := len(a.tags); ln != tt.want { 41 | t.Errorf("Tags len = %v, want %v", ln, tt.want) 42 | } 43 | }) 44 | } 45 | } 46 | 47 | func TestAnnotation_String(t *testing.T) { 48 | type fields struct { 49 | tags []tag 50 | } 51 | tests := []struct { 52 | name string 53 | fields fields 54 | want string 55 | }{ 56 | { 57 | name: "Should print one tag", 58 | fields: fields{[]tag{{"tag1", []string{"valueA"}}}}, 59 | want: `tag1:"valueA"`, 60 | }, 61 | { 62 | name: "Should print several tags", 63 | fields: fields{[]tag{ 64 | {"tag1", []string{"valueA"}}, 65 | {"tag2", []string{"valueB"}}, 66 | {"tag3", []string{"valueC"}}, 67 | }}, 68 | want: `tag1:"valueA" tag2:"valueB" tag3:"valueC"`, 69 | }, 70 | { 71 | name: "Should print several tags with several values", 72 | fields: fields{[]tag{ 73 | {"tag1", []string{"valueA"}}, 74 | {"tag2", []string{"valueB", "valueC"}}, 75 | }}, 76 | want: `tag1:"valueA" tag2:"valueB,valueC"`, 77 | }, 78 | } 79 | for _, tt := range tests { 80 | t.Run(tt.name, func(t *testing.T) { 81 | a := &Annotation{ 82 | tags: tt.fields.tags, 83 | } 84 | if got := a.String(); got != tt.want { 85 | t.Errorf("Annotation.String() = %v, want %v", got, tt.want) 86 | } 87 | }) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /generators/validate/generator.go: -------------------------------------------------------------------------------- 1 | package validate 2 | 3 | import ( 4 | "github.com/dizzyfool/genna/generators/base" 5 | "github.com/dizzyfool/genna/model" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | const ( 10 | keepPK = "keep-pk" 11 | ) 12 | 13 | // CreateCommand creates generator command 14 | func CreateCommand() *cobra.Command { 15 | return base.CreateCommand("validation", "Validation generator for go-pg models", New()) 16 | } 17 | 18 | // Validate represents validate generator 19 | type Validate struct { 20 | options Options 21 | } 22 | 23 | // New creates generator 24 | func New() *Validate { 25 | return &Validate{} 26 | } 27 | 28 | // Options gets options 29 | func (g *Validate) Options() *Options { 30 | return &g.options 31 | } 32 | 33 | // SetOptions sets options 34 | func (g *Validate) SetOptions(options Options) { 35 | g.options = options 36 | } 37 | 38 | // AddFlags adds flags to command 39 | func (g *Validate) AddFlags(command *cobra.Command) { 40 | base.AddFlags(command) 41 | 42 | flags := command.Flags() 43 | flags.SortFlags = false 44 | 45 | flags.BoolP(keepPK, "k", false, "keep primary key name as is (by default it should be converted to 'ID')") 46 | } 47 | 48 | // ReadFlags read flags from command 49 | func (g *Validate) ReadFlags(command *cobra.Command) error { 50 | var err error 51 | 52 | g.options.URL, g.options.Output, g.options.Package, g.options.Tables, g.options.FollowFKs, g.options.GoPgVer, g.options.CustomTypes, err = base.ReadFlags(command) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | flags := command.Flags() 58 | 59 | if g.options.KeepPK, err = flags.GetBool(keepPK); err != nil { 60 | return err 61 | } 62 | 63 | // setting defaults 64 | g.options.Def() 65 | 66 | return nil 67 | } 68 | 69 | // Generate runs whole generation process 70 | func (g *Validate) Generate() error { 71 | return base.NewGenerator(g.options.URL). 72 | Generate( 73 | g.options.Tables, 74 | g.options.FollowFKs, 75 | false, 76 | g.options.Output, 77 | Template, 78 | g.Packer(), 79 | g.options.GoPgVer, 80 | g.options.CustomTypes, 81 | ) 82 | } 83 | 84 | // Packer returns packer function for compile entities into package 85 | func (g *Validate) Packer() base.Packer { 86 | return func(entities []model.Entity) (interface{}, error) { 87 | return NewTemplatePackage(entities, g.options), nil 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /generators/search/template.go: -------------------------------------------------------------------------------- 1 | package search 2 | 3 | const Template = `//nolint 4 | //lint:file-ignore U1000 ignore unused code, it's generated 5 | package {{.Package}} 6 | 7 | import ({{if .HasImports}}{{range .Imports}} 8 | "{{.}}"{{end}} 9 | {{end}} 10 | "github.com/go-pg/pg{{.GoPGVer}}" 11 | "github.com/go-pg/pg{{.GoPGVer}}/orm" 12 | ) 13 | 14 | const condition = "?.? = ?" 15 | 16 | // base filters 17 | type applier func(query *orm.Query) (*orm.Query, error) 18 | 19 | type search struct { 20 | appliers[] applier 21 | } 22 | 23 | func (s *search) apply(query *orm.Query) { 24 | for _, applier := range s.appliers { 25 | query.Apply(applier) 26 | } 27 | } 28 | 29 | func (s *search) where(query *orm.Query, table, field string, value interface{}) { 30 | {{if eq .GoPGVer ""}} 31 | query.Where(condition, pg.F(table), pg.F(field), value) 32 | {{else}} 33 | query.Where(condition, pg.Ident(table), pg.Ident(field), value) 34 | {{end}} 35 | } 36 | 37 | func (s *search) WithApply(a applier) { 38 | if s.appliers == nil { 39 | s.appliers = []applier{} 40 | } 41 | s.appliers = append(s.appliers, a) 42 | } 43 | 44 | func (s *search) With(condition string, params ...interface{}) { 45 | s.WithApply(func(query *orm.Query) (*orm.Query, error) { 46 | return query.Where(condition, params...), nil 47 | }) 48 | } 49 | 50 | // Searcher is interface for every generated filter 51 | type Searcher interface { 52 | Apply(query *orm.Query) *orm.Query 53 | Q() applier 54 | 55 | With(condition string, params ...interface{}) 56 | WithApply(a applier) 57 | } 58 | 59 | {{range $model := .Entities}} 60 | type {{.GoName}}Search struct { 61 | search 62 | 63 | {{range .Columns}} 64 | {{.GoName}} {{.Type}}{{if .HasTags}} {{.Tag}}{{end}}{{end}} 65 | } 66 | 67 | func (s *{{.GoName}}Search) Apply(query *orm.Query) *orm.Query { {{range .Columns}}{{if .Relaxed}} 68 | if !reflect.ValueOf(s.{{.GoName}}).IsNil(){ {{else}} 69 | if s.{{.GoName}} != nil { {{end}}{{if .UseCustomRender}} 70 | {{.CustomRender}}{{else}} 71 | s.where(query, Tables.{{$model.GoName}}.{{if not $model.NoAlias}}Alias{{else}}Name{{end}}, Columns.{{$model.GoName}}.{{.GoName}}, s.{{.GoName}}){{end}} 72 | }{{end}} 73 | 74 | s.apply(query) 75 | 76 | return query 77 | } 78 | 79 | func (s *{{.GoName}}Search) Q() applier { 80 | return func(query *orm.Query) (*orm.Query, error) { 81 | return s.Apply(query), nil 82 | } 83 | } 84 | {{end}} 85 | ` 86 | -------------------------------------------------------------------------------- /util/util_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestSchemas(t *testing.T) { 9 | type args struct { 10 | tables []string 11 | } 12 | tests := []struct { 13 | name string 14 | args args 15 | wantSchemas []string 16 | }{ 17 | { 18 | name: "Should get public schema", 19 | args: args{ 20 | []string{"users", "locations"}, 21 | }, 22 | wantSchemas: []string{PublicSchema}, 23 | }, 24 | { 25 | name: "Should get public schema from full table names", 26 | args: args{ 27 | []string{"users", "public.locations"}, 28 | }, 29 | wantSchemas: []string{PublicSchema}, 30 | }, 31 | { 32 | name: "Should get different schemas from full table names", 33 | args: args{ 34 | []string{"users.users", "users.locations", "orders.orders"}, 35 | }, 36 | wantSchemas: []string{"users", "orders"}, 37 | }, 38 | } 39 | for _, tt := range tests { 40 | t.Run(tt.name, func(t *testing.T) { 41 | if gotSchemas := Schemas(tt.args.tables); !reflect.DeepEqual(gotSchemas, tt.wantSchemas) { 42 | t.Errorf("Schemas() = %v, want %v", gotSchemas, tt.wantSchemas) 43 | } 44 | }) 45 | } 46 | } 47 | 48 | func TestSplit(t *testing.T) { 49 | type args struct { 50 | input string 51 | } 52 | tests := []struct { 53 | name string 54 | args args 55 | want string 56 | want1 string 57 | }{ 58 | { 59 | name: "Should split full name", 60 | args: args{"public.users"}, 61 | want: "public", 62 | want1: "users", 63 | }, 64 | { 65 | name: "Should split simple name", 66 | args: args{"users"}, 67 | want: PublicSchema, 68 | want1: "users", 69 | }, 70 | } 71 | for _, tt := range tests { 72 | t.Run(tt.name, func(t *testing.T) { 73 | got, got1 := Split(tt.args.input) 74 | if got != tt.want { 75 | t.Errorf("Split() got = %v, want %v", got, tt.want) 76 | } 77 | if got1 != tt.want1 { 78 | t.Errorf("Split() got1 = %v, want %v", got1, tt.want1) 79 | } 80 | }) 81 | } 82 | } 83 | 84 | func TestJoin(t *testing.T) { 85 | type args struct { 86 | schema string 87 | table string 88 | } 89 | tests := []struct { 90 | name string 91 | args args 92 | want string 93 | }{ 94 | { 95 | name: "Should join", 96 | args: args{"public", "users"}, 97 | want: "public.users", 98 | }, 99 | } 100 | for _, tt := range tests { 101 | t.Run(tt.name, func(t *testing.T) { 102 | if got := Join(tt.args.schema, tt.args.table); got != tt.want { 103 | t.Errorf("Join() = %v, want %v", got, tt.want) 104 | } 105 | }) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /lib/genna.go: -------------------------------------------------------------------------------- 1 | package genna 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/dizzyfool/genna/model" 8 | "github.com/dizzyfool/genna/util" 9 | 10 | "github.com/go-pg/pg/v10/orm" 11 | ) 12 | 13 | // Genna is struct should be embedded to custom generator when genna used as library 14 | type Genna struct { 15 | url string 16 | 17 | DB orm.DB 18 | Store *store 19 | 20 | Logger *log.Logger 21 | } 22 | 23 | // New creates Genna 24 | func New(url string, logger *log.Logger) Genna { 25 | return Genna{ 26 | url: url, 27 | Logger: logger, 28 | } 29 | } 30 | 31 | func (g *Genna) Connect() error { 32 | var err error 33 | 34 | if g.DB == nil { 35 | if g.DB, err = newDatabase(g.url, g.Logger); err != nil { 36 | return fmt.Errorf("unable to connect to DB: %w", err) 37 | } 38 | 39 | g.Store = newStore(g.DB) 40 | } 41 | 42 | return nil 43 | } 44 | 45 | // Read reads database and gets entities with columns and relations 46 | func (g *Genna) Read(selected []string, followFK, useSQLNulls bool, goPGVer int, customTypes model.CustomTypeMapping) ([]model.Entity, error) { 47 | if err := g.Connect(); err != nil { 48 | return nil, err 49 | } 50 | 51 | tables, err := g.Store.Tables(selected) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | if len(tables) == 0 { 57 | return nil, fmt.Errorf("no tables found") 58 | } 59 | 60 | relations, err := g.Store.Relations(tables) 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | if followFK { 66 | set := util.NewSet() 67 | for _, t := range tables { 68 | set.Add(util.Join(t.Schema, t.Name)) 69 | } 70 | 71 | for _, r := range relations { 72 | t := r.Target() 73 | if set.Add(util.Join(t.Schema, t.Name)) { 74 | tables = append(tables, t) 75 | } 76 | } 77 | } 78 | 79 | tables = Sort(tables) 80 | 81 | columns, err := g.Store.Columns(tables) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | entities := make([]model.Entity, len(tables)) 87 | index := map[string]int{} 88 | for i, t := range tables { 89 | index[util.Join(t.Schema, t.Name)] = i 90 | entities[i] = t.Entity() 91 | } 92 | 93 | for _, c := range columns { 94 | if i, ok := index[util.Join(c.Schema, c.Table)]; ok { 95 | entities[i].AddColumn(c.Column(useSQLNulls, goPGVer, customTypes)) 96 | } 97 | } 98 | 99 | for _, r := range relations { 100 | rel := r.Relation() 101 | if i, ok := index[util.Join(r.SourceSchema, r.SourceTable)]; ok { 102 | entities[i].AddRelation(rel) 103 | } 104 | if i, ok := index[util.Join(r.TargetSchema, r.TargetTable)]; ok { 105 | rel.AddEntity(&entities[i]) 106 | } 107 | } 108 | 109 | return entities, nil 110 | } 111 | -------------------------------------------------------------------------------- /generators/model/generator_test.output: -------------------------------------------------------------------------------- 1 | //nolint 2 | //lint:file-ignore U1000 ignore unused code, it's generated 3 | package model 4 | 5 | import ( 6 | "github.com/google/uuid" 7 | "time" 8 | ) 9 | 10 | var Columns = struct { 11 | Project struct { 12 | ID, Code, Name string 13 | } 14 | User struct { 15 | ID, Email, Activated, Name, CountryID, Avatar, AvatarAlt, ApiKeys, LoggedAt string 16 | 17 | Country string 18 | } 19 | GeoCountry struct { 20 | ID, Code, Coords string 21 | } 22 | }{ 23 | Project: struct { 24 | ID, Code, Name string 25 | }{ 26 | ID: "projectId", 27 | Code: "code", 28 | Name: "name", 29 | }, 30 | User: struct { 31 | ID, Email, Activated, Name, CountryID, Avatar, AvatarAlt, ApiKeys, LoggedAt string 32 | 33 | Country string 34 | }{ 35 | ID: "userId", 36 | Email: "email", 37 | Activated: "activated", 38 | Name: "name", 39 | CountryID: "countryId", 40 | Avatar: "avatar", 41 | AvatarAlt: "avatarAlt", 42 | ApiKeys: "apiKeys", 43 | LoggedAt: "loggedAt", 44 | 45 | Country: "Country", 46 | }, 47 | GeoCountry: struct { 48 | ID, Code, Coords string 49 | }{ 50 | ID: "countryId", 51 | Code: "code", 52 | Coords: "coords", 53 | }, 54 | } 55 | 56 | var Tables = struct { 57 | Project struct { 58 | Name, Alias string 59 | } 60 | User struct { 61 | Name, Alias string 62 | } 63 | GeoCountry struct { 64 | Name, Alias string 65 | } 66 | }{ 67 | Project: struct { 68 | Name, Alias string 69 | }{ 70 | Name: "projects", 71 | Alias: "t", 72 | }, 73 | User: struct { 74 | Name, Alias string 75 | }{ 76 | Name: "users", 77 | Alias: "t", 78 | }, 79 | GeoCountry: struct { 80 | Name, Alias string 81 | }{ 82 | Name: "geo.countries", 83 | Alias: "t", 84 | }, 85 | } 86 | 87 | type Project struct { 88 | tableName struct{} `pg:"projects,alias:t,discard_unknown_columns"` 89 | 90 | ID uuid.UUID `pg:"projectId,pk,type:uuid"` 91 | Code *uuid.UUID `pg:"code,type:uuid"` 92 | Name string `pg:"name,use_zero"` 93 | } 94 | 95 | type User struct { 96 | tableName struct{} `pg:"users,alias:t,discard_unknown_columns"` 97 | 98 | ID int `pg:"userId,pk"` 99 | Email string `pg:"email,use_zero"` 100 | Activated bool `pg:"activated,use_zero"` 101 | Name *string `pg:"name"` 102 | CountryID *int `pg:"countryId"` 103 | Avatar []byte `pg:"avatar,use_zero"` 104 | AvatarAlt []byte `pg:"avatarAlt"` 105 | ApiKeys [][]byte `pg:"apiKeys,array"` 106 | LoggedAt *time.Time `pg:"loggedAt"` 107 | 108 | Country *GeoCountry `pg:"fk:countryId,rel:has-one"` 109 | } 110 | 111 | type GeoCountry struct { 112 | tableName struct{} `pg:"geo.countries,alias:t,discard_unknown_columns"` 113 | 114 | ID int `pg:"countryId,pk"` 115 | Code string `pg:"code,use_zero"` 116 | Coords []int `pg:"coords,array"` 117 | } 118 | -------------------------------------------------------------------------------- /generators/validate/README.md: -------------------------------------------------------------------------------- 1 | ## Validate functions generator 2 | 3 | **Basic model required, which can be generated with one of the model generators** 4 | 5 | Use `validation` sub-command to execute generator: 6 | 7 | `genna validation -h` 8 | 9 | First create your database and tables in it 10 | 11 | ```sql 12 | create table "projects" 13 | ( 14 | "projectId" serial not null, 15 | "name" text not null, 16 | 17 | primary key ("projectId") 18 | ); 19 | 20 | create table "users" 21 | ( 22 | "userId" serial not null, 23 | "email" varchar(64) not null, 24 | "activated" bool not null default false, 25 | "name" varchar(128), 26 | "countryId" integer, 27 | 28 | primary key ("userId") 29 | ); 30 | 31 | create schema "geo"; 32 | create table geo."countries" 33 | ( 34 | "countryId" serial not null, 35 | "code" varchar(3) not null, 36 | "coords" integer[], 37 | 38 | primary key ("countryId") 39 | ); 40 | 41 | alter table "users" 42 | add constraint "fk_user_country" 43 | foreign key ("countryId") 44 | references geo."countries" ("countryId") on update restrict on delete restrict; 45 | ``` 46 | 47 | ### Run generator 48 | 49 | `genna validate -c postgres://user:password@localhost:5432/yourdb -o ~/output/model.go -t public.* -f` 50 | 51 | You should get following functions on model package: 52 | 53 | ```go 54 | //lint:file-ignore U1000 ignore unused code, it's generated 55 | package model 56 | 57 | import ( 58 | "unicode/utf8" 59 | ) 60 | 61 | const ( 62 | ErrEmptyValue = "empty" 63 | ErrMaxLength = "len" 64 | ErrWrongValue = "value" 65 | ) 66 | 67 | func (m User) Validate() (errors map[string]string, valid bool) { 68 | errors = map[string]string{} 69 | 70 | if utf8.RuneCountInString(m.Email) > 64 { 71 | errors[Columns.User.Email] = ErrMaxLength 72 | } 73 | 74 | if m.Name != nil && utf8.RuneCountInString(*m.Name) > 128 { 75 | errors[Columns.User.Name] = ErrMaxLength 76 | } 77 | 78 | if m.CountryID != nil && *m.CountryID == 0 { 79 | errors[Columns.User.CountryID] = ErrEmptyValue 80 | } 81 | 82 | return errors, len(errors) == 0 83 | } 84 | 85 | func (m GeoCountry) Validate() (errors map[string]string, valid bool) { 86 | errors = map[string]string{} 87 | 88 | if utf8.RuneCountInString(m.Code) > 3 { 89 | errors[Columns.GeoCountry.Code] = ErrMaxLength 90 | } 91 | 92 | return errors, len(errors) == 0 93 | } 94 | 95 | ``` 96 | 97 | ### Try it 98 | 99 | ```go 100 | package model 101 | 102 | import ( 103 | "fmt" 104 | "testing" 105 | ) 106 | 107 | func TestModel(t *testing.T) { 108 | code := "should fail on length" 109 | country := GeoCountry{ 110 | Code: code, 111 | } 112 | errors, valid := country.Validate() 113 | 114 | fmt.Printf("%#v\n", errors) 115 | fmt.Printf("%#v\n", valid) 116 | } 117 | 118 | ``` 119 | -------------------------------------------------------------------------------- /generators/named/generator_test.output: -------------------------------------------------------------------------------- 1 | //nolint 2 | //lint:file-ignore U1000 ignore unused code, it's generated 3 | package model 4 | 5 | import ( 6 | "time" 7 | ) 8 | 9 | type ColumnsProject struct { 10 | ID, Code, Name string 11 | } 12 | 13 | type ColumnsUser struct { 14 | ID, Email, Activated, Name, CountryID, Avatar, AvatarAlt, ApiKeys, LoggedAt string 15 | Country string 16 | } 17 | 18 | type ColumnsGeoCountry struct { 19 | ID, Code, Coords string 20 | } 21 | 22 | type ColumnsSt struct { 23 | Project ColumnsProject 24 | User ColumnsUser 25 | GeoCountry ColumnsGeoCountry 26 | } 27 | 28 | var Columns = ColumnsSt{ 29 | Project: ColumnsProject{ 30 | ID: "projectId", 31 | Code: "code", 32 | Name: "name", 33 | }, 34 | User: ColumnsUser{ 35 | ID: "userId", 36 | Email: "email", 37 | Activated: "activated", 38 | Name: "name", 39 | CountryID: "countryId", 40 | Avatar: "avatar", 41 | AvatarAlt: "avatarAlt", 42 | ApiKeys: "apiKeys", 43 | LoggedAt: "loggedAt", 44 | 45 | Country: "Country", 46 | }, 47 | GeoCountry: ColumnsGeoCountry{ 48 | ID: "countryId", 49 | Code: "code", 50 | Coords: "coords", 51 | }, 52 | } 53 | 54 | type TableProject struct { 55 | Name, Alias string 56 | } 57 | 58 | type TableUser struct { 59 | Name, Alias string 60 | } 61 | 62 | type TableGeoCountry struct { 63 | Name, Alias string 64 | } 65 | 66 | type TablesSt struct { 67 | Project TableProject 68 | User TableUser 69 | GeoCountry TableGeoCountry 70 | } 71 | 72 | var Tables = TablesSt{ 73 | Project: TableProject{ 74 | Name: "projects", 75 | Alias: "t", 76 | }, 77 | User: TableUser{ 78 | Name: "users", 79 | Alias: "t", 80 | }, 81 | GeoCountry: TableGeoCountry{ 82 | Name: "geo.countries", 83 | Alias: "t", 84 | }, 85 | } 86 | 87 | type Project struct { 88 | tableName struct{} `pg:"projects,alias:t,discard_unknown_columns"` 89 | 90 | ID string `pg:"projectId,pk,type:uuid"` 91 | Code *string `pg:"code,type:uuid"` 92 | Name string `pg:"name,use_zero"` 93 | } 94 | 95 | type User struct { 96 | tableName struct{} `pg:"users,alias:t,discard_unknown_columns"` 97 | 98 | ID int `pg:"userId,pk"` 99 | Email string `pg:"email,use_zero"` 100 | Activated bool `pg:"activated,use_zero"` 101 | Name *string `pg:"name"` 102 | CountryID *int `pg:"countryId"` 103 | Avatar []byte `pg:"avatar,use_zero"` 104 | AvatarAlt []byte `pg:"avatarAlt"` 105 | ApiKeys [][]byte `pg:"apiKeys,array"` 106 | LoggedAt *time.Time `pg:"loggedAt"` 107 | 108 | Country *GeoCountry `pg:"fk:countryId,rel:has-one"` 109 | } 110 | 111 | type GeoCountry struct { 112 | tableName struct{} `pg:"geo.countries,alias:t,discard_unknown_columns"` 113 | 114 | ID int `pg:"countryId,pk"` 115 | Code string `pg:"code,use_zero"` 116 | Coords []int `pg:"coords,array"` 117 | } 118 | -------------------------------------------------------------------------------- /model/entity.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/dizzyfool/genna/util" 5 | ) 6 | 7 | // Entity stores information about table 8 | type Entity struct { 9 | GoName string 10 | GoNamePlural string 11 | PGName string 12 | PGSchema string 13 | PGFullName string 14 | 15 | ViewName string 16 | 17 | Columns []Column 18 | Relations []Relation 19 | 20 | Imports []string 21 | 22 | // helper indexes 23 | colIndex util.Index 24 | impIndex map[string]struct{} 25 | } 26 | 27 | // NewEntity creates new Entity from pg info 28 | func NewEntity(schema, pgName string, columns []Column, relations []Relation) Entity { 29 | goName := util.EntityName(pgName) 30 | if schema != util.PublicSchema { 31 | goName = util.CamelCased(schema) + goName 32 | } 33 | 34 | goNamePlural := util.CamelCased(util.Sanitize(pgName)) 35 | if schema != util.PublicSchema { 36 | goNamePlural = util.CamelCased(schema) + goNamePlural 37 | } 38 | 39 | entity := Entity{ 40 | GoName: goName, 41 | GoNamePlural: goNamePlural, 42 | PGName: pgName, 43 | PGSchema: schema, 44 | PGFullName: util.JoinF(schema, pgName), 45 | 46 | Columns: []Column{}, 47 | Relations: []Relation{}, 48 | colIndex: util.NewIndex(), 49 | 50 | Imports: []string{}, 51 | impIndex: map[string]struct{}{}, 52 | } 53 | 54 | if columns != nil { 55 | for _, col := range columns { 56 | entity.AddColumn(col) 57 | } 58 | } 59 | 60 | if relations != nil { 61 | for _, rel := range relations { 62 | entity.AddRelation(rel) 63 | } 64 | } 65 | 66 | return entity 67 | } 68 | 69 | // AddColumn adds column to entity 70 | func (e *Entity) AddColumn(column Column) { 71 | if !e.colIndex.Available(column.GoName) { 72 | column.GoName = e.colIndex.GetNext(column.GoName) 73 | } 74 | e.colIndex.Add(column.GoName) 75 | 76 | e.Columns = append(e.Columns, column) 77 | 78 | if imp := column.Import; imp != "" { 79 | if _, ok := e.impIndex[imp]; !ok { 80 | e.impIndex[imp] = struct{}{} 81 | e.Imports = append(e.Imports, imp) 82 | } 83 | } 84 | } 85 | 86 | // AddRelation adds relation to entity 87 | func (e *Entity) AddRelation(relation Relation) { 88 | if !e.colIndex.Available(relation.GoName) { 89 | relation.GoName = e.colIndex.GetNext(relation.GoName + util.Rel) 90 | } 91 | e.colIndex.Add(relation.GoName) 92 | 93 | e.Relations = append(e.Relations, relation) 94 | 95 | // adding relation to column 96 | for _, field := range relation.FKFields { 97 | for i, column := range e.Columns { 98 | if column.PGName == field { 99 | e.Columns[i].AddRelation(&relation) 100 | } 101 | } 102 | } 103 | } 104 | 105 | // HasMultiplePKs checks if entity has many primary keys 106 | func (e *Entity) HasMultiplePKs() bool { 107 | counter := 0 108 | for _, col := range e.Columns { 109 | if col.IsPK { 110 | counter++ 111 | } 112 | 113 | if counter > 1 { 114 | return true 115 | } 116 | } 117 | 118 | return false 119 | } 120 | -------------------------------------------------------------------------------- /model/custom_types.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | var reg = regexp.MustCompile(`/v\d+$`) 11 | 12 | type CustomType struct { 13 | PGType string 14 | 15 | GoType string 16 | GoImport string 17 | } 18 | 19 | type CustomTypeMapping map[string]CustomType 20 | 21 | func (c CustomTypeMapping) Add(pgType, goType, goImport string) { 22 | c[pgType] = CustomType{ 23 | PGType: pgType, 24 | GoType: goType, 25 | GoImport: goImport, 26 | } 27 | } 28 | 29 | func (c CustomTypeMapping) Imports() []string { 30 | index := map[string]struct{}{} 31 | 32 | var result []string 33 | for _, customType := range c { 34 | if _, ok := index[customType.GoImport]; ok { 35 | continue 36 | } 37 | 38 | if customType.GoImport == "" { 39 | continue 40 | } 41 | 42 | result = append(result, customType.GoImport) 43 | index[customType.GoImport] = struct{}{} 44 | } 45 | 46 | return result 47 | } 48 | 49 | func (c CustomTypeMapping) Has(pgType string) bool { 50 | _, ok := c[pgType] 51 | return ok 52 | } 53 | 54 | func (c CustomTypeMapping) GoType(pgType string) (string, bool) { 55 | if customType, ok := c[pgType]; ok && customType.GoType != "" { 56 | return customType.GoType, true 57 | } 58 | 59 | return "", false 60 | } 61 | 62 | func (c CustomTypeMapping) GoImport(pgType string) (string, bool) { 63 | if customType, ok := c[pgType]; ok && customType.GoType != "" { 64 | return customType.GoImport, true 65 | } 66 | 67 | return "", false 68 | } 69 | 70 | func ParseCustomTypes(raw []string) (CustomTypeMapping, error) { 71 | ctm := CustomTypeMapping{} 72 | 73 | for _, customType := range raw { 74 | pgType, goType, goImport, err := parseCustomType(customType) 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | ctm.Add(pgType, goType, goImport) 80 | } 81 | 82 | return ctm, nil 83 | } 84 | 85 | func parseCustomType(raw string) (pgType, goType, goImport string, err error) { 86 | split := strings.SplitN(raw, ":", 2) 87 | if len(split) < 2 { 88 | err = fmt.Errorf("custom type mapping has invalid format (missing ':')") 89 | return 90 | } 91 | 92 | pgType = split[0] 93 | 94 | ind := strings.LastIndexByte(split[1], '.') 95 | if ind == -1 { 96 | goType = split[1] 97 | return 98 | } 99 | 100 | goImport, goType = split[1][:ind], split[1][ind:] 101 | 102 | if goType == "." || goImport == "" { 103 | err = fmt.Errorf("custom type mapping has invalid format (missing type or import)") 104 | return 105 | } 106 | 107 | if strings.Contains(goType, "/") { 108 | err = fmt.Errorf("type not found") 109 | return 110 | } 111 | 112 | ind = strings.LastIndexByte(goImport, '/') 113 | if ind == -1 { 114 | goType = fmt.Sprintf("%s%s", goImport, goType) 115 | return 116 | } 117 | 118 | base := path.Base(goImport) 119 | if reg.MatchString(goImport) { 120 | base = path.Base(path.Dir(goImport)) 121 | } 122 | 123 | goType = fmt.Sprintf("%s%s", base, goType) 124 | 125 | return 126 | } 127 | -------------------------------------------------------------------------------- /generators/search/generator.go: -------------------------------------------------------------------------------- 1 | package search 2 | 3 | import ( 4 | "github.com/dizzyfool/genna/generators/base" 5 | "github.com/dizzyfool/genna/model" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | const ( 10 | keepPK = "keep-pk" 11 | noAlias = "no-alias" 12 | relaxed = "relaxed" 13 | jsonTag = "json-tag" 14 | ) 15 | 16 | // CreateCommand creates generator command 17 | func CreateCommand() *cobra.Command { 18 | return base.CreateCommand("search", "Search generator for go-pg models", New()) 19 | } 20 | 21 | // Search represents search generator 22 | type Search struct { 23 | options Options 24 | } 25 | 26 | // New creates generator 27 | func New() *Search { 28 | return &Search{} 29 | } 30 | 31 | // Options gets options 32 | func (g *Search) Options() *Options { 33 | return &g.options 34 | } 35 | 36 | // SetOptions sets options 37 | func (g *Search) SetOptions(options Options) { 38 | g.options = options 39 | } 40 | 41 | // AddFlags adds flags to command 42 | func (g *Search) AddFlags(command *cobra.Command) { 43 | base.AddFlags(command) 44 | 45 | flags := command.Flags() 46 | flags.SortFlags = false 47 | 48 | flags.BoolP(keepPK, "k", false, "keep primary key name as is (by default it should be converted to 'ID')") 49 | 50 | flags.BoolP(noAlias, "w", false, `do not set 'alias' tag to "t"`) 51 | 52 | flags.BoolP(relaxed, "r", false, "use interface{} type in search filters\n") 53 | 54 | flags.Bool(jsonTag, false, "add json tag to annotations") 55 | } 56 | 57 | // ReadFlags read flags from command 58 | func (g *Search) ReadFlags(command *cobra.Command) error { 59 | var err error 60 | 61 | g.options.URL, g.options.Output, g.options.Package, g.options.Tables, g.options.FollowFKs, g.options.GoPgVer, g.options.CustomTypes, err = base.ReadFlags(command) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | flags := command.Flags() 67 | 68 | if g.options.KeepPK, err = flags.GetBool(keepPK); err != nil { 69 | return err 70 | } 71 | 72 | if g.options.NoAlias, err = flags.GetBool(noAlias); err != nil { 73 | return err 74 | } 75 | 76 | if g.options.Relaxed, err = flags.GetBool(relaxed); err != nil { 77 | return err 78 | } 79 | 80 | if g.options.AddJSONTag, err = flags.GetBool(jsonTag); err != nil { 81 | return err 82 | } 83 | 84 | // setting defaults 85 | g.options.Def() 86 | 87 | return nil 88 | } 89 | 90 | // Generate runs whole generation process 91 | func (g *Search) Generate() error { 92 | return base.NewGenerator(g.options.URL). 93 | Generate( 94 | g.options.Tables, 95 | g.options.FollowFKs, 96 | false, 97 | g.options.Output, 98 | Template, 99 | g.Packer(), 100 | g.options.GoPgVer, 101 | g.options.CustomTypes, 102 | ) 103 | } 104 | 105 | // Repack runs generator with custom packer 106 | func (g *Search) Repack(packer base.Packer) error { 107 | return base.NewGenerator(g.options.URL). 108 | Generate( 109 | g.options.Tables, 110 | g.options.FollowFKs, 111 | false, 112 | g.options.Output, 113 | Template, 114 | packer, 115 | g.options.GoPgVer, 116 | g.options.CustomTypes, 117 | ) 118 | } 119 | 120 | // Packer returns packer function for compile entities into package 121 | func (g *Search) Packer() base.Packer { 122 | return func(entities []model.Entity) (interface{}, error) { 123 | return NewTemplatePackage(entities, g.options), nil 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /model/column_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestColumn_GoName(t *testing.T) { 8 | tests := []struct { 9 | name string 10 | pgName string 11 | want string 12 | }{ 13 | { 14 | name: "Should generate from simple word", 15 | pgName: "title", 16 | want: "Title", 17 | }, 18 | { 19 | name: "Should generate from underscored", 20 | pgName: "short_title", 21 | want: "ShortTitle", 22 | }, 23 | { 24 | name: "Should generate from camelCased", 25 | pgName: "shortTitle", 26 | want: "ShortTitle", 27 | }, 28 | { 29 | name: "Should generate with underscored_id", 30 | pgName: "location_id", 31 | want: "LocationID", 32 | }, 33 | { 34 | name: "Should generate with camelCasedId", 35 | pgName: "locationId", 36 | want: "LocationID", 37 | }, 38 | } 39 | for _, tt := range tests { 40 | t.Run(tt.name, func(t *testing.T) { 41 | c := NewColumn(tt.pgName, TypePGText, "", false, false, false, false, 0, false, false, 0, []string{}, 9, CustomTypeMapping{}) 42 | if c.GoName != tt.want { 43 | t.Errorf("Column.Name = %v, want %v", c.GoName, tt.want) 44 | } 45 | }) 46 | } 47 | } 48 | 49 | func TestColumn_GoType(t *testing.T) { 50 | type fields struct { 51 | pgType string 52 | array bool 53 | dims int 54 | nullable bool 55 | sqlNulls bool 56 | hasDefault bool 57 | defaultValue string 58 | } 59 | tests := []struct { 60 | name string 61 | fields fields 62 | want string 63 | }{ 64 | { 65 | name: "Should generate int2 type", 66 | fields: fields{ 67 | pgType: TypePGInt2, 68 | array: false, 69 | dims: 0, 70 | nullable: false, 71 | }, 72 | want: "int", 73 | }, 74 | { 75 | name: "Should generate int2 array type", 76 | fields: fields{ 77 | pgType: TypePGInt2, 78 | array: true, 79 | dims: 2, 80 | nullable: false, 81 | }, 82 | want: "[][]int", 83 | }, 84 | { 85 | name: "Should generate int2 nullable type", 86 | fields: fields{ 87 | pgType: TypePGInt2, 88 | array: true, 89 | dims: 2, 90 | nullable: true, 91 | }, 92 | want: "[][]int", 93 | }, 94 | { 95 | name: "Should generate struct type", 96 | fields: fields{ 97 | pgType: TypePGTimetz, 98 | array: false, 99 | dims: 0, 100 | nullable: true, 101 | }, 102 | want: "*time.Time", 103 | }, 104 | { 105 | name: "Should generate struct type", 106 | fields: fields{ 107 | pgType: TypePGTimetz, 108 | array: false, 109 | dims: 0, 110 | nullable: true, 111 | sqlNulls: true, 112 | }, 113 | want: "pg.NullTime", 114 | }, 115 | { 116 | name: "Should generate interface for unknown type", 117 | fields: fields{ 118 | pgType: "unknown", 119 | array: false, 120 | dims: 0, 121 | nullable: true, 122 | }, 123 | want: "interface{}", 124 | }, 125 | } 126 | for _, tt := range tests { 127 | t.Run(tt.name, func(t *testing.T) { 128 | c := NewColumn("test", tt.fields.pgType, tt.fields.defaultValue, tt.fields.hasDefault, tt.fields.nullable, tt.fields.sqlNulls, tt.fields.array, tt.fields.dims, false, false, 0, []string{}, 9, CustomTypeMapping{}) 129 | if got := c.Type; got != tt.want { 130 | t.Errorf("Column.Type = %v, want %v", got, tt.want) 131 | } 132 | }) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /generators/search/model.go: -------------------------------------------------------------------------------- 1 | package search 2 | 3 | import ( 4 | "fmt" 5 | "html/template" 6 | 7 | "github.com/dizzyfool/genna/model" 8 | "github.com/dizzyfool/genna/util" 9 | ) 10 | 11 | // TemplatePackage stores package info 12 | type TemplatePackage struct { 13 | Package string 14 | 15 | HasImports bool 16 | Imports []string 17 | 18 | GoPGVer string 19 | 20 | Entities []TemplateEntity 21 | } 22 | 23 | // NewTemplatePackage creates a package for template 24 | func NewTemplatePackage(entities []model.Entity, options Options) TemplatePackage { 25 | 26 | imports := util.NewSet() 27 | 28 | var models []TemplateEntity 29 | for _, entity := range entities { 30 | mdl := NewTemplateEntity(entity, options) 31 | if len(mdl.Columns) == 0 { 32 | continue 33 | } 34 | 35 | for _, imp := range mdl.Imports { 36 | imports.Add(imp) 37 | } 38 | 39 | for _, col := range mdl.Columns { 40 | if col.Relaxed { 41 | imports.Add("reflect") 42 | } 43 | } 44 | 45 | models = append(models, mdl) 46 | } 47 | 48 | goPGVer := "" 49 | if options.GoPgVer >= 9 { 50 | goPGVer = fmt.Sprintf("/v%d", options.GoPgVer) 51 | } 52 | 53 | return TemplatePackage{ 54 | Package: options.Package, 55 | 56 | HasImports: imports.Len() > 0, 57 | Imports: imports.Elements(), 58 | 59 | GoPGVer: goPGVer, 60 | 61 | Entities: models, 62 | } 63 | } 64 | 65 | // TemplateEntity stores struct info 66 | type TemplateEntity struct { 67 | model.Entity 68 | 69 | NoAlias bool 70 | Alias string 71 | 72 | Columns []TemplateColumn 73 | 74 | Imports []string 75 | } 76 | 77 | // NewTemplateEntity creates an entity for template 78 | func NewTemplateEntity(entity model.Entity, options Options) TemplateEntity { 79 | if entity.HasMultiplePKs() { 80 | options.KeepPK = true 81 | } 82 | 83 | imports := util.NewSet() 84 | 85 | var columns []TemplateColumn 86 | for _, column := range entity.Columns { 87 | if column.IsArray || column.GoType == model.TypeMapInterface || column.GoType == model.TypeMapString { 88 | continue 89 | } 90 | 91 | columns = append(columns, NewTemplateColumn(entity, column, options)) 92 | if column.Import != "" { 93 | imports.Add(column.Import) 94 | } 95 | } 96 | 97 | return TemplateEntity{ 98 | Entity: entity, 99 | 100 | NoAlias: options.NoAlias, 101 | Alias: util.DefaultAlias, 102 | 103 | Columns: columns, 104 | Imports: imports.Elements(), 105 | } 106 | } 107 | 108 | // TemplateColumn stores column info 109 | type TemplateColumn struct { 110 | model.Column 111 | 112 | Relaxed bool 113 | 114 | HasTags bool 115 | Tag template.HTML 116 | 117 | UseCustomRender bool 118 | CustomRender template.HTML 119 | } 120 | 121 | // NewTemplateColumn creates a column for template 122 | func NewTemplateColumn(_ model.Entity, column model.Column, options Options) TemplateColumn { 123 | if !options.KeepPK && column.IsPK { 124 | column.GoName = util.ID 125 | } 126 | 127 | if options.Relaxed { 128 | column.Type = model.TypeInterface 129 | } else { 130 | column.Type = fmt.Sprintf("*%s", column.GoType) 131 | } 132 | 133 | // add json tag 134 | tags := util.NewAnnotation() 135 | if options.AddJSONTag { 136 | tags.AddTag("json", util.Underscore(column.PGName)) 137 | } 138 | 139 | return TemplateColumn{ 140 | Relaxed: options.Relaxed, 141 | Column: column, 142 | HasTags: tags.Len() > 0, 143 | Tag: template.HTML(fmt.Sprintf("`%s`", tags.String())), 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /generators/model/generator.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/dizzyfool/genna/generators/base" 5 | "github.com/dizzyfool/genna/model" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | const ( 11 | keepPK = "keep-pk" 12 | noDiscard = "no-discard" 13 | noAlias = "no-alias" 14 | softDelete = "soft-delete" 15 | json = "json" 16 | jsonTag = "json-tag" 17 | ) 18 | 19 | // CreateCommand creates generator command 20 | func CreateCommand() *cobra.Command { 21 | return base.CreateCommand("model", "Basic go-pg model generator", New()) 22 | } 23 | 24 | // Basic represents basic generator 25 | type Basic struct { 26 | options Options 27 | } 28 | 29 | // New creates basic generator 30 | func New() *Basic { 31 | return &Basic{} 32 | } 33 | 34 | // Options gets options 35 | func (g *Basic) Options() Options { 36 | return g.options 37 | } 38 | 39 | // SetOptions sets options 40 | func (g *Basic) SetOptions(options Options) { 41 | g.options = options 42 | } 43 | 44 | // AddFlags adds flags to command 45 | func (g *Basic) AddFlags(command *cobra.Command) { 46 | base.AddFlags(command) 47 | 48 | flags := command.Flags() 49 | flags.SortFlags = false 50 | 51 | flags.BoolP(keepPK, "k", false, "keep primary key name as is (by default it should be converted to 'ID')") 52 | flags.StringP(softDelete, "s", "", "field for soft_delete tag\n") 53 | 54 | flags.BoolP(noAlias, "w", false, `do not set 'alias' tag to "t"`) 55 | flags.BoolP(noDiscard, "d", false, "do not use 'discard_unknown_columns' tag\n") 56 | 57 | flags.StringToStringP(json, "j", map[string]string{"*": "map[string]interface{}"}, "type for json columns\nuse format: table.column=type, separate by comma\nuse asterisk as wildcard in table name") 58 | flags.Bool(jsonTag, false, "add json tag to annotations") 59 | } 60 | 61 | // ReadFlags read flags from command 62 | func (g *Basic) ReadFlags(command *cobra.Command) error { 63 | var err error 64 | 65 | g.options.URL, g.options.Output, g.options.Package, g.options.Tables, g.options.FollowFKs, g.options.GoPgVer, g.options.CustomTypes, err = base.ReadFlags(command) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | flags := command.Flags() 71 | 72 | if g.options.KeepPK, err = flags.GetBool(keepPK); err != nil { 73 | return err 74 | } 75 | 76 | if g.options.SoftDelete, err = flags.GetString(softDelete); err != nil { 77 | return err 78 | } 79 | 80 | if g.options.NoDiscard, err = flags.GetBool(noDiscard); err != nil { 81 | return err 82 | } 83 | 84 | if g.options.NoAlias, err = flags.GetBool(noAlias); err != nil { 85 | return err 86 | } 87 | 88 | if g.options.JSONTypes, err = flags.GetStringToString(json); err != nil { 89 | return err 90 | } 91 | 92 | if g.options.AddJSONTag, err = flags.GetBool(jsonTag); err != nil { 93 | return err 94 | } 95 | 96 | // setting defaults 97 | g.options.Def() 98 | 99 | return nil 100 | } 101 | 102 | // Generate runs whole generation process 103 | func (g *Basic) Generate() error { 104 | return base.NewGenerator(g.options.URL). 105 | Generate( 106 | g.options.Tables, 107 | g.options.FollowFKs, 108 | g.options.UseSQLNulls, 109 | g.options.Output, 110 | Template, 111 | g.Packer(), 112 | g.options.GoPgVer, 113 | g.options.CustomTypes, 114 | ) 115 | } 116 | 117 | // Packer returns packer function for compile entities into package 118 | func (g *Basic) Packer() base.Packer { 119 | return func(entities []model.Entity) (interface{}, error) { 120 | return NewTemplatePackage(entities, g.options), nil 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /model/relation_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/dizzyfool/genna/util" 7 | ) 8 | 9 | func TestRelation_GoName(t *testing.T) { 10 | type fields struct { 11 | SourceColumns []string 12 | TargetSchema string 13 | TargetTable string 14 | } 15 | tests := []struct { 16 | name string 17 | fields fields 18 | want string 19 | }{ 20 | { 21 | name: "Should generate simple name", 22 | fields: fields{ 23 | SourceColumns: []string{"locationId"}, 24 | TargetSchema: util.PublicSchema, 25 | TargetTable: "locations", 26 | }, 27 | want: "Location", 28 | }, 29 | { 30 | name: "Should generate multiple name", 31 | fields: fields{ 32 | SourceColumns: []string{"city", "locationId"}, 33 | TargetSchema: util.PublicSchema, 34 | TargetTable: "locations", 35 | }, 36 | want: "CityLocation", 37 | }, 38 | } 39 | for _, tt := range tests { 40 | t.Run(tt.name, func(t *testing.T) { 41 | r := NewRelation(tt.fields.SourceColumns, tt.fields.TargetSchema, tt.fields.TargetTable) 42 | if got := r.GoName; got != tt.want { 43 | t.Errorf("Relation.GoName = %v, want %v", got, tt.want) 44 | } 45 | }) 46 | } 47 | } 48 | 49 | func TestRelation_GoType(t *testing.T) { 50 | type fields struct { 51 | TargetSchema string 52 | TargetTable string 53 | } 54 | tests := []struct { 55 | name string 56 | fields fields 57 | want string 58 | }{ 59 | { 60 | name: "Should generate from simple word", 61 | fields: fields{ 62 | TargetSchema: util.PublicSchema, 63 | TargetTable: "users", 64 | }, 65 | want: "User", 66 | }, 67 | { 68 | name: "Should generate from non-countable", 69 | fields: fields{ 70 | TargetSchema: util.PublicSchema, 71 | TargetTable: "audio", 72 | }, 73 | want: "Audio", 74 | }, 75 | { 76 | name: "Should generate from underscored", 77 | fields: fields{ 78 | TargetSchema: util.PublicSchema, 79 | TargetTable: "user_orders", 80 | }, 81 | want: "UserOrder", 82 | }, 83 | { 84 | name: "Should generate from camelCased", 85 | fields: fields{ 86 | TargetSchema: util.PublicSchema, 87 | TargetTable: "userOrders", 88 | }, 89 | want: "UserOrder", 90 | }, 91 | { 92 | name: "Should generate from plural in last place", 93 | fields: fields{ 94 | TargetSchema: util.PublicSchema, 95 | TargetTable: "usersWithOrders", 96 | }, 97 | want: "UsersWithOrder", 98 | }, 99 | { 100 | name: "Should generate from abracadabra", 101 | fields: fields{ 102 | TargetSchema: util.PublicSchema, 103 | TargetTable: "abracadabra", 104 | }, 105 | want: "Abracadabra", 106 | }, 107 | { 108 | name: "Should generate from numbers in first place", 109 | fields: fields{ 110 | TargetSchema: util.PublicSchema, 111 | TargetTable: "123-abc", 112 | }, 113 | want: "T123Abc", 114 | }, 115 | { 116 | name: "Should generate from name with dash & underscore", 117 | fields: fields{ 118 | TargetSchema: util.PublicSchema, 119 | TargetTable: "abc-123_abc", 120 | }, 121 | want: "Abc123Abc", 122 | }, 123 | { 124 | name: "Should generate with schema", 125 | fields: fields{ 126 | TargetSchema: "information_schema", 127 | TargetTable: "users", 128 | }, 129 | want: "InformationSchemaUser", 130 | }, 131 | { 132 | name: "Should generate without schema", 133 | fields: fields{ 134 | TargetSchema: util.PublicSchema, 135 | TargetTable: "users", 136 | }, 137 | want: "User", 138 | }, 139 | } 140 | for _, tt := range tests { 141 | t.Run(tt.name, func(t *testing.T) { 142 | r := NewRelation([]string{"ID"}, tt.fields.TargetSchema, tt.fields.TargetTable) 143 | if got := r.GoType; got != tt.want { 144 | t.Errorf("Relation.GoType = %v, want %v", got, tt.want) 145 | } 146 | }) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /model/custom_types_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func Test_parseCustomType(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | raw string 12 | wantPgType string 13 | wantGoType string 14 | wantGoImport string 15 | wantErr bool 16 | }{ 17 | { 18 | name: "should parse same package import", 19 | raw: "uuid:Params", 20 | wantPgType: "uuid", 21 | wantGoType: "Params", 22 | wantGoImport: "", 23 | wantErr: false, 24 | }, 25 | { 26 | name: "should parse std package import", 27 | raw: "uuid:bytes.Buffer", 28 | wantPgType: "uuid", 29 | wantGoType: "bytes.Buffer", 30 | wantGoImport: "bytes", 31 | }, 32 | { 33 | name: "should parse std sub-package import", 34 | raw: "uuid:encodings/json.RawMessage", 35 | wantPgType: "uuid", 36 | wantGoType: "json.RawMessage", 37 | wantGoImport: "encodings/json", 38 | }, 39 | { 40 | name: "should parse external import", 41 | raw: "uuid:github.com/google/uuid.UUID", 42 | wantPgType: "uuid", 43 | wantGoType: "uuid.UUID", 44 | wantGoImport: "github.com/google/uuid", 45 | }, 46 | { 47 | name: "should parse external version import", 48 | raw: "uuid:github.com/google/uuid/v4.UUID", 49 | wantPgType: "uuid", 50 | wantGoType: "uuid.UUID", 51 | wantGoImport: "github.com/google/uuid/v4", 52 | }, 53 | { 54 | name: "should error on incorrect format", 55 | raw: "uuid;github.com/google/uuid.UUID", 56 | wantErr: true, 57 | }, 58 | { 59 | name: "should error on incorrect import format", 60 | raw: "uuid:github.com/google/uuid/v4", 61 | wantErr: true, 62 | }, 63 | { 64 | name: "should error on incorrect import format v2", 65 | raw: "uuid:github.com/google/uuid/v4.", 66 | wantErr: true, 67 | }, 68 | } 69 | for _, tt := range tests { 70 | t.Run(tt.name, func(t *testing.T) { 71 | gotPgType, gotGoTyp, gotGoImport, err := parseCustomType(tt.raw) 72 | 73 | if err == nil == tt.wantErr { 74 | t.Errorf("parseCustomType() gotErr = %v", err) 75 | } 76 | if err != nil { 77 | return 78 | } 79 | 80 | if gotPgType != tt.wantPgType { 81 | t.Errorf("parseCustomType() gotPgType = %v, want %v", gotPgType, tt.wantPgType) 82 | } 83 | if gotGoTyp != tt.wantGoType { 84 | t.Errorf("parseCustomType() gotGoTyp = %v, want %v", gotGoTyp, tt.wantGoType) 85 | } 86 | if gotGoImport != tt.wantGoImport { 87 | t.Errorf("parseCustomType() gotGoImport = %v, want %v", gotGoImport, tt.wantGoImport) 88 | } 89 | }) 90 | } 91 | } 92 | 93 | func TestParseCustomTypes(t *testing.T) { 94 | n := func(params ...string) CustomTypeMapping { 95 | ctm := CustomTypeMapping{} 96 | 97 | for i := 0; i < len(params); i += 3 { 98 | ctm.Add(params[i], params[i+1], params[i+2]) 99 | } 100 | 101 | return ctm 102 | } 103 | 104 | tests := []struct { 105 | name string 106 | args []string 107 | want CustomTypeMapping 108 | wantErr bool 109 | }{ 110 | { 111 | name: "Should parse correct mapping", 112 | args: []string{"uuid:github.com/google/uuid.UUID", "point:src/db.Point"}, 113 | want: n("uuid", "uuid.UUID", "github.com/google/uuid", "point", "db.Point", "src/db"), 114 | }, 115 | { 116 | name: "Should error on wrong format", 117 | args: []string{"uuidgithub.com/google/uuid.UUID", "point:src/dbPoint."}, 118 | wantErr: true, 119 | }, 120 | } 121 | for _, tt := range tests { 122 | t.Run(tt.name, func(t *testing.T) { 123 | got, err := ParseCustomTypes(tt.args) 124 | if (err != nil) != tt.wantErr { 125 | t.Errorf("ParseCustomTypes() error = %v, wantErr %v", err, tt.wantErr) 126 | return 127 | } 128 | if !reflect.DeepEqual(got, tt.want) { 129 | t.Errorf("ParseCustomTypes() got = %v, want %v", got, tt.want) 130 | } 131 | }) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /generators/search/generator_test.output: -------------------------------------------------------------------------------- 1 | //nolint 2 | //lint:file-ignore U1000 ignore unused code, it's generated 3 | package model 4 | 5 | import ( 6 | "github.com/google/uuid" 7 | "time" 8 | 9 | "github.com/go-pg/pg/v10" 10 | "github.com/go-pg/pg/v10/orm" 11 | ) 12 | 13 | const condition = "?.? = ?" 14 | 15 | // base filters 16 | type applier func(query *orm.Query) (*orm.Query, error) 17 | 18 | type search struct { 19 | appliers []applier 20 | } 21 | 22 | func (s *search) apply(query *orm.Query) { 23 | for _, applier := range s.appliers { 24 | query.Apply(applier) 25 | } 26 | } 27 | 28 | func (s *search) where(query *orm.Query, table, field string, value interface{}) { 29 | 30 | query.Where(condition, pg.Ident(table), pg.Ident(field), value) 31 | 32 | } 33 | 34 | func (s *search) WithApply(a applier) { 35 | if s.appliers == nil { 36 | s.appliers = []applier{} 37 | } 38 | s.appliers = append(s.appliers, a) 39 | } 40 | 41 | func (s *search) With(condition string, params ...interface{}) { 42 | s.WithApply(func(query *orm.Query) (*orm.Query, error) { 43 | return query.Where(condition, params...), nil 44 | }) 45 | } 46 | 47 | // Searcher is interface for every generated filter 48 | type Searcher interface { 49 | Apply(query *orm.Query) *orm.Query 50 | Q() applier 51 | 52 | With(condition string, params ...interface{}) 53 | WithApply(a applier) 54 | } 55 | 56 | type ProjectSearch struct { 57 | search 58 | 59 | ID *uuid.UUID 60 | Code *uuid.UUID 61 | Name *string 62 | } 63 | 64 | func (s *ProjectSearch) Apply(query *orm.Query) *orm.Query { 65 | if s.ID != nil { 66 | s.where(query, Tables.Project.Alias, Columns.Project.ID, s.ID) 67 | } 68 | if s.Code != nil { 69 | s.where(query, Tables.Project.Alias, Columns.Project.Code, s.Code) 70 | } 71 | if s.Name != nil { 72 | s.where(query, Tables.Project.Alias, Columns.Project.Name, s.Name) 73 | } 74 | 75 | s.apply(query) 76 | 77 | return query 78 | } 79 | 80 | func (s *ProjectSearch) Q() applier { 81 | return func(query *orm.Query) (*orm.Query, error) { 82 | return s.Apply(query), nil 83 | } 84 | } 85 | 86 | type UserSearch struct { 87 | search 88 | 89 | ID *int 90 | Email *string 91 | Activated *bool 92 | Name *string 93 | CountryID *int 94 | Avatar *[]byte 95 | AvatarAlt *[]byte 96 | LoggedAt *time.Time 97 | } 98 | 99 | func (s *UserSearch) Apply(query *orm.Query) *orm.Query { 100 | if s.ID != nil { 101 | s.where(query, Tables.User.Alias, Columns.User.ID, s.ID) 102 | } 103 | if s.Email != nil { 104 | s.where(query, Tables.User.Alias, Columns.User.Email, s.Email) 105 | } 106 | if s.Activated != nil { 107 | s.where(query, Tables.User.Alias, Columns.User.Activated, s.Activated) 108 | } 109 | if s.Name != nil { 110 | s.where(query, Tables.User.Alias, Columns.User.Name, s.Name) 111 | } 112 | if s.CountryID != nil { 113 | s.where(query, Tables.User.Alias, Columns.User.CountryID, s.CountryID) 114 | } 115 | if s.Avatar != nil { 116 | s.where(query, Tables.User.Alias, Columns.User.Avatar, s.Avatar) 117 | } 118 | if s.AvatarAlt != nil { 119 | s.where(query, Tables.User.Alias, Columns.User.AvatarAlt, s.AvatarAlt) 120 | } 121 | if s.LoggedAt != nil { 122 | s.where(query, Tables.User.Alias, Columns.User.LoggedAt, s.LoggedAt) 123 | } 124 | 125 | s.apply(query) 126 | 127 | return query 128 | } 129 | 130 | func (s *UserSearch) Q() applier { 131 | return func(query *orm.Query) (*orm.Query, error) { 132 | return s.Apply(query), nil 133 | } 134 | } 135 | 136 | type GeoCountrySearch struct { 137 | search 138 | 139 | ID *int 140 | Code *string 141 | } 142 | 143 | func (s *GeoCountrySearch) Apply(query *orm.Query) *orm.Query { 144 | if s.ID != nil { 145 | s.where(query, Tables.GeoCountry.Alias, Columns.GeoCountry.ID, s.ID) 146 | } 147 | if s.Code != nil { 148 | s.where(query, Tables.GeoCountry.Alias, Columns.GeoCountry.Code, s.Code) 149 | } 150 | 151 | s.apply(query) 152 | 153 | return query 154 | } 155 | 156 | func (s *GeoCountrySearch) Q() applier { 157 | return func(query *orm.Query) (*orm.Query, error) { 158 | return s.Apply(query), nil 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /util/texts.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | "unicode" 7 | "unicode/utf8" 8 | 9 | "github.com/fatih/camelcase" 10 | "github.com/jinzhu/inflection" 11 | ) 12 | 13 | const ( 14 | // Id is camelCased Id 15 | Id = "Id" 16 | // Ids is camelCased Ids 17 | Ids = "Ids" 18 | // ID is golang ID 19 | ID = "ID" 20 | // IDs is golang IDs 21 | IDs = "IDs" 22 | // Rel if suffix for Relation 23 | Rel = "Rel" 24 | ) 25 | 26 | func init() { 27 | inflection.AddUncountable("sms", "mms", "rls") 28 | } 29 | 30 | // Singular makes singular of plural english word 31 | func Singular(s string) string { 32 | return inflection.Singular(s) 33 | } 34 | 35 | // IsUpper check rune for upper case 36 | func IsUpper(c byte) bool { 37 | return c >= 'A' && c <= 'Z' 38 | } 39 | 40 | // IsLower check rune for lower case 41 | func IsLower(c byte) bool { 42 | return c >= 'a' && c <= 'z' 43 | } 44 | 45 | // ToUpper converts rune to upper 46 | func ToUpper(c byte) byte { 47 | return c - 32 48 | } 49 | 50 | // ToLower converts rune to lower 51 | func ToLower(c byte) byte { 52 | return c + 32 53 | } 54 | 55 | // CamelCased converts string to camelCase 56 | // from github.com/go-pg/pg/v9/internal 57 | func CamelCased(s string) string { 58 | r := make([]byte, 0, len(s)) 59 | upperNext := true 60 | for i := 0; i < len(s); i++ { 61 | c := s[i] 62 | if c == '_' { 63 | upperNext = true 64 | continue 65 | } 66 | if upperNext { 67 | if IsLower(c) { 68 | c = ToUpper(c) 69 | } 70 | upperNext = false 71 | } 72 | r = append(r, c) 73 | } 74 | return string(r) 75 | } 76 | 77 | // Underscore converts string to under_scored 78 | // from github.com/go-pg/pg/v9/internal 79 | func Underscore(s string) string { 80 | r := make([]byte, 0, len(s)+5) 81 | for i := 0; i < len(s); i++ { 82 | c := s[i] 83 | if IsUpper(c) { 84 | if i > 0 && i+1 < len(s) && (IsLower(s[i-1]) || IsLower(s[i+1])) { 85 | r = append(r, '_', ToLower(c)) 86 | } else { 87 | r = append(r, ToLower(c)) 88 | } 89 | } else { 90 | r = append(r, c) 91 | } 92 | } 93 | return string(r) 94 | } 95 | 96 | // Sanitize makes string suitable for golang var, const, field, type name 97 | func Sanitize(s string) string { 98 | rgxp := regexp.MustCompile(`[^a-zA-Z\d\-_]`) 99 | sanitized := strings.Replace(rgxp.ReplaceAllString(s, ""), "-", "_", -1) 100 | 101 | if len(sanitized) != 0 && ((sanitized[0] >= '0' && sanitized[0] <= '9') || sanitized[0] == '_') { 102 | sanitized = "T" + sanitized 103 | } 104 | 105 | return sanitized 106 | } 107 | 108 | // PackageName gets string usable as package name 109 | func PackageName(s string) string { 110 | return strings.ToLower(Sanitize(s)) 111 | } 112 | 113 | // EntityName gets string usable as struct name 114 | func EntityName(s string) string { 115 | splitted := camelcase.Split(CamelCased(Sanitize(s))) 116 | 117 | ln := len(splitted) - 1 118 | for i := ln; i >= 0; i-- { 119 | split := splitted[i] 120 | singular := Singular(split) 121 | if strings.ToLower(singular) != strings.ToLower(split) { 122 | splitted[i] = strings.Title(singular) 123 | break 124 | } 125 | } 126 | 127 | return strings.Join(splitted, "") 128 | } 129 | 130 | // ColumnName gets string usable as struct field name 131 | func ColumnName(s string) string { 132 | camelCased := CamelCased(Sanitize(s)) 133 | camelCased = ReplaceSuffix(ReplaceSuffix(camelCased, Id, ID), Ids, IDs) 134 | 135 | return strings.Title(camelCased) 136 | } 137 | 138 | // HasUpper checks if string contains upper case 139 | func HasUpper(s string) bool { 140 | for i := 0; i < len(s); i++ { 141 | c := s[i] 142 | if IsUpper(c) { 143 | return true 144 | } 145 | } 146 | return false 147 | } 148 | 149 | // ReplaceSuffix replaces substring on the end of string 150 | func ReplaceSuffix(in, suffix, replace string) string { 151 | if strings.HasSuffix(in, suffix) { 152 | in = in[:len(in)-len(suffix)] + replace 153 | } 154 | return in 155 | } 156 | 157 | // LowerFirst lowers the first letter 158 | func LowerFirst(s string) string { 159 | if s == "" { 160 | return "" 161 | } 162 | r, n := utf8.DecodeRuneInString(s) 163 | return string(unicode.ToLower(r)) + s[n:] 164 | } 165 | -------------------------------------------------------------------------------- /generators/validate/model.go: -------------------------------------------------------------------------------- 1 | package validate 2 | 3 | import ( 4 | "fmt" 5 | "html/template" 6 | "strings" 7 | 8 | "github.com/dizzyfool/genna/model" 9 | "github.com/dizzyfool/genna/util" 10 | ) 11 | 12 | const ( 13 | // Nil is nil check types 14 | Nil = "nil" 15 | // Zero is 0 check types 16 | Zero = "zero" 17 | // PZero is 0 check types for pointers 18 | PZero = "pzero" 19 | // Len is length check types 20 | Len = "len" 21 | // PLen is length check types for pointers 22 | PLen = "plen" 23 | // Enum is allowed values check types 24 | Enum = "enum" 25 | // PEnum is allowed values check types for pointers 26 | PEnum = "penum" 27 | ) 28 | 29 | // TemplatePackage stores package info 30 | type TemplatePackage struct { 31 | Package string 32 | 33 | HasImports bool 34 | Imports []string 35 | 36 | Entities []TemplateEntity 37 | } 38 | 39 | // NewTemplatePackage creates a package for template 40 | func NewTemplatePackage(entities []model.Entity, options Options) TemplatePackage { 41 | imports := util.NewSet() 42 | 43 | var models []TemplateEntity 44 | for _, entity := range entities { 45 | mdl := NewTemplateEntity(entity, options) 46 | if len(mdl.Columns) == 0 { 47 | continue 48 | } 49 | 50 | for _, imp := range mdl.Imports { 51 | imports.Add(imp) 52 | } 53 | 54 | models = append(models, mdl) 55 | } 56 | 57 | return TemplatePackage{ 58 | Package: options.Package, 59 | 60 | HasImports: imports.Len() > 0, 61 | Imports: imports.Elements(), 62 | 63 | Entities: models, 64 | } 65 | } 66 | 67 | // TemplateEntity stores struct info 68 | type TemplateEntity struct { 69 | model.Entity 70 | 71 | Columns []TemplateColumn 72 | Imports []string 73 | } 74 | 75 | // NewTemplateEntity creates an entity for template 76 | func NewTemplateEntity(entity model.Entity, options Options) TemplateEntity { 77 | if entity.HasMultiplePKs() { 78 | options.KeepPK = true 79 | } 80 | 81 | imports := util.NewSet() 82 | 83 | var columns []TemplateColumn 84 | for _, column := range entity.Columns { 85 | if !isValidatable(column) { 86 | continue 87 | } 88 | 89 | tmpl := NewTemplateColumn(column, options) 90 | 91 | columns = append(columns, tmpl) 92 | if tmpl.Import != "" { 93 | imports.Add(tmpl.Import) 94 | } 95 | } 96 | 97 | return TemplateEntity{ 98 | Entity: entity, 99 | 100 | Columns: columns, 101 | Imports: imports.Elements(), 102 | } 103 | } 104 | 105 | // TemplateColumn stores column info 106 | type TemplateColumn struct { 107 | model.Column 108 | 109 | Check string 110 | Enum template.HTML 111 | 112 | Import string 113 | } 114 | 115 | // NewTemplateColumn creates a column for template 116 | func NewTemplateColumn(column model.Column, options Options) TemplateColumn { 117 | if !options.KeepPK && column.IsPK { 118 | column.GoName = util.ID 119 | } 120 | 121 | tmpl := TemplateColumn{ 122 | Column: column, 123 | 124 | Check: check(column), 125 | } 126 | 127 | if len(column.Values) > 0 { 128 | tmpl.Enum = template.HTML(fmt.Sprintf(`"%s"`, strings.Join(column.Values, `", "`))) 129 | } 130 | 131 | if tmpl.Check == PLen || tmpl.Check == Len { 132 | tmpl.Import = "unicode/utf8" 133 | } 134 | 135 | return tmpl 136 | } 137 | 138 | // isValidatable checks if field can be validated 139 | func isValidatable(c model.Column) bool { 140 | // validate FK 141 | if c.IsFK { 142 | return true 143 | } 144 | 145 | // validate complex types 146 | if !c.Nullable && (c.IsArray || c.GoType == model.TypeMapInterface || c.GoType == model.TypeMapString) { 147 | return true 148 | } 149 | 150 | // validate strings len 151 | if c.GoType == model.TypeString && c.MaxLen > 0 { 152 | return true 153 | } 154 | 155 | // validate enum 156 | if len(c.Values) > 0 { 157 | return true 158 | } 159 | 160 | return false 161 | } 162 | 163 | // check return check type for validation 164 | func check(c model.Column) string { 165 | if !isValidatable(c) { 166 | return "" 167 | } 168 | 169 | if c.IsArray || c.GoType == model.TypeMapInterface || c.GoType == model.TypeMapString { 170 | return Nil 171 | } 172 | 173 | if c.IsFK { 174 | if c.Nullable { 175 | return PZero 176 | } 177 | return Zero 178 | } 179 | 180 | if c.GoType == model.TypeString && c.MaxLen > 0 { 181 | if c.Nullable { 182 | return PLen 183 | } 184 | return Len 185 | } 186 | 187 | if len(c.Values) > 0 { 188 | if c.Nullable { 189 | return PEnum 190 | } 191 | return Enum 192 | } 193 | 194 | return "" 195 | } 196 | -------------------------------------------------------------------------------- /generators/model/README.md: -------------------------------------------------------------------------------- 1 | ## Basic model generator 2 | 3 | Use `model` sub-command to execute generator: 4 | 5 | `genna model -h` 6 | 7 | First create your database and tables in it 8 | 9 | ```sql 10 | create table "projects" 11 | ( 12 | "projectId" serial not null, 13 | "name" text not null, 14 | 15 | primary key ("projectId") 16 | ); 17 | 18 | create table "users" 19 | ( 20 | "userId" serial not null, 21 | "email" varchar(64) not null, 22 | "activated" bool not null default false, 23 | "name" varchar(128), 24 | "countryId" integer, 25 | 26 | primary key ("userId") 27 | ); 28 | 29 | create schema "geo"; 30 | create table geo."countries" 31 | ( 32 | "countryId" serial not null, 33 | "code" varchar(3) not null, 34 | "coords" integer[], 35 | 36 | primary key ("countryId") 37 | ); 38 | 39 | alter table "users" 40 | add constraint "fk_user_country" 41 | foreign key ("countryId") 42 | references geo."countries" ("countryId") on update restrict on delete restrict; 43 | ``` 44 | 45 | ### Run generator 46 | 47 | `genna model -c postgres://user:password@localhost:5432/yourdb -o ~/output/model.go -t public.* -f` 48 | 49 | You should get following models on model package: 50 | 51 | ```go 52 | //lint:file-ignore U1000 ignore unused code, it's generated 53 | package model 54 | 55 | var Columns = struct { 56 | Project struct { 57 | ID, Name string 58 | } 59 | User struct { 60 | ID, Email, Activated, Name, CountryID string 61 | 62 | Country string 63 | } 64 | GeoCountry struct { 65 | ID, Code, Coords string 66 | } 67 | }{ 68 | Project: struct { 69 | ID, Name string 70 | }{ 71 | ID: "projectId", 72 | Name: "name", 73 | }, 74 | User: struct { 75 | ID, Email, Activated, Name, CountryID string 76 | 77 | Country string 78 | }{ 79 | ID: "userId", 80 | Email: "email", 81 | Activated: "activated", 82 | Name: "name", 83 | CountryID: "countryId", 84 | 85 | Country: "Country", 86 | }, 87 | GeoCountry: struct { 88 | ID, Code, Coords string 89 | }{ 90 | ID: "countryId", 91 | Code: "code", 92 | Coords: "coords", 93 | }, 94 | } 95 | 96 | var Tables = struct { 97 | Project struct { 98 | Name, Alias string 99 | } 100 | User struct { 101 | Name, Alias string 102 | } 103 | GeoCountry struct { 104 | Name, Alias string 105 | } 106 | }{ 107 | Project: struct { 108 | Name, Alias string 109 | }{ 110 | Name: "projects", 111 | Alias: "t", 112 | }, 113 | User: struct { 114 | Name, Alias string 115 | }{ 116 | Name: "users", 117 | Alias: "t", 118 | }, 119 | GeoCountry: struct { 120 | Name, Alias string 121 | }{ 122 | Name: "geo.countries", 123 | Alias: "t", 124 | }, 125 | } 126 | 127 | type Project struct { 128 | tableName struct{} `sql:"projects,alias:t" pg:",discard_unknown_columns"` 129 | 130 | ID int `sql:"projectId,pk"` 131 | Name string `sql:"name,notnull"` 132 | } 133 | 134 | type User struct { 135 | tableName struct{} `sql:"users,alias:t" pg:",discard_unknown_columns"` 136 | 137 | ID int `sql:"userId,pk"` 138 | Email string `sql:"email,notnull"` 139 | Activated bool `sql:"activated,notnull"` 140 | Name *string `sql:"name"` 141 | CountryID *int `sql:"countryId"` 142 | 143 | Country *GeoCountry `pg:"fk:countryId"` 144 | } 145 | 146 | type GeoCountry struct { 147 | tableName struct{} `sql:"geo.countries,alias:t" pg:",discard_unknown_columns"` 148 | 149 | ID int `sql:"countryId,pk"` 150 | Code string `sql:"code,notnull"` 151 | Coords []int `sql:"coords,array"` 152 | } 153 | 154 | ``` 155 | 156 | ### Try it 157 | 158 | ```go 159 | package model 160 | 161 | import ( 162 | "fmt" 163 | "testing" 164 | 165 | "github.com/go-pg/pg/v9" 166 | ) 167 | 168 | const AllColumns = "t.*" 169 | 170 | func TestModel(t *testing.T) { 171 | // connecting to db 172 | options, _ := pg.ParseURL("postgres://user:password@localhost:5432/yourdb") 173 | db := pg.Connect(options) 174 | 175 | if _, err := db.Exec(`truncate table users; truncate table geo.countries cascade;`); err != nil { 176 | panic(err) 177 | } 178 | 179 | // objects to insert 180 | toInsert := []GeoCountry{ 181 | GeoCountry{ 182 | Code: "us", 183 | Coords: []int{1, 2}, 184 | }, 185 | GeoCountry{ 186 | Code: "uk", 187 | Coords: nil, 188 | }, 189 | } 190 | 191 | // inserting 192 | if _, err := db.Model(&toInsert).Insert(); err != nil { 193 | panic(err) 194 | } 195 | 196 | // selecting 197 | var toSelect []GeoCountry 198 | 199 | if err := db.Model(&toSelect).Select(); err != nil { 200 | panic(err) 201 | } 202 | 203 | fmt.Printf("%#v\n", toSelect) 204 | 205 | // user with fk 206 | newUser := User{ 207 | Email: "test@gmail.com", 208 | Activated: true, 209 | CountryID: &toSelect[0].ID, 210 | } 211 | 212 | // inserting 213 | if _, err := db.Model(&newUser).Insert(); err != nil { 214 | panic(err) 215 | } 216 | 217 | // selecting inserted user 218 | user := User{} 219 | m := db.Model(&user). 220 | Column(AllColumns, Columns.User.Country). 221 | Where(`? = ?`, pg.F(Columns.User.Email), "test@gmail.com") 222 | 223 | if err := m.Select(); err != nil { 224 | panic(err) 225 | } 226 | 227 | fmt.Printf("%#v\n", user) 228 | fmt.Printf("%#v\n", user.Country) 229 | } 230 | 231 | ``` 232 | -------------------------------------------------------------------------------- /generators/search/README.md: -------------------------------------------------------------------------------- 1 | ## Search model generator 2 | 3 | **Basic model required, which can be generated with one of the model generators** 4 | 5 | Use `search` sub-command to execute generator: 6 | 7 | `genna search -h` 8 | 9 | First create your database and tables in it 10 | 11 | ```sql 12 | create table "projects" 13 | ( 14 | "projectId" serial not null, 15 | "name" text not null, 16 | 17 | primary key ("projectId") 18 | ); 19 | 20 | create table "users" 21 | ( 22 | "userId" serial not null, 23 | "email" varchar(64) not null, 24 | "activated" bool not null default false, 25 | "name" varchar(128), 26 | "countryId" integer, 27 | 28 | primary key ("userId") 29 | ); 30 | 31 | create schema "geo"; 32 | create table geo."countries" 33 | ( 34 | "countryId" serial not null, 35 | "code" varchar(3) not null, 36 | "coords" integer[], 37 | 38 | primary key ("countryId") 39 | ); 40 | 41 | alter table "users" 42 | add constraint "fk_user_country" 43 | foreign key ("countryId") 44 | references geo."countries" ("countryId") on update restrict on delete restrict; 45 | ``` 46 | 47 | ### Run generator 48 | 49 | `genna search -c postgres://user:password@localhost:5432/yourdb -o ~/output/model.go -t public.* -f` 50 | 51 | You should get following search structs on model package: 52 | 53 | ```go 54 | //lint:file-ignore U1000 ignore unused code, it's generated 55 | package model 56 | 57 | import ( 58 | "github.com/go-pg/pg/v9" 59 | "github.com/go-pg/pg/v9/orm" 60 | ) 61 | 62 | // base filters 63 | 64 | type applier func(query *orm.Query) (*orm.Query, error) 65 | 66 | type search struct { 67 | custom map[string][]interface{} 68 | } 69 | 70 | func (s *search) apply(table string, values map[string]interface{}, query *orm.Query) *orm.Query { 71 | for field, value := range values { 72 | if value != nil { 73 | query.Where("?.? = ?", pg.F(table), pg.F(field), value) 74 | } 75 | } 76 | 77 | if s.custom != nil { 78 | for condition, params := range s.custom { 79 | query.Where(condition, params...) 80 | } 81 | } 82 | 83 | return query 84 | } 85 | 86 | func (s *search) with(condition string, params ...interface{}) { 87 | if s.custom == nil { 88 | s.custom = map[string][]interface{}{} 89 | } 90 | s.custom[condition] = params 91 | } 92 | 93 | // Searcher is interface for every generated filter 94 | type Searcher interface { 95 | Apply(query *orm.Query) *orm.Query 96 | Q() applier 97 | } 98 | 99 | type ProjectSearch struct { 100 | search 101 | 102 | ID *int 103 | Name *string 104 | } 105 | 106 | func (s *ProjectSearch) Apply(query *orm.Query) *orm.Query { 107 | return s.apply(Tables.Project.Alias, map[string]interface{}{ 108 | Columns.Project.ID: s.ID, 109 | Columns.Project.Name: s.Name, 110 | }, query) 111 | } 112 | 113 | func (s *ProjectSearch) Q() applier { 114 | return func(query *orm.Query) (*orm.Query, error) { 115 | return s.Apply(query), nil 116 | } 117 | } 118 | 119 | type UserSearch struct { 120 | search 121 | 122 | ID *int 123 | Email *string 124 | Activated *bool 125 | Name *string 126 | CountryID *int 127 | } 128 | 129 | func (s *UserSearch) Apply(query *orm.Query) *orm.Query { 130 | return s.apply(Tables.User.Alias, map[string]interface{}{ 131 | Columns.User.ID: s.ID, 132 | Columns.User.Email: s.Email, 133 | Columns.User.Activated: s.Activated, 134 | Columns.User.Name: s.Name, 135 | Columns.User.CountryID: s.CountryID, 136 | }, query) 137 | } 138 | 139 | func (s *UserSearch) Q() applier { 140 | return func(query *orm.Query) (*orm.Query, error) { 141 | return s.Apply(query), nil 142 | } 143 | } 144 | 145 | type GeoCountrySearch struct { 146 | search 147 | 148 | ID *int 149 | Code *string 150 | } 151 | 152 | func (s *GeoCountrySearch) Apply(query *orm.Query) *orm.Query { 153 | return s.apply(Tables.GeoCountry.Alias, map[string]interface{}{ 154 | Columns.GeoCountry.ID: s.ID, 155 | Columns.GeoCountry.Code: s.Code, 156 | }, query) 157 | } 158 | 159 | func (s *GeoCountrySearch) Q() applier { 160 | return func(query *orm.Query) (*orm.Query, error) { 161 | return s.Apply(query), nil 162 | } 163 | } 164 | 165 | ``` 166 | 167 | ### Try it 168 | 169 | ```go 170 | package model 171 | 172 | import ( 173 | "fmt" 174 | "testing" 175 | 176 | "github.com/go-pg/pg/v9" 177 | ) 178 | 179 | func TestModel(t *testing.T) { 180 | // connecting to db 181 | options, _ := pg.ParseURL("postgres://user:password@localhost:5432/yourdb") 182 | db := pg.Connect(options) 183 | 184 | if _, err := db.Exec(`truncate table users; truncate table geo.countries cascade;`); err != nil { 185 | panic(err) 186 | } 187 | 188 | // objects to insert 189 | toInsert := []GeoCountry{ 190 | GeoCountry{ 191 | Code: "us", 192 | Coords: []int{1, 2}, 193 | }, 194 | GeoCountry{ 195 | Code: "uk", 196 | Coords: nil, 197 | }, 198 | } 199 | 200 | // inserting 201 | if _, err := db.Model(&toInsert).Insert(); err != nil { 202 | panic(err) 203 | } 204 | 205 | code := "us" 206 | country := GeoCountry{} 207 | search := GeoCountrySearch{ 208 | Code: &code, 209 | } 210 | m = db.Model(&country).Apply(search.Q()) 211 | 212 | if err := m.Select(); err != nil { 213 | panic(err) 214 | } 215 | 216 | fmt.Printf("%#v\n", country) 217 | } 218 | 219 | ``` 220 | -------------------------------------------------------------------------------- /generators/named/README.md: -------------------------------------------------------------------------------- 1 | ## Basic model generator with named structures 2 | 3 | Author: [@Dionid](https://github.com/Dionid) 4 | 5 | Use `model-named` sub-command to execute generator: 6 | 7 | `genna model-named -h` 8 | 9 | First create your database and tables in it 10 | 11 | ```sql 12 | create table "projects" 13 | ( 14 | "projectId" serial not null, 15 | "name" text not null, 16 | 17 | primary key ("projectId") 18 | ); 19 | 20 | create table "users" 21 | ( 22 | "userId" serial not null, 23 | "email" varchar(64) not null, 24 | "activated" bool not null default false, 25 | "name" varchar(128), 26 | "countryId" integer, 27 | 28 | primary key ("userId") 29 | ); 30 | 31 | create schema "geo"; 32 | create table geo."countries" 33 | ( 34 | "countryId" serial not null, 35 | "code" varchar(3) not null, 36 | "coords" integer[], 37 | 38 | primary key ("countryId") 39 | ); 40 | 41 | alter table "users" 42 | add constraint "fk_user_country" 43 | foreign key ("countryId") 44 | references geo."countries" ("countryId") on update restrict on delete restrict; 45 | ``` 46 | 47 | ### Run generator 48 | 49 | `genna model-named -c postgres://user:password@localhost:5432/yourdb -o ~/output/model.go -t public.* -f` 50 | 51 | You should get following models on model package: 52 | 53 | ```go 54 | //lint:file-ignore U1000 ignore unused code, it's generated 55 | package model 56 | 57 | type ColumnsProject struct { 58 | ID, Name string 59 | } 60 | 61 | type ColumnsUser struct { 62 | ID, Email, Activated, Name, CountryID string 63 | Country string 64 | } 65 | 66 | type ColumnsGeoCountry struct { 67 | ID, Code, Coords string 68 | } 69 | 70 | type ColumnsSt struct { 71 | Project ColumnsProject 72 | User ColumnsUser 73 | GeoCountry ColumnsGeoCountry 74 | } 75 | 76 | var Columns = ColumnsSt{ 77 | Project: ColumnsProject{ 78 | ID: "projectId", 79 | Name: "name", 80 | }, 81 | User: ColumnsUser{ 82 | ID: "userId", 83 | Email: "email", 84 | Activated: "activated", 85 | Name: "name", 86 | CountryID: "countryId", 87 | 88 | Country: "Country", 89 | }, 90 | GeoCountry: ColumnsGeoCountry{ 91 | ID: "countryId", 92 | Code: "code", 93 | Coords: "coords", 94 | }, 95 | } 96 | 97 | type TableProject struct { 98 | Name, Alias string 99 | } 100 | 101 | type TableUser struct { 102 | Name, Alias string 103 | } 104 | 105 | type TableGeoCountry struct { 106 | Name, Alias string 107 | } 108 | 109 | type TablesSt struct { 110 | Project TableProject 111 | User TableUser 112 | GeoCountry TableGeoCountry 113 | } 114 | 115 | var Tables = TablesSt{ 116 | Project: TableProject{ 117 | Name: "projects", 118 | Alias: "t", 119 | }, 120 | User: TableUser{ 121 | Name: "users", 122 | Alias: "t", 123 | }, 124 | GeoCountry: TableGeoCountry{ 125 | Name: "geo.countries", 126 | Alias: "t", 127 | }, 128 | } 129 | 130 | type Project struct { 131 | tableName struct{} `sql:"projects,alias:t" pg:",discard_unknown_columns"` 132 | 133 | ID int `sql:"projectId,pk"` 134 | Name string `sql:"name,notnull"` 135 | } 136 | 137 | type User struct { 138 | tableName struct{} `sql:"users,alias:t" pg:",discard_unknown_columns"` 139 | 140 | ID int `sql:"userId,pk"` 141 | Email string `sql:"email,notnull"` 142 | Activated bool `sql:"activated,notnull"` 143 | Name *string `sql:"name"` 144 | CountryID *int `sql:"countryId"` 145 | 146 | Country *GeoCountry `pg:"fk:countryId"` 147 | } 148 | 149 | type GeoCountry struct { 150 | tableName struct{} `sql:"geo.countries,alias:t" pg:",discard_unknown_columns"` 151 | 152 | ID int `sql:"countryId,pk"` 153 | Code string `sql:"code,notnull"` 154 | Coords []int `sql:"coords,array"` 155 | } 156 | 157 | ``` 158 | 159 | ### Try it 160 | 161 | ```go 162 | package model 163 | 164 | import ( 165 | "fmt" 166 | "testing" 167 | 168 | "github.com/go-pg/pg/v9" 169 | ) 170 | 171 | const AllColumns = "t.*" 172 | 173 | func TestModel(t *testing.T) { 174 | // connecting to db 175 | options, _ := pg.ParseURL("postgres://user:password@localhost:5432/yourdb") 176 | db := pg.Connect(options) 177 | 178 | if _, err := db.Exec(`truncate table users; truncate table geo.countries cascade;`); err != nil { 179 | panic(err) 180 | } 181 | 182 | // objects to insert 183 | toInsert := []GeoCountry{ 184 | GeoCountry{ 185 | Code: "us", 186 | Coords: []int{1, 2}, 187 | }, 188 | GeoCountry{ 189 | Code: "uk", 190 | Coords: nil, 191 | }, 192 | } 193 | 194 | // inserting 195 | if _, err := db.Model(&toInsert).Insert(); err != nil { 196 | panic(err) 197 | } 198 | 199 | // selecting 200 | var toSelect []GeoCountry 201 | 202 | if err := db.Model(&toSelect).Select(); err != nil { 203 | panic(err) 204 | } 205 | 206 | fmt.Printf("%#v\n", toSelect) 207 | 208 | // user with fk 209 | newUser := User{ 210 | Email: "test@gmail.com", 211 | Activated: true, 212 | CountryID: &toSelect[0].ID, 213 | } 214 | 215 | // inserting 216 | if _, err := db.Model(&newUser).Insert(); err != nil { 217 | panic(err) 218 | } 219 | 220 | // selecting inserted user 221 | user := User{} 222 | m := db.Model(&user). 223 | Column(AllColumns, Columns.User.Country). 224 | Where(`? = ?`, pg.F(Columns.User.Email), "test@gmail.com") 225 | 226 | if err := m.Select(); err != nil { 227 | panic(err) 228 | } 229 | 230 | fmt.Printf("%#v\n", user) 231 | fmt.Printf("%#v\n", user.Country) 232 | } 233 | 234 | ``` 235 | -------------------------------------------------------------------------------- /model/entity_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/dizzyfool/genna/util" 7 | ) 8 | 9 | func TestTable_GoName(t *testing.T) { 10 | type fields struct { 11 | Name string 12 | Schema string 13 | } 14 | tests := []struct { 15 | name string 16 | fields fields 17 | withSchema bool 18 | want string 19 | }{ 20 | { 21 | name: "Should generate from simple word", 22 | fields: fields{Name: "users"}, 23 | want: "User", 24 | }, 25 | { 26 | name: "Should generate from non-countable", 27 | fields: fields{Name: "audio"}, 28 | want: "Audio", 29 | }, 30 | { 31 | name: "Should generate from underscored", 32 | fields: fields{Name: "user_orders"}, 33 | want: "UserOrder", 34 | }, 35 | { 36 | name: "Should generate from camelCased", 37 | fields: fields{Name: "userOrders"}, 38 | want: "UserOrder", 39 | }, 40 | { 41 | name: "Should generate from plural in last place", 42 | fields: fields{Name: "usersWithOrders"}, 43 | want: "UsersWithOrder", 44 | }, 45 | { 46 | name: "Should generate from abracadabra", 47 | fields: fields{Name: "abracadabra"}, 48 | want: "Abracadabra", 49 | }, 50 | { 51 | name: "Should generate from simple word with public schema", 52 | fields: fields{Name: "users", Schema: "public"}, 53 | withSchema: true, 54 | want: "User", 55 | }, 56 | { 57 | name: "Should generate from simple word with custom schema", 58 | fields: fields{Name: "users", Schema: "users"}, 59 | withSchema: true, 60 | want: "UsersUser", 61 | }, 62 | } 63 | for _, tt := range tests { 64 | t.Run(tt.name, func(t *testing.T) { 65 | tbl := NewEntity(tt.fields.Schema, tt.fields.Name, nil, nil) 66 | if got := tbl.GoName; got != tt.want { 67 | t.Errorf("Entity.GoName = %v, want %v", got, tt.want) 68 | } 69 | }) 70 | } 71 | } 72 | 73 | func TestEntity_AddColumn(t *testing.T) { 74 | entity := NewEntity(util.PublicSchema, "test", nil, nil) 75 | 76 | t.Run("Should add column", func(t *testing.T) { 77 | column1 := NewColumn("name", TypePGText, "", false, false, false, false, 0, false, false, 0, []string{}, 9, CustomTypeMapping{}) 78 | column2 := NewColumn("name_", TypePGText, "", false, false, false, false, 0, false, false, 0, []string{}, 9, CustomTypeMapping{}) 79 | column3 := NewColumn("timeout", TypePGInterval, "", false, false, false, false, 0, false, false, 0, []string{}, 9, CustomTypeMapping{}) 80 | column4 := NewColumn("duration", TypePGInterval, "", false, false, false, false, 0, false, false, 0, []string{}, 9, CustomTypeMapping{}) 81 | 82 | t.Run("Should add first column", func(t *testing.T) { 83 | entity.AddColumn(column1) 84 | if len(entity.Columns) != 1 { 85 | t.Errorf("Entity.Columns = %v, want %v", len(entity.Columns), 1) 86 | } 87 | }) 88 | 89 | t.Run("Should add second column with same name", func(t *testing.T) { 90 | entity.AddColumn(column2) 91 | if len(entity.Columns) != 2 { 92 | t.Errorf("Entity.Columns = %v, want %v", len(entity.Columns), 2) 93 | } 94 | if entity.Columns[1].GoName != "Name1" { 95 | t.Errorf("Entity.Columns[1].GoName = %v, want %v", entity.Columns[1].GoName, "Name1") 96 | } 97 | }) 98 | 99 | t.Run("Should add column with import", func(t *testing.T) { 100 | entity.AddColumn(column3) 101 | if len(entity.Imports) != 1 { 102 | t.Errorf("Entity.Imports = %v, want %v", len(entity.Imports), 1) 103 | } 104 | }) 105 | 106 | t.Run("Should add column without import", func(t *testing.T) { 107 | entity.AddColumn(column4) 108 | if len(entity.Imports) != 1 { 109 | t.Errorf("Entity.Imports = %v, want %v", len(entity.Imports), 1) 110 | } 111 | }) 112 | }) 113 | } 114 | 115 | func TestEntity_AddRelation(t *testing.T) { 116 | column1 := NewColumn("test", TypePGText, "", false, false, false, false, 0, false, false, 0, []string{}, 9, CustomTypeMapping{}) 117 | relation1 := NewRelation([]string{"userId"}, util.PublicSchema, "users") 118 | 119 | entity := NewEntity(util.PublicSchema, "test", []Column{column1}, []Relation{relation1}) 120 | 121 | t.Run("Should add column", func(t *testing.T) { 122 | relation2 := NewRelation([]string{"locationId"}, util.PublicSchema, "locations") 123 | relation3 := NewRelation([]string{"testId"}, util.PublicSchema, "tests") 124 | relation4 := NewRelation([]string{"testId"}, util.PublicSchema, "tests_") 125 | 126 | t.Run("Should add second relation", func(t *testing.T) { 127 | entity.AddRelation(relation2) 128 | if len(entity.Relations) != 2 { 129 | t.Errorf("Entity.Relations = %v, want %v", len(entity.Relations), 2) 130 | } 131 | }) 132 | 133 | t.Run("Should add third relation with same name", func(t *testing.T) { 134 | entity.AddRelation(relation3) 135 | if len(entity.Relations) != 3 { 136 | t.Errorf("Entity.Relations = %v, want %v", len(entity.Relations), 3) 137 | } 138 | if entity.Relations[2].GoName != "TestRel" { 139 | t.Errorf("Entity.Relations[2].GoName = %v, want %v", entity.Relations[2].GoName, "Test1") 140 | } 141 | }) 142 | 143 | t.Run("Should add forth relation with same name", func(t *testing.T) { 144 | entity.AddRelation(relation4) 145 | if len(entity.Relations) != 4 { 146 | t.Errorf("Entity.Relations = %v, want %v", len(entity.Relations), 4) 147 | } 148 | if entity.Relations[3].GoName != "TestRel1" { 149 | t.Errorf("Entity.Relations[3].GoName = %v, want %v", entity.Relations[3].GoName, "TestRel1") 150 | } 151 | }) 152 | }) 153 | } 154 | 155 | func TestEntity_HasMultiplePKs(t *testing.T) { 156 | entity := NewEntity(util.PublicSchema, "test", nil, nil) 157 | 158 | t.Run("Should add column", func(t *testing.T) { 159 | column1 := NewColumn("userId", TypePGText, "", false, false, false, false, 0, true, false, 0, []string{}, 9, CustomTypeMapping{}) 160 | column2 := NewColumn("locationId", TypePGText, "", false, false, false, false, 0, true, false, 0, []string{}, 9, CustomTypeMapping{}) 161 | 162 | t.Run("Should check for one key", func(t *testing.T) { 163 | entity.AddColumn(column1) 164 | if v := entity.HasMultiplePKs(); v { 165 | t.Errorf("Entity.HasMultiplePKs() = %v, want %v", v, false) 166 | } 167 | }) 168 | 169 | t.Run("Should check for several keys", func(t *testing.T) { 170 | entity.AddColumn(column2) 171 | if v := entity.HasMultiplePKs(); !v { 172 | t.Errorf("Entity.HasMultiplePKs() = %v, want %v", v, true) 173 | } 174 | }) 175 | }) 176 | } 177 | -------------------------------------------------------------------------------- /model/types.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | const ( 8 | // TypePGInt2 is a postgres type 9 | TypePGInt2 = "int2" 10 | // TypePGInt4 is a postgres type 11 | TypePGInt4 = "int4" 12 | // TypePGInt8 is a postgres type 13 | TypePGInt8 = "int8" 14 | // TypePGNumeric is a postgres type 15 | TypePGNumeric = "numeric" 16 | // TypePGFloat4 is a postgres type 17 | TypePGFloat4 = "float4" 18 | // TypePGFloat8 is a postgres type 19 | TypePGFloat8 = "float8" 20 | // TypePGText is a postgres type 21 | TypePGText = "text" 22 | // TypePGVarchar is a postgres type 23 | TypePGVarchar = "varchar" 24 | // TypePGUuid is a postgres type 25 | TypePGUuid = "uuid" 26 | // TypePGBpchar is a postgres type 27 | TypePGBpchar = "bpchar" 28 | // TypePGBytea is a postgres type 29 | TypePGBytea = "bytea" 30 | // TypePGBool is a postgres type 31 | TypePGBool = "bool" 32 | // TypePGTimestamp is a postgres type 33 | TypePGTimestamp = "timestamp" 34 | // TypePGTimestamptz is a postgres type 35 | TypePGTimestamptz = "timestamptz" 36 | // TypePGDate is a postgres type 37 | TypePGDate = "date" 38 | // TypePGTime is a postgres type 39 | TypePGTime = "time" 40 | // TypePGTimetz is a postgres type 41 | TypePGTimetz = "timetz" 42 | // TypePGInterval is a postgres type 43 | TypePGInterval = "interval" 44 | // TypePGJSONB is a postgres type 45 | TypePGJSONB = "jsonb" 46 | // TypePGJSON is a postgres type 47 | TypePGJSON = "json" 48 | // TypePGHstore is a postgres type 49 | TypePGHstore = "hstore" 50 | // TypePGInet is a postgres type 51 | TypePGInet = "inet" 52 | // TypePGCidr is a postgres type 53 | TypePGCidr = "cidr" 54 | // TypePGPoint is a postgres type 55 | TypePGPoint = "point" 56 | 57 | // TypeInt is a go type 58 | TypeInt = "int" 59 | // TypeInt32 is a go type 60 | TypeInt32 = "int32" 61 | // TypeInt64 is a go type 62 | TypeInt64 = "int64" 63 | // TypeFloat32 is a go type 64 | TypeFloat32 = "float32" 65 | // TypeFloat64 is a go type 66 | TypeFloat64 = "float64" 67 | // TypeString is a go type 68 | TypeString = "string" 69 | // TypeByteSlice is a go type 70 | TypeByteSlice = "[]byte" 71 | // TypeBool is a go type 72 | TypeBool = "bool" 73 | // TypeTime is a go type 74 | TypeTime = "time.Time" 75 | // TypeDuration is a go type 76 | TypeDuration = "time.Duration" 77 | // TypeMapInterface is a go type 78 | TypeMapInterface = "map[string]interface{}" 79 | // TypeMapString is a go type 80 | TypeMapString = "map[string]string" 81 | // TypeIP is a go type 82 | TypeIP = "net.IP" 83 | // TypeIPNet is a go type 84 | TypeIPNet = "net.IPNet" 85 | 86 | // TypeInterface is a go type 87 | TypeInterface = "interface{}" 88 | ) 89 | 90 | // GoType generates simple go type from pg type 91 | func GoType(pgType string) (string, error) { 92 | switch pgType { 93 | case TypePGInt2, TypePGInt4: 94 | return TypeInt, nil 95 | case TypePGInt8: 96 | return TypeInt64, nil 97 | case TypePGFloat4: 98 | return TypeFloat32, nil 99 | case TypePGNumeric, TypePGFloat8: 100 | return TypeFloat64, nil 101 | case TypePGText, TypePGVarchar, TypePGUuid, TypePGBpchar, TypePGPoint: 102 | return TypeString, nil 103 | case TypePGBytea: 104 | return TypeByteSlice, nil 105 | case TypePGBool: 106 | return TypeBool, nil 107 | case TypePGTimestamp, TypePGTimestamptz, TypePGDate, TypePGTime, TypePGTimetz: 108 | return TypeTime, nil 109 | case TypePGInterval: 110 | return TypeDuration, nil 111 | case TypePGJSONB, TypePGJSON: 112 | return TypeMapInterface, nil 113 | case TypePGHstore: 114 | return TypeMapString, nil 115 | case TypePGInet: 116 | return TypeIP, nil 117 | case TypePGCidr: 118 | return TypeIPNet, nil 119 | } 120 | 121 | return "", fmt.Errorf("unsupported type: %s", pgType) 122 | } 123 | 124 | // GoSlice generates go slice type from pg array 125 | func GoSlice(pgType string, dimensions int) (string, error) { 126 | switch pgType { 127 | case TypePGTimestamp, TypePGTimestamptz, TypePGDate, TypePGTime, TypePGTimetz, 128 | TypePGInterval, TypePGHstore, TypePGInet, TypePGCidr: 129 | return "", fmt.Errorf("unsupported array type: %s", pgType) 130 | } 131 | 132 | typ, err := GoType(pgType) 133 | if err != nil { 134 | return "", err 135 | } 136 | 137 | // slice can not have 0 dimensions 138 | if dimensions == 0 { 139 | dimensions = 1 140 | } 141 | 142 | for i := 0; i < dimensions; i++ { 143 | typ = fmt.Sprintf("[]%s", typ) 144 | } 145 | 146 | return typ, nil 147 | } 148 | 149 | // GoNullable generates all go types from pg type with pointer 150 | func GoNullable(pgType string, useSQLNull bool, customTypes CustomTypeMapping) (string, error) { 151 | // avoiding pointers with sql.Null... types 152 | if useSQLNull { 153 | switch pgType { 154 | case TypePGInt2, TypePGInt4, TypePGInt8: 155 | return "sql.NullInt64", nil 156 | case TypePGNumeric, TypePGFloat4, TypePGFloat8: 157 | return "sql.NullFloat64", nil 158 | case TypePGBool: 159 | return "sql.NullBool", nil 160 | case TypePGText, TypePGVarchar, TypePGUuid, TypePGBpchar, TypePGPoint: 161 | return "sql.NullString", nil 162 | case TypePGTimestamp, TypePGTimestamptz, TypePGDate, TypePGTime, TypePGTimetz: 163 | return "pg.NullTime", nil 164 | } 165 | } 166 | 167 | if typ, ok := customTypes.GoType(pgType); ok && typ != "" { 168 | return fmt.Sprintf("*%s", typ), nil 169 | } 170 | 171 | typ, err := GoType(pgType) 172 | if err != nil { 173 | return "", err 174 | } 175 | 176 | switch pgType { 177 | case TypePGHstore, TypePGJSON, TypePGJSONB, TypePGBytea: 178 | // hstore & json & bytea types without pointers 179 | return typ, nil 180 | default: 181 | return fmt.Sprintf("*%s", typ), nil 182 | } 183 | } 184 | 185 | // GoImport generates import from go type 186 | func GoImport(pgType string, nullable, useSQLNull bool, ver int) string { 187 | if nullable && useSQLNull { 188 | switch pgType { 189 | case TypePGInt2, TypePGInt4, TypePGInt8, 190 | TypePGNumeric, TypePGFloat4, TypePGFloat8, 191 | TypePGBool, 192 | TypePGText, TypePGVarchar, TypePGUuid, TypePGBpchar, TypePGPoint: 193 | return "database/sql" 194 | case TypePGTimestamp, TypePGTimestamptz, TypePGDate, TypePGTime, TypePGTimetz: 195 | if ver >= 9 { 196 | return fmt.Sprintf("github.com/go-pg/pg/v%d", ver) 197 | } else { 198 | return "github.com/go-pg/pg" 199 | } 200 | } 201 | } 202 | 203 | switch pgType { 204 | case TypePGInet, TypePGCidr: 205 | return "net" 206 | case TypePGTimestamp, TypePGTimestamptz, TypePGDate, TypePGTime, TypePGTimetz, TypePGInterval: 207 | return "time" 208 | } 209 | 210 | return "" 211 | } 212 | -------------------------------------------------------------------------------- /generators/model/model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "html/template" 6 | "strings" 7 | 8 | "github.com/dizzyfool/genna/model" 9 | "github.com/dizzyfool/genna/util" 10 | ) 11 | 12 | // TemplatePackage stores package info 13 | type TemplatePackage struct { 14 | Package string 15 | 16 | HasImports bool 17 | Imports []string 18 | 19 | Entities []TemplateEntity 20 | } 21 | 22 | // NewTemplatePackage creates a package for template 23 | func NewTemplatePackage(entities []model.Entity, options Options) TemplatePackage { 24 | imports := util.NewSet() 25 | 26 | models := make([]TemplateEntity, len(entities)) 27 | for i, entity := range entities { 28 | for _, imp := range entity.Imports { 29 | imports.Add(imp) 30 | } 31 | 32 | models[i] = NewTemplateEntity(entity, options) 33 | } 34 | 35 | return TemplatePackage{ 36 | Package: options.Package, 37 | 38 | HasImports: imports.Len() > 0, 39 | Imports: imports.Elements(), 40 | 41 | Entities: models, 42 | } 43 | } 44 | 45 | // TemplateEntity stores struct info 46 | type TemplateEntity struct { 47 | model.Entity 48 | 49 | Tag template.HTML 50 | 51 | NoAlias bool 52 | Alias string 53 | 54 | Columns []TemplateColumn 55 | 56 | HasRelations bool 57 | Relations []TemplateRelation 58 | } 59 | 60 | // NewTemplateEntity creates an entity for template 61 | func NewTemplateEntity(entity model.Entity, options Options) TemplateEntity { 62 | if entity.HasMultiplePKs() { 63 | options.KeepPK = true 64 | } 65 | 66 | columns := make([]TemplateColumn, len(entity.Columns)) 67 | for i, column := range entity.Columns { 68 | columns[i] = NewTemplateColumn(entity, column, options) 69 | } 70 | 71 | relations := make([]TemplateRelation, len(entity.Relations)) 72 | for i, relation := range entity.Relations { 73 | relations[i] = NewTemplateRelation(relation, options) 74 | } 75 | 76 | tagName := tagName(options) 77 | tags := util.NewAnnotation() 78 | if options.GoPgVer < 10 { 79 | tags.AddTag(tagName, util.Quoted(entity.PGFullName, true)) 80 | } else { 81 | tags.AddTag(tagName, entity.PGFullName) 82 | } 83 | 84 | if !options.NoAlias { 85 | tags.AddTag(tagName, fmt.Sprintf("alias:%s", util.DefaultAlias)) 86 | } 87 | 88 | if !options.NoDiscard { 89 | if options.GoPgVer == 8 { 90 | tags.AddTag("pg", "") 91 | } 92 | tags.AddTag("pg", "discard_unknown_columns") 93 | } 94 | 95 | return TemplateEntity{ 96 | Entity: entity, 97 | Tag: template.HTML(fmt.Sprintf("`%s`", tags.String())), 98 | 99 | NoAlias: options.NoAlias, 100 | Alias: util.DefaultAlias, 101 | 102 | Columns: columns, 103 | 104 | HasRelations: len(relations) > 0, 105 | Relations: relations, 106 | } 107 | } 108 | 109 | // TemplateColumn stores column info 110 | type TemplateColumn struct { 111 | model.Column 112 | 113 | Tag template.HTML 114 | Comment template.HTML 115 | } 116 | 117 | // NewTemplateColumn creates a column for template 118 | func NewTemplateColumn(entity model.Entity, column model.Column, options Options) TemplateColumn { 119 | if !options.KeepPK && column.IsPK { 120 | column.GoName = util.ID 121 | } 122 | 123 | if column.PGType == model.TypePGJSON || column.PGType == model.TypePGJSONB { 124 | if typ, ok := jsonType(options.JSONTypes, entity.PGSchema, entity.PGName, column.PGName); ok { 125 | column.Type = typ 126 | } 127 | } 128 | 129 | comment := "" 130 | tagName := tagName(options) 131 | tags := util.NewAnnotation() 132 | tags.AddTag(tagName, column.PGName) 133 | 134 | // pk tag 135 | if column.IsPK { 136 | tags.AddTag(tagName, "pk") 137 | } 138 | 139 | // types tag 140 | if column.PGType == model.TypePGHstore { 141 | tags.AddTag(tagName, "hstore") 142 | } else if column.IsArray { 143 | tags.AddTag(tagName, "array") 144 | } 145 | if column.PGType == model.TypePGUuid { 146 | tags.AddTag(tagName, "type:uuid") 147 | } 148 | 149 | // nullable tag 150 | if !column.Nullable && !column.IsPK { 151 | if options.GoPgVer == 8 { 152 | tags.AddTag(tagName, "notnull") 153 | } else { 154 | tags.AddTag(tagName, "use_zero") 155 | } 156 | } 157 | 158 | // soft_delete tag 159 | if options.SoftDelete == column.PGName && column.Nullable && column.GoType == model.TypeTime && !column.IsArray { 160 | tags.AddTag("pg", ",soft_delete") 161 | } 162 | 163 | // ignore tag 164 | if column.GoType == model.TypeInterface { 165 | comment = "// unsupported" 166 | tags = util.NewAnnotation().AddTag(tagName, "-") 167 | } 168 | 169 | // add json tag 170 | if options.AddJSONTag { 171 | tags.AddTag("json", util.Underscore(column.PGName)) 172 | } 173 | 174 | return TemplateColumn{ 175 | Column: column, 176 | 177 | Tag: template.HTML(fmt.Sprintf("`%s`", tags.String())), 178 | Comment: template.HTML(comment), 179 | } 180 | } 181 | 182 | // TemplateRelation stores relation info 183 | type TemplateRelation struct { 184 | model.Relation 185 | 186 | Tag template.HTML 187 | Comment template.HTML 188 | } 189 | 190 | // NewTemplateRelation creates relation for template 191 | func NewTemplateRelation(relation model.Relation, options Options) TemplateRelation { 192 | comment := "" 193 | tagName := tagName(options) 194 | tags := util.NewAnnotation().AddTag("pg", "fk:"+strings.Join(relation.FKFields, ",")) 195 | if options.GoPgVer >= 10 { 196 | tags.AddTag("pg", "rel:has-one") 197 | } 198 | 199 | if len(relation.FKFields) > 1 { 200 | comment = "// unsupported" 201 | tags.AddTag(tagName, "-") 202 | } 203 | 204 | // add json tag 205 | if options.AddJSONTag { 206 | tags.AddTag("json", util.Underscore(relation.GoName)) 207 | } 208 | 209 | return TemplateRelation{ 210 | Relation: relation, 211 | 212 | Tag: template.HTML(fmt.Sprintf("`%s`", tags.String())), 213 | Comment: template.HTML(comment), 214 | } 215 | } 216 | 217 | func jsonType(mp map[string]string, schema, table, field string) (string, bool) { 218 | if mp == nil { 219 | return "", false 220 | } 221 | 222 | patterns := [][3]string{ 223 | {schema, table, field}, 224 | {schema, "*", field}, 225 | {schema, table, "*"}, 226 | {schema, "*", "*"}, 227 | } 228 | 229 | var names []string 230 | for _, parts := range patterns { 231 | names = append(names, fmt.Sprintf("%s.%s", util.Join(parts[0], parts[1]), parts[2])) 232 | names = append(names, fmt.Sprintf("%s.%s", util.JoinF(parts[0], parts[1]), parts[2])) 233 | } 234 | names = append(names, util.Join(schema, table), "*") 235 | 236 | for _, name := range names { 237 | if v, ok := mp[name]; ok { 238 | return v, true 239 | } 240 | } 241 | 242 | return "", false 243 | } 244 | 245 | func tagName(options Options) string { 246 | if options.GoPgVer == 8 { 247 | return "sql" 248 | } 249 | return "pg" 250 | } 251 | -------------------------------------------------------------------------------- /generators/base/base.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "html/template" 7 | "log" 8 | "os" 9 | "path" 10 | "strings" 11 | 12 | "github.com/dizzyfool/genna/lib" 13 | "github.com/dizzyfool/genna/model" 14 | "github.com/dizzyfool/genna/util" 15 | 16 | "github.com/spf13/cobra" 17 | ) 18 | 19 | const ( 20 | // Conn is connection string (-c) basic flag 21 | Conn = "conn" 22 | 23 | // Output is output filename (-o) basic flag 24 | Output = "output" 25 | 26 | // Tables is basic flag (-t) for tables to generate 27 | Tables = "tables" 28 | 29 | // FollowFKs is basic flag (-f) for generate foreign keys models for selected tables 30 | FollowFKs = "follow-fk" 31 | 32 | // Go-PG version to use 33 | GoPgVer = "gopg" 34 | 35 | // Package for model files 36 | Pkg = "pkg" 37 | 38 | // uuid type flag 39 | uuidFlag = "uuid" 40 | 41 | // custom types flag 42 | customTypesFlag = "custom-types" 43 | ) 44 | 45 | // Gen is interface for all generators 46 | type Gen interface { 47 | AddFlags(command *cobra.Command) 48 | ReadFlags(command *cobra.Command) error 49 | 50 | Generate() error 51 | } 52 | 53 | // Packer is a function that compile entities to package 54 | type Packer func(entities []model.Entity) (interface{}, error) 55 | 56 | // Options is common options for all generators 57 | type Options struct { 58 | // URL connection string 59 | URL string 60 | 61 | // Output file path 62 | Output string 63 | 64 | // List of Tables to generate 65 | // Default []string{"public.*"} 66 | Tables []string 67 | 68 | // Generate model for foreign keys, 69 | // even if Tables not listed in Tables param 70 | // will not generate fks if schema not listed 71 | FollowFKs bool 72 | 73 | // go-pg version 74 | GoPgVer int 75 | 76 | // Custom types goes here 77 | CustomTypes model.CustomTypeMapping 78 | } 79 | 80 | // Def sets default options if empty 81 | func (o *Options) Def() { 82 | if len(o.Tables) == 0 { 83 | o.Tables = []string{util.Join(util.PublicSchema, "*")} 84 | } 85 | 86 | if o.GoPgVer == 0 { 87 | o.GoPgVer = 10 88 | } 89 | 90 | if o.CustomTypes == nil { 91 | o.CustomTypes = model.CustomTypeMapping{} 92 | } 93 | } 94 | 95 | // Generator is base generator used in other generators 96 | type Generator struct { 97 | genna.Genna 98 | } 99 | 100 | // NewGenerator creates generator 101 | func NewGenerator(url string) Generator { 102 | return Generator{ 103 | Genna: genna.New(url, nil), 104 | } 105 | } 106 | 107 | // AddFlags adds basic flags to command 108 | func AddFlags(command *cobra.Command) { 109 | flags := command.Flags() 110 | 111 | flags.StringP(Conn, "c", "", "connection string to your postgres database") 112 | if err := command.MarkFlagRequired(Conn); err != nil { 113 | panic(err) 114 | } 115 | 116 | flags.StringP(Output, "o", "", "output file name") 117 | if err := command.MarkFlagRequired(Output); err != nil { 118 | panic(err) 119 | } 120 | 121 | flags.StringP(Pkg, "p", "", "package for model files. if not set last folder name in output path will be used") 122 | 123 | flags.StringSliceP(Tables, "t", []string{"public.*"}, "table names for model generation separated by comma\nuse 'schema_name.*' to generate model for every table in model") 124 | flags.BoolP(FollowFKs, "f", false, "generate models for foreign keys, even if it not listed in Tables\n") 125 | 126 | flags.Bool(uuidFlag, false, "use github.com/google/uuid as type for uuid") 127 | 128 | flags.StringSlice(customTypesFlag, []string{}, "set custom types separated by comma\nformat: :.\nexamples: uuid:github.com/google/uuid.UUID,point:src/model.Point,bytea:string\n") 129 | 130 | flags.IntP(GoPgVer, "g", 10, "specify go-pg version (8, 9 and 10 are supported)") 131 | 132 | return 133 | } 134 | 135 | // ReadFlags reads basic flags from command 136 | func ReadFlags(command *cobra.Command) (conn, output, pkg string, tables []string, followFKs bool, gopgVer int, customTypes model.CustomTypeMapping, err error) { 137 | var customTypesStrings []string 138 | uuid := false 139 | 140 | flags := command.Flags() 141 | 142 | if conn, err = flags.GetString(Conn); err != nil { 143 | return 144 | } 145 | 146 | if output, err = flags.GetString(Output); err != nil { 147 | return 148 | } 149 | 150 | if pkg, err = flags.GetString(Pkg); err != nil { 151 | return 152 | } 153 | 154 | if strings.Trim(pkg, " ") == "" { 155 | pkg = path.Base(path.Dir(output)) 156 | } 157 | 158 | if tables, err = flags.GetStringSlice(Tables); err != nil { 159 | return 160 | } 161 | 162 | if followFKs, err = flags.GetBool(FollowFKs); err != nil { 163 | return 164 | } 165 | 166 | if gopgVer, err = flags.GetInt(GoPgVer); err != nil { 167 | return 168 | } 169 | 170 | if customTypesStrings, err = flags.GetStringSlice(customTypesFlag); err != nil { 171 | return 172 | } 173 | 174 | if customTypes, err = model.ParseCustomTypes(customTypesStrings); err != nil { 175 | return 176 | } 177 | 178 | if uuid, err = flags.GetBool(uuidFlag); err != nil { 179 | return 180 | } 181 | 182 | if uuid && !customTypes.Has(model.TypePGUuid) { 183 | customTypes.Add(model.TypePGUuid, "uuid.UUID", "github.com/google/uuid") 184 | } 185 | 186 | if gopgVer < 8 && gopgVer > 10 { 187 | err = fmt.Errorf("go-pg version %d not supported", gopgVer) 188 | return 189 | } 190 | 191 | return 192 | } 193 | 194 | // Generate runs whole generation process 195 | func (g Generator) Generate(tables []string, followFKs, useSQLNulls bool, output, tmpl string, packer Packer, goPGVer int, customTypes model.CustomTypeMapping) error { 196 | entities, err := g.Read(tables, followFKs, useSQLNulls, goPGVer, customTypes) 197 | if err != nil { 198 | return fmt.Errorf("read database error: %w", err) 199 | } 200 | 201 | return g.GenerateFromEntities(entities, output, tmpl, packer) 202 | } 203 | 204 | func (g Generator) GenerateFromEntities(entities []model.Entity, output, tmpl string, packer Packer) error { 205 | parsed, err := template.New("base").Parse(tmpl) 206 | if err != nil { 207 | return fmt.Errorf("parsing template error: %w", err) 208 | } 209 | 210 | pack, err := packer(entities) 211 | if err != nil { 212 | return fmt.Errorf("packing data error: %w", err) 213 | } 214 | 215 | var buffer bytes.Buffer 216 | if err := parsed.ExecuteTemplate(&buffer, "base", pack); err != nil { 217 | return fmt.Errorf("processing model template error: %w", err) 218 | } 219 | 220 | saved, err := util.FmtAndSave(buffer.Bytes(), output) 221 | if err != nil { 222 | if !saved { 223 | return fmt.Errorf("saving file error: %w", err) 224 | } 225 | log.Printf("formatting file %s error: %s", output, err) 226 | } 227 | 228 | log.Printf("successfully generated %d models", len(entities)) 229 | 230 | return nil 231 | } 232 | 233 | // CreateCommand creates cobra command 234 | func CreateCommand(name, description string, generator Gen) *cobra.Command { 235 | command := &cobra.Command{ 236 | Use: name, 237 | Short: description, 238 | Long: "", 239 | Run: func(command *cobra.Command, args []string) { 240 | if !command.HasFlags() { 241 | if err := command.Help(); err != nil { 242 | log.Printf("help not found, error: %s", err) 243 | } 244 | os.Exit(0) 245 | return 246 | } 247 | 248 | if err := generator.ReadFlags(command); err != nil { 249 | log.Printf("read flags error: %s", err) 250 | return 251 | } 252 | 253 | if err := generator.Generate(); err != nil { 254 | log.Printf("generate error: %s", err) 255 | return 256 | } 257 | }, 258 | FParseErrWhitelist: cobra.FParseErrWhitelist{ 259 | UnknownFlags: true, 260 | }, 261 | } 262 | 263 | generator.AddFlags(command) 264 | 265 | return command 266 | } 267 | -------------------------------------------------------------------------------- /lib/store_test.go: -------------------------------------------------------------------------------- 1 | package genna 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/dizzyfool/genna/model" 8 | 9 | "github.com/go-pg/pg/v10" 10 | ) 11 | 12 | func prepareStore() (*store, error) { 13 | db, err := newDatabase(prepareReq()) 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | return newStore(db), nil 19 | } 20 | 21 | func Test_format(t *testing.T) { 22 | 23 | tests := []struct { 24 | name string 25 | pattern string 26 | values []interface{} 27 | want string 28 | }{ 29 | { 30 | name: "Should format pg.Multi", 31 | pattern: "(id, name) in (?)", 32 | values: []interface{}{ 33 | []string{"1", "test"}, 34 | []string{"2", "test"}, 35 | }, 36 | want: "(id, name) in (('1','test'),('2','test'))", 37 | }, 38 | } 39 | for _, tt := range tests { 40 | t.Run(tt.name, func(t *testing.T) { 41 | if got := format(tt.pattern, pg.InMulti(tt.values...)); got != tt.want { 42 | t.Errorf("format() = %v, want %v", got, tt.want) 43 | } 44 | }) 45 | } 46 | } 47 | 48 | func Test_table_Entity(t *testing.T) { 49 | type fields struct { 50 | Schema string 51 | Name string 52 | } 53 | tests := []struct { 54 | name string 55 | fields fields 56 | want model.Entity 57 | }{ 58 | { 59 | name: "Should create entity", 60 | fields: fields{ 61 | Schema: "public", 62 | Name: "users", 63 | }, 64 | want: model.NewEntity("public", "users", nil, nil), 65 | }, 66 | } 67 | for _, tt := range tests { 68 | t.Run(tt.name, func(t *testing.T) { 69 | z := table{ 70 | Schema: tt.fields.Schema, 71 | Name: tt.fields.Name, 72 | } 73 | if got := z.Entity(); !reflect.DeepEqual(got, tt.want) { 74 | t.Errorf("table.Entity() = %v, want %v", got, tt.want) 75 | } 76 | }) 77 | } 78 | } 79 | 80 | func Test_relation_Relation(t *testing.T) { 81 | type fields struct { 82 | Constraint string 83 | SourceSchema string 84 | SourceTable string 85 | SourceColumns []string 86 | TargetSchema string 87 | TargetTable string 88 | TargetColumns []string 89 | } 90 | tests := []struct { 91 | name string 92 | fields fields 93 | want model.Relation 94 | }{ 95 | { 96 | name: "Should create relation", 97 | fields: fields{ 98 | Constraint: "test", 99 | SourceSchema: "public", 100 | SourceTable: "users", 101 | SourceColumns: []string{"locationId"}, 102 | TargetSchema: "geo", 103 | TargetTable: "locations", 104 | TargetColumns: []string{"locationId"}, 105 | }, 106 | want: model.NewRelation([]string{"locationId"}, "geo", "locations"), 107 | }, 108 | } 109 | for _, tt := range tests { 110 | t.Run(tt.name, func(t *testing.T) { 111 | r := relation{ 112 | Constraint: tt.fields.Constraint, 113 | SourceSchema: tt.fields.SourceSchema, 114 | SourceTable: tt.fields.SourceTable, 115 | SourceColumns: tt.fields.SourceColumns, 116 | TargetSchema: tt.fields.TargetSchema, 117 | TargetTable: tt.fields.TargetTable, 118 | TargetColumns: tt.fields.TargetColumns, 119 | } 120 | if got := r.Relation(); !reflect.DeepEqual(got, tt.want) { 121 | t.Errorf("relation.Relation() = %v, want %v", got, tt.want) 122 | } 123 | }) 124 | } 125 | } 126 | 127 | func Test_relation_Target(t *testing.T) { 128 | type fields struct { 129 | Constraint string 130 | SourceSchema string 131 | SourceTable string 132 | SourceColumns []string 133 | TargetSchema string 134 | TargetTable string 135 | TargetColumns []string 136 | } 137 | tests := []struct { 138 | name string 139 | fields fields 140 | want table 141 | }{ 142 | { 143 | name: "Should create target table", 144 | fields: fields{ 145 | Constraint: "test", 146 | SourceSchema: "public", 147 | SourceTable: "users", 148 | SourceColumns: []string{"locationId"}, 149 | TargetSchema: "geo", 150 | TargetTable: "locations", 151 | TargetColumns: []string{"locationId"}, 152 | }, 153 | want: table{ 154 | Schema: "geo", 155 | Name: "locations", 156 | }, 157 | }, 158 | } 159 | for _, tt := range tests { 160 | t.Run(tt.name, func(t *testing.T) { 161 | r := relation{ 162 | Constraint: tt.fields.Constraint, 163 | SourceSchema: tt.fields.SourceSchema, 164 | SourceTable: tt.fields.SourceTable, 165 | SourceColumns: tt.fields.SourceColumns, 166 | TargetSchema: tt.fields.TargetSchema, 167 | TargetTable: tt.fields.TargetTable, 168 | TargetColumns: tt.fields.TargetColumns, 169 | } 170 | if got := r.Target(); !reflect.DeepEqual(got, tt.want) { 171 | t.Errorf("relation.Target() = %v, want %v", got, tt.want) 172 | } 173 | }) 174 | } 175 | } 176 | 177 | func Test_column_Column(t *testing.T) { 178 | type fields struct { 179 | Schema string 180 | Table string 181 | Name string 182 | IsNullable bool 183 | IsArray bool 184 | Dimensions int 185 | Type string 186 | Default string 187 | HasDefault bool 188 | IsPK bool 189 | IsFK bool 190 | MaxLen int 191 | Values []string 192 | } 193 | tests := []struct { 194 | name string 195 | fields fields 196 | want model.Column 197 | }{ 198 | { 199 | name: "Should create column", 200 | fields: fields{ 201 | Schema: "public", 202 | Table: "users", 203 | Name: "userId", 204 | IsNullable: false, 205 | IsArray: false, 206 | Dimensions: 0, 207 | Type: model.TypePGInt8, 208 | Default: "nextval('\"news_newsId_seq\"'::regclass)", 209 | HasDefault: true, 210 | IsPK: true, 211 | IsFK: false, 212 | MaxLen: 0, 213 | Values: []string{}, 214 | }, 215 | want: model.NewColumn("userId", model.TypePGInt8, "nextval('\"news_newsId_seq\"'::regclass)", true, false, false, false, 0, true, false, 0, []string{}, 9, nil), 216 | }, 217 | } 218 | for _, tt := range tests { 219 | t.Run(tt.name, func(t *testing.T) { 220 | c := column{ 221 | Schema: tt.fields.Schema, 222 | Table: tt.fields.Table, 223 | Name: tt.fields.Name, 224 | IsNullable: tt.fields.IsNullable, 225 | IsArray: tt.fields.IsArray, 226 | Dimensions: tt.fields.Dimensions, 227 | Type: tt.fields.Type, 228 | Default: tt.fields.Default, 229 | HasDefault: tt.fields.HasDefault, 230 | IsPK: tt.fields.IsPK, 231 | IsFK: tt.fields.IsFK, 232 | MaxLen: tt.fields.MaxLen, 233 | Values: tt.fields.Values, 234 | } 235 | if got := c.Column(false, 9, nil); !reflect.DeepEqual(got, tt.want) { 236 | t.Errorf("column.Column() = %v, want %v", got, tt.want) 237 | } 238 | }) 239 | } 240 | } 241 | 242 | func Test_store_Tables(t *testing.T) { 243 | store, err := prepareStore() 244 | if err != nil { 245 | t.Errorf("prepare Store error = %v", err) 246 | return 247 | } 248 | 249 | t.Run("Should get all tables from test DB", func(t *testing.T) { 250 | tables, err := store.Tables([]string{"public.*", "geo.*"}) 251 | if err != nil { 252 | t.Errorf("get tables error = %v", err) 253 | return 254 | } 255 | 256 | if ln := len(tables); ln != 3 { 257 | t.Errorf("len(Store.Tables()) = %v, want %v", ln, 3) 258 | return 259 | } 260 | }) 261 | 262 | t.Run("Should get specific table from test DB", func(t *testing.T) { 263 | tables, err := store.Tables([]string{"public.users"}) 264 | if err != nil { 265 | t.Errorf("get tables error = %v", err) 266 | return 267 | } 268 | 269 | if ln := len(tables); ln != 1 { 270 | t.Errorf("len(Store.Tables()) = %v, want %v", ln, 1) 271 | return 272 | } 273 | }) 274 | 275 | t.Run("Should get specific & geo tables from test DB", func(t *testing.T) { 276 | tables, err := store.Tables([]string{"public.users", "geo.*"}) 277 | if err != nil { 278 | t.Errorf("get tables error = %v", err) 279 | return 280 | } 281 | 282 | if ln := len(tables); ln != 2 { 283 | t.Errorf("len(Store.Tables()) = %v, want %v", ln, 2) 284 | return 285 | } 286 | }) 287 | } 288 | 289 | func Test_store_Relations(t *testing.T) { 290 | store, err := prepareStore() 291 | if err != nil { 292 | t.Errorf("prepare Store error = %v", err) 293 | return 294 | } 295 | 296 | t.Run("Should get all relations from test DB", func(t *testing.T) { 297 | tables, err := store.Tables([]string{"public.*"}) 298 | if err != nil { 299 | t.Errorf("get tables error = %v", err) 300 | return 301 | } 302 | 303 | relations, err := store.Relations(tables) 304 | if err != nil { 305 | t.Errorf("get tables error = %v", err) 306 | return 307 | } 308 | 309 | if ln := len(relations); ln != 1 { 310 | t.Errorf("len(Store.Relations()) = %v, want %v", ln, 1) 311 | return 312 | } 313 | }) 314 | } 315 | 316 | func Test_store_Schemas(t *testing.T) { 317 | store, err := prepareStore() 318 | if err != nil { 319 | t.Errorf("prepare Store error = %v", err) 320 | return 321 | } 322 | 323 | t.Run("Should get all schemas from test DB", func(t *testing.T) { 324 | tables, err := store.Schemas() 325 | if err != nil { 326 | t.Errorf("get tables error = %v", err) 327 | return 328 | } 329 | 330 | schemas := []string{"public", "geo", "information_schema", "pg_catalog"} 331 | for _, sch := range schemas { 332 | contains := false 333 | for _, tbl := range tables { 334 | if tbl == sch { 335 | contains = true 336 | break 337 | } 338 | } 339 | if !contains { 340 | t.Errorf("Store.Schemas() does not countain %v but it should", sch) 341 | } 342 | } 343 | }) 344 | } 345 | 346 | func Test_store_Columns(t *testing.T) { 347 | store, err := prepareStore() 348 | if err != nil { 349 | t.Errorf("prepare Store error = %v", err) 350 | return 351 | } 352 | 353 | t.Run("Should get all columns from test DB", func(t *testing.T) { 354 | tables, err := store.Tables([]string{"public.*"}) 355 | if err != nil { 356 | t.Errorf("get tables error = %v", err) 357 | return 358 | } 359 | 360 | columns, err := store.Columns(tables) 361 | if err != nil { 362 | t.Errorf("get tables error = %v", err) 363 | return 364 | } 365 | 366 | if ln := len(columns); ln != 12 { 367 | t.Errorf("len(Store.Columns()) = %v, want %v", ln, 12) 368 | return 369 | } 370 | }) 371 | } 372 | -------------------------------------------------------------------------------- /lib/store.go: -------------------------------------------------------------------------------- 1 | package genna 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strings" 7 | 8 | "github.com/dizzyfool/genna/model" 9 | "github.com/dizzyfool/genna/util" 10 | 11 | "github.com/go-pg/pg/v10" 12 | "github.com/go-pg/pg/v10/orm" 13 | ) 14 | 15 | var formatter = orm.Formatter{} 16 | 17 | func format(pattern string, values ...interface{}) string { 18 | return string(formatter.FormatQuery([]byte{}, pattern, values...)) 19 | } 20 | 21 | type table struct { 22 | Schema string `pg:"table_schema"` 23 | Name string `pg:"table_name"` 24 | } 25 | 26 | func (t table) Entity() model.Entity { 27 | return model.NewEntity(t.Schema, t.Name, nil, nil) 28 | } 29 | 30 | type relation struct { 31 | Constraint string `pg:"constraint_name"` 32 | SourceSchema string `pg:"schema_name"` 33 | SourceTable string `pg:"table_name"` 34 | SourceColumns []string `pg:"columns,array"` 35 | TargetSchema string `pg:"target_schema"` 36 | TargetTable string `pg:"target_table"` 37 | TargetColumns []string `pg:"target_columns,array"` 38 | } 39 | 40 | func (r relation) Relation() model.Relation { 41 | return model.NewRelation(r.SourceColumns, r.TargetSchema, r.TargetTable) 42 | } 43 | 44 | func (r relation) Target() table { 45 | return table{ 46 | Schema: r.TargetSchema, 47 | Name: r.TargetTable, 48 | } 49 | } 50 | 51 | type column struct { 52 | tableName struct{} `pg:",discard_unknown_columns"` 53 | 54 | Schema string `pg:"schema_name"` 55 | Table string `pg:"table_name"` 56 | Name string `pg:"column_name"` 57 | IsNullable bool `pg:"nullable"` 58 | IsArray bool `pg:"is_array"` 59 | Dimensions int `pg:"dims"` 60 | Type string `pg:"type"` 61 | Default string `pg:"def"` 62 | HasDefault bool `pg:"has_def"` 63 | IsPK bool `pg:"is_pk"` 64 | IsFK bool `pg:"is_fk"` 65 | MaxLen int `pg:"len"` 66 | Values []string `pg:"enum,array"` 67 | } 68 | 69 | func (c column) Column(useSQLNulls bool, goPGVer int, customTypes model.CustomTypeMapping) model.Column { 70 | return model.NewColumn(c.Name, c.Type, c.Default, c.HasDefault, c.IsNullable, useSQLNulls, c.IsArray, c.Dimensions, c.IsPK, c.IsFK, c.MaxLen, c.Values, goPGVer, customTypes) 71 | } 72 | 73 | // Store is database helper 74 | type store struct { 75 | db orm.DB 76 | } 77 | 78 | // NewStore creates Store 79 | func newStore(db orm.DB) *store { 80 | return &store{db: db} 81 | } 82 | 83 | func (s *store) Schemas() ([]string, error) { 84 | query := `select nspname from pg_catalog.pg_namespace` 85 | 86 | var result []string 87 | if _, err := s.db.Query(&result, query); err != nil { 88 | return nil, fmt.Errorf("getting schemas info error: %w", err) 89 | } 90 | 91 | return result, nil 92 | } 93 | 94 | func (s *store) Tables(selected []string) ([]table, error) { 95 | var schemas []string 96 | var tables []interface{} 97 | 98 | for _, s := range selected { 99 | schema, table := util.Split(s) 100 | if table == "*" { 101 | schemas = append(schemas, schema) 102 | } else { 103 | tables = append(tables, []string{schema, table}) 104 | } 105 | } 106 | 107 | var where []string 108 | if len(schemas) > 0 { 109 | where = append(where, format("(table_schema) in (?)", pg.In(schemas))) 110 | } 111 | if len(tables) > 0 { 112 | where = append(where, format("(table_schema, table_name) in (?)", pg.InMulti(tables...))) 113 | } 114 | 115 | query := ` 116 | select 117 | table_schema, 118 | table_name 119 | from information_schema.tables 120 | where 121 | table_type = 'BASE TABLE' and 122 | ( 123 | ` + strings.Join(where, "or \n") + ` 124 | )` 125 | 126 | var result []table 127 | if _, err := s.db.Query(&result, query); err != nil { 128 | return nil, fmt.Errorf("getting tables info error: %w", err) 129 | } 130 | 131 | return result, nil 132 | } 133 | 134 | // Relations gets relations of a selected table 135 | func (s *store) Relations(tables []table) ([]relation, error) { 136 | ts := make([]interface{}, len(tables)) 137 | for i, t := range tables { 138 | ts[i] = []string{t.Schema, t.Name} 139 | } 140 | 141 | query := ` 142 | with 143 | schemas as ( 144 | select nspname, oid 145 | from pg_namespace 146 | ), 147 | tables as ( 148 | select oid, relnamespace, relname, relkind 149 | from pg_class 150 | ), 151 | columns as ( 152 | select attrelid, attname, attnum 153 | from pg_attribute a 154 | where a.attisdropped = false 155 | ) 156 | select distinct 157 | co.conname as constraint_name, 158 | ss.nspname as schema_name, 159 | s.relname as table_name, 160 | array_agg(sc.attname) as columns, 161 | ts.nspname as target_schema, 162 | t.relname as target_table, 163 | array_agg(tc.attname) as target_columns 164 | from pg_constraint co 165 | left join tables s on co.conrelid = s.oid 166 | left join schemas ss on s.relnamespace = ss.oid 167 | left join columns sc on s.oid = sc.attrelid and sc.attnum = any (co.conkey) 168 | left join tables t on co.confrelid = t.oid 169 | left join schemas ts on t.relnamespace = ts.oid 170 | left join columns tc on t.oid = tc.attrelid and tc.attnum = any (co.confkey) 171 | where co.contype = 'f' 172 | and co.conrelid in (select oid from pg_class c where c.relkind = 'r') 173 | and array_position(co.conkey, sc.attnum) = array_position(co.confkey, tc.attnum) 174 | and (ss.nspname, s.relname) in (?) 175 | group by constraint_name, schema_name, table_name, target_schema, target_table 176 | ` 177 | 178 | var relations []relation 179 | if _, err := s.db.Query(&relations, query, pg.InMulti(ts...)); err != nil { 180 | return nil, fmt.Errorf("getting relations info error: %w", err) 181 | } 182 | 183 | return relations, nil 184 | } 185 | 186 | func (s store) Columns(tables []table) ([]column, error) { 187 | ts := make([]interface{}, len(tables)) 188 | for i, t := range tables { 189 | ts[i] = []string{t.Schema, t.Name} 190 | } 191 | 192 | query := ` 193 | with 194 | enums as ( 195 | select distinct true as is_enum, 196 | sch.nspname as table_schema, 197 | tb.relname as table_name, 198 | col.attname as column_name, 199 | array_agg(e.enumlabel) as enum_values 200 | from pg_class tb 201 | left join pg_namespace sch on sch.oid = tb.relnamespace 202 | left join pg_attribute col on col.attrelid = tb.oid 203 | inner join pg_enum e on e.enumtypid = col.atttypid 204 | group by 1, 2, 3, 4 205 | ), 206 | arrays as ( 207 | select sch.nspname as table_schema, 208 | tb.relname as table_name, 209 | col.attname as column_name, 210 | col.attndims as array_dims 211 | from pg_class tb 212 | left join pg_namespace sch on sch.oid = tb.relnamespace 213 | left join pg_attribute col on col.attrelid = tb.oid 214 | where col.attndims > 0 215 | ), 216 | info as ( 217 | select distinct 218 | kcu.table_schema as table_schema, 219 | kcu.table_name as table_name, 220 | kcu.column_name as column_name, 221 | array_agg(( 222 | select constraint_type::text 223 | from information_schema.table_constraints tc 224 | where tc.constraint_name = kcu.constraint_name 225 | and tc.constraint_schema = kcu.constraint_schema 226 | and tc.constraint_catalog = kcu.constraint_catalog 227 | limit 1 228 | )) as constraint_types 229 | from information_schema.key_column_usage kcu 230 | group by kcu.table_schema, kcu.table_name, kcu.column_name 231 | ) 232 | select distinct c.table_schema = 'public' as is_public, 233 | c.table_schema as schema_name, 234 | c.table_name as table_name, 235 | c.column_name as column_name, 236 | c.ordinal_position as ordinal, 237 | case 238 | when i.constraint_types is null 239 | then false 240 | else 'PRIMARY KEY'=any (i.constraint_types) 241 | end as is_pk, 242 | 'FOREIGN KEY'=any (i.constraint_types) as is_fk, 243 | c.is_nullable = 'YES' as nullable, 244 | c.data_type = 'ARRAY' as is_array, 245 | coalesce(a.array_dims, 0) as dims, 246 | case 247 | when e.is_enum = true 248 | then 'varchar' 249 | else ltrim(c.udt_name, '_') 250 | end as type, 251 | c.column_default as def, 252 | (c.column_default is not null or c.is_identity = 'YES') as has_def, 253 | c.character_maximum_length as len, 254 | e.enum_values as enum 255 | from information_schema.tables t 256 | left join information_schema.columns c using (table_name, table_schema) 257 | left join info i using (table_name, table_schema, column_name) 258 | left join arrays a using (table_name, table_schema, column_name) 259 | left join enums e using (table_name, table_schema, column_name) 260 | where (t.table_schema, t.table_name) in (?) 261 | and t.table_type = 'BASE TABLE' 262 | order by 1 desc, 2, 3, 5 asc, 6 desc nulls last 263 | ` 264 | 265 | var columns []column 266 | if _, err := s.db.Query(&columns, query, pg.InMulti(ts...)); err != nil { 267 | return nil, fmt.Errorf("getting columns info error: %w", err) 268 | } 269 | 270 | return columns, nil 271 | } 272 | 273 | // Sort sorts table by schema and name (public tables always first) 274 | func Sort(tables []table) []table { 275 | sort.Slice(tables, func(i, j int) bool { 276 | ti := tables[i] 277 | tj := tables[j] 278 | 279 | if ti.Schema == tj.Schema { 280 | return ti.Name < tj.Name 281 | } 282 | 283 | if ti.Schema == util.PublicSchema { 284 | return true 285 | } 286 | if tj.Schema == util.PublicSchema { 287 | return false 288 | } 289 | 290 | return util.Join(ti.Schema, ti.Name) < util.Join(tj.Schema, tj.Name) 291 | }) 292 | 293 | return tables 294 | } 295 | -------------------------------------------------------------------------------- /util/texts_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestSingular(t *testing.T) { 8 | type args struct { 9 | input string 10 | } 11 | tests := []struct { 12 | name string 13 | args args 14 | want string 15 | }{ 16 | { 17 | name: "Should get normal singular", 18 | args: args{"dogs"}, 19 | want: "dog", 20 | }, 21 | { 22 | name: "Should get irregular singular", 23 | args: args{"children"}, 24 | want: "child", 25 | }, 26 | { 27 | name: "Should get non-countable", 28 | args: args{"fish"}, 29 | want: "fish", 30 | }, 31 | { 32 | name: "Should get added non-countable", 33 | args: args{"sms"}, 34 | want: "sms", 35 | }, 36 | { 37 | name: "Should ignore non plural", 38 | args: args{"test"}, 39 | want: "test", 40 | }, 41 | } 42 | for _, tt := range tests { 43 | t.Run(tt.name, func(t *testing.T) { 44 | if got := Singular(tt.args.input); got != tt.want { 45 | t.Errorf("Singular() = %v, want %v", got, tt.want) 46 | } 47 | }) 48 | } 49 | } 50 | 51 | func TestEntityName(t *testing.T) { 52 | type args struct { 53 | input string 54 | } 55 | tests := []struct { 56 | name string 57 | args args 58 | want string 59 | }{ 60 | { 61 | name: "Should generate from simple word", 62 | args: args{"users"}, 63 | want: "User", 64 | }, 65 | { 66 | name: "Should generate from simple word end with es", 67 | args: args{"companies"}, 68 | want: "Company", 69 | }, 70 | { 71 | name: "Should generate from simple word end with es", 72 | args: args{"glasses"}, 73 | want: "Glass", 74 | }, 75 | { 76 | name: "Should generate from non-countable", 77 | args: args{"audio"}, 78 | want: "Audio", 79 | }, 80 | { 81 | name: "Should generate from underscored", 82 | args: args{"user_orders"}, 83 | want: "UserOrder", 84 | }, 85 | { 86 | name: "Should generate from camelCased", 87 | args: args{"userOrders"}, 88 | want: "UserOrder", 89 | }, 90 | { 91 | name: "Should generate from plural in last place", 92 | args: args{"usersWithOrders"}, 93 | want: "UsersWithOrder", 94 | }, 95 | { 96 | name: "Should generate from abracadabra", 97 | args: args{"abracadabra"}, 98 | want: "Abracadabra", 99 | }, 100 | } 101 | for _, tt := range tests { 102 | t.Run(tt.name, func(t *testing.T) { 103 | if got := EntityName(tt.args.input); got != tt.want { 104 | t.Errorf("EntityName() = %v, want %v", got, tt.want) 105 | } 106 | }) 107 | } 108 | } 109 | 110 | func TestColumnName(t *testing.T) { 111 | type args struct { 112 | input string 113 | } 114 | tests := []struct { 115 | name string 116 | args args 117 | want string 118 | }{ 119 | { 120 | name: "Should generate from simple word", 121 | args: args{"title"}, 122 | want: "Title", 123 | }, 124 | { 125 | name: "Should generate from underscored", 126 | args: args{"short_title"}, 127 | want: "ShortTitle", 128 | }, 129 | { 130 | name: "Should generate from camelCased", 131 | args: args{"shortTitle"}, 132 | want: "ShortTitle", 133 | }, 134 | { 135 | name: "Should generate with underscored id", 136 | args: args{"location_id"}, 137 | want: "LocationID", 138 | }, 139 | { 140 | name: "Should generate with camelCased id", 141 | args: args{"locationId"}, 142 | want: "LocationID", 143 | }, 144 | } 145 | for _, tt := range tests { 146 | t.Run(tt.name, func(t *testing.T) { 147 | if got := ColumnName(tt.args.input); got != tt.want { 148 | t.Errorf("ColumnName() = %v, want %v", got, tt.want) 149 | } 150 | }) 151 | } 152 | } 153 | 154 | func TestHasUpper(t *testing.T) { 155 | type args struct { 156 | input string 157 | } 158 | tests := []struct { 159 | name string 160 | args args 161 | want bool 162 | }{ 163 | { 164 | name: "Should detect upper case", 165 | args: args{"upPer"}, 166 | want: true, 167 | }, 168 | { 169 | name: "Should not detect only lower case", 170 | args: args{"lower"}, 171 | want: false, 172 | }, 173 | } 174 | for _, tt := range tests { 175 | t.Run(tt.name, func(t *testing.T) { 176 | if got := HasUpper(tt.args.input); got != tt.want { 177 | t.Errorf("HasUpper() = %v, want %v", got, tt.want) 178 | } 179 | }) 180 | } 181 | } 182 | 183 | func TestReplaceSuffix(t *testing.T) { 184 | type args struct { 185 | input string 186 | suffix string 187 | replace string 188 | } 189 | tests := []struct { 190 | name string 191 | args args 192 | want string 193 | }{ 194 | { 195 | name: "Should replace suffix", 196 | args: args{"locationId", "Id", "ID"}, 197 | want: "locationID", 198 | }, 199 | { 200 | name: "Should not replace if not found", 201 | args: args{"location", "Id", "ID"}, 202 | want: "location", 203 | }, 204 | { 205 | name: "Should not replace if not suffix", 206 | args: args{"locationIdHere", "Id", "ID"}, 207 | want: "locationIdHere", 208 | }, 209 | } 210 | for _, tt := range tests { 211 | t.Run(tt.name, func(t *testing.T) { 212 | if got := ReplaceSuffix(tt.args.input, tt.args.suffix, tt.args.replace); got != tt.want { 213 | t.Errorf("ReplaceSuffix() = %v, want %v", got, tt.want) 214 | } 215 | }) 216 | } 217 | } 218 | 219 | func TestPackageName(t *testing.T) { 220 | type args struct { 221 | input string 222 | } 223 | tests := []struct { 224 | name string 225 | args args 226 | want string 227 | }{ 228 | { 229 | name: "Should generate valid package name with lower", 230 | args: args{"tesT"}, 231 | want: "test", 232 | }, 233 | { 234 | name: "Should generate valid package name with only letters", 235 | args: args{"te_sT$"}, 236 | want: "te_st", 237 | }, 238 | } 239 | for _, tt := range tests { 240 | t.Run(tt.name, func(t *testing.T) { 241 | if got := PackageName(tt.args.input); got != tt.want { 242 | t.Errorf("PackageName() = %v, want %v", got, tt.want) 243 | } 244 | }) 245 | } 246 | } 247 | 248 | func TestSanitize(t *testing.T) { 249 | tests := []struct { 250 | name string 251 | s string 252 | want string 253 | }{ 254 | { 255 | name: "should sanitize string contains special chars", 256 | s: "te$t-Str1ng0§", 257 | want: "tet_Str1ng0", 258 | }, 259 | { 260 | name: "should keep letters and numbers and dash", 261 | s: "abcdef_12345-67890", 262 | want: "abcdef_12345_67890", 263 | }, 264 | { 265 | name: "should add prefix if starting with number", 266 | s: "1234abcdef", 267 | want: "T1234abcdef", 268 | }, 269 | { 270 | name: "should add prefix if starting with number after sanitize", 271 | s: "#1234abcdef", 272 | want: "T1234abcdef", 273 | }, 274 | { 275 | name: "should add prefix if starting with dash", 276 | s: "#-1234abcdef", 277 | want: "T_1234abcdef", 278 | }, 279 | } 280 | for _, tt := range tests { 281 | t.Run(tt.name, func(t *testing.T) { 282 | if got := Sanitize(tt.s); got != tt.want { 283 | t.Errorf("Sanitize() = %v, want %v", got, tt.want) 284 | } 285 | }) 286 | } 287 | } 288 | 289 | func TestIsUpper(t *testing.T) { 290 | tests := []struct { 291 | name string 292 | c byte 293 | want bool 294 | }{ 295 | { 296 | name: "Should detect upper A", 297 | c: 'A', 298 | want: true, 299 | }, 300 | { 301 | name: "Should detect upper Z", 302 | c: 'Z', 303 | want: true, 304 | }, 305 | { 306 | name: "Should not detect lower z", 307 | c: 'z', 308 | want: false, 309 | }, 310 | { 311 | name: "Should not detect 1", 312 | c: '1', 313 | want: false, 314 | }, 315 | } 316 | for _, tt := range tests { 317 | t.Run(tt.name, func(t *testing.T) { 318 | if got := IsUpper(tt.c); got != tt.want { 319 | t.Errorf("IsUpper() = %v, want %v", got, tt.want) 320 | } 321 | }) 322 | } 323 | } 324 | 325 | func TestIsLower(t *testing.T) { 326 | tests := []struct { 327 | name string 328 | c byte 329 | want bool 330 | }{ 331 | { 332 | name: "Should detect lower a", 333 | c: 'a', 334 | want: true, 335 | }, 336 | { 337 | name: "Should detect lower z", 338 | c: 'z', 339 | want: true, 340 | }, 341 | { 342 | name: "Should not detect upper Z", 343 | c: 'Z', 344 | want: false, 345 | }, 346 | { 347 | name: "Should not detect 1", 348 | c: '1', 349 | want: false, 350 | }, 351 | } 352 | for _, tt := range tests { 353 | t.Run(tt.name, func(t *testing.T) { 354 | if got := IsLower(tt.c); got != tt.want { 355 | t.Errorf("IsLower() = %v, want %v", got, tt.want) 356 | } 357 | }) 358 | } 359 | } 360 | 361 | func TestToUpper(t *testing.T) { 362 | tests := []struct { 363 | name string 364 | c byte 365 | want byte 366 | }{ 367 | { 368 | name: "Should convert lower a to A", 369 | c: 'a', 370 | want: 'A', 371 | }, 372 | { 373 | name: "Should convert lower z to Z", 374 | c: 'z', 375 | want: 'Z', 376 | }, 377 | } 378 | for _, tt := range tests { 379 | t.Run(tt.name, func(t *testing.T) { 380 | if got := ToUpper(tt.c); got != tt.want { 381 | t.Errorf("ToUpper() = %v, want %v", got, tt.want) 382 | } 383 | }) 384 | } 385 | } 386 | 387 | func TestToLower(t *testing.T) { 388 | tests := []struct { 389 | name string 390 | c byte 391 | want byte 392 | }{ 393 | { 394 | name: "Should convert upper A to a", 395 | c: 'A', 396 | want: 'a', 397 | }, 398 | { 399 | name: "Should convert upper Z to z", 400 | c: 'Z', 401 | want: 'z', 402 | }, 403 | } 404 | for _, tt := range tests { 405 | t.Run(tt.name, func(t *testing.T) { 406 | if got := ToLower(tt.c); got != tt.want { 407 | t.Errorf("ToLower() = %v, want %v", got, tt.want) 408 | } 409 | }) 410 | } 411 | } 412 | 413 | func TestCamelCased(t *testing.T) { 414 | tests := []struct { 415 | name string 416 | s string 417 | want string 418 | }{ 419 | { 420 | name: "Should convert word to Word", 421 | s: "word", 422 | want: "Word", 423 | }, 424 | { 425 | name: "Should convert word_word to WordWord", 426 | s: "word_word", 427 | want: "WordWord", 428 | }, 429 | } 430 | for _, tt := range tests { 431 | t.Run(tt.name, func(t *testing.T) { 432 | if got := CamelCased(tt.s); got != tt.want { 433 | t.Errorf("CamelCased() = %v, want %v", got, tt.want) 434 | } 435 | }) 436 | } 437 | } 438 | 439 | func TestUnderscore(t *testing.T) { 440 | tests := []struct { 441 | name string 442 | s string 443 | want string 444 | }{ 445 | { 446 | name: "Should convert Word to word", 447 | s: "Word", 448 | want: "word", 449 | }, 450 | { 451 | name: "Should convert WordWord to word_word", 452 | s: "WordWord", 453 | want: "word_word", 454 | }, 455 | } 456 | for _, tt := range tests { 457 | t.Run(tt.name, func(t *testing.T) { 458 | if got := Underscore(tt.s); got != tt.want { 459 | t.Errorf("Underscore() = %v, want %v", got, tt.want) 460 | } 461 | }) 462 | } 463 | } 464 | 465 | func TestLowerFirst(t *testing.T) { 466 | tests := []struct { 467 | name string 468 | s string 469 | want string 470 | }{ 471 | { 472 | name: "Should convert Word to word", 473 | s: "Word", 474 | want: "word", 475 | }, 476 | { 477 | name: "Should convert WordWord to wordWord", 478 | s: "WordWord", 479 | want: "wordWord", 480 | }, 481 | { 482 | name: "Should convert 1WordWord to 1WordWord", 483 | s: "1WordWord", 484 | want: "1WordWord", 485 | }, 486 | } 487 | for _, tt := range tests { 488 | t.Run(tt.name, func(t *testing.T) { 489 | if got := LowerFirst(tt.s); got != tt.want { 490 | t.Errorf("LowerFirst() = %v, want %v", got, tt.want) 491 | } 492 | }) 493 | } 494 | } 495 | -------------------------------------------------------------------------------- /model/types_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func Test_goType(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | pgTypes []string 12 | want string 13 | wantErr bool 14 | }{ 15 | { 16 | name: "Should not get unknown type", 17 | pgTypes: []string{"unknown"}, 18 | wantErr: true, 19 | }, 20 | { 21 | name: "Should get int", 22 | pgTypes: []string{TypePGInt2, TypePGInt4}, 23 | want: TypeInt, 24 | }, 25 | { 26 | name: "Should get int64", 27 | pgTypes: []string{TypePGInt8}, 28 | want: TypeInt64, 29 | }, 30 | { 31 | name: "Should get float32", 32 | pgTypes: []string{TypePGFloat4}, 33 | want: TypeFloat32, 34 | }, 35 | { 36 | name: "Should get float64", 37 | pgTypes: []string{TypePGNumeric, TypePGFloat8}, 38 | want: TypeFloat64, 39 | }, 40 | { 41 | name: "Should get string", 42 | pgTypes: []string{TypePGText, TypePGVarchar, TypePGUuid, TypePGBpchar, TypePGPoint}, 43 | want: TypeString, 44 | }, 45 | { 46 | name: "Should get byte", 47 | pgTypes: []string{TypePGBytea}, 48 | want: TypeByteSlice, 49 | }, 50 | { 51 | name: "Should get bool", 52 | pgTypes: []string{TypePGBool}, 53 | want: TypeBool, 54 | }, 55 | { 56 | name: "Should get time.Time", 57 | pgTypes: []string{TypePGTimestamp, TypePGTimestamptz, TypePGDate, TypePGTime, TypePGTimetz}, 58 | want: TypeTime, 59 | }, 60 | { 61 | name: "Should get duration", 62 | pgTypes: []string{TypePGInterval}, 63 | want: TypeDuration, 64 | }, 65 | { 66 | name: "Should get map[string]interface{}", 67 | pgTypes: []string{TypePGJSONB, TypePGJSON}, 68 | want: TypeMapInterface, 69 | }, 70 | { 71 | name: "Should get map[string]string", 72 | pgTypes: []string{TypePGHstore}, 73 | want: TypeMapString, 74 | }, 75 | { 76 | name: "Should get netIP", 77 | pgTypes: []string{TypePGInet}, 78 | want: TypeIP, 79 | }, 80 | { 81 | name: "Should get netIPNet", 82 | pgTypes: []string{TypePGCidr}, 83 | want: TypeIPNet, 84 | }, 85 | } 86 | for _, tt := range tests { 87 | t.Run(tt.name, func(t *testing.T) { 88 | for _, typ := range tt.pgTypes { 89 | got, err := GoType(typ) 90 | if (err != nil) != tt.wantErr { 91 | t.Errorf("GoType() error = %v, wantErr %v", err, tt.wantErr) 92 | return 93 | } 94 | if !reflect.DeepEqual(got, tt.want) { 95 | t.Errorf("GoType() = %v, want %v", got, tt.want) 96 | } 97 | } 98 | }) 99 | } 100 | } 101 | 102 | func Test_goSlice(t *testing.T) { 103 | type args struct { 104 | pgType string 105 | dimensions int 106 | } 107 | tests := []struct { 108 | name string 109 | args args 110 | want string 111 | wantErr bool 112 | }{ 113 | { 114 | name: "Should generate multi-dimension array", 115 | args: args{TypePGInt4, 3}, 116 | want: "[][][]int", 117 | }, 118 | { 119 | name: "Should generate int2 array", 120 | args: args{TypePGInt2, 1}, 121 | want: "[]int", 122 | }, 123 | { 124 | name: "Should generate int4 array", 125 | args: args{TypePGInt4, 1}, 126 | want: "[]int", 127 | }, 128 | { 129 | name: "Should generate int8 array", 130 | args: args{TypePGInt8, 1}, 131 | want: "[]int64", 132 | }, 133 | { 134 | name: "Should generate numeric array", 135 | args: args{TypePGNumeric, 1}, 136 | want: "[]float64", 137 | }, 138 | { 139 | name: "Should generate float4 array", 140 | args: args{TypePGFloat4, 1}, 141 | want: "[]float32", 142 | }, 143 | { 144 | name: "Should generate float8 array", 145 | args: args{TypePGFloat8, 1}, 146 | want: "[]float64", 147 | }, 148 | { 149 | name: "Should generate text array", 150 | args: args{TypePGText, 1}, 151 | want: "[]string", 152 | }, 153 | { 154 | name: "Should generate varchar array", 155 | args: args{TypePGVarchar, 1}, 156 | want: "[]string", 157 | }, 158 | { 159 | name: "Should generate uuid array", 160 | args: args{TypePGUuid, 1}, 161 | want: "[]string", 162 | }, 163 | { 164 | name: "Should generate char array", 165 | args: args{TypePGBpchar, 1}, 166 | want: "[]string", 167 | }, 168 | { 169 | name: "Should generate bool array", 170 | args: args{TypePGBool, 1}, 171 | want: "[]bool", 172 | }, 173 | { 174 | name: "Should generate json array", 175 | args: args{TypePGJSON, 1}, 176 | want: "[]map[string]interface{}", 177 | }, 178 | { 179 | name: "Should generate jsonb array", 180 | args: args{TypePGJSONB, 1}, 181 | want: "[]map[string]interface{}", 182 | }, 183 | { 184 | name: "Should generate point array", 185 | args: args{TypePGPoint, 1}, 186 | want: "[]string", 187 | }, 188 | { 189 | name: "Should not generate not supported type array", 190 | args: args{TypePGTimetz, 1}, 191 | wantErr: true, 192 | }, 193 | { 194 | name: "Should not generate unknown type array", 195 | args: args{"unknown", 1}, 196 | wantErr: true, 197 | }, 198 | } 199 | for _, tt := range tests { 200 | t.Run(tt.name, func(t *testing.T) { 201 | got, err := GoSlice(tt.args.pgType, tt.args.dimensions) 202 | if (err != nil) != tt.wantErr { 203 | t.Errorf("GoSlice() error = %v, wantErr %v", err, tt.wantErr) 204 | return 205 | } 206 | if err == nil && got != tt.want { 207 | t.Errorf("GoSlice() = %v, want %v", got, tt.want) 208 | } 209 | }) 210 | } 211 | } 212 | 213 | func Test_goNullable(t *testing.T) { 214 | tests := []struct { 215 | name string 216 | pgType string 217 | avoidPointers bool 218 | want string 219 | wantErr bool 220 | }{ 221 | { 222 | name: "Should generate int2 type", 223 | pgType: TypePGInt2, 224 | want: "*int", 225 | }, 226 | { 227 | name: "Should generate int4 type", 228 | pgType: TypePGInt4, 229 | want: "*int", 230 | }, 231 | { 232 | name: "Should generate int8 type", 233 | pgType: TypePGInt8, 234 | want: "*int64", 235 | }, 236 | { 237 | name: "Should generate numeric type", 238 | pgType: TypePGNumeric, 239 | want: "*float64", 240 | }, 241 | { 242 | name: "Should generate float4 type", 243 | pgType: TypePGFloat4, 244 | want: "*float32", 245 | }, 246 | { 247 | name: "Should generate float8 type", 248 | pgType: TypePGFloat8, 249 | want: "*float64", 250 | }, 251 | { 252 | name: "Should generate text type", 253 | pgType: TypePGText, 254 | want: "*string", 255 | }, 256 | { 257 | name: "Should generate varchar type", 258 | pgType: TypePGVarchar, 259 | want: "*string", 260 | }, 261 | { 262 | name: "Should generate uuid type", 263 | pgType: TypePGUuid, 264 | want: "*string", 265 | }, 266 | { 267 | name: "Should generate char type", 268 | pgType: TypePGBpchar, 269 | want: "*string", 270 | }, 271 | { 272 | name: "Should generate bool type", 273 | pgType: TypePGBool, 274 | want: "*bool", 275 | }, 276 | { 277 | name: "Should generate time type", 278 | pgType: TypePGTimestamp, 279 | want: "*time.Time", 280 | }, 281 | { 282 | name: "Should generate interval type", 283 | pgType: TypePGInterval, 284 | want: "*time.Duration", 285 | }, 286 | { 287 | name: "Should generate json type", 288 | pgType: TypePGJSON, 289 | want: "map[string]interface{}", 290 | }, 291 | { 292 | name: "Should generate hstore type", 293 | pgType: TypePGHstore, 294 | want: "map[string]string", 295 | }, 296 | { 297 | name: "Should generate ip type", 298 | pgType: TypePGInet, 299 | want: "*net.IP", 300 | }, 301 | { 302 | name: "Should generate cidr type", 303 | pgType: TypePGCidr, 304 | want: "*net.IPNet", 305 | }, 306 | { 307 | name: "Should generate point type", 308 | pgType: TypePGPoint, 309 | want: "*string", 310 | }, 311 | { 312 | name: "Should not generate unknown type", 313 | pgType: "unknown", 314 | wantErr: true, 315 | }, 316 | { 317 | name: "Should generate int2 type avoiding pointers to sql.NullInt64", 318 | pgType: TypePGInt2, 319 | avoidPointers: true, 320 | want: "sql.NullInt64", 321 | }, 322 | { 323 | name: "Should generate varchar type avoiding pointers to sql.NullInt64", 324 | pgType: TypePGVarchar, 325 | avoidPointers: true, 326 | want: "sql.NullString", 327 | }, 328 | { 329 | name: "Should generate uuid type avoiding pointers to sql.NullInt64", 330 | pgType: TypePGUuid, 331 | avoidPointers: true, 332 | want: "sql.NullString", 333 | }, 334 | { 335 | name: "Should generate bool type avoiding pointers to sql.NullBool", 336 | pgType: TypePGBool, 337 | avoidPointers: true, 338 | want: "sql.NullBool", 339 | }, 340 | { 341 | name: "Should generate float64 type avoiding pointers to sql.NullFloat64", 342 | pgType: TypePGFloat8, 343 | avoidPointers: true, 344 | want: "sql.NullFloat64", 345 | }, 346 | } 347 | for _, tt := range tests { 348 | t.Run(tt.name, func(t *testing.T) { 349 | got, err := GoNullable(tt.pgType, tt.avoidPointers, CustomTypeMapping{}) 350 | if (err != nil) != tt.wantErr { 351 | t.Errorf("GoNullable() error = %v, wantErr %v", err, tt.wantErr) 352 | return 353 | } 354 | if err == nil && got != tt.want { 355 | t.Errorf("GoNullable() = %v, want %v", got, tt.want) 356 | } 357 | }) 358 | } 359 | } 360 | 361 | func Test_goImport(t *testing.T) { 362 | type args struct { 363 | pgTypes []string 364 | nullable bool 365 | avoidPointers bool 366 | ver int 367 | } 368 | tests := []struct { 369 | name string 370 | args args 371 | want string 372 | }{ 373 | { 374 | name: "Should not generate import for simple type", 375 | args: args{ 376 | pgTypes: []string{ 377 | TypePGInt2, TypePGInt4, TypePGInt8, TypePGNumeric, TypePGFloat4, TypePGFloat8, TypePGBool, TypePGText, TypePGVarchar, TypePGUuid, TypePGBpchar, 378 | }, 379 | ver: 8, 380 | }, 381 | want: "", 382 | }, 383 | { 384 | name: "Should not generate import for unknown type", 385 | args: args{ 386 | pgTypes: []string{"unknown"}, 387 | ver: 8, 388 | }, 389 | want: "", 390 | }, 391 | { 392 | name: "Should generate time import for interval type", 393 | args: args{ 394 | pgTypes: []string{TypePGInterval}, 395 | ver: 8, 396 | }, 397 | want: "time", 398 | }, 399 | { 400 | name: "Should generate net import for net types", 401 | args: args{ 402 | pgTypes: []string{ 403 | TypePGInet, TypePGCidr, 404 | }, 405 | ver: 8, 406 | }, 407 | want: "net", 408 | }, 409 | { 410 | name: "Should generate net import for json types", 411 | args: args{ 412 | pgTypes: []string{ 413 | TypePGJSONB, TypePGJSON, 414 | }, 415 | ver: 8, 416 | }, 417 | want: "", 418 | }, 419 | { 420 | name: "Should generate sql import for nullable simple types avoiding pointer", 421 | args: args{ 422 | pgTypes: []string{ 423 | TypePGInt2, TypePGInt4, TypePGInt8, TypePGNumeric, TypePGFloat4, TypePGFloat8, TypePGBool, TypePGText, TypePGVarchar, TypePGUuid, TypePGBpchar, 424 | }, 425 | ver: 8, 426 | nullable: true, 427 | avoidPointers: true, 428 | }, 429 | want: "database/sql", 430 | }, 431 | { 432 | name: "Should not generate sql import for nullable simple types", 433 | args: args{ 434 | pgTypes: []string{ 435 | TypePGInt2, TypePGInt4, TypePGInt8, TypePGNumeric, TypePGFloat4, TypePGFloat8, TypePGBool, TypePGText, TypePGVarchar, TypePGUuid, TypePGBpchar, 436 | }, 437 | ver: 8, 438 | nullable: true, 439 | avoidPointers: false, 440 | }, 441 | want: "", 442 | }, 443 | { 444 | name: "Should generate time import for nullable date time types", 445 | args: args{ 446 | pgTypes: []string{ 447 | TypePGTimestamp, TypePGTimestamptz, TypePGDate, TypePGTime, TypePGTimetz, 448 | }, 449 | ver: 8, 450 | nullable: true, 451 | }, 452 | want: "time", 453 | }, 454 | { 455 | name: "Should generate go-pg import for nullable date time types", 456 | args: args{ 457 | pgTypes: []string{ 458 | TypePGTimestamp, TypePGTimestamptz, TypePGDate, TypePGTime, TypePGTimetz, 459 | }, 460 | ver: 8, 461 | nullable: true, 462 | avoidPointers: true, 463 | }, 464 | want: "github.com/go-pg/pg", 465 | }, 466 | { 467 | name: "Should generate go-pg import for nullable date time types", 468 | args: args{ 469 | pgTypes: []string{ 470 | TypePGTimestamp, TypePGTimestamptz, TypePGDate, TypePGTime, TypePGTimetz, 471 | }, 472 | ver: 9, 473 | nullable: true, 474 | avoidPointers: true, 475 | }, 476 | want: "github.com/go-pg/pg/v9", 477 | }, 478 | } 479 | for _, tt := range tests { 480 | t.Run(tt.name, func(t *testing.T) { 481 | for _, pgType := range tt.args.pgTypes { 482 | if got := GoImport(pgType, tt.args.nullable, tt.args.avoidPointers, tt.args.ver); got != tt.want { 483 | t.Errorf("GoImport() = %v, want %v", got, tt.want) 484 | } 485 | } 486 | }) 487 | } 488 | } 489 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 9 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 10 | cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= 11 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 12 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 13 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 14 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 15 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 16 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 17 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 18 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 19 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= 20 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 21 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 22 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 23 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 24 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 25 | github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= 26 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 27 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 28 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 29 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 30 | github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 31 | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 32 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 33 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 34 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 35 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 36 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 37 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 38 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 39 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 40 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 41 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 42 | github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= 43 | github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= 44 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 45 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 46 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 47 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 48 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 49 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 50 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 51 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 52 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 53 | github.com/go-pg/pg/v10 v10.10.5 h1:RRW8NqxVu4vgzN9k05TT9rM5X+2VQHcIBRLeK9djMBE= 54 | github.com/go-pg/pg/v10 v10.10.5/go.mod h1:EmoJGYErc+stNN/1Jf+o4csXuprjxcRztBnn6cHe38E= 55 | github.com/go-pg/zerochecker v0.2.0 h1:pp7f72c3DobMWOb2ErtZsnrPaSvHd2W4o9//8HtF4mU= 56 | github.com/go-pg/zerochecker v0.2.0/go.mod h1:NJZ4wKL0NmTtz0GKCoJ8kym6Xn/EQzXRl2OnAe7MmDo= 57 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 58 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 59 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 60 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 61 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 62 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 63 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 64 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 65 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 66 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 67 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 68 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 69 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 70 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 71 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 72 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 73 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 74 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 75 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 76 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 77 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 78 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 79 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 80 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 81 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 82 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 83 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 84 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 85 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 86 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 87 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 88 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 89 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 90 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 91 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 92 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 93 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 94 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 95 | github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= 96 | github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= 97 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 98 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 99 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 100 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 101 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 102 | github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= 103 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 104 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= 105 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 106 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 107 | github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= 108 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 109 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 110 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 111 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 112 | github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= 113 | github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= 114 | github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= 115 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 116 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 117 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 118 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 119 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 120 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 121 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 122 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 123 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 124 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 125 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 126 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 127 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 128 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 129 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 130 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 131 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 132 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 133 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 134 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 135 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 136 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 137 | github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 138 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 139 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 140 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 141 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 142 | github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= 143 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= 144 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 145 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 146 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 147 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 148 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 149 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 150 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 151 | github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= 152 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 153 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 154 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 155 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 156 | github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M= 157 | github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 158 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 159 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 160 | github.com/onsi/gomega v1.10.3 h1:gph6h/qe9GSUw1NhH1gp+qb+h8rXD8Cy60Z32Qw3ELA= 161 | github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= 162 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 163 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 164 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 165 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 166 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 167 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 168 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 169 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 170 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 171 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 172 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 173 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 174 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 175 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 176 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 177 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 178 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 179 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 180 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 181 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 182 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 183 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 184 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 185 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 186 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 187 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 188 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 189 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 190 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 191 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 192 | github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M= 193 | github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= 194 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 195 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 196 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 197 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 198 | github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= 199 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 200 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 201 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 202 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 203 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 204 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 205 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 206 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 207 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 208 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 209 | github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= 210 | github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= 211 | github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6czKAd94= 212 | github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ= 213 | github.com/vmihailenco/msgpack/v5 v5.3.1 h1:0i85a4dsZh8mC//wmyyTEzidDLPQfQAxZIOLtafGbFY= 214 | github.com/vmihailenco/msgpack/v5 v5.3.1/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= 215 | github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc= 216 | github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= 217 | github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= 218 | github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 219 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 220 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 221 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 222 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 223 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 224 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 225 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 226 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 227 | golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 228 | golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 229 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 230 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 231 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 232 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 233 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg= 234 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 235 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 236 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 237 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 238 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 239 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 240 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 241 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 242 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 243 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 244 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 245 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 246 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 247 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 248 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 249 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 250 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 251 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 252 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 253 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 254 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 255 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 256 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 257 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 258 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 259 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 260 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 261 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 262 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 263 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 264 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 265 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 266 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 267 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 268 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 269 | golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 270 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= 271 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 272 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 273 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 274 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 275 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 276 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 277 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 278 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 279 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 280 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 281 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 282 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 283 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 284 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 285 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 286 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 287 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 288 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 289 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 290 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 291 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 292 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 293 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 294 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 295 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 296 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 297 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 298 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 299 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 300 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 301 | golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7 h1:iGu644GcxtEcrInvDsQRCwJjtCIOlT2V7IRt6ah2Whw= 302 | golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 303 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 304 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 305 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 306 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 307 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 308 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 309 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 310 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 311 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 312 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 313 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 314 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 315 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 316 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 317 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 318 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 319 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 320 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 321 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 322 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 323 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 324 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 325 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 326 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 327 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 328 | golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 329 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 330 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 331 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 332 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 333 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 334 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 335 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 336 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 337 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 338 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 339 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 340 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 341 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 342 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 343 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 344 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 345 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 346 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 347 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 348 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 349 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 350 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 351 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 352 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 353 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 354 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 355 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 356 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 357 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 358 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 359 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 360 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 361 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 362 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 363 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 364 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 365 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 366 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 367 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 368 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 369 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 370 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 371 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 372 | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 373 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 374 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 375 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 376 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 377 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 378 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 379 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 380 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 381 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 382 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 383 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 384 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 385 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 386 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 387 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 388 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 389 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 390 | mellium.im/sasl v0.2.1 h1:nspKSRg7/SyO0cRGY71OkfHab8tf9kCts6a6oTDut0w= 391 | mellium.im/sasl v0.2.1/go.mod h1:ROaEDLQNuf9vjKqE1SrAfnsobm2YKXT1gnN1uDp1PjQ= 392 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 393 | --------------------------------------------------------------------------------