github.com/duskeagle/pop@v4.10.1-0.20190417200916-92f2b794aab5+incompatible/soda/cmd/generate/model.go (about)

     1  package generate
     2  
     3  import (
     4  	"fmt"
     5  	"go/ast"
     6  	"go/importer"
     7  	"go/parser"
     8  	"go/token"
     9  	"go/types"
    10  	"io"
    11  	"io/ioutil"
    12  	"os"
    13  	"path/filepath"
    14  	"strings"
    15  
    16  	"github.com/gobuffalo/fizz"
    17  	"github.com/gobuffalo/flect"
    18  	nflect "github.com/gobuffalo/flect/name"
    19  	"github.com/gobuffalo/makr"
    20  	"github.com/gobuffalo/pop"
    21  	"github.com/markbates/going/defaults"
    22  	"github.com/pkg/errors"
    23  )
    24  
    25  type model struct {
    26  	Package               string
    27  	ModelPath             string
    28  	Imports               []string
    29  	Name                  nflect.Ident
    30  	attributesCache       map[string]struct{}
    31  	Attributes            []attribute
    32  	ValidatableAttributes []attribute
    33  	StructTag             string
    34  
    35  	HasNulls  bool
    36  	HasUUID   bool
    37  	HasSlices bool
    38  	HasID     bool
    39  }
    40  
    41  func (m model) Generate() error {
    42  	g := makr.New()
    43  	defer g.Fmt(".")
    44  	ctx := makr.Data{}
    45  	ctx["model"] = m
    46  	ctx["plural_model_name"] = m.modelNamePlural()
    47  	ctx["model_name"] = m.modelName()
    48  	ctx["package_name"] = m.Package
    49  
    50  	ctx["test_package_name"] = m.testPkgName()
    51  
    52  	ctx["char"] = m.Name.Char()
    53  	ctx["encoding_type"] = m.StructTag
    54  	ctx["encoding_type_char"] = nflect.Char(m.StructTag)
    55  
    56  	fname := filepath.Join(m.ModelPath, m.Name.File(".go").String())
    57  	g.Add(makr.NewFile(fname, modelTemplate))
    58  	tfname := filepath.Join(m.ModelPath, m.Name.File("_test.go").String())
    59  	g.Add(makr.NewFile(tfname, modelTestTemplate))
    60  	return g.Run(".", ctx)
    61  }
    62  
    63  func (m model) modelName() string {
    64  	x := strings.Split(m.Name.String(), "/")
    65  	for i, s := range x {
    66  		x[i] = flect.New(s).Singularize().Pascalize().String()
    67  	}
    68  	return strings.Join(x, "")
    69  }
    70  
    71  func (m model) modelNamePlural() string {
    72  	return flect.New(m.modelName()).Pluralize().Pascalize().String()
    73  }
    74  
    75  func (m model) testPkgName() string {
    76  	pkg := m.Package
    77  
    78  	path, _ := os.Getwd()
    79  	path = filepath.Join(path, m.ModelPath)
    80  
    81  	if _, err := os.Stat(path); err != nil {
    82  		return pkg
    83  	}
    84  	filepath.Walk(path, func(p string, info os.FileInfo, err error) error {
    85  		if strings.HasSuffix(p, "_test.go") {
    86  			fset := token.NewFileSet()
    87  
    88  			b, err := ioutil.ReadFile(p)
    89  			if err != nil {
    90  				return errors.WithStack(err)
    91  			}
    92  			f, err := parser.ParseFile(fset, p, string(b), 0)
    93  			if err != nil {
    94  				return errors.WithStack(err)
    95  			}
    96  
    97  			conf := types.Config{Importer: importer.Default()}
    98  			p, err := conf.Check("cmd/hello", fset, []*ast.File{f}, nil)
    99  			if err != nil {
   100  				return errors.WithStack(err)
   101  			}
   102  			pkg = p.Name()
   103  
   104  			return io.EOF
   105  		}
   106  		return nil
   107  	})
   108  
   109  	return pkg
   110  }
   111  
   112  func (m *model) addAttribute(a attribute) error {
   113  	k := a.Name.String()
   114  	if _, found := m.attributesCache[k]; found {
   115  		return fmt.Errorf("duplicated field \"%s\"", k)
   116  	}
   117  	m.attributesCache[k] = struct{}{}
   118  	if a.Name.String() == "id" {
   119  		// No need to create a default ID
   120  		m.HasID = true
   121  		a.Primary = true
   122  		// Ensure ID is the first attribute
   123  		m.Attributes = append([]attribute{a}, m.Attributes...)
   124  	} else {
   125  		m.Attributes = append(m.Attributes, a)
   126  	}
   127  
   128  	if a.Nullable {
   129  		return nil
   130  	}
   131  
   132  	if a.IsValidable() {
   133  		if a.GoType == "time.Time" {
   134  			a.GoType = "Time"
   135  		}
   136  		m.ValidatableAttributes = append(m.ValidatableAttributes, a)
   137  	}
   138  	return nil
   139  }
   140  
   141  func (m *model) addID() {
   142  	if m.HasID {
   143  		return
   144  	}
   145  
   146  	if !m.HasUUID {
   147  		m.HasUUID = true
   148  		m.Imports = append(m.Imports, "github.com/gofrs/uuid")
   149  	}
   150  
   151  	id := flect.New("id")
   152  	a := attribute{Name: id, OriginalType: "uuid.UUID", GoType: "uuid.UUID", Primary: true}
   153  	// Ensure ID is the first attribute
   154  	m.Attributes = append([]attribute{a}, m.Attributes...)
   155  	m.HasID = true
   156  }
   157  
   158  func (m model) generateModelFile() error {
   159  	err := os.MkdirAll(m.ModelPath, 0766)
   160  	if err != nil {
   161  		return errors.Wrapf(err, "couldn't create folder %s", m.ModelPath)
   162  	}
   163  
   164  	return m.Generate()
   165  }
   166  
   167  func (m model) generateFizz(path string) error {
   168  	migrationPath := defaults.String(path, "./migrations")
   169  	return pop.MigrationCreate(migrationPath, fmt.Sprintf("create_%s", m.Name.Tableize()), "fizz", []byte(m.Fizz()), []byte(m.UnFizz()))
   170  }
   171  
   172  func (m model) generateSQL(path, env string) error {
   173  	migrationPath := defaults.String(path, "./migrations")
   174  	db, err := pop.Connect(env)
   175  	if err != nil {
   176  		return err
   177  	}
   178  
   179  	d := db.Dialect
   180  	f := d.FizzTranslator()
   181  
   182  	return pop.MigrationCreate(migrationPath, fmt.Sprintf("create_%s.%s", m.Name.Tableize(), d.Name()), "sql", []byte(m.GenerateSQLFromFizz(m.Fizz(), f)), []byte(m.GenerateSQLFromFizz(m.UnFizz(), f)))
   183  }
   184  
   185  // Fizz generates the create table instructions
   186  func (m model) Fizz() string {
   187  	s := []string{fmt.Sprintf("create_table(\"%s\") {", m.Name.Tableize())}
   188  	for _, a := range m.Attributes {
   189  		switch a.Name.String() {
   190  		case "created_at", "updated_at":
   191  		default:
   192  			col := fizz.Column{
   193  				Name:    a.Name.Underscore().String(),
   194  				ColType: fizzColType(a.OriginalType),
   195  				Options: map[string]interface{}{},
   196  			}
   197  			if a.Primary {
   198  				col.Options["primary"] = true
   199  			}
   200  			if a.Nullable {
   201  				col.Options["null"] = true
   202  			}
   203  			s = append(s, "\t"+col.String())
   204  		}
   205  	}
   206  	s = append(s, "}")
   207  	return strings.Join(s, "\n")
   208  }
   209  
   210  // UnFizz generates the drop table instructions
   211  func (m model) UnFizz() string {
   212  	return fmt.Sprintf("drop_table(\"%s\")", m.Name.Tableize())
   213  }
   214  
   215  // GenerateSQLFromFizz generates SQL instructions from fizz instructions
   216  func (m model) GenerateSQLFromFizz(content string, f fizz.Translator) string {
   217  	content, err := fizz.AString(content, f)
   218  	if err != nil {
   219  		return ""
   220  	}
   221  	return content
   222  }
   223  
   224  func newModel(name, structTag, modelPath string) (model, error) {
   225  	m := model{
   226  		Package:               filepath.Base(modelPath),
   227  		ModelPath:             modelPath,
   228  		Imports:               []string{"time", "github.com/gobuffalo/pop", "github.com/gobuffalo/validate"},
   229  		Name:                  nflect.New(name),
   230  		Attributes:            []attribute{},
   231  		ValidatableAttributes: []attribute{},
   232  		attributesCache:       map[string]struct{}{},
   233  		StructTag:             structTag,
   234  	}
   235  
   236  	switch structTag {
   237  	case "json":
   238  		m.Imports = append(m.Imports, "encoding/json")
   239  	case "xml":
   240  		m.Imports = append(m.Imports, "encoding/xml")
   241  	default:
   242  		return model{}, errors.New("invalid struct tags (use xml or json)")
   243  	}
   244  
   245  	_ = m.addAttribute(attribute{Name: flect.New("created_at"), OriginalType: "time.Time", GoType: "time.Time", PreventValidation: true, StructTag: structTag})
   246  	_ = m.addAttribute(attribute{Name: flect.New("updated_at"), OriginalType: "time.Time", GoType: "time.Time", PreventValidation: true, StructTag: structTag})
   247  
   248  	return m, nil
   249  }
   250  
   251  func fizzColType(s string) string {
   252  	switch strings.ToLower(s) {
   253  	case "int":
   254  		return "integer"
   255  	case "time", "datetime":
   256  		return "timestamp"
   257  	case "uuid.uuid", "uuid":
   258  		return "uuid"
   259  	case "nulls.float32", "nulls.float64":
   260  		return "float"
   261  	case "slices.string", "slices.uuid", "[]string":
   262  		return "varchar[]"
   263  	case "slices.float", "[]float", "[]float32", "[]float64":
   264  		return "numeric[]"
   265  	case "slices.int":
   266  		return "int[]"
   267  	case "slices.map":
   268  		return "jsonb"
   269  	case "float32", "float64", "float":
   270  		return "decimal"
   271  	case "blob", "[]byte":
   272  		return "blob"
   273  	default:
   274  		if strings.HasPrefix(s, "nulls.") {
   275  			return fizzColType(strings.Replace(s, "nulls.", "", -1))
   276  		}
   277  		return strings.ToLower(s)
   278  	}
   279  }