diff --git a/Dockerfile b/Dockerfile index 50aefa613..195ca08d9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,3 +26,4 @@ RUN buffalo db migrate -e test RUN buffalo test -race RUN buffalo g goth facebook twitter linkedin github RUN buffalo test -race +RUN buffalo build diff --git a/buffalo/cmd/app_generators.go b/buffalo/cmd/app_generators.go index 503208e4b..b8a7f712f 100644 --- a/buffalo/cmd/app_generators.go +++ b/buffalo/cmd/app_generators.go @@ -11,8 +11,6 @@ func newAppGenerator(data gentronics.Data) *gentronics.Generator { g := gentronics.New() g.Add(gentronics.NewFile("README.md", nREADME)) g.Add(gentronics.NewFile("main.go", nMain)) - g.Add(gentronics.NewFile("Procfile", nProcfile)) - g.Add(gentronics.NewFile("Procfile.development", nProcfileDev)) g.Add(gentronics.NewFile(".buffalo.dev.yml", nRefresh)) g.Add(gentronics.NewFile(".codeclimate.yml", nCodeClimate)) g.Add(gentronics.NewFile("actions/app.go", nApp)) @@ -49,24 +47,24 @@ func appGoGet() *exec.Cmd { return exec.Command("go", appArgs...) } -const nREADME = `# {{name}} +const nREADME = `# {{name}} ## Documentation - + To view generated docs for {{name}}, run the below command and point your brower to http://127.0.0.1:6060/pkg/ - + godoc -http=:6060 2>/dev/null & - -### Buffalo - + +### Buffalo + http://gobuffalo.io/docs/getting-started - + ### Pop/Soda - + http://gobuffalo.io/docs/db - + ## Database Configuration - + development: dialect: postgres database: {{name}}_development @@ -74,14 +72,14 @@ http://gobuffalo.io/docs/db password: host: 127.0.0.1 pool: 5 - + test: dialect: postgres database: {{name}}_test user: password: host: 127.0.0.1 - + production: dialect: postgres database: {{name}}_production @@ -89,23 +87,22 @@ http://gobuffalo.io/docs/db password: host: 127.0.0.1 pool: 25 - + ### Running Migrations - + buffalo soda migrate - + ## Run Tests - + buffalo test - + ## Run in dev - + buffalo dev - + [Powered by Buffalo](http://gobuffalo.io) ` - const nMain = `package main import ( @@ -176,15 +173,16 @@ import ( ) var r *render.Engine -var resolver = &resolvers.RiceBox{ - Box: rice.MustFindBox("../templates"), -} func init() { r = render.New(render.Options{ HTMLLayout: "application.html", CacheTemplates: ENV == "production", - FileResolver: resolver, + FileResolverFunc: func() resolvers.FileResolver { + return &resolvers.RiceBox{ + Box: rice.MustFindBox("../templates"), + } + }, }) } @@ -287,6 +285,8 @@ const nGitignore = `vendor/ bin/ node_modules/ .sass-cache/ +rice-box.go +public/assets/ {{ name }} ` @@ -340,13 +340,6 @@ enable_colors: true log_name: buffalo ` -const nProcfile = `web: {{name}}` -const nProcfileDev = `web: buffalo dev -{{#if withWebpack}} -assets: webpack --watch -{{/if}} -` - const nCodeClimate = `engines: golint: enabled: true diff --git a/buffalo/cmd/build.go b/buffalo/cmd/build.go new file mode 100644 index 000000000..339a636b8 --- /dev/null +++ b/buffalo/cmd/build.go @@ -0,0 +1,441 @@ +// Copyright © 2016 Mark Bates +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package cmd + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "time" + + "github.com/gobuffalo/velvet" + "github.com/spf13/cobra" +) + +var outputBinName string +var zipBin bool + +type builder struct { + cleanup []string + original_main []byte + workDir string +} + +func (b *builder) clean(name ...string) string { + path := filepath.Join(name...) + b.cleanup = append(b.cleanup, path) + return path +} + +func (b *builder) exec(name string, args ...string) error { + cmd := exec.Command(name, args...) + fmt.Printf("--> running %s\n", strings.Join(cmd.Args, " ")) + cmd.Stdin = os.Stdin + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + return cmd.Run() +} + +func (b *builder) execQuiet(name string, args ...string) error { + cmd := exec.Command(name, args...) + return cmd.Run() +} + +func (b *builder) buildWebpack() error { + _, err := os.Stat("webpack.config.js") + if err == nil { + // build webpack + return b.exec("webpack") + } + return nil +} + +func (b *builder) buildAPack() error { + err := os.MkdirAll(b.clean("a"), 0766) + if err != nil { + return err + } + err = b.buildAInit() + if err != nil { + return err + } + err = b.buildDatabase() + if err != nil { + return err + } + return nil +} + +func (b *builder) buildAInit() error { + a, err := os.Create(b.clean("a", "a.go")) + if err != nil { + return err + } + a.WriteString(aGo) + return nil +} + +func (b *builder) buildDatabase() error { + bb := &bytes.Buffer{} + dgo, err := os.Create(b.clean("a", "database.go")) + if err != nil { + return err + } + _, err = os.Stat("database.yml") + if err == nil { + // copy the database.yml file to the migrations folder so it's available through rice + d, err := os.Open("database.yml") + if err != nil { + return err + } + _, err = io.Copy(bb, d) + if err != nil { + return err + } + } + dgo.WriteString("package a\n") + dgo.WriteString(fmt.Sprintf("var DB_CONFIG = `%s`", bb.String())) + return nil +} + +func (b *builder) buildRiceZip() error { + defer os.Chdir(b.workDir) + _, err := exec.LookPath("rice") + if err == nil { + paths := map[string]bool{} + // if rice exists, try and build some cleanup: + err = filepath.Walk(b.workDir, func(path string, info os.FileInfo, err error) error { + if info.IsDir() { + base := filepath.Base(path) + if base == "node_modules" || base == ".git" || base == "bin" { + return filepath.SkipDir + } + } else { + err = os.Chdir(filepath.Dir(path)) + if err != nil { + return err + } + + s, err := ioutil.ReadFile(path) + if err != nil { + return err + } + rx := regexp.MustCompile("(rice.FindBox|rice.MustFindBox)") + if rx.Match(s) { + gopath := filepath.Join(os.Getenv("GOPATH"), "src") + pkg := filepath.Dir(strings.Replace(path, gopath+"/", "", -1)) + paths[pkg] = true + } + } + return nil + }) + if err != nil { + return err + } + if len(paths) != 0 { + args := []string{"append", "--exec", filepath.Join(b.workDir, outputBinName)} + for k := range paths { + args = append(args, "-i", k) + } + return b.exec("rice", args...) + } + // rice append --exec example + } + return nil +} +func (b *builder) buildRiceEmbedded() error { + defer os.Chdir(b.workDir) + _, err := exec.LookPath("rice") + if err == nil { + // if rice exists, try and build some cleanup: + err = filepath.Walk(b.workDir, func(path string, info os.FileInfo, err error) error { + if info.IsDir() { + base := filepath.Base(path) + if base == "node_modules" || base == ".git" { + return filepath.SkipDir + } + err = os.Chdir(path) + if err != nil { + return err + } + err = b.execQuiet("rice", "embed-go") + if err == nil { + bp := filepath.Join(path, "rice-box.go") + _, err := os.Stat(bp) + if err == nil { + fmt.Printf("--> built rice box %s\n", bp) + b.clean(bp) + } + } + } + return nil + }) + if err != nil { + return err + } + // rice append --exec example + } + return nil +} + +func (b *builder) buildMain() error { + new_main := strings.Replace(string(b.original_main), "func main()", "func original_main()", 1) + maingo, err := os.Create("main.go") + if err != nil { + return err + } + _, err = maingo.WriteString(new_main) + if err != nil { + return err + } + + root, err := rootPath("") + if err != nil { + return err + } + ctx := velvet.NewContext() + ctx.Set("root", root) + ctx.Set("modelsPack", filepath.Join(packagePath(root), "models")) + ctx.Set("aPack", filepath.Join(packagePath(root), "a")) + ctx.Set("name", filepath.Base(root)) + s, err := velvet.Render(buildMainTmpl, ctx) + if err != nil { + return err + } + f, err := os.Create(b.clean("buffalo_build_main.go")) + if err != nil { + return err + } + f.WriteString(s) + + return nil +} + +func (b *builder) cleanupBuild() { + fmt.Println("--> cleaning up build") + for _, b := range b.cleanup { + fmt.Printf("--> cleaning up %s\n", b) + os.RemoveAll(b) + } + maingo, _ := os.Create("main.go") + maingo.Write(b.original_main) +} + +func (b *builder) run() error { + err := b.buildMain() + if err != nil { + return err + } + + err = b.buildWebpack() + if err != nil { + return err + } + + err = b.buildAPack() + if err != nil { + return err + } + + err = b.buildMain() + if err != nil { + return err + } + + if zipBin { + err = b.buildBin() + if err != nil { + return err + } + return b.buildRiceZip() + } + + err = b.buildRiceEmbedded() + if err != nil { + return err + } + return b.buildBin() +} + +func (b *builder) buildBin() error { + buildArgs := []string{"build", "-v", "-o", outputBinName} + _, err := exec.LookPath("git") + buildTime := fmt.Sprintf("\"%s\"", time.Now().Format(time.RFC3339)) + version := buildTime + if err == nil { + cmd := exec.Command("git", "rev-parse", "--short", "HEAD") + out := &bytes.Buffer{} + cmd.Stdout = out + err = cmd.Run() + if err == nil && out.String() != "" { + version = strings.TrimSpace(out.String()) + } + } + buildArgs = append(buildArgs, "-ldflags", fmt.Sprintf("-X main.version=%s -X main.buildTime=%s", version, buildTime)) + + return b.exec("go", buildArgs...) +} + +// buildCmd represents the build command +var buildCmd = &cobra.Command{ + Use: "build", + Aliases: []string{"b", "bill"}, + Short: "Builds a Buffalo binary, including bundling of assets (go.rice & webpack)", + RunE: func(cc *cobra.Command, args []string) error { + original_main := &bytes.Buffer{} + maingo, err := os.Open("main.go") + _, err = original_main.ReadFrom(maingo) + if err != nil { + return err + } + maingo.Close() + pwd, _ := os.Getwd() + b := builder{ + cleanup: []string{}, + original_main: original_main.Bytes(), + workDir: pwd, + } + defer b.cleanupBuild() + + return b.run() + }, +} + +func init() { + RootCmd.AddCommand(buildCmd) + pwd, _ := os.Getwd() + buildCmd.Flags().StringVarP(&outputBinName, "output", "o", filepath.Join("bin", filepath.Base(pwd)), "set the name of the binary") + buildCmd.Flags().BoolVarP(&zipBin, "zip", "z", false, "zips the assets to the binary, this requires zip installed") +} + +var buildMainTmpl = `package main + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + + rice "github.com/GeertJohan/go.rice" + _ "{{aPack}}" + "{{modelsPack}}" +) + +var version = "unknown" +var buildTime = "unknown" +var migrationBox *rice.Box + +func main() { + args := os.Args + if len(args) == 1 { + original_main() + } + c := args[1] + switch c { + case "migrate": + migrate() + case "start", "run", "serve": + printVersion() + original_main() + case "version": + printVersion() + default: + log.Fatalf("Could not find a command named: %s", c) + } +} + +func printVersion() { + fmt.Printf("{{name}} version %s (%s)\n\n", version, buildTime) +} + +func migrate() { + var err error + migrationBox, err = rice.FindBox("./migrations") + if err != nil { + fmt.Println("--> No migrations found.") + return + } + fmt.Println("--> Running migrations") + path, err := unpackMigrations() + if err != nil { + log.Fatalf("Failed to unpack migrations: %s", err) + } + defer os.RemoveAll(path) + + models.DB.MigrateUp(path) +} + +func unpackMigrations() (string, error) { + dir, err := ioutil.TempDir("", "{{name}}-migrations") + if err != nil { + log.Fatalf("Unable to create temp directory: %s", err) + } + + migrationBox.Walk("", func(path string, fi os.FileInfo, e error) error { + if !fi.IsDir() { + content := migrationBox.MustBytes(path) + file := filepath.Join(dir, path) + if err := ioutil.WriteFile(file, content, 0666); err != nil { + log.Fatalf("Failed to write migration to disk: %s", err) + } + } + return e + }) + + return dir, nil +}` + +var aGo = `package a + +import ( + "log" + "os" +) + +func init() { + dropDatabaseYml() +} + +func dropDatabaseYml() { + if DB_CONFIG != "" { + + _, err := os.Stat("database.yml") + if err == nil { + // yaml already exists, don't do anything + return + } + f, err := os.Create("database.yml") + if err != nil { + log.Fatal(err) + } + _, err = f.WriteString(DB_CONFIG) + if err != nil { + log.Fatal(err) + } + } +}` diff --git a/buffalo/cmd/dev.go b/buffalo/cmd/dev.go index 050deb7fd..c4f33c44e 100644 --- a/buffalo/cmd/dev.go +++ b/buffalo/cmd/dev.go @@ -15,10 +15,13 @@ package cmd import ( + "context" "html/template" + "log" "os" + "os/exec" - "github.com/markbates/refresh/cmd" + "github.com/markbates/refresh/refresh" "github.com/spf13/cobra" ) @@ -29,28 +32,68 @@ var devCmd = &cobra.Command{ Long: `Runs your Buffalo app in 'development' mode. This includes rebuilding your application when files change. This behavior can be changed in your .buffalo.dev.yml file.`, - RunE: func(c *cobra.Command, args []string) error { + Run: func(c *cobra.Command, args []string) { os.Setenv("GO_ENV", "development") - cfgFile := "./.buffalo.dev.yml" - _, err := os.Stat(cfgFile) - if err != nil { - f, err := os.Create(cfgFile) + ctx := context.Background() + ctx, cancelFunc := context.WithCancel(ctx) + go func() { + err := startDevServer(ctx) if err != nil { - return err + cancelFunc() + log.Fatal(err) } - t, err := template.New("").Parse(nRefresh) - err = t.Execute(f, map[string]interface{}{ - "name": "buffalo", - }) + }() + go func() { + err := startWebpack(ctx) if err != nil { - return err + cancelFunc() + log.Fatal(err) } - } - cmd.Run(cfgFile) - return nil + }() + // wait for the ctx to finish + <-ctx.Done() }, } +func startWebpack(ctx context.Context) error { + cfgFile := "./webpack.config.js" + _, err := os.Stat(cfgFile) + if err != nil { + // there's no webpack, so don't do anything + return nil + } + cmd := exec.Command("webpack", "--watch") + cmd.Stdin = os.Stdin + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + return cmd.Run() +} + +func startDevServer(ctx context.Context) error { + cfgFile := "./.buffalo.dev.yml" + _, err := os.Stat(cfgFile) + if err != nil { + f, err := os.Create(cfgFile) + if err != nil { + return err + } + t, err := template.New("").Parse(nRefresh) + err = t.Execute(f, map[string]interface{}{ + "name": "buffalo", + }) + if err != nil { + return err + } + } + c := &refresh.Configuration{} + err = c.Load(cfgFile) + if err != nil { + return err + } + r := refresh.New(c) + return r.Start() +} + func init() { RootCmd.AddCommand(devCmd) } diff --git a/buffalo/cmd/generate.go b/buffalo/cmd/generate.go index 261465b1c..9a06c2e53 100644 --- a/buffalo/cmd/generate.go +++ b/buffalo/cmd/generate.go @@ -37,4 +37,3 @@ func init() { generateCmd.AddCommand(generate.WebpackCmd) RootCmd.AddCommand(generateCmd) } - diff --git a/buffalo/cmd/generate/resource.go b/buffalo/cmd/generate/resource.go index baa4d902c..cfe7c90bc 100644 --- a/buffalo/cmd/generate/resource.go +++ b/buffalo/cmd/generate/resource.go @@ -70,7 +70,9 @@ type {{camel}}Resource struct{ } func init() { - App().Resource("/{{under}}", &{{camel}}Resource{&buffalo.BaseResource{}}) + var resource buffalo.Resource + resource = &{{camel}}Resource{&buffalo.BaseResource{}} + App().Resource("/{{under}}", resource) } {{#each actions}} diff --git a/buffalo/cmd/version.go b/buffalo/cmd/version.go index b20793d44..2134fea62 100644 --- a/buffalo/cmd/version.go +++ b/buffalo/cmd/version.go @@ -1,4 +1,4 @@ package cmd // Version is the current version of the buffalo binary -var Version = "0.6.0" +var Version = "0.7.0" diff --git a/default_context.go b/default_context.go index 25def6f6f..4bab15293 100644 --- a/default_context.go +++ b/default_context.go @@ -1,18 +1,20 @@ package buffalo import ( + "bytes" "encoding/json" "encoding/xml" "fmt" + "io" "net/http" "net/url" "strconv" "strings" "time" + "github.com/gobuffalo/buffalo/render" "github.com/gorilla/schema" "github.com/gorilla/websocket" - "github.com/gobuffalo/buffalo/render" "github.com/pkg/errors" ) @@ -25,6 +27,7 @@ type DefaultContext struct { logger Logger session *Session contentType string + notFound http.Handler data map[string]interface{} } @@ -92,15 +95,21 @@ func (d *DefaultContext) Render(status int, rr render.Renderer) error { d.LogField("render", time.Now().Sub(now)) }() if rr != nil { - d.Response().Header().Set("Content-Type", rr.ContentType()) - d.Response().WriteHeader(status) data := d.data pp := map[string]string{} for k, v := range d.params { pp[k] = v[0] } data["params"] = pp - return rr.Render(d.Response(), data) + bb := &bytes.Buffer{} + err := rr.Render(bb, data) + if err != nil { + return err + } + d.Response().Header().Set("Content-Type", rr.ContentType()) + d.Response().WriteHeader(status) + _, err = io.Copy(d.Response(), bb) + return err } d.Response().WriteHeader(status) return nil @@ -144,6 +153,12 @@ func (d *DefaultContext) LogFields(values map[string]interface{}) { } func (d *DefaultContext) Error(status int, err error) error { + if status == 404 { + req := d.Request() + req.URL.Query().Set("error", err.Error()) + d.notFound.ServeHTTP(d.Response(), req) + return nil + } err = errors.WithStack(err) d.Logger().Error(err) msg := fmt.Sprintf("%+v", err) @@ -158,7 +173,7 @@ func (d *DefaultContext) Error(status int, err error) error { }) case "application/xml", "text/xml", "xml": default: - _, err = d.Response().Write([]byte(msg)) + _, err = d.Response().Write([]byte(fmt.Sprintf("
%+v
", msg))) } return err } diff --git a/examples/hello-world/actions/render.go b/examples/hello-world/actions/render.go index 8d47cc95b..7f259a667 100644 --- a/examples/hello-world/actions/render.go +++ b/examples/hello-world/actions/render.go @@ -9,15 +9,16 @@ import ( ) var r *render.Engine -var resolver = &resolvers.RiceBox{ - Box: rice.MustFindBox("../templates"), -} func init() { r = render.New(render.Options{ HTMLLayout: "application.html", CacheTemplates: ENV == "production", - FileResolver: resolver, + FileResolverFunc: func() resolvers.FileResolver { + return &resolvers.RiceBox{ + Box: rice.MustFindBox("../templates"), + } + }, }) } diff --git a/examples/html-crud/actions/render.go b/examples/html-crud/actions/render.go index 8d47cc95b..7f259a667 100644 --- a/examples/html-crud/actions/render.go +++ b/examples/html-crud/actions/render.go @@ -9,15 +9,16 @@ import ( ) var r *render.Engine -var resolver = &resolvers.RiceBox{ - Box: rice.MustFindBox("../templates"), -} func init() { r = render.New(render.Options{ HTMLLayout: "application.html", CacheTemplates: ENV == "production", - FileResolver: resolver, + FileResolverFunc: func() resolvers.FileResolver { + return &resolvers.RiceBox{ + Box: rice.MustFindBox("../templates"), + } + }, }) } diff --git a/examples/html-crud/templates/_errors.html b/examples/html-crud/templates/_errors.html index dd2b2723c..9d810de13 100644 --- a/examples/html-crud/templates/_errors.html +++ b/examples/html-crud/templates/_errors.html @@ -1,7 +1,7 @@ -{{# if verrs }} +{{#if verrs }}
    - {{#each verrs as |v k|}} + {{#each verrs as |k v|}}
  • {{ v }}
  • {{/each}}
diff --git a/examples/html-resource/actions/render.go b/examples/html-resource/actions/render.go index 8d47cc95b..7f259a667 100644 --- a/examples/html-resource/actions/render.go +++ b/examples/html-resource/actions/render.go @@ -9,15 +9,16 @@ import ( ) var r *render.Engine -var resolver = &resolvers.RiceBox{ - Box: rice.MustFindBox("../templates"), -} func init() { r = render.New(render.Options{ HTMLLayout: "application.html", CacheTemplates: ENV == "production", - FileResolver: resolver, + FileResolverFunc: func() resolvers.FileResolver { + return &resolvers.RiceBox{ + Box: rice.MustFindBox("../templates"), + } + }, }) } diff --git a/examples/html-resource/templates/_errors.html b/examples/html-resource/templates/_errors.html index dd2b2723c..56b44b268 100644 --- a/examples/html-resource/templates/_errors.html +++ b/examples/html-resource/templates/_errors.html @@ -1,7 +1,7 @@ {{# if verrs }}
    - {{#each verrs as |v k|}} + {{#each verrs as |k v|}}
  • {{ v }}
  • {{/each}}
diff --git a/examples/websockets/actions/render.go b/examples/websockets/actions/render.go index 8d47cc95b..7f259a667 100644 --- a/examples/websockets/actions/render.go +++ b/examples/websockets/actions/render.go @@ -9,15 +9,16 @@ import ( ) var r *render.Engine -var resolver = &resolvers.RiceBox{ - Box: rice.MustFindBox("../templates"), -} func init() { r = render.New(render.Options{ HTMLLayout: "application.html", CacheTemplates: ENV == "production", - FileResolver: resolver, + FileResolverFunc: func() resolvers.FileResolver { + return &resolvers.RiceBox{ + Box: rice.MustFindBox("../templates"), + } + }, }) } diff --git a/handler.go b/handler.go index f671f8d9e..baa62995e 100644 --- a/handler.go +++ b/handler.go @@ -42,6 +42,7 @@ func (a *App) handlerToHandler(h Handler) http.Handler { params: params, logger: a.Logger, session: a.getSession(req, ws), + notFound: a.notFound(), data: map[string]interface{}{ "routes": a.Routes(), }, diff --git a/middleware.go b/middleware.go index e36cb3602..0bdaa3c36 100644 --- a/middleware.go +++ b/middleware.go @@ -34,6 +34,17 @@ type MiddlewareStack struct { skips map[string]bool } +func (ms *MiddlewareStack) clone() *MiddlewareStack { + n := newMiddlewareStack() + for _, s := range ms.stack { + n.stack = append(n.stack, s) + } + for k, v := range ms.skips { + n.skips[k] = v + } + return n +} + // Clear wipes out the current middleware stack for the App/Group, // any middleware previously defined will be removed leaving an empty // middleware stack. @@ -57,9 +68,27 @@ func (ms *MiddlewareStack) Use(mw ...MiddlewareFunc) { /* a.Middleware.Skip(Authorization, HomeHandler, LoginHandler, RegistrationHandler) */ +// NOTE: When skipping Resource handlers, you need to first declare your +// resource handler as a type of buffalo.Resource for the Skip function to +// properly recognize and match it. +/* + // Works: + var ur Resource + cr = &carsResource{&buffaloBaseResource{}} + g = a.Resource("/cars", cr) + g.Use(SomeMiddleware) + g.Middleware.Skip(SomeMiddleware, cr.Show) + + // Doesn't Work: + cr := &carsResource{&buffaloBaseResource{}} + g = a.Resource("/cars", cr) + g.Use(SomeMiddleware) + g.Middleware.Skip(SomeMiddleware, cr.Show) +*/ func (ms *MiddlewareStack) Skip(mw MiddlewareFunc, handlers ...Handler) { for _, h := range handlers { - ms.skips[funcKey(mw, h)] = true + key := funcKey(mw, h) + ms.skips[key] = true } } @@ -89,7 +118,8 @@ func (ms *MiddlewareStack) handler(h Handler) Handler { sl := len(ms.stack) - 1 for i := sl; i >= 0; i-- { mw := ms.stack[i] - if !ms.skips[funcKey(mw, h)] { + key := funcKey(mw, h) + if !ms.skips[key] { tstack = append(tstack, mw) } } @@ -112,8 +142,20 @@ func newMiddlewareStack(mws ...MiddlewareFunc) *MiddlewareStack { func funcKey(funcs ...interface{}) string { names := []string{} for _, f := range funcs { - n := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name() + rv := reflect.ValueOf(f) + ptr := rv.Pointer() + if n, ok := keyMap[rv.Pointer()]; ok { + names = append(names, n) + // fmt.Printf("### found %+v -> %+v\n", ptr, n) + continue + } + fnc := runtime.FuncForPC(ptr) + n := fnc.Name() + // fmt.Printf("### not found %+v -> %+v\n", ptr, n) + keyMap[ptr] = n names = append(names, n) } return strings.Join(names, "/") } + +var keyMap = map[uintptr]string{} diff --git a/middleware_test.go b/middleware_test.go index 9cfc8edb0..487889eea 100644 --- a/middleware_test.go +++ b/middleware_test.go @@ -3,6 +3,7 @@ package buffalo import ( "testing" + "github.com/gobuffalo/buffalo/render" "github.com/markbates/willie" "github.com/stretchr/testify/require" ) @@ -120,6 +121,69 @@ func Test_Middleware_Skip(t *testing.T) { r.Equal([]string{"mw1 start", "mw2 start", "h1", "mw2 end", "mw1 end"}, log) } +type carsResource struct { + Resource +} + +func (ur *carsResource) Show(c Context) error { + return c.Render(200, render.String("show")) +} + +func (ur *carsResource) List(c Context) error { + return c.Render(200, render.String("list")) +} + +// Test_Middleware_Skip tests that middleware gets skipped +func Test_Middleware_Skip_Resource(t *testing.T) { + r := require.New(t) + + log := []string{} + mw1 := func(h Handler) Handler { + return func(c Context) error { + log = append(log, "mw1 start") + err := h(c) + log = append(log, "mw1 end") + return err + } + } + + a := New(Options{}) + var cr Resource + cr = &carsResource{&BaseResource{}} + g := a.Resource("/autos", cr) + g.Use(mw1) + + var ur Resource + ur = &carsResource{&BaseResource{}} + g = a.Resource("/cars", ur) + g.Use(mw1) + + // fmt.Println("set up skip") + g.Middleware.Skip(mw1, ur.Show) + + w := willie.New(a) + + // fmt.Println("make autos call") + log = []string{} + res := w.Request("/autos/1").Get() + r.Len(log, 2) + r.Equal("show", res.Body.String()) + + // fmt.Println("make list call") + log = []string{} + res = w.Request("/cars").Get() + r.Len(log, 2) + r.Equal([]string{"mw1 start", "mw1 end"}, log) + r.Equal("list", res.Body.String()) + + // fmt.Println("make show call") + log = []string{} + res = w.Request("/cars/1").Get() + r.Len(log, 0) + r.Equal("show", res.Body.String()) + +} + // Test_Middleware_Clear confirms that middle gets cleared func Test_Middleware_Clear(t *testing.T) { r := require.New(t) diff --git a/not_found.go b/not_found.go index 34591e499..fada8de22 100644 --- a/not_found.go +++ b/not_found.go @@ -17,6 +17,7 @@ func (a *App) notFound() http.Handler { "routes": routes, "method": req.Method, "path": req.URL.String(), + "error": req.URL.Query().Get("error"), } switch req.Header.Get("Content-Type") { case "application/json": @@ -88,6 +89,11 @@ var htmlNotFound = ` {{end}} +{{if .error}} +
+

Error

+
{{.error}}
+{{end}} ` diff --git a/render/helpers/helpers.go b/render/helpers/helpers.go deleted file mode 100644 index af899ed80..000000000 --- a/render/helpers/helpers.go +++ /dev/null @@ -1,76 +0,0 @@ -package helpers - -import ( - "encoding/json" - "fmt" - "html/template" - "strings" - - "github.com/aymerick/raymond" - "github.com/markbates/inflect" - "github.com/shurcooL/github_flavored_markdown" -) - -// Helpers that are automatically injected into templates. -var Helpers = map[string]interface{}{ - "js_escape": template.JSEscapeString, - "html_escape": template.HTMLEscapeString, - "json": ToJSON, - "content_for": ContentFor, - "content_of": ContentOf, - "upcase": strings.ToUpper, - "downcase": strings.ToLower, - "markdown": Markdown, - "debug": Debug, -} - -func init() { - for k, v := range inflect.Helpers { - Helpers[k] = v - } -} - -// ToJSON converts an interface into a string. -func ToJSON(v interface{}) string { - b, err := json.Marshal(v) - if err != nil { - return err.Error() - } - return string(b) -} - -// ContentFor stores a block of templating code to be re-used later in the template. -/* - {{#content_for "buttons"}} - - {{/content_for}} -*/ -func ContentFor(name string, options *raymond.Options) string { - ctx := options.Ctx().(map[string]interface{}) - body := options.Fn() - ctx[name] = raymond.SafeString(body) - return "" -} - -// ContentOf retrieves a stored block for templating and renders it. -/* - {{content_of "buttons"}} -*/ -func ContentOf(name string, options *raymond.Options) raymond.SafeString { - ctx := options.Ctx().(map[string]interface{}) - if s, ok := ctx[name]; ok { - return s.(raymond.SafeString) - } - return raymond.SafeString("") -} - -// Markdown converts the string into HTML using GitHub flavored markdown. -func Markdown(body string) raymond.SafeString { - b := github_flavored_markdown.Markdown([]byte(body)) - return raymond.SafeString(string(b)) -} - -// Debug by verbosely printing out using 'pre' tags. -func Debug(v interface{}) raymond.SafeString { - return raymond.SafeString(fmt.Sprintf("
%+v
", v)) -} diff --git a/render/helpers/helpers_test.go b/render/helpers/helpers_test.go deleted file mode 100644 index ac54fc18f..000000000 --- a/render/helpers/helpers_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package helpers - -import ( - "testing" - - "github.com/aymerick/raymond" - "github.com/stretchr/testify/require" -) - -func Test_ToJSON(t *testing.T) { - r := require.New(t) - s := ToJSON([]string{"mark", "bates"}) - r.Equal(`["mark","bates"]`, s) -} - -func Test_ContentForOf(t *testing.T) { - r := require.New(t) - html := ` - {{#content_for "buttons"}}{{/content_for}} - {{content_of "buttons"}} - {{content_of "buttons"}} - ` - tmpl := raymond.MustParse(html) - tmpl.RegisterHelpers(Helpers) - - body := tmpl.MustExec(map[string]interface{}{}) - r.Contains(body, "") - r.Contains(body, "") -} diff --git a/render/options.go b/render/options.go index 41235e0b8..2c7821f32 100644 --- a/render/options.go +++ b/render/options.go @@ -8,11 +8,24 @@ type Options struct { HTMLLayout string // TemplatesPath is the location of the templates directory on disk. TemplatesPath string - // FileResolver will attempt to file a file and return it's bytes, if possible - FileResolver resolvers.FileResolver + // FileResolverFunc will attempt to file a file and return it's bytes, if possible + FileResolverFunc func() resolvers.FileResolver + fileResolver resolvers.FileResolver // Helpers to be rendered with the templates Helpers map[string]interface{} // CacheTemplates reduced overheads, but won't reload changed templates. // This should only be set to true in production environments. CacheTemplates bool } + +// Resolver calls the FileResolverFunc and returns the resolver. The resolver +// is cached, so the function can be called multiple times without penalty. +// This is necessary because certain resolvers, like the RiceBox one, require +// a fully initialized state to work properly and can not be run directly from +// init functions. +func (o *Options) Resolver() resolvers.FileResolver { + if o.fileResolver == nil { + o.fileResolver = o.FileResolverFunc() + } + return o.fileResolver +} diff --git a/render/render.go b/render/render.go index 6f2aaca81..5c8878b24 100644 --- a/render/render.go +++ b/render/render.go @@ -3,9 +3,8 @@ package render import ( "sync" - "github.com/aymerick/raymond" - "github.com/gobuffalo/buffalo/render/helpers" "github.com/gobuffalo/buffalo/render/resolvers" + "github.com/gobuffalo/velvet" ) // Engine used to power all defined renderers. @@ -14,7 +13,7 @@ import ( // the defaults. type Engine struct { Options - templateCache map[string]*raymond.Template + templateCache map[string]*velvet.Template moot *sync.Mutex } @@ -27,43 +26,16 @@ func New(opts Options) *Engine { if opts.Helpers == nil { opts.Helpers = map[string]interface{}{} } - h := opts.Helpers - if opts.FileResolver == nil { - opts.FileResolver = &resolvers.SimpleResolver{} + if opts.FileResolverFunc == nil { + opts.FileResolverFunc = func() resolvers.FileResolver { + return &resolvers.SimpleResolver{} + } } e := &Engine{ Options: opts, - templateCache: map[string]*raymond.Template{}, + templateCache: map[string]*velvet.Template{}, moot: &sync.Mutex{}, } - e.RegisterHelpers(helpers.Helpers) - e.RegisterHelpers(h) return e } - -// RegisterHelper adds a helper to a template with the given name. -// See github.com/aymerick/raymond for more details on helpers. -/* - e.RegisterHelper("upcase", strings.ToUpper) -*/ -func (e *Engine) RegisterHelper(name string, helper interface{}) { - e.moot.Lock() - defer e.moot.Unlock() - e.Helpers[name] = helper -} - -// RegisterHelpers adds helpers to a template with the given name. -// See github.com/aymerick/raymond for more details on helpers. -/* - h := map[string]interface{}{ - "upcase": strings.ToUpper, - "downcase": strings.ToLower, - } - e.RegisterHelpers(h) -*/ -func (e *Engine) RegisterHelpers(helpers map[string]interface{}) { - for k, v := range helpers { - e.RegisterHelper(k, v) - } -} diff --git a/render/renderer.go b/render/renderer.go index dfc2b08d7..05f6e7c3b 100644 --- a/render/renderer.go +++ b/render/renderer.go @@ -1,6 +1,10 @@ package render -import "io" +import ( + "io" + + "github.com/gobuffalo/velvet" +) // Renderer interface that must be satisified to be used with // buffalo.Context.Render @@ -12,3 +16,8 @@ type Renderer interface { // Data type to be provided to the Render function on the // Renderer interface. type Data map[string]interface{} + +// ToVelvet converts the render data into a velvet.Context +func (d Data) ToVelvet() *velvet.Context { + return velvet.NewContextWith(d) +} diff --git a/render/resolvers/gopath.go b/render/resolvers/gopath.go index cab877385..9a1aedfa0 100644 --- a/render/resolvers/gopath.go +++ b/render/resolvers/gopath.go @@ -1,6 +1,7 @@ package resolvers import ( + "fmt" "os" "path/filepath" ) @@ -19,11 +20,13 @@ type GoPathResolver struct { // very very slow the first you try to find a file, and there is no // guarantees of finding the right now. func (g *GoPathResolver) Read(name string) ([]byte, error) { + fmt.Printf("### name -> %+v\n", name) if g.RecursiveResolver == nil { g.RecursiveResolver = &RecursiveResolver{ Path: filepath.Join(os.Getenv("GOPATH"), "src", g.Path), } } + fmt.Printf("### g.RecursiveResolver -> %+v\n", g.RecursiveResolver) return g.RecursiveResolver.Read(name) } @@ -32,6 +35,7 @@ func (g *GoPathResolver) Read(name string) ([]byte, error) { // very very slow the first you try to find a file, and there is no // guarantees of finding the right now. func (g *GoPathResolver) Resolve(name string) (string, error) { + fmt.Printf("### name -> %+v\n", name) if g.RecursiveResolver == nil { g.RecursiveResolver = &RecursiveResolver{ Path: filepath.Join(os.Getenv("GOPATH"), "src", g.Path), diff --git a/render/resolvers/recursive.go b/render/resolvers/recursive.go index 6d09fd3ce..2601fb334 100644 --- a/render/resolvers/recursive.go +++ b/render/resolvers/recursive.go @@ -33,6 +33,8 @@ func (r *RecursiveResolver) Resolve(name string) (string, error) { var p string var err error var found bool + fmt.Printf("### r.Path -> %+v\n", r.Path) + fmt.Printf("### name -> %+v\n", name) err = filepath.Walk(r.Path, func(path string, info os.FileInfo, err error) error { if strings.HasSuffix(path, name) { found = true diff --git a/render/resolvers/rice_box.go b/render/resolvers/rice_box.go index c34c0004d..e4feed274 100644 --- a/render/resolvers/rice_box.go +++ b/render/resolvers/rice_box.go @@ -4,10 +4,13 @@ import ( "fmt" "os" "strings" + "sync" rice "github.com/GeertJohan/go.rice" ) +var moot = &sync.Mutex{} + // RiceBox uses the go.rice package to resolve files type RiceBox struct { Box *rice.Box @@ -21,9 +24,8 @@ func (r *RiceBox) Read(name string) ([]byte, error) { // Resolve the file from the rice.Box func (r *RiceBox) Resolve(name string) (string, error) { var p string - var err error var found bool - err = r.Box.Walk(".", func(path string, info os.FileInfo, err error) error { + err := r.Box.Walk(".", func(path string, info os.FileInfo, err error) error { if strings.HasSuffix(path, name) { found = true p = path diff --git a/render/string.go b/render/string.go index 519e11bcb..612c06a1f 100644 --- a/render/string.go +++ b/render/string.go @@ -3,7 +3,7 @@ package render import ( "io" - "github.com/aymerick/raymond" + "github.com/gobuffalo/velvet" ) type stringRenderer struct { @@ -16,12 +16,11 @@ func (s stringRenderer) ContentType() string { } func (s stringRenderer) Render(w io.Writer, data Data) error { - t, err := raymond.Parse(s.body) + t, err := velvet.Parse(s.body) if err != nil { return err } - t.RegisterHelpers(s.Helpers) - b, err := t.Exec(data) + b, err := t.Exec(data.ToVelvet()) if err != nil { return err } diff --git a/render/template.go b/render/template.go index d806f558c..53d36077e 100644 --- a/render/template.go +++ b/render/template.go @@ -3,11 +3,12 @@ package render import ( "bytes" "fmt" + "html/template" "io" "path/filepath" "strings" - "github.com/aymerick/raymond" + "github.com/gobuffalo/velvet" "github.com/pkg/errors" "github.com/shurcooL/github_flavored_markdown" ) @@ -23,12 +24,13 @@ func (s templateRenderer) ContentType() string { } func (s *templateRenderer) Render(w io.Writer, data Data) error { - var yield raymond.SafeString + var yield template.HTML var err error for _, name := range s.names { - yield, err = s.execute(name, data) + yield, err = s.execute(name, data.ToVelvet()) if err != nil { - return errors.WithMessage(errors.WithStack(err), name) + err = errors.Errorf("error rendering %s:\n%+v", name, err) + return err } data["yield"] = yield } @@ -39,32 +41,31 @@ func (s *templateRenderer) Render(w io.Writer, data Data) error { return nil } -func (s *templateRenderer) execute(name string, data Data) (raymond.SafeString, error) { +func (s *templateRenderer) execute(name string, data *velvet.Context) (template.HTML, error) { source, err := s.source(name) if err != nil { - return raymond.SafeString(fmt.Sprintf("
%s: %s
", name, err.Error())), err + return "", err } - source.RegisterHelper("partial", func(name string, options *raymond.Options) raymond.SafeString { - d := data - for k, v := range options.Hash() { - d[k] = v - defer delete(data, k) - } - p, err := s.partial(name, d) + + err = source.Helpers.Add("partial", func(name string, help velvet.HelperContext) (template.HTML, error) { + p, err := s.partial(name, help.Context) if err != nil { - return raymond.SafeString(fmt.Sprintf("
%s: %s
", name, err.Error())) + return template.HTML(fmt.Sprintf("
%s: %s
", name, err.Error())), err } - return p + return p, nil }) + if err != nil { + return template.HTML(fmt.Sprintf("
%s: %s
", name, err.Error())), err + } yield, err := source.Exec(data) if err != nil { - return raymond.SafeString(fmt.Sprintf("
%s: %s
", name, err.Error())), err + return template.HTML(fmt.Sprintf("
%s: %s
", name, err.Error())), err } - return raymond.SafeString(yield), nil + return template.HTML(yield), nil } -func (s *templateRenderer) source(name string) (*raymond.Template, error) { - var t *raymond.Template +func (s *templateRenderer) source(name string) (*velvet.Template, error) { + var t *velvet.Template var ok bool var err error if s.CacheTemplates { @@ -72,7 +73,7 @@ func (s *templateRenderer) source(name string) (*raymond.Template, error) { return t.Clone(), nil } } - b, err := s.FileResolver.Read(filepath.Join(s.TemplatesPath, name)) + b, err := s.Resolver().Read(filepath.Join(s.TemplatesPath, name)) if err != nil { return nil, errors.WithStack(fmt.Errorf("could not find template: %s", name)) } @@ -82,18 +83,22 @@ func (s *templateRenderer) source(name string) (*raymond.Template, error) { b = bytes.Replace(b, []byte("""), []byte("\""), -1) } source := string(b) - t, err = raymond.Parse(source) + t, err = velvet.Parse(source) if err != nil { return t, errors.Errorf("Error parsing %s: %+v", name, errors.WithStack(err)) } - t.RegisterHelpers(s.Helpers) + + err = t.Helpers.AddMany(s.Helpers) + if err != nil { + return nil, err + } if s.CacheTemplates { s.templateCache[name] = t } return t.Clone(), err } -func (s *templateRenderer) partial(name string, data Data) (raymond.SafeString, error) { +func (s *templateRenderer) partial(name string, data *velvet.Context) (template.HTML, error) { d, f := filepath.Split(name) name = filepath.Join(d, "_"+f) return s.execute(name, data) diff --git a/request_logger.go b/request_logger.go index 61dbfc6cb..4329e8cc4 100644 --- a/request_logger.go +++ b/request_logger.go @@ -19,9 +19,15 @@ var RequestLogger = RequestLoggerFunc // code of the response. func RequestLoggerFunc(h Handler) Handler { return func(c Context) error { + var irid interface{} + if irid = c.Session().Get("requestor_id"); irid == nil { + irid = randx.String(10) + c.Session().Set("requestor_id", irid) + c.Session().Save() + } now := time.Now() c.LogFields(logrus.Fields{ - "request_id": randx.String(10), + "request_id": irid.(string) + "-" + randx.String(10), "method": c.Request().Method, "path": c.Request().URL.String(), }) diff --git a/resource.go b/resource.go index 708a55654..1653555f5 100644 --- a/resource.go +++ b/resource.go @@ -5,6 +5,23 @@ import "errors" // Resource interface allows for the easy mapping // of common RESTful actions to a set of paths. See // the a.Resource documentation for more details. +// NOTE: When skipping Resource handlers, you need to first declare your +// resource handler as a type of buffalo.Resource for the Skip function to +// properly recognize and match it. +/* + // Works: + var ur Resource + cr = &carsResource{&buffaloBaseResource{}} + g = a.Resource("/cars", cr) + g.Use(SomeMiddleware) + g.Middleware.Skip(SomeMiddleware, cr.Show) + + // Doesn't Work: + cr := &carsResource{&buffaloBaseResource{}} + g = a.Resource("/cars", cr) + g.Use(SomeMiddleware) + g.Middleware.Skip(SomeMiddleware, cr.Show) +*/ type Resource interface { List(Context) error Show(Context) error diff --git a/router.go b/router.go index afdd77ea2..3353d116b 100644 --- a/router.go +++ b/router.go @@ -114,9 +114,7 @@ func (a *App) Group(path string) *App { g := New(a.Options) g.prefix = filepath.Join(a.prefix, path) g.router = a.router - g.Middleware = newMiddlewareStack() - g.Middleware.skips = a.Middleware.skips - g.Middleware.stack = a.Middleware.stack + g.Middleware = a.Middleware.clone() g.root = a if a.root != nil { g.root = a.root diff --git a/router_test.go b/router_test.go index a5f0a769d..98d4dad7b 100644 --- a/router_test.go +++ b/router_test.go @@ -172,13 +172,13 @@ func Test_Resource(t *testing.T) { a := Automatic(Options{}) a.Resource("/users", &userResource{}) - a.Resource("/api/v1/people", &userResource{}) + a.Resource("/api/v1/users", &userResource{}) ts := httptest.NewServer(a) defer ts.Close() c := http.Client{} - for _, path := range []string{"/users", "/api/v1/people"} { + for _, path := range []string{"/users", "/api/v1/users"} { for _, test := range tests { u := ts.URL + filepath.Join(path, test.Path) req, err := http.NewRequest(test.Method, u, nil) @@ -200,7 +200,7 @@ func (u *userResource) List(c Context) error { } func (u *userResource) Show(c Context) error { - return c.Render(200, render.String("show {{params.user_id}}{{params.person_id}}")) + return c.Render(200, render.String("show {{params.user_id}}")) } func (u *userResource) New(c Context) error { @@ -212,13 +212,13 @@ func (u *userResource) Create(c Context) error { } func (u *userResource) Edit(c Context) error { - return c.Render(200, render.String("edit {{params.user_id}}{{params.person_id}}")) + return c.Render(200, render.String("edit {{params.user_id}}")) } func (u *userResource) Update(c Context) error { - return c.Render(200, render.String("update {{params.user_id}}{{params.person_id}}")) + return c.Render(200, render.String("update {{params.user_id}}")) } func (u *userResource) Destroy(c Context) error { - return c.Render(200, render.String("destroy {{params.user_id}}{{params.person_id}}")) + return c.Render(200, render.String("destroy {{params.user_id}}")) }