diff --git a/README.md b/README.md index 401ef18..b597b54 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,14 @@ Bee is a command line tool facilitating development with beego framework. - Go version >= 1.1. + ## Installation Begin by installing `bee` using `go get` command. - go get github.com/beego/bee +```bash +go get github.com/beego/bee +``` Then you can add `bee` binary to PATH environment variable in your `~/.bashrc` or `~/.bash_profile` file: @@ -23,21 +26,24 @@ export PATH=$PATH:/bin/bee > If you already have `bee` installed, updating `bee` is simple: - go get -u github.com/beego/bee +```bash +go get -u github.com/beego/bee +``` ## Basic commands Bee provides a variety of commands which can be helpful at various stage of development. The top level commands include: - - new create an application base on beego framework - run run the app which can hot compile - pack compress an beego project - api create an api application base on beego framework - bale packs non-Go files to Go source files - version show the bee & beego version - generate source code generator - migrate run database migrations - +```base + new create an application base on beego framework + run run the app which can hot compile + pack compress an beego project + api create an api application base on beego framework + bale packs non-Go files to Go source files + version show the bee & beego version + generate source code generator + migrate run database migrations + hprose create an rpc application use hprose base on beego framework +``` ## bee version The first command is the easiest: displaying which version of `bee`, `beego` and `go` is installed on your machine: @@ -121,26 +127,55 @@ bee api [appname] [-tables=""] [-driver="mysql"] [-conn="root:@tcp(127.0.0.1:330 -conn: the connection string used by the driver, the default is '' e.g. for mysql: root:@tcp(127.0.0.1:3306)/test e.g. for postgres: postgres://postgres:postgres@127.0.0.1:5432/postgres - + if conn is empty will create a example api application. otherwise generate api application based on an existing database. In the current path, will create a folder named [appname] In the appname folder has the follow struct: - ├── conf - │ └── app.conf - ├── controllers - │ └── object.go - │ └── user.go - ├── routers - │ └── router.go - ├── tests - │ └── default_test.go - ├── main.go - └── models - └── object.go - └── user.go + ├── conf + │ └── app.conf + ├── controllers + │ └── object.go + │ └── user.go + ├── routers + │ └── router.go + ├── tests + │ └── default_test.go + ├── main.go + └── models + └── object.go + └── user.go +``` + +## bee hprose + +```bash +usage: bee hprose [appname] + +create an rpc application use hprose base on beego framework + +bee hprose [appname] [-tables=""] [-driver=mysql] [-conn=root:@tcp(127.0.0.1:3306)/test] + -tables: a list of table names separated by ',', default is empty, indicating all tables + -driver: [mysql | postgres | sqlite], the default is mysql + -conn: the connection string used by the driver, the default is '' + e.g. for mysql: root:@tcp(127.0.0.1:3306)/test + e.g. for postgres: postgres://postgres:postgres@127.0.0.1:5432/postgres + +if conn is empty will create a example rpc application. otherwise generate rpc application use hprose based on an existing database. + +In the current path, will create a folder named [appname] + +In the appname folder has the follow struct: + + ├── conf + │ └── app.conf + ├── main.go + └── models + └── object.go + └── user.go + ``` ## bee bale @@ -211,7 +246,7 @@ bee generate view [viewpath] bee generate migration [migrationfile] [-fields=""] generate migration file for making database schema update -fields: a list of table fields. Format: field:type, ... - + bee generate docs generate swagger doc file @@ -258,13 +293,13 @@ usage: bee run [appname] [watchall] [-main=*.go] [-downdoc=true] [-gendoc=true] start the appname throw exec.Command then start a inotify watch for current dir - + when the file has changed bee will auto go build and restart the app - file changed - | + file changed + | check if it's go file - | + | yes no | | go build do nothing diff --git a/bee.go b/bee.go index 59cfa9d..3e9690c 100644 --- a/bee.go +++ b/bee.go @@ -77,6 +77,7 @@ var commands = []*Command{ cmdRun, cmdPack, cmdApiapp, + cmdHproseapp, //cmdRouter, //cmdTest, cmdBale, diff --git a/g_appcode.go b/g_appcode.go index 9d848d5..c3c295c 100644 --- a/g_appcode.go +++ b/g_appcode.go @@ -499,7 +499,9 @@ func (mysqlDB *MysqlDB) GetColumns(db *sql.DB, table *Table, blackList map[strin // getGoDataType maps an SQL data type to Golang data type func (*MysqlDB) GetGoDataType(sqlType string) (goType string) { - if v, ok := typeMappingMysql[sqlType]; ok { + var typeMapping = map[string]string{} + typeMapping = typeMappingMysql + if v, ok := typeMapping[sqlType]; ok { return v } else { ColorLog("[ERRO] data type (%s) not found!\n", sqlType) diff --git a/g_hproseappcode.go b/g_hproseappcode.go new file mode 100644 index 0000000..86e3d02 --- /dev/null +++ b/g_hproseappcode.go @@ -0,0 +1,314 @@ +/**********************************************************\ +| | +| hprose | +| | +| Official WebSite: http://www.hprose.com/ | +| http://www.hprose.org/ | +| | +\**********************************************************/ +/**********************************************************\ + * * + * Build rpc application use Hprose base on beego * + * * + * LastModified: Oct 23, 2014 * + * Author: Liu jian * + * * +\**********************************************************/ + +package main + +import ( + "database/sql" + "os" + "path" + "strings" + + _ "github.com/go-sql-driver/mysql" + _ "github.com/lib/pq" +) + +func generateHproseAppcode(driver, connStr, level, tables, currpath string) { + var mode byte + switch level { + case "1": + mode = O_MODEL + case "2": + mode = O_MODEL | O_CONTROLLER + case "3": + mode = O_MODEL | O_CONTROLLER | O_ROUTER + default: + ColorLog("[ERRO] Invalid 'level' option: %s\n", level) + ColorLog("[HINT] Level must be either 1, 2 or 3\n") + os.Exit(2) + } + var selectedTables map[string]bool + if tables != "" { + selectedTables = make(map[string]bool) + for _, v := range strings.Split(tables, ",") { + selectedTables[v] = true + } + } + switch driver { + case "mysql": + case "postgres": + case "sqlite": + ColorLog("[ERRO] Generating app code from SQLite database is not supported yet.\n") + os.Exit(2) + default: + ColorLog("[ERRO] Unknown database driver: %s\n", driver) + ColorLog("[HINT] Driver must be one of mysql, postgres or sqlite\n") + os.Exit(2) + } + genHprose(driver, connStr, mode, selectedTables, currpath) +} + +// Generate takes table, column and foreign key information from database connection +// and generate corresponding golang source files +func genHprose(dbms, connStr string, mode byte, selectedTableNames map[string]bool, currpath string) { + db, err := sql.Open(dbms, connStr) + if err != nil { + ColorLog("[ERRO] Could not connect to %s database: %s, %s\n", dbms, connStr, err) + os.Exit(2) + } + defer db.Close() + if trans, ok := dbDriver[dbms]; ok { + ColorLog("[INFO] Analyzing database tables...\n") + tableNames := trans.GetTableNames(db) + tables := getTableObjects(tableNames, db, trans) + mvcPath := new(MvcPath) + mvcPath.ModelPath = path.Join(currpath, "models") + createPaths(mode, mvcPath) + pkgPath := getPackagePath(currpath) + writeHproseSourceFiles(pkgPath, tables, mode, mvcPath, selectedTableNames) + } else { + ColorLog("[ERRO] Generating app code from %s database is not supported yet.\n", dbms) + os.Exit(2) + } +} + +// writeHproseSourceFiles generates source files for model/controller/router +// It will wipe the following directories and recreate them:./models, ./controllers, ./routers +// Newly geneated files will be inside these folders. +func writeHproseSourceFiles(pkgPath string, tables []*Table, mode byte, paths *MvcPath, selectedTables map[string]bool) { + if (O_MODEL & mode) == O_MODEL { + ColorLog("[INFO] Creating model files...\n") + writeHproseModelFiles(tables, paths.ModelPath, selectedTables) + } +} + +// writeHproseModelFiles generates model files +func writeHproseModelFiles(tables []*Table, mPath string, selectedTables map[string]bool) { + for _, tb := range tables { + // if selectedTables map is not nil and this table is not selected, ignore it + if selectedTables != nil { + if _, selected := selectedTables[tb.Name]; !selected { + continue + } + } + filename := getFileName(tb.Name) + fpath := path.Join(mPath, filename+".go") + var f *os.File + var err error + if isExist(fpath) { + ColorLog("[WARN] %v is exist, do you want to overwrite it? Yes or No?\n", fpath) + if askForConfirmation() { + f, err = os.OpenFile(fpath, os.O_RDWR|os.O_TRUNC, 0666) + if err != nil { + ColorLog("[WARN] %v\n", err) + continue + } + } else { + ColorLog("[WARN] skip create file\n") + continue + } + } else { + f, err = os.OpenFile(fpath, os.O_CREATE|os.O_RDWR, 0666) + if err != nil { + ColorLog("[WARN] %v\n", err) + continue + } + } + template := "" + if tb.Pk == "" { + template = HPROSE_STRUCT_MODEL_TPL + } else { + template = HPROSE_MODEL_TPL + hproseAddFunctions = append(hproseAddFunctions, strings.Replace(HPROSE_ADDFUNCTION, "{{modelName}}", camelCase(tb.Name), -1)) + } + fileStr := strings.Replace(template, "{{modelStruct}}", tb.String(), 1) + fileStr = strings.Replace(fileStr, "{{modelName}}", camelCase(tb.Name), -1) + // if table contains time field, import time.Time package + timePkg := "" + importTimePkg := "" + if tb.ImportTimePkg { + timePkg = "\"time\"\n" + importTimePkg = "import \"time\"\n" + } + fileStr = strings.Replace(fileStr, "{{timePkg}}", timePkg, -1) + fileStr = strings.Replace(fileStr, "{{importTimePkg}}", importTimePkg, -1) + if _, err := f.WriteString(fileStr); err != nil { + ColorLog("[ERRO] Could not write model file to %s\n", fpath) + os.Exit(2) + } + f.Close() + ColorLog("[INFO] model => %s\n", fpath) + formatSourceCode(fpath) + } +} + +const ( + HPROSE_ADDFUNCTION = ` + // publish about {{modelName}} function + service.AddFunction("Add{{modelName}}", models.Add{{modelName}}) + service.AddFunction("Get{{modelName}}ById", models.Get{{modelName}}ById) + service.AddFunction("GetAll{{modelName}}", models.GetAll{{modelName}}) + service.AddFunction("Update{{modelName}}ById", models.Update{{modelName}}ById) + service.AddFunction("Delete{{modelName}}", models.Delete{{modelName}}) + +` + HPROSE_STRUCT_MODEL_TPL = `package models +{{importTimePkg}} +{{modelStruct}} +` + + HPROSE_MODEL_TPL = `package models + +import ( + "errors" + "fmt" + "reflect" + "strings" + {{timePkg}} + "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/hproseapp.go b/hproseapp.go new file mode 100644 index 0000000..5167c31 --- /dev/null +++ b/hproseapp.go @@ -0,0 +1,317 @@ +/**********************************************************\ +| | +| hprose | +| | +| Official WebSite: http://www.hprose.com/ | +| http://www.hprose.org/ | +| | +\**********************************************************/ +/**********************************************************\ + * * + * Build rpc application use Hprose base on beego * + * * + * LastModified: Oct 13, 2014 * + * Author: Liu jian * + * * +\**********************************************************/ + +package main + +import ( + "fmt" + "os" + path "path/filepath" + "strings" +) + +var cmdHproseapp = &Command{ + // CustomFlags: true, + UsageLine: "hprose [appname]", + Short: "create an rpc application use hprose base on beego framework", + Long: ` +create an rpc application use hprose base on beego framework + +bee hprose [appname] [-tables=""] [-driver=mysql] [-conn=root:@tcp(127.0.0.1:3306)/test] + -tables: a list of table names separated by ',', default is empty, indicating all tables + -driver: [mysql | postgres | sqlite], the default is mysql + -conn: the connection string used by the driver, the default is '' + e.g. for mysql: root:@tcp(127.0.0.1:3306)/test + e.g. for postgres: postgres://postgres:postgres@127.0.0.1:5432/postgres + +if conn is empty will create a example rpc application. otherwise generate rpc application use hprose based on an existing database. + +In the current path, will create a folder named [appname] + +In the appname folder has the follow struct: + + ├── conf + │ └── app.conf + ├── main.go + └── models + └── object.go + └── user.go +`, +} + +var hproseconf = `appname = {{.Appname}} +httpport = 8080 +runmode = dev +autorender = false +copyrequestbody = true +EnableDocs = true +` +var hproseMaingo = `package main + +import ( + "{{.Appname}}/models" + "github.com/hprose/hprose-go/hprose" + + "github.com/astaxie/beego" +) + +func main() { + service := hprose.NewHttpService() + service.AddFunction("AddOne", models.AddOne) + service.AddFunction("GetOne", models.GetOne) + beego.Handler("/", service) + beego.Run() +} +` + +var hproseMainconngo = `package main + +import ( + "{{.Appname}}/models" + "github.com/hprose/hprose-go/hprose" + + "github.com/astaxie/beego" + "github.com/astaxie/beego/orm" + {{.DriverPkg}} +) + +func init() { + orm.RegisterDataBase("default", "{{.DriverName}}", "{{.conn}}") +} + +func main() { + service := hprose.NewHttpService() + {{HproseFunctionList}} + beego.Handler("/", service) + beego.Run() +} + +` + +var hproseModels = `package models + +import ( + "errors" + "strconv" + "time" +) + +var ( + Objects map[string]*Object +) + +type Object struct { + ObjectId string + Score int64 + PlayerName string +} + +func init() { + Objects = make(map[string]*Object) + Objects["hjkhsbnmn123"] = &Object{"hjkhsbnmn123", 100, "astaxie"} + Objects["mjjkxsxsaa23"] = &Object{"mjjkxsxsaa23", 101, "someone"} +} + +func AddOne(object Object) (ObjectId string) { + object.ObjectId = "astaxie" + strconv.FormatInt(time.Now().UnixNano(), 10) + Objects[object.ObjectId] = &object + return object.ObjectId +} + +func GetOne(ObjectId string) (object *Object, err error) { + if v, ok := Objects[ObjectId]; ok { + return v, nil + } + return nil, errors.New("ObjectId Not Exist") +} + +func GetAll() map[string]*Object { + return Objects +} + +func Update(ObjectId string, Score int64) (err error) { + if v, ok := Objects[ObjectId]; ok { + v.Score = Score + return nil + } + return errors.New("ObjectId Not Exist") +} + +func Delete(ObjectId string) { + delete(Objects, ObjectId) +} + +` + +var hproseModels2 = `package models + +import ( + "errors" + "strconv" + "time" +) + +var ( + UserList map[string]*User +) + +func init() { + UserList = make(map[string]*User) + u := User{"user_11111", "astaxie", "11111", Profile{"male", 20, "Singapore", "astaxie@gmail.com"}} + UserList["user_11111"] = &u +} + +type User struct { + Id string + Username string + Password string + Profile Profile +} + +type Profile struct { + Gender string + Age int + Address string + Email string +} + +func AddUser(u User) string { + u.Id = "user_" + strconv.FormatInt(time.Now().UnixNano(), 10) + UserList[u.Id] = &u + return u.Id +} + +func GetUser(uid string) (u *User, err error) { + if u, ok := UserList[uid]; ok { + return u, nil + } + return nil, errors.New("User not exists") +} + +func GetAllUsers() map[string]*User { + return UserList +} + +func UpdateUser(uid string, uu *User) (a *User, err error) { + if u, ok := UserList[uid]; ok { + if uu.Username != "" { + u.Username = uu.Username + } + if uu.Password != "" { + u.Password = uu.Password + } + if uu.Profile.Age != 0 { + u.Profile.Age = uu.Profile.Age + } + if uu.Profile.Address != "" { + u.Profile.Address = uu.Profile.Address + } + if uu.Profile.Gender != "" { + u.Profile.Gender = uu.Profile.Gender + } + if uu.Profile.Email != "" { + u.Profile.Email = uu.Profile.Email + } + return u, nil + } + return nil, errors.New("User Not Exist") +} + +func Login(username, password string) bool { + for _, u := range UserList { + if u.Username == username && u.Password == password { + return true + } + } + return false +} + +func DeleteUser(uid string) { + delete(UserList, uid) +} +` + +var hproseAddFunctions = []string{} + +func init() { + cmdHproseapp.Run = createhprose + cmdHproseapp.Flag.Var(&tables, "tables", "specify tables to generate model") + cmdHproseapp.Flag.Var(&driver, "driver", "database driver: mysql, postgresql, etc.") + cmdHproseapp.Flag.Var(&conn, "conn", "connection string used by the driver to connect to a database instance") +} + +func createhprose(cmd *Command, args []string) int { + curpath, _ := os.Getwd() + if len(args) > 1 { + cmd.Flag.Parse(args[1:]) + } + apppath, packpath, err := checkEnv(args[0]) + if err != nil { + fmt.Println(err) + os.Exit(2) + } + if driver == "" { + driver = "mysql" + } + if conn == "" { + } + os.MkdirAll(apppath, 0755) + fmt.Println("create app folder:", apppath) + os.Mkdir(path.Join(apppath, "conf"), 0755) + fmt.Println("create conf:", path.Join(apppath, "conf")) + fmt.Println("create conf app.conf:", path.Join(apppath, "conf", "app.conf")) + writetofile(path.Join(apppath, "conf", "app.conf"), + strings.Replace(hproseconf, "{{.Appname}}", args[0], -1)) + + if conn != "" { + ColorLog("[INFO] Using '%s' as 'driver'\n", driver) + ColorLog("[INFO] Using '%s' as 'conn'\n", conn) + ColorLog("[INFO] Using '%s' as 'tables'\n", tables) + generateHproseAppcode(string(driver), string(conn), "1", string(tables), path.Join(curpath, args[0])) + fmt.Println("create main.go:", path.Join(apppath, "main.go")) + maingoContent := strings.Replace(hproseMainconngo, "{{.Appname}}", packpath, -1) + maingoContent = strings.Replace(maingoContent, "{{.DriverName}}", string(driver), -1) + maingoContent = strings.Replace(maingoContent, "{{HproseFunctionList}}", strings.Join(hproseAddFunctions, ""), -1) + if driver == "mysql" { + maingoContent = strings.Replace(maingoContent, "{{.DriverPkg}}", `_ "github.com/go-sql-driver/mysql"`, -1) + } else if driver == "postgres" { + maingoContent = strings.Replace(maingoContent, "{{.DriverPkg}}", `_ "github.com/lib/pq"`, -1) + } + writetofile(path.Join(apppath, "main.go"), + strings.Replace( + maingoContent, + "{{.conn}}", + conn.String(), + -1, + ), + ) + } else { + os.Mkdir(path.Join(apppath, "models"), 0755) + fmt.Println("create models:", path.Join(apppath, "models")) + + fmt.Println("create models object.go:", path.Join(apppath, "models", "object.go")) + writetofile(path.Join(apppath, "models", "object.go"), apiModels) + + fmt.Println("create models user.go:", path.Join(apppath, "models", "user.go")) + writetofile(path.Join(apppath, "models", "user.go"), apiModels2) + + fmt.Println("create main.go:", path.Join(apppath, "main.go")) + writetofile(path.Join(apppath, "main.go"), + strings.Replace(hproseMaingo, "{{.Appname}}", packpath, -1)) + } + return 0 +}