github.com/pjdufour-truss/pop@v4.11.2-0.20190705085848-4c90b0ff4d5a+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/gobuffalo/pop/internal/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 err 91 } 92 f, err := parser.ParseFile(fset, p, string(b), 0) 93 if err != nil { 94 return 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 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, "\tt.Timestamps()") 207 s = append(s, "}") 208 return strings.Join(s, "\n") 209 } 210 211 // UnFizz generates the drop table instructions 212 func (m model) UnFizz() string { 213 return fmt.Sprintf("drop_table(\"%s\")", m.Name.Tableize()) 214 } 215 216 // GenerateSQLFromFizz generates SQL instructions from fizz instructions 217 func (m model) GenerateSQLFromFizz(content string, f fizz.Translator) string { 218 content, err := fizz.AString(content, f) 219 if err != nil { 220 return "" 221 } 222 return content 223 } 224 225 func newModel(name, structTag, modelPath string) (model, error) { 226 m := model{ 227 Package: filepath.Base(modelPath), 228 ModelPath: modelPath, 229 Imports: []string{"time", "github.com/gobuffalo/pop", "github.com/gobuffalo/validate"}, 230 Name: nflect.New(name), 231 Attributes: []attribute{}, 232 ValidatableAttributes: []attribute{}, 233 attributesCache: map[string]struct{}{}, 234 StructTag: structTag, 235 } 236 237 switch structTag { 238 case "json": 239 m.Imports = append(m.Imports, "encoding/json") 240 case "xml": 241 m.Imports = append(m.Imports, "encoding/xml") 242 default: 243 return model{}, errors.New("invalid struct tags (use xml or json)") 244 } 245 246 _ = m.addAttribute(attribute{Name: flect.New("created_at"), OriginalType: "time.Time", GoType: "time.Time", PreventValidation: true, StructTag: structTag}) 247 _ = m.addAttribute(attribute{Name: flect.New("updated_at"), OriginalType: "time.Time", GoType: "time.Time", PreventValidation: true, StructTag: structTag}) 248 249 return m, nil 250 } 251 252 func fizzColType(s string) string { 253 switch strings.ToLower(s) { 254 case "int": 255 return "integer" 256 case "time", "datetime": 257 return "timestamp" 258 case "uuid.uuid", "uuid": 259 return "uuid" 260 case "nulls.float32", "nulls.float64": 261 return "float" 262 case "slices.string", "slices.uuid", "[]string": 263 return "varchar[]" 264 case "slices.float", "[]float", "[]float32", "[]float64": 265 return "numeric[]" 266 case "slices.int": 267 return "int[]" 268 case "slices.map": 269 return "jsonb" 270 case "float32", "float64", "float": 271 return "decimal" 272 case "blob", "[]byte": 273 return "blob" 274 default: 275 if strings.HasPrefix(s, "nulls.") { 276 return fizzColType(strings.Replace(s, "nulls.", "", -1)) 277 } 278 return strings.ToLower(s) 279 } 280 }