diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..418384a --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +bee +*.exe +*.exe~ diff --git a/README.md b/README.md index 2007357..28b76ff 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ bee === + [![Build Status](https://drone.io/github.com/astaxie/bee/status.png)](https://drone.io/github.com/astaxie/bee/latest) -Bee is a tool for managing beego framework. \ No newline at end of file +Bee is a tool for managing beego framework. + +## License + +[Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html). \ No newline at end of file diff --git a/apiapp.go b/apiapp.go new file mode 100644 index 0000000..ef16306 --- /dev/null +++ b/apiapp.go @@ -0,0 +1,285 @@ +package main + +import ( + "fmt" + "os" + path "path/filepath" + "strings" +) + +var cmdApiapp = &Command{ + // CustomFlags: true, + UsageLine: "api [appname]", + Short: "create an api application base on beego framework", + Long: ` +create an api application base on beego framework + +In the current path, will create a folder named [appname] + +In the appname folder has the follow struct: + + ├── conf + │ └── app.conf + ├── controllers + │ └── default.go + ├── main.go + └── models + └── object.go + +`, +} + +var apiconf = ` +appname = {{.Appname}} +httpport = 8080 +runmode = dev +autorender = false +copyrequestbody = true +` +var apiMaingo = `package main + +import ( + "github.com/astaxie/beego" + "{{.Appname}}/controllers" +) + +// Objects + +// URL HTTP Verb Functionality +// /object POST Creating Objects +// /object/ GET Retrieving Objects +// /object/ PUT Updating Objects +// /object GET Queries +// /object/ DELETE Deleting Objects + +func main() { + beego.RESTRouter("/object", &controllers.ObejctController{}) + beego.Router("/ping", &controllers.ObejctController{},"get:Ping") + beego.Run() +} +` +var apiModels = `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 apiControllers = `package controllers + +import ( + "encoding/json" + "github.com/astaxie/beego" + "{{.Appname}}/models" +) + +type ResponseInfo struct { +} + +type ObejctController struct { + beego.Controller +} + +func (this *ObejctController) Post() { + var ob models.Object + json.Unmarshal(this.Ctx.Input.RequestBody, &ob) + objectid := models.AddOne(ob) + this.Data["json"] = map[string]string{"ObjectId": objectid} + this.ServeJson() +} + +func (this *ObejctController) Get() { + objectId := this.Ctx.Params[":objectId"] + if objectId != "" { + ob, err := models.GetOne(objectId) + if err != nil { + this.Data["json"] = err + } else { + this.Data["json"] = ob + } + } else { + obs := models.GetAll() + this.Data["json"] = obs + } + this.ServeJson() +} + +func (this *ObejctController) Put() { + objectId := this.Ctx.Params[":objectId"] + var ob models.Object + json.Unmarshal(this.Ctx.Input.RequestBody, &ob) + + err := models.Update(objectId, ob.Score) + if err != nil { + this.Data["json"] = err + } else { + this.Data["json"] = "update success!" + } + this.ServeJson() +} + +func (this *ObejctController) Delete() { + objectId := this.Ctx.Input.Params[":objectId"] + models.Delete(objectId) + this.Data["json"] = "delete success!" + this.ServeJson() +} + +func (this *ObejctController) Ping() { + this.Ctx.WriteString("pong") +} + +` + +var apiTests = `package tests + +import ( + "testing" + beetest "github.com/astaxie/beego/testing" + "io/ioutil" +) + +func TestHelloWorld(t *testing.T) { + request:=beetest.Get("/ping") + response,_:=request.Response() + defer response.Body.Close() + contents, _ := ioutil.ReadAll(response.Body) + if string(contents)!="pong"{ + t.Errorf("response sould be pong") + } +} + +` + +func init() { + cmdApiapp.Run = createapi +} + +func createapi(cmd *Command, args []string) { + if len(args) != 1 { + fmt.Println("error args") + os.Exit(2) + } + apppath, packpath, err := checkEnv(args[0]) + if err != nil { + fmt.Println(err) + os.Exit(2) + } + 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")) + os.Mkdir(path.Join(apppath, "controllers"), 0755) + fmt.Println("create controllers:", path.Join(apppath, "controllers")) + os.Mkdir(path.Join(apppath, "models"), 0755) + fmt.Println("create models:", path.Join(apppath, "models")) + os.Mkdir(path.Join(apppath, "tests"), 0755) + fmt.Println("create tests:", path.Join(apppath, "tests")) + + fmt.Println("create conf app.conf:", path.Join(apppath, "conf", "app.conf")) + writetofile(path.Join(apppath, "conf", "app.conf"), + strings.Replace(apiconf, "{{.Appname}}", args[0], -1)) + + fmt.Println("create controllers default.go:", path.Join(apppath, "controllers", "default.go")) + writetofile(path.Join(apppath, "controllers", "default.go"), + strings.Replace(apiControllers, "{{.Appname}}", packpath, -1)) + + fmt.Println("create tests default.go:", path.Join(apppath, "tests", "default_test.go")) + writetofile(path.Join(apppath, "tests", "default_test.go"), + apiTests) + + fmt.Println("create models object.go:", path.Join(apppath, "models", "object.go")) + writetofile(path.Join(apppath, "models", "object.go"), apiModels) + + fmt.Println("create main.go:", path.Join(apppath, "main.go")) + writetofile(path.Join(apppath, "main.go"), + strings.Replace(apiMaingo, "{{.Appname}}", packpath, -1)) +} + +func checkEnv(appname string) (apppath, packpath string, err error) { + curpath, err := os.Getwd() + if err != nil { + return + } + + gopath := os.Getenv("GOPATH") + Debugf("gopath:%s", gopath) + if gopath == "" { + err = fmt.Errorf("you should set GOPATH in the env") + return + } + + appsrcpath := "" + haspath := false + wgopath := path.SplitList(gopath) + for _, wg := range wgopath { + wg = path.Join(wg, "src") + + if path.HasPrefix(strings.ToLower(curpath), strings.ToLower(wg)) { + haspath = true + appsrcpath = wg + break + } + } + + if !haspath { + err = fmt.Errorf("can't create application outside of GOPATH `%s`\n"+ + "you first should `cd $GOPATH%ssrc` then use create\n", gopath, string(path.Separator)) + return + } + apppath = path.Join(curpath, appname) + + if _, e := os.Stat(apppath); os.IsNotExist(e) == false { + err = fmt.Errorf("path `%s` exists, can not create app without remove it\n", apppath) + return + } + packpath = strings.Join(strings.Split(apppath[len(appsrcpath)+1:], string(path.Separator)), "/") + return +} diff --git a/autorouter.go b/autorouter.go new file mode 100644 index 0000000..2de7bfb --- /dev/null +++ b/autorouter.go @@ -0,0 +1,282 @@ +// Copyright 2013 Bee Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package main + +import ( + "bytes" + "errors" + "fmt" + "go/ast" + gobuild "go/build" + "go/doc" + "go/parser" + "go/token" + "io" + "io/ioutil" + "os" + "path" + "runtime" + "strings" + "time" +) + +var cmdRouter = &Command{ + UsageLine: "router", + Short: "auto-generate routers for the app controllers", + Long: ` + +`, +} + +func init() { + cmdRouter.Run = autoRouter +} + +func autoRouter(cmd *Command, args []string) { + fmt.Println("[INFO] Starting auto-generating routers...") +} + +// getControllerInfo returns controllers that embeded "beego.controller" +// and their methods of package in given path. +func getControllerInfo(path string) (map[string][]string, error) { + now := time.Now() + path = strings.TrimSuffix(path, "/") + dir, err := os.Open(path) + if err != nil { + return nil, err + } + + fis, err := dir.Readdir(0) + if err != nil { + return nil, err + } + + files := make([]*source, 0, len(fis)) + for _, fi := range fis { + // Only load go files. + if strings.HasSuffix(fi.Name(), ".go") { + f, err := os.Open(path + "/" + fi.Name()) + if err != nil { + return nil, err + } + + p := make([]byte, fi.Size()) + _, err = f.Read(p) + if err != nil { + return nil, err + } + + f.Close() + files = append(files, &source{ + name: path + "/" + fi.Name(), + data: p, + }) + } + } + + rw := &routerWalker{ + pdoc: &Package{ + ImportPath: path, + }, + } + + cm := make(map[string][]string) + pdoc, err := rw.build(files) + for _, t := range pdoc.Types { + // Check if embeded "beego.Controller". + if strings.Index(t.Decl, "beego.Controller") > -1 { + for _, f := range t.Methods { + cm[t.Name] = append(cm[t.Name], f.Name) + } + } + } + fmt.Println(time.Since(now)) + return cm, nil +} + +// A source describles a source code file. +type source struct { + name string + data []byte +} + +func (s *source) Name() string { return s.name } +func (s *source) Size() int64 { return int64(len(s.data)) } +func (s *source) Mode() os.FileMode { return 0 } +func (s *source) ModTime() time.Time { return time.Time{} } +func (s *source) IsDir() bool { return false } +func (s *source) Sys() interface{} { return nil } + +// A routerWalker holds the state used when building the documentation. +type routerWalker struct { + pdoc *Package + srcs map[string]*source // Source files. + fset *token.FileSet + buf []byte // scratch space for printNode method. +} + +// Package represents full information and documentation for a package. +type Package struct { + ImportPath string + + // Top-level declarations. + Types []*Type +} + +// Type represents structs and interfaces. +type Type struct { + Name string // Type name. + Decl string + Methods []*Func // Exported methods. +} + +// Func represents functions +type Func struct { + Name string +} + +// build generates data from source files. +func (w *routerWalker) build(srcs []*source) (*Package, error) { + // Add source files to walker, I skipped references here. + w.srcs = make(map[string]*source) + for _, src := range srcs { + w.srcs[src.name] = src + } + + w.fset = token.NewFileSet() + + // Find the package and associated files. + ctxt := gobuild.Context{ + GOOS: runtime.GOOS, + GOARCH: runtime.GOARCH, + CgoEnabled: true, + JoinPath: path.Join, + IsAbsPath: path.IsAbs, + SplitPathList: func(list string) []string { return strings.Split(list, ":") }, + IsDir: func(path string) bool { panic("unexpected") }, + HasSubdir: func(root, dir string) (rel string, ok bool) { panic("unexpected") }, + ReadDir: func(dir string) (fi []os.FileInfo, err error) { return w.readDir(dir) }, + OpenFile: func(path string) (r io.ReadCloser, err error) { return w.openFile(path) }, + Compiler: "gc", + } + + bpkg, err := ctxt.ImportDir(w.pdoc.ImportPath, 0) + // Continue if there are no Go source files; we still want the directory info. + _, nogo := err.(*gobuild.NoGoError) + if err != nil { + if nogo { + err = nil + } else { + return nil, errors.New("routerWalker.build -> " + err.Error()) + } + } + + // Parse the Go files + files := make(map[string]*ast.File) + for _, name := range append(bpkg.GoFiles, bpkg.CgoFiles...) { + file, err := parser.ParseFile(w.fset, name, w.srcs[name].data, parser.ParseComments) + if err != nil { + return nil, errors.New("routerWalker.build -> parse go files: " + err.Error()) + } + files[name] = file + } + + apkg, _ := ast.NewPackage(w.fset, files, simpleImporter, nil) + + mode := doc.Mode(0) + if w.pdoc.ImportPath == "builtin" { + mode |= doc.AllDecls + } + + pdoc := doc.New(apkg, w.pdoc.ImportPath, mode) + + w.pdoc.Types = w.types(pdoc.Types) + + return w.pdoc, err +} + +func (w *routerWalker) funcs(fdocs []*doc.Func) []*Func { + var result []*Func + for _, d := range fdocs { + result = append(result, &Func{ + Name: d.Name, + }) + } + return result +} + +func (w *routerWalker) types(tdocs []*doc.Type) []*Type { + var result []*Type + for _, d := range tdocs { + result = append(result, &Type{ + Decl: w.printDecl(d.Decl), + Name: d.Name, + Methods: w.funcs(d.Methods), + }) + } + return result +} + +func (w *routerWalker) printDecl(decl ast.Node) string { + var d Code + d, w.buf = printDecl(decl, w.fset, w.buf) + return d.Text +} + +func (w *routerWalker) readDir(dir string) ([]os.FileInfo, error) { + if dir != w.pdoc.ImportPath { + panic("unexpected") + } + fis := make([]os.FileInfo, 0, len(w.srcs)) + for _, src := range w.srcs { + fis = append(fis, src) + } + return fis, nil +} + +func (w *routerWalker) openFile(path string) (io.ReadCloser, error) { + if strings.HasPrefix(path, w.pdoc.ImportPath+"/") { + if src, ok := w.srcs[path[len(w.pdoc.ImportPath)+1:]]; ok { + return ioutil.NopCloser(bytes.NewReader(src.data)), nil + } + } + panic("unexpected") +} + +func simpleImporter(imports map[string]*ast.Object, path string) (*ast.Object, error) { + pkg := imports[path] + if pkg == nil { + // Guess the package name without importing it. Start with the last + // element of the path. + name := path[strings.LastIndex(path, "/")+1:] + + // Trim commonly used prefixes and suffixes containing illegal name + // runes. + name = strings.TrimSuffix(name, ".go") + name = strings.TrimSuffix(name, "-go") + name = strings.TrimPrefix(name, "go.") + name = strings.TrimPrefix(name, "go-") + name = strings.TrimPrefix(name, "biogo.") + + // It's also common for the last element of the path to contain an + // extra "go" prefix, but not always. TODO: examine unresolved ids to + // detect when trimming the "go" prefix is appropriate. + + pkg = ast.NewObj(ast.Pkg, name) + pkg.Data = ast.NewScope(nil) + imports[path] = pkg + } + return pkg, nil +} diff --git a/autorouter_test.go b/autorouter_test.go new file mode 100644 index 0000000..437f3ee --- /dev/null +++ b/autorouter_test.go @@ -0,0 +1,9 @@ +package main + +import ( + "testing" +) + +func TestGetControllerInfo(t *testing.T) { + getControllerInfo("testdata/router/") +} diff --git a/main.go b/bee.go similarity index 82% rename from main.go rename to bee.go index 3749707..e31c536 100644 --- a/main.go +++ b/bee.go @@ -1,3 +1,18 @@ +// Copyright 2013 Bee Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +// Bee is a tool for developling applications based on beego framework. package main import ( @@ -56,9 +71,12 @@ func (c *Command) Runnable() bool { } var commands = []*Command{ - cmdCreate, - cmdStart, + cmdNew, + cmdRun, cmdPack, + cmdApiapp, + cmdRouter, + cmdTest, //cmdReStart, } diff --git a/code.go b/code.go new file mode 100644 index 0000000..5e4c3e2 --- /dev/null +++ b/code.go @@ -0,0 +1,262 @@ +// Copyright 2011 Gary Burd +// Copyright 2013 Unknown +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package main + +import ( + "go/ast" + "go/printer" + "go/scanner" + "go/token" + "math" + "strconv" +) + +const ( + notPredeclared = iota + predeclaredType + predeclaredConstant + predeclaredFunction +) + +// predeclared represents the set of all predeclared identifiers. +var predeclared = map[string]int{ + "bool": predeclaredType, + "byte": predeclaredType, + "complex128": predeclaredType, + "complex64": predeclaredType, + "error": predeclaredType, + "float32": predeclaredType, + "float64": predeclaredType, + "int16": predeclaredType, + "int32": predeclaredType, + "int64": predeclaredType, + "int8": predeclaredType, + "int": predeclaredType, + "rune": predeclaredType, + "string": predeclaredType, + "uint16": predeclaredType, + "uint32": predeclaredType, + "uint64": predeclaredType, + "uint8": predeclaredType, + "uint": predeclaredType, + "uintptr": predeclaredType, + + "true": predeclaredConstant, + "false": predeclaredConstant, + "iota": predeclaredConstant, + "nil": predeclaredConstant, + + "append": predeclaredFunction, + "cap": predeclaredFunction, + "close": predeclaredFunction, + "complex": predeclaredFunction, + "copy": predeclaredFunction, + "delete": predeclaredFunction, + "imag": predeclaredFunction, + "len": predeclaredFunction, + "make": predeclaredFunction, + "new": predeclaredFunction, + "panic": predeclaredFunction, + "print": predeclaredFunction, + "println": predeclaredFunction, + "real": predeclaredFunction, + "recover": predeclaredFunction, +} + +const ( + ExportLinkAnnotation AnnotationKind = iota + AnchorAnnotation + CommentAnnotation + PackageLinkAnnotation + BuiltinAnnotation +) + +// annotationVisitor collects annotations. +type annotationVisitor struct { + annotations []Annotation +} + +func (v *annotationVisitor) add(kind AnnotationKind, importPath string) { + v.annotations = append(v.annotations, Annotation{Kind: kind, ImportPath: importPath}) +} + +func (v *annotationVisitor) ignoreName() { + v.add(-1, "") +} + +func (v *annotationVisitor) Visit(n ast.Node) ast.Visitor { + switch n := n.(type) { + case *ast.TypeSpec: + v.ignoreName() + ast.Walk(v, n.Type) + case *ast.FuncDecl: + if n.Recv != nil { + ast.Walk(v, n.Recv) + } + v.ignoreName() + ast.Walk(v, n.Type) + case *ast.Field: + for _ = range n.Names { + v.ignoreName() + } + ast.Walk(v, n.Type) + case *ast.ValueSpec: + for _ = range n.Names { + v.add(AnchorAnnotation, "") + } + if n.Type != nil { + ast.Walk(v, n.Type) + } + for _, x := range n.Values { + ast.Walk(v, x) + } + case *ast.Ident: + switch { + case n.Obj == nil && predeclared[n.Name] != notPredeclared: + v.add(BuiltinAnnotation, "") + case n.Obj != nil && ast.IsExported(n.Name): + v.add(ExportLinkAnnotation, "") + default: + v.ignoreName() + } + case *ast.SelectorExpr: + if x, _ := n.X.(*ast.Ident); x != nil { + if obj := x.Obj; obj != nil && obj.Kind == ast.Pkg { + if spec, _ := obj.Decl.(*ast.ImportSpec); spec != nil { + if path, err := strconv.Unquote(spec.Path.Value); err == nil { + v.add(PackageLinkAnnotation, path) + if path == "C" { + v.ignoreName() + } else { + v.add(ExportLinkAnnotation, path) + } + return nil + } + } + } + } + ast.Walk(v, n.X) + v.ignoreName() + default: + return v + } + return nil +} + +func printDecl(decl ast.Node, fset *token.FileSet, buf []byte) (Code, []byte) { + v := &annotationVisitor{} + ast.Walk(v, decl) + + buf = buf[:0] + err := (&printer.Config{Mode: printer.UseSpaces, Tabwidth: 4}).Fprint(sliceWriter{&buf}, fset, decl) + if err != nil { + return Code{Text: err.Error()}, buf + } + + var annotations []Annotation + var s scanner.Scanner + fset = token.NewFileSet() + file := fset.AddFile("", fset.Base(), len(buf)) + s.Init(file, buf, nil, scanner.ScanComments) +loop: + for { + pos, tok, lit := s.Scan() + switch tok { + case token.EOF: + break loop + case token.COMMENT: + p := file.Offset(pos) + e := p + len(lit) + if p > math.MaxInt16 || e > math.MaxInt16 { + break loop + } + annotations = append(annotations, Annotation{Kind: CommentAnnotation, Pos: int16(p), End: int16(e)}) + case token.IDENT: + if len(v.annotations) == 0 { + // Oops! + break loop + } + annotation := v.annotations[0] + v.annotations = v.annotations[1:] + if annotation.Kind == -1 { + continue + } + p := file.Offset(pos) + e := p + len(lit) + if p > math.MaxInt16 || e > math.MaxInt16 { + break loop + } + annotation.Pos = int16(p) + annotation.End = int16(e) + if len(annotations) > 0 && annotation.Kind == ExportLinkAnnotation { + prev := annotations[len(annotations)-1] + if prev.Kind == PackageLinkAnnotation && + prev.ImportPath == annotation.ImportPath && + prev.End+1 == annotation.Pos { + // merge with previous + annotation.Pos = prev.Pos + annotations[len(annotations)-1] = annotation + continue loop + } + } + annotations = append(annotations, annotation) + } + } + return Code{Text: string(buf), Annotations: annotations}, buf +} + +type AnnotationKind int16 + +type Annotation struct { + Pos, End int16 + Kind AnnotationKind + ImportPath string +} + +type Code struct { + Text string + Annotations []Annotation +} + +func commentAnnotations(src string) []Annotation { + var annotations []Annotation + var s scanner.Scanner + fset := token.NewFileSet() + file := fset.AddFile("", fset.Base(), len(src)) + s.Init(file, []byte(src), nil, scanner.ScanComments) + for { + pos, tok, lit := s.Scan() + switch tok { + case token.EOF: + return annotations + case token.COMMENT: + p := file.Offset(pos) + e := p + len(lit) + if p > math.MaxInt16 || e > math.MaxInt16 { + return annotations + } + annotations = append(annotations, Annotation{Kind: CommentAnnotation, Pos: int16(p), End: int16(e)}) + } + } + return nil +} + +type sliceWriter struct{ p *[]byte } + +func (w sliceWriter) Write(p []byte) (int, error) { + *w.p = append(*w.p, p...) + return len(p), nil +} diff --git a/createapp.go b/createapp.go deleted file mode 100644 index 7c2fce9..0000000 --- a/createapp.go +++ /dev/null @@ -1,165 +0,0 @@ -package main - -import ( - "fmt" - "os" - path "path/filepath" - "strings" -) - -var cmdCreate = &Command{ - UsageLine: "create [appname]", - Short: "create an application base on beego framework", - Long: ` -create an application base on beego framework - -In the current path, will create a folder named [appname] - -In the appname folder has the follow struct: - - |- main.go - |- conf - |- app.conf - |- controllers - |- default.go - |- models - |- static - |- js - |- css - |- img - |- views - index.tpl - -`, -} - -func init() { - cmdCreate.Run = createapp -} - -func createapp(cmd *Command, args []string) { - curpath, _ := os.Getwd() - if len(args) != 1 { - fmt.Println("error args") - os.Exit(2) - } - - gopath := os.Getenv("GOPATH") - Debugf("gopath:%s", gopath) - if gopath == "" { - fmt.Println("you should set GOPATH in the env") - os.Exit(2) - } - haspath := false - appsrcpath := "" - - wgopath := path.SplitList(gopath) - for _, wg := range wgopath { - wg = path.Join(wg, "src") - - if path.HasPrefix(strings.ToLower(curpath), strings.ToLower(wg)) { - haspath = true - appsrcpath = wg - break - } - } - - if !haspath { - fmt.Printf("can't create application outside of GOPATH `%s`\n", gopath) - fmt.Printf("you first should `cd $GOPATH%ssrc` then use create\n", string(path.Separator)) - os.Exit(2) - } - - apppath := path.Join(curpath, args[0]) - - if _, err := os.Stat(apppath); os.IsNotExist(err) == false { - fmt.Printf("path `%s` exists, can not create app without remove it\n", apppath) - os.Exit(2) - } - - 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")) - os.Mkdir(path.Join(apppath, "controllers"), 0755) - fmt.Println("create controllers:", path.Join(apppath, "controllers")) - os.Mkdir(path.Join(apppath, "models"), 0755) - fmt.Println("create models:", path.Join(apppath, "models")) - os.Mkdir(path.Join(apppath, "static"), 0755) - fmt.Println("create static:", path.Join(apppath, "static")) - os.Mkdir(path.Join(apppath, "static", "js"), 0755) - fmt.Println("create static js:", path.Join(apppath, "static", "js")) - os.Mkdir(path.Join(apppath, "static", "css"), 0755) - fmt.Println("create static css:", path.Join(apppath, "static", "css")) - os.Mkdir(path.Join(apppath, "static", "img"), 0755) - fmt.Println("create static img:", path.Join(apppath, "static", "img")) - fmt.Println("create views:", path.Join(apppath, "views")) - os.Mkdir(path.Join(apppath, "views"), 0755) - fmt.Println("create conf app.conf:", path.Join(apppath, "conf", "app.conf")) - writetofile(path.Join(apppath, "conf", "app.conf"), strings.Replace(appconf, "{{.Appname}}", args[0], -1)) - - fmt.Println("create controllers default.go:", path.Join(apppath, "controllers", "default.go")) - writetofile(path.Join(apppath, "controllers", "default.go"), controllers) - - fmt.Println("create views index.tpl:", path.Join(apppath, "views", "index.tpl")) - writetofile(path.Join(apppath, "views", "index.tpl"), indextpl) - - fmt.Println("create main.go:", path.Join(apppath, "main.go")) - writetofile(path.Join(apppath, "main.go"), strings.Replace(maingo, "{{.Appname}}", strings.Join(strings.Split(apppath[len(appsrcpath)+1:], string(path.Separator)), "/"), -1)) -} - -var appconf = ` -appname = {{.Appname}} -httpport = 8080 -runmode = dev -` - -var maingo = `package main - -import ( - "{{.Appname}}/controllers" - "github.com/astaxie/beego" -) - -func main() { - beego.Router("/", &controllers.MainController{}) - beego.Run() -} - -` -var controllers = `package controllers - -import ( - "github.com/astaxie/beego" -) - -type MainController struct { - beego.Controller -} - -func (this *MainController) Get() { - this.Data["Username"] = "astaxie" - this.Data["Email"] = "astaxie@gmail.com" - this.TplNames = "index.tpl" -} -` - -var indextpl = ` - - - beego welcome template - - -

Hello, world!{{.Username}},{{.Email}}

- - -` - -func writetofile(filename, content string) { - f, err := os.Create(filename) - if err != nil { - panic(err) - } - defer f.Close() - f.WriteString(content) -} diff --git a/new.go b/new.go new file mode 100644 index 0000000..b4ed4ca --- /dev/null +++ b/new.go @@ -0,0 +1,242 @@ +package main + +import ( + "fmt" + "os" + path "path/filepath" + "strings" +) + +var cmdNew = &Command{ + UsageLine: "new [appname]", + Short: "create an application base on beego framework", + Long: ` +create an application base on beego framework, + +which in the current path with folder named [appname]. + +The [appname] folder has following structure: + + |- main.go + |- conf + |- app.conf + |- controllers + |- default.go + |- models + |- static + |- js + |- css + |- img + |- views + index.tpl + +`, +} + +func init() { + cmdNew.Run = createApp +} + +func createApp(cmd *Command, args []string) { + curpath, _ := os.Getwd() + if len(args) != 1 { + colorLog("[ERRO] Argument [appname] is missing\n") + os.Exit(2) + } + + gopath := os.Getenv("GOPATH") + Debugf("gopath:%s", gopath) + if gopath == "" { + colorLog("[ERRO] $GOPATH not found\n") + colorLog("[HINT] Set $GOPATH in your environment vairables\n") + os.Exit(2) + } + haspath := false + appsrcpath := "" + + wgopath := path.SplitList(gopath) + for _, wg := range wgopath { + wg, _ = path.EvalSymlinks(path.Join(wg, "src")) + + if strings.HasPrefix(strings.ToLower(curpath), strings.ToLower(wg)) { + haspath = true + appsrcpath = wg + break + } + } + + if !haspath { + colorLog("[ERRO] Unable to create an application outside of $GOPATH(%s)\n", gopath) + colorLog("[HINT] Change your work directory by `cd ($GOPATH%ssrc)`\n", string(path.Separator)) + os.Exit(2) + } + + apppath := path.Join(curpath, args[0]) + + if _, err := os.Stat(apppath); os.IsNotExist(err) == false { + fmt.Printf("[ERRO] Path(%s) has alreay existed\n", apppath) + os.Exit(2) + } + + fmt.Println("[INFO] Creating application...") + + os.MkdirAll(apppath, 0755) + fmt.Println(apppath + string(path.Separator)) + os.Mkdir(path.Join(apppath, "conf"), 0755) + fmt.Println(path.Join(apppath, "conf") + string(path.Separator)) + os.Mkdir(path.Join(apppath, "controllers"), 0755) + fmt.Println(path.Join(apppath, "controllers") + string(path.Separator)) + os.Mkdir(path.Join(apppath, "models"), 0755) + fmt.Println(path.Join(apppath, "models") + string(path.Separator)) + os.Mkdir(path.Join(apppath, "static"), 0755) + fmt.Println(path.Join(apppath, "static") + string(path.Separator)) + os.Mkdir(path.Join(apppath, "static", "js"), 0755) + fmt.Println(path.Join(apppath, "static", "js") + string(path.Separator)) + os.Mkdir(path.Join(apppath, "static", "css"), 0755) + fmt.Println(path.Join(apppath, "static", "css") + string(path.Separator)) + os.Mkdir(path.Join(apppath, "static", "img"), 0755) + fmt.Println(path.Join(apppath, "static", "img") + string(path.Separator)) + fmt.Println(path.Join(apppath, "views") + string(path.Separator)) + os.Mkdir(path.Join(apppath, "views"), 0755) + fmt.Println(path.Join(apppath, "conf", "app.conf")) + writetofile(path.Join(apppath, "conf", "app.conf"), strings.Replace(appconf, "{{.Appname}}", args[0], -1)) + + fmt.Println(path.Join(apppath, "controllers", "default.go")) + writetofile(path.Join(apppath, "controllers", "default.go"), controllers) + + fmt.Println(path.Join(apppath, "views", "index.tpl")) + writetofile(path.Join(apppath, "views", "index.tpl"), indextpl) + + fmt.Println(path.Join(apppath, "main.go")) + writetofile(path.Join(apppath, "main.go"), strings.Replace(maingo, "{{.Appname}}", strings.Join(strings.Split(apppath[len(appsrcpath)+1:], string(path.Separator)), string(path.Separator)), -1)) + + colorLog("[SUCC] New application successfully created!\n") +} + +var appconf = `appname = {{.Appname}} +httpport = 8080 +runmode = dev +` + +var maingo = `package main + +import ( + "{{.Appname}}/controllers" + "github.com/astaxie/beego" +) + +func main() { + beego.Router("/", &controllers.MainController{}) + beego.Run() +} + +` +var controllers = `package controllers + +import ( + "github.com/astaxie/beego" +) + +type MainController struct { + beego.Controller +} + +func (this *MainController) Get() { + this.Data["Website"] = "beego.me" + this.Data["Email"] = "astaxie@gmail.com" + this.TplNames = "index.tpl" +} +` + +var indextpl = ` + + + + Beego + + + + + + +
+
+
+
+

Welcome to Beego!

+

+ Beego is a simple & powerful Go web framework which is inspired by tornado and sinatra. +
+ Official website: {{.Website}} +
+ Contact me: {{.Email}} +

+
+
+
+
+ + +` + +func writetofile(filename, content string) { + f, err := os.Create(filename) + if err != nil { + panic(err) + } + defer f.Close() + f.WriteString(content) +} diff --git a/run.go b/run.go new file mode 100644 index 0000000..4fb3c97 --- /dev/null +++ b/run.go @@ -0,0 +1,132 @@ +package main + +import ( + "encoding/json" + "os" + path "path/filepath" + "runtime" +) + +var cmdRun = &Command{ + UsageLine: "run [appname]", + Short: "run the app which can hot compile", + Long: ` +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 + | + check if it's go file + | + yes no + | | + go build do nothing + | + restart app +`, +} + +var defaultJson = ` +{ + "go_install": false, + "dir_structure":{ + "controllers": "", + "models": "", + "others": [] + }, + "main_files":{ + "main.go": "", + "others": [] + } +} +` + +func init() { + cmdRun.Run = runApp +} + +var appname string +var conf struct { + // Indicates whether execute "go install" before "go build". + GoInstall bool `json:"go_install"` + + DirStruct struct { + Controllers string + Models string + Others []string // Other directories. + } `json:"dir_structure"` + + MainFiles struct { + Main string `json:"main.go"` + Others []string // Others files of package main. + } `json:"main_files"` +} + +func runApp(cmd *Command, args []string) { + exit := make(chan bool) + if len(args) != 1 { + colorLog("[ERRO] Cannot start running[ %s ]\n", + "argument 'appname' is missing") + os.Exit(2) + } + crupath, _ := os.Getwd() + Debugf("current path:%s\n", crupath) + + err := loadConfig() + if err != nil { + colorLog("[ERRO] Fail to parse bee.json[ %s ]", err) + } + var paths []string + paths = append(paths, + path.Join(crupath, conf.DirStruct.Controllers), + path.Join(crupath, conf.DirStruct.Models), + path.Join(crupath, "./")) // Current path. + // Because monitor files has some issues, we watch current directory + // and ignore non-go files. + paths = append(paths, conf.DirStruct.Others...) + paths = append(paths, conf.MainFiles.Others...) + + NewWatcher(paths) + appname = args[0] + Autobuild() + for { + select { + case <-exit: + runtime.Goexit() + } + } +} + +// loadConfig loads customized configuration. +func loadConfig() error { + f, err := os.Open("bee.json") + if err != nil { + // Use default. + err = json.Unmarshal([]byte(defaultJson), &conf) + if err != nil { + return err + } + } else { + defer f.Close() + colorLog("[INFO] Detected bee.json\n") + d := json.NewDecoder(f) + err = d.Decode(&conf) + if err != nil { + return err + } + } + // Set variables. + if len(conf.DirStruct.Controllers) == 0 { + conf.DirStruct.Controllers = "controllers" + } + if len(conf.DirStruct.Models) == 0 { + conf.DirStruct.Models = "models" + } + if len(conf.MainFiles.Main) == 0 { + conf.MainFiles.Main = "main.go" + } + return nil +} diff --git a/start.go b/start.go deleted file mode 100644 index 4e8acef..0000000 --- a/start.go +++ /dev/null @@ -1,54 +0,0 @@ -package main - -import ( - "fmt" - "os" - path "path/filepath" - "runtime" -) - -var cmdStart = &Command{ - UsageLine: "start [appname]", - Short: "start the app which can hot compile", - Long: ` -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 - | - checked is go file - | - yes no - | | - go build do nothing - | - restart app -`, -} - -func init() { - cmdStart.Run = startapp -} - -var appname string - -func startapp(cmd *Command, args []string) { - if len(args) != 1 { - fmt.Println("error args") - os.Exit(2) - } - crupath, _ := os.Getwd() - Debugf("current path:%s\n", crupath) - - var paths []string - paths = append(paths, path.Join(crupath, "controllers"), path.Join(crupath, "models")) - NewWatcher(paths) - appname = args[0] - Autobuild() - for { - runtime.Gosched() - } -} diff --git a/test.go b/test.go new file mode 100644 index 0000000..56446b4 --- /dev/null +++ b/test.go @@ -0,0 +1,82 @@ +package main + +import ( + "os" + path "path/filepath" + "os/exec" + "time" + "bytes" +) + +var cmdTest = &Command{ + UsageLine: "test [appname]", + Short: "test the app", + Long: ``, +} + +func init() { + cmdTest.Run = testApp +} + +var started= make(chan bool) + +func testApp(cmd *Command, args []string) { + if len(args) != 1 { + colorLog("[ERRO] Cannot start running[ %s ]\n", + "argument 'appname' is missing") + os.Exit(2) + } + crupath, _ := os.Getwd() + Debugf("current path:%s\n", crupath) + + err := loadConfig() + if err != nil { + colorLog("[ERRO] Fail to parse bee.json[ %s ]", err) + } + var paths []string + paths = append(paths, + path.Join(crupath, conf.DirStruct.Controllers), + path.Join(crupath, conf.DirStruct.Models), + path.Join(crupath, "./")) // Current path. + // Because monitor files has some issues, we watch current directory + // and ignore non-go files. + paths = append(paths, conf.DirStruct.Others...) + paths = append(paths, conf.MainFiles.Others...) + + NewWatcher(paths) + appname = args[0] + Autobuild() + for { + select { + case <-started: + runTest() + Kill() + os.Exit(0) + } + } +} + +func runTest(){ + colorLog("[INFO] Start testing...\n") + time.Sleep(time.Second*5) + path, _ := os.Getwd() + os.Chdir(path+"/tests") + + var err error + icmd := exec.Command("go", "test") + var out,errbuffer bytes.Buffer + icmd.Stdout = &out + icmd.Stderr = &errbuffer + colorLog("[INFO] ============== Test Begin ===================\n") + err = icmd.Run() + colorLog(out.String()) + colorLog(errbuffer.String()) + colorLog("[INFO] ============== Test End ===================\n") + + if err != nil { + colorLog("[ERRO] ============== Test failed ===================\n") + colorLog("[ERRO] " ,err) + return + } + colorLog("[SUCC] Test finish\n") +} diff --git a/testdata/router/router.go b/testdata/router/router.go new file mode 100644 index 0000000..beb8ba2 --- /dev/null +++ b/testdata/router/router.go @@ -0,0 +1,28 @@ +package router + +import ( + "github.com/astaxie/beego" +) + +type Router struct { + beego.Controller +} + +func (this *Router) Get() { + +} + +func (this *Router) Post() { + +} + +type Controller struct { +} + +func (this *Controller) Put() { + +} + +func (this *Controller) Delete() { + +} diff --git a/util.go b/util.go index f8b7e13..6cafb46 100644 --- a/util.go +++ b/util.go @@ -1,9 +1,12 @@ package main -import "fmt" -import "os" -import "runtime" -import "path/filepath" +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "strings" +) // Go is a basic promise implementation: it wraps calls a function in a goroutine // and returns a channel which will later return the function's return value. @@ -28,3 +31,71 @@ func Debugf(format string, a ...interface{}) { fmt.Fprintf(os.Stderr, fmt.Sprintf("[debug] %s:%d %s\n", file, line, format), a...) } } + +const ( + Gray = uint8(iota + 90) + Red + Green + Yellow + Blue + Magenta + //NRed = uint8(31) // Normal + EndColor = "\033[0m" +) + +// colorLog colors log and print to stdout. +// Log format: [] [ error ]. +// Level: ERRO -> red; WARN -> Magenta; SUCC -> green; others -> default. +// Content: default; path: yellow; error -> red. +// Errors have to surrounded by "[ " and " ]"(space). +func colorLog(format string, a ...interface{}) { + log := fmt.Sprintf(format, a...) + if len(log) == 0 { + return + } + + if runtime.GOOS != "windows" { + var clog string + + // Level. + i := strings.Index(log, "]") + if log[0] == '[' && i > -1 { + clog += "[" + getColorLevel(log[1:i]) + "]" + } + + log = log[i+1:] + + // Error. + log = strings.Replace(log, "[ ", fmt.Sprintf("[\033[%dm", Red), -1) + log = strings.Replace(log, " ]", EndColor+"]", -1) + + // Path. + log = strings.Replace(log, "( ", fmt.Sprintf("(\033[%dm", Yellow), -1) + log = strings.Replace(log, " )", EndColor+")", -1) + + // Highlights. + log = strings.Replace(log, "# ", fmt.Sprintf("\033[%dm", Gray), -1) + log = strings.Replace(log, " #", EndColor, -1) + + log = clog + log + } + + fmt.Print(log) +} + +// getColorLevel returns colored level string by given level. +func getColorLevel(level string) string { + level = strings.ToUpper(level) + switch level { + case "TRAC": + return fmt.Sprintf("\033[%dm%s\033[0m", Blue, level) + case "ERRO": + return fmt.Sprintf("\033[%dm%s\033[0m", Red, level) + case "WARN": + return fmt.Sprintf("\033[%dm%s\033[0m", Magenta, level) + case "SUCC": + return fmt.Sprintf("\033[%dm%s\033[0m", Green, level) + default: + return level + } +} diff --git a/watch.go b/watch.go index 3188777..34cd6a7 100644 --- a/watch.go +++ b/watch.go @@ -6,6 +6,7 @@ import ( "log" "os" "os/exec" + "strings" "sync" "time" ) @@ -13,13 +14,14 @@ import ( var ( cmd *exec.Cmd state sync.Mutex - eventTime = make(map[string]time.Time) + eventTime = make(map[string]int64) ) func NewWatcher(paths []string) { watcher, err := fsnotify.NewWatcher() if err != nil { - log.Fatal(err) + colorLog("[ERRO] Fail to create new Watcher[ %s ]\n", err) + os.Exit(2) } go func() { @@ -27,17 +29,25 @@ func NewWatcher(paths []string) { select { case e := <-watcher.Event: isbuild := true - if t, ok := eventTime[e.String()]; ok { - // if 500ms change many times, then ignore it. - // for liteide often gofmt code after save. - if t.Add(time.Millisecond * 500).After(time.Now()) { - isbuild = false - } + + // Skip TMP files for Sublime Text. + if checkTMPFile(e.Name) { + continue } - eventTime[e.String()] = time.Now() + if !checkIsGoFile(e.Name) { + continue + } + + mt := getFileModTime(e.Name) + if t := eventTime[e.Name]; mt == t { + colorLog("[SKIP] # %s #\n", e.String()) + isbuild = false + } + + eventTime[e.Name] = mt if isbuild { - fmt.Println(e) + colorLog("[EVEN] %s\n", e) go Autobuild() } case err := <-watcher.Error: @@ -45,33 +55,67 @@ func NewWatcher(paths []string) { } } }() + + colorLog("[INFO] Initializing watcher...\n") for _, path := range paths { - fmt.Println(path) + colorLog("[TRAC] Directory( %s )\n", path) err = watcher.Watch(path) if err != nil { - log.Fatal(err) + colorLog("[ERRO] Fail to watch directory[ %s ]\n", err) + os.Exit(2) } } } +// getFileModTime retuens unix timestamp of `os.File.ModTime` by given path. +func getFileModTime(path string) int64 { + f, err := os.Open(path) + if err != nil { + colorLog("[ERRO] Fail to open file[ %s ]", err) + return time.Now().Unix() + } + defer f.Close() + + fi, err := f.Stat() + if err != nil { + colorLog("[ERRO] Fail to get file information[ %s ]", err) + return time.Now().Unix() + } + + return fi.ModTime().Unix() +} + func Autobuild() { state.Lock() defer state.Unlock() - fmt.Println("start autobuild") + colorLog("[INFO] Start building...\n") path, _ := os.Getwd() os.Chdir(path) - bcmd := exec.Command("go", "build") - bcmd.Stdout = os.Stdout - bcmd.Stderr = os.Stderr - err := bcmd.Run() + + var err error + // For applications use full import path like "github.com/.../.." + // are able to use "go install" to reduce build time. + if conf.GoInstall { + icmd := exec.Command("go", "install") + icmd.Stdout = os.Stdout + icmd.Stderr = os.Stderr + err = icmd.Run() + } + + if err == nil { + bcmd := exec.Command("go", "build") + bcmd.Stdout = os.Stdout + bcmd.Stderr = os.Stderr + err = bcmd.Run() + } if err != nil { - fmt.Println("============== build failed ===================") + colorLog("[ERRO] ============== Build failed ===================\n") return } - fmt.Println("build success") + colorLog("[SUCC] Build was successful\n") Restart(appname) } @@ -93,11 +137,31 @@ func Restart(appname string) { } func Start(appname string) { - fmt.Println("start", appname) + colorLog("[INFO] Restarting %s ...\n", appname) + if strings.Index(appname, "./") == -1 { + appname = "./" + appname + } cmd = exec.Command(appname) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr go cmd.Run() + started<-true +} + +// checkTMPFile returns true if the event was for TMP files. +func checkTMPFile(name string) bool { + if strings.HasSuffix(strings.ToLower(name), ".tmp") { + return true + } + return false +} + +// checkIsGoFile returns true if the name HasSuffix ".go". +func checkIsGoFile(name string) bool { + if strings.HasSuffix(name, ".go") { + return true + } + return false }