github.com/solongordon/pop@v4.10.0+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 }