diff --git a/apiapp.go b/apiapp.go index 24d334d..65bb906 100644 --- a/apiapp.go +++ b/apiapp.go @@ -585,7 +585,7 @@ func createapi(cmd *Command, args []string) { ColorLog("[INFO] Using '%s' as 'driver'\n", driver) ColorLog("[INFO] Using '%s' as 'conn'\n", conn) ColorLog("[INFO] Using '%s' as 'tables'", tables) - generateModel(string(driver), string(conn), "3", string(tables), path.Join(curpath, packpath)) + generateAppcode(string(driver), string(conn), "3", string(tables), path.Join(curpath, packpath)) } else { os.Mkdir(path.Join(apppath, "models"), 0755) fmt.Println("create models:", path.Join(apppath, "models")) diff --git a/g.go b/g.go index 93a0824..c1dc8df 100644 --- a/g.go +++ b/g.go @@ -20,20 +20,13 @@ var cmdGenerate = &Command{ UsageLine: "generate [Command]", Short: "generate code based on application", Long: ` -bee generate scaffold [modelname] [-fields=""] +bee generate scaffold [scaffoldname] [-fields=""] The generate scaffold command will do a number of things for you。 -fields: a list of database fields. example: bee generate scaffold post -fields="title:string,body:text" - -bee generate model [-tables=""] [-driver=mysql] [-conn=root:@tcp(127.0.0.1:3306)/test] [-level=1] - generate model based on an existing database - -tables: a list of table names separated by ',', default is empty, indicating all tables - -driver: [mysql | postgresql | sqlite], the default is mysql - -conn: the connection string used by the driver, the default is root:@tcp(127.0.0.1:3306)/test - -level: [1 | 2 | 3], 1 = model; 2 = models,controller; 3 = models,controllers,router -bee generate migration [migrationfile] - generate migration file for making database schema update +bee generate model [modelname] [-fields=""] + generate RESTFul model based on fields bee generate controller [controllerfile] generate RESTFul controllers @@ -41,11 +34,21 @@ bee generate controller [controllerfile] bee generate view [viewpath] generate CRUD view in viewpath +bee generate migration [migrationfile] + generate migration file for making database schema update + bee generate docs generate swagger doc file bee generate test [routerfile] generate testcase + +bee generate appcode [-tables=""] [-driver=mysql] [-conn=root:@tcp(127.0.0.1:3306)/test] [-level=1] + generate appcode based on an existing database + -tables: a list of table names separated by ',', default is empty, indicating all tables + -driver: [mysql | postgresql | sqlite], the default is mysql + -conn: the connection string used by the driver, the default is root:@tcp(127.0.0.1:3306)/test + -level: [1 | 2 | 3], 1 = model; 2 = models,controller; 3 = models,controllers,router `, } @@ -53,6 +56,7 @@ var driver docValue var conn docValue var level docValue var tables docValue +var fields docValue func init() { cmdGenerate.Run = generateCode @@ -60,6 +64,7 @@ func init() { cmdGenerate.Flag.Var(&driver, "driver", "database driver: mysql, postgresql, etc.") cmdGenerate.Flag.Var(&conn, "conn", "connection string used by the driver to connect to a database instance") cmdGenerate.Flag.Var(&level, "level", "1 = models only; 2 = models and controllers; 3 = models, controllers and routers") + cmdGenerate.Flag.Var(&fields, "fields", "specify the fields want to generate.") } func generateCode(cmd *Command, args []string) { @@ -79,9 +84,24 @@ func generateCode(cmd *Command, args []string) { gcmd := args[0] switch gcmd { + case "scaffold": + if len(args) < 2 { + ColorLog("[ERRO] Wrong number of arguments\n") + ColorLog("[HINT] Usage: bee generate scaffold [scaffoldname] [-fields=\"\"]\n") + os.Exit(2) + } + cmd.Flag.Parse(args[2:]) + if fields == "" { + ColorLog("[ERRO] Wrong number of arguments\n") + ColorLog("[HINT] Usage: bee generate scaffold [scaffoldname] [-fields=\"title:string,body:text\"]\n") + os.Exit(2) + } + sname := args[1] + ColorLog("[INFO] Using '%s' as scaffold name\n", sname) + generateScaffold(sname, fields.String(), curpath) case "docs": generateDocs(curpath) - case "model": + case "appcode": cmd.Flag.Parse(args[1:]) if driver == "" { driver = "mysql" @@ -96,7 +116,7 @@ func generateCode(cmd *Command, args []string) { ColorLog("[INFO] Using '%s' as 'conn'\n", conn) ColorLog("[INFO] Using '%s' as 'tables'", tables) ColorLog("[INFO] Using '%s' as 'level'\n", level) - generateModel(string(driver), string(conn), string(level), string(tables), curpath) + generateAppcode(string(driver), string(conn), string(level), string(tables), curpath) case "migration": if len(args) == 2 { mname := args[1] @@ -104,7 +124,7 @@ func generateCode(cmd *Command, args []string) { generateMigration(mname, curpath) } else { ColorLog("[ERRO] Wrong number of arguments\n") - ColorLog("[HINT] Usage: bee generate migration [filename]\n") + ColorLog("[HINT] Usage: bee generate migration [migrationname]\n") os.Exit(2) } case "controller": @@ -113,16 +133,31 @@ func generateCode(cmd *Command, args []string) { generateController(cname, curpath) } else { ColorLog("[ERRO] Wrong number of arguments\n") - ColorLog("[HINT] Usage: bee generate controller [filename]\n") + ColorLog("[HINT] Usage: bee generate controller [controllername]\n") os.Exit(2) } + case "model": + if len(args) < 2 { + ColorLog("[ERRO] Wrong number of arguments\n") + ColorLog("[HINT] Usage: bee generate model [modelname] [-fields=\"\"]\n") + os.Exit(2) + } + cmd.Flag.Parse(args[2:]) + if fields == "" { + ColorLog("[ERRO] Wrong number of arguments\n") + ColorLog("[HINT] Usage: bee generate model [modelname] [-fields=\"title:string,body:text\"]\n") + os.Exit(2) + } + sname := args[1] + ColorLog("[INFO] Using '%s' as model name\n", sname) + generateModel(sname, fields.String(), curpath) case "view": if len(args) == 2 { cname := args[1] generateView(cname, curpath) } else { ColorLog("[ERRO] Wrong number of arguments\n") - ColorLog("[HINT] Usage: bee generate view [filename]\n") + ColorLog("[HINT] Usage: bee generate view [viewpath]\n") os.Exit(2) } default: diff --git a/g_models.go b/g_appcode.go similarity index 99% rename from g_models.go rename to g_appcode.go index 1521c05..524cfc5 100644 --- a/g_models.go +++ b/g_appcode.go @@ -191,7 +191,7 @@ func (tag *OrmTag) String() string { return fmt.Sprintf("`orm:\"%s\"`", strings.Join(ormOptions, ";")) } -func generateModel(driver string, connStr string, level string, tables string, currpath string) { +func generateAppcode(driver string, connStr string, level string, tables string, currpath string) { var mode byte if level == "1" { mode = O_MODEL @@ -663,14 +663,12 @@ func getFileName(tbName string) (filename string) { } const ( - STRUCT_MODEL_TPL = ` -package models + STRUCT_MODEL_TPL = `package models {{modelStruct}} ` - MODEL_TPL = ` -package models + MODEL_TPL = `package models {{modelStruct}} diff --git a/g_model.go b/g_model.go new file mode 100644 index 0000000..1d080d5 --- /dev/null +++ b/g_model.go @@ -0,0 +1,271 @@ +package main + +import ( + "errors" + "os" + "path" + "strings" +) + +func generateModel(mname, fields, crupath string) { + p, f := path.Split(mname) + modelName := strings.Title(f) + packageName := "models" + if p != "" { + i := strings.LastIndex(p[:len(p)-1], "/") + packageName = p[i+1 : len(p)-1] + } + modelStruct, err := getStruct(modelName, fields) + if err != nil { + ColorLog("[ERRO] Could not genrate models struct: %s\n", err) + os.Exit(2) + } + ColorLog("[INFO] Using '%s' as model name\n", modelName) + ColorLog("[INFO] Using '%s' as package name\n", packageName) + fp := path.Join(crupath, "models", p) + if _, err := os.Stat(fp); os.IsNotExist(err) { + // create controller directory + if err := os.MkdirAll(fp, 0777); err != nil { + ColorLog("[ERRO] Could not create models directory: %s\n", err) + os.Exit(2) + } + } + fpath := path.Join(fp, strings.ToLower(modelName)+".go") + if f, err := os.OpenFile(fpath, os.O_CREATE|os.O_EXCL|os.O_RDWR, 0666); err == nil { + defer f.Close() + content := strings.Replace(modelTpl, "{{packageName}}", packageName, -1) + content = strings.Replace(content, "{{modelName}}", modelName, -1) + content = strings.Replace(content, "{{modelStruct}}", modelStruct, -1) + f.WriteString(content) + // gofmt generated source code + formatAndFixImports(fpath) + ColorLog("[INFO] model file generated: %s\n", fpath) + } else { + // error creating file + ColorLog("[ERRO] Could not create model file: %s\n", err) + os.Exit(2) + } +} + +func getStruct(structname, fields string) (string, error) { + if fields == "" { + return "", errors.New("fields can't empty") + } + structStr := "type " + structname + " struct{\n" + fds := strings.Split(fields, ",") + for i, v := range fds { + kv := strings.SplitN(v, ":", 2) + if len(kv) != 2 { + return "", errors.New("the filds format is wrong. should key:type,key:type " + v) + } + typ, tag := getType(kv[1]) + if typ == "" { + return "", errors.New("the filds format is wrong. should key:type,key:type " + v) + } + if i == 0 && strings.ToLower(kv[0]) != "id" { + structStr = structStr + "Id int64 `orm:\"auto\"`\n" + } + structStr = structStr + camelString(kv[0]) + " " + typ + " " + tag + "\n" + } + structStr += "}\n" + return structStr, nil +} + +func camelString(s string) string { + data := make([]byte, 0, len(s)) + j := false + k := false + num := len(s) - 1 + for i := 0; i <= num; i++ { + d := s[i] + if k == false && d >= 'A' && d <= 'Z' { + k = true + } + if d >= 'a' && d <= 'z' && (j || k == false) { + d = d - 32 + j = false + k = true + } + if k && d == '_' && num > i && s[i+1] >= 'a' && s[i+1] <= 'z' { + j = true + continue + } + data = append(data, d) + } + return string(data[:len(data)]) +} + +// fields support type +// http://beego.me/docs/mvc/model/models.md#mysql +func getType(ktype string) (kt, tag string) { + kv := strings.SplitN(ktype, ":", 2) + switch kv[0] { + case "string": + if len(kv) == 2 { + return "string", "`orm:\"size(" + kv[1] + ")\"`" + } else { + return "string", "`orm:\"size(128)\"`" + } + case "text": + return "string", "`orm:\"type(longtext)\"`" + case "auto": + return "int64", "`orm:\"auto\"`" + case "pk": + return "int64", "`orm:\"pk\"`" + case "datetime": + return "time.Time", "`orm:\"type(datetime)\"`" + case "int", "int8", "int16", "int32", "int64": + fallthrough + case "uint", "uint8", "uint16", "uint32", "uint64": + fallthrough + case "bool": + fallthrough + case "float32", "float64": + return kv[0], "" + case "float": + return "float64", "" + } + return "", "" +} + +var modelTpl = `package {{packageName}} + +import ( + "errors" + "fmt" + "reflect" + "strings" + "time" + + "github.com/astaxie/beego/orm" +) + +{{modelStruct}} + +func init() { + orm.RegisterModel(new({{modelName}})) +} + +// Add{{modelName}} insert a new {{modelName}} into database and returns +// last inserted Id on success. +func Add{{modelName}}(m *{{modelName}}) (id int64, err error) { + o := orm.NewOrm() + id, err = o.Insert(m) + return +} + +// Get{{modelName}}ById retrieves {{modelName}} by Id. Returns error if +// Id doesn't exist +func Get{{modelName}}ById(id int) (v *{{modelName}}, err error) { + o := orm.NewOrm() + v = &{{modelName}}{Id: id} + if err = o.Read(v); err == nil { + return v, nil + } + return nil, err +} + +// GetAll{{modelName}} retrieves all {{modelName}} matches certain condition. Returns empty list if +// no records exist +func GetAll{{modelName}}(query map[string]string, fields []string, sortby []string, order []string, + offset int64, limit int64) (ml []interface{}, err error) { + o := orm.NewOrm() + qs := o.QueryTable(new({{modelName}})) + // query k=v + for k, v := range query { + // rewrite dot-notation to Object__Attribute + k = strings.Replace(k, ".", "__", -1) + qs = qs.Filter(k, v) + } + // order by: + var sortFields []string + if len(sortby) != 0 { + if len(sortby) == len(order) { + // 1) for each sort field, there is an associated order + for i, v := range sortby { + orderby := "" + if order[i] == "desc" { + orderby = "-" + v + } else if order[i] == "asc" { + orderby = v + } else { + return nil, errors.New("Error: Invalid order. Must be either [asc|desc]") + } + sortFields = append(sortFields, orderby) + } + qs = qs.OrderBy(sortFields...) + } else if len(sortby) != len(order) && len(order) == 1 { + // 2) there is exactly one order, all the sorted fields will be sorted by this order + for _, v := range sortby { + orderby := "" + if order[0] == "desc" { + orderby = "-" + v + } else if order[0] == "asc" { + orderby = v + } else { + return nil, errors.New("Error: Invalid order. Must be either [asc|desc]") + } + sortFields = append(sortFields, orderby) + } + } else if len(sortby) != len(order) && len(order) != 1 { + return nil, errors.New("Error: 'sortby', 'order' sizes mismatch or 'order' size is not 1") + } + } else { + if len(order) != 0 { + return nil, errors.New("Error: unused 'order' fields") + } + } + + var l []{{modelName}} + qs = qs.OrderBy(sortFields...) + if _, err := qs.Limit(limit, offset).All(&l, fields...); err == nil { + if len(fields) == 0 { + for _, v := range l { + ml = append(ml, v) + } + } else { + // trim unused fields + for _, v := range l { + m := make(map[string]interface{}) + val := reflect.ValueOf(v) + for _, fname := range fields { + m[fname] = val.FieldByName(fname).Interface() + } + ml = append(ml, m) + } + } + return ml, nil + } + return nil, err +} + +// Update{{modelName}} updates {{modelName}} by Id and returns error if +// the record to be updated doesn't exist +func Update{{modelName}}ById(m *{{modelName}}) (err error) { + o := orm.NewOrm() + v := {{modelName}}{Id: m.Id} + // ascertain id exists in the database + if err = o.Read(&v); err == nil { + var num int64 + if num, err = o.Update(m); err == nil { + fmt.Println("Number of records updated in database:", num) + } + } + return +} + +// Delete{{modelName}} deletes {{modelName}} by Id and returns error if +// the record to be deleted doesn't exist +func Delete{{modelName}}(id int) (err error) { + o := orm.NewOrm() + v := {{modelName}}{Id: id} + // ascertain id exists in the database + if err = o.Read(&v); err == nil { + var num int64 + if num, err = o.Delete(&{{modelName}}{Id: id}); err == nil { + fmt.Println("Number of records deleted in database:", num) + } + } + return +} +` diff --git a/g_scaffold.go b/g_scaffold.go index 85f0393..8ec54ef 100644 --- a/g_scaffold.go +++ b/g_scaffold.go @@ -1 +1,33 @@ -package main \ No newline at end of file +package main + +import "strings" + +func generateScaffold(sname, fields, crupath string) { + // generate model + ColorLog("[INFO] Do you want me to create a %v model? [yes|no]] ", sname) + if askForConfirmation() { + generateModel(sname, fields, crupath) + } + + // generate controller + ColorLog("[INFO] Do you want me to create a %v controller? [yes|no]] ", sname) + if askForConfirmation() { + generateController(sname, crupath) + } + // generate view + ColorLog("[INFO] Do you want me to create views for this %v resource? [yes|no]] ", sname) + if askForConfirmation() { + generateView(sname, crupath) + } + // generate migration + ColorLog("[INFO] Do you want me to create a %v migration and schema for this resource? [yes|no]] ", sname) + if askForConfirmation() { + generateMigration(sname, crupath) + } + // run migration + ColorLog("[INFO] Do you want to go ahead and migrate the database? [yes|no]] ") + if askForConfirmation() { + // @todo + } + ColorLog("[INFO] All done! Don't forget to add beego.Router(\"/%v\" ,&controllers.%vController{}) to routers/route.go\n", sname, strings.Title(sname)) +} diff --git a/g_views.go b/g_views.go index 160efb6..413c86f 100644 --- a/g_views.go +++ b/g_views.go @@ -8,7 +8,7 @@ import ( // recipe // admin/recipe func generateView(vpath, crupath string) { - absvpath := path.Join(crupath, vpath) + absvpath := path.Join(crupath, "views", vpath) os.MkdirAll(absvpath, os.ModePerm) cfile := path.Join(absvpath, "index.tpl") if f, err := os.OpenFile(cfile, os.O_CREATE|os.O_EXCL|os.O_RDWR, 0666); err == nil {