Repository: gobuffalo/buffalo Branch: main Commit: 0acef9701e38 Files: 177 Total size: 366.3 KB Directory structure: gitextract_pzjmh4ui/ ├── .github/ │ ├── CODEOWNERS │ ├── FUNDING.yml │ └── workflows/ │ ├── standard-go-test.yml │ └── standard-stale.yml ├── .gitignore ├── BACKERS.md ├── Dockerfile ├── Dockerfile.build ├── Dockerfile.slim.build ├── LICENSE.txt ├── README.md ├── SECURITY.md ├── SHOULDERS.md ├── app.go ├── app_test.go ├── binding/ │ ├── bindable.go │ ├── bindable_test.go │ ├── binding.go │ ├── binding_test.go │ ├── decoders/ │ │ ├── decoders.go │ │ ├── null_time.go │ │ ├── null_time_test.go │ │ ├── parse_time.go │ │ ├── time.go │ │ └── time_test.go │ ├── file.go │ ├── file_request_type_binder.go │ ├── file_test.go │ ├── html_content_type_binder.go │ ├── json_content_type_binder.go │ ├── request_binder.go │ ├── request_binder_test.go │ ├── types.go │ └── xml_request_type_binder.go ├── buffalo.go ├── context.go ├── cookies.go ├── cookies_test.go ├── default_context.go ├── default_context_test.go ├── errors.go ├── errors_test.go ├── events.go ├── flash.go ├── flash_test.go ├── fs.go ├── fs_test.go ├── go.mod ├── go.sum ├── handler.go ├── home.go ├── internal/ │ ├── defaults/ │ │ ├── defaults.go │ │ └── defaults_test.go │ ├── env/ │ │ └── env.go │ ├── fakesmtp/ │ │ ├── connection.go │ │ └── server.go │ ├── httpx/ │ │ ├── content_type.go │ │ └── content_type_test.go │ ├── meta/ │ │ ├── meta.go │ │ └── meta_test.go │ ├── nulls/ │ │ └── nulls.go │ ├── templates/ │ │ ├── error.dev.html │ │ ├── error.prod.html │ │ └── notfound.prod.html │ └── testdata/ │ ├── disk/ │ │ ├── file.txt │ │ ├── file2.txt │ │ └── under/ │ │ └── sub/ │ │ └── subfile │ ├── embedded/ │ │ ├── embed.go │ │ ├── file.txt │ │ └── under/ │ │ └── sub/ │ │ └── subfile │ └── panic.txt ├── logger.go ├── mail/ │ ├── README.md │ ├── attachment.go │ ├── body.go │ ├── dialer.go │ ├── mail.go │ ├── mail_test.go │ ├── message.go │ ├── sender.go │ ├── smtp_auth.go │ ├── smtp_errors.go │ ├── smtp_message.go │ ├── smtp_mime.go │ ├── smtp_send.go │ ├── smtp_sender.go │ ├── smtp_sender_test.go │ └── smtp_writeto.go ├── method_override.go ├── method_override_test.go ├── middleware.go ├── middleware_test.go ├── not_found_test.go ├── options.go ├── options_test.go ├── plugins/ │ ├── cache.go │ ├── command.go │ ├── decorate.go │ ├── events.go │ ├── log.go │ ├── log_debug.go │ ├── plugcmds/ │ │ ├── available.go │ │ ├── available_test.go │ │ ├── plug_map.go │ │ └── plug_map_test.go │ ├── plugdeps/ │ │ ├── command.go │ │ ├── plugdeps.go │ │ ├── plugdeps_test.go │ │ ├── plugin.go │ │ ├── plugin_test.go │ │ ├── plugins.go │ │ ├── plugins_test.go │ │ └── pop.go │ ├── plugins.go │ └── plugins_test.go ├── plugins.go ├── render/ │ ├── auto.go │ ├── auto_test.go │ ├── download.go │ ├── download_test.go │ ├── func.go │ ├── func_test.go │ ├── helpers.go │ ├── html.go │ ├── html_test.go │ ├── js.go │ ├── js_test.go │ ├── json.go │ ├── json_test.go │ ├── markdown_test.go │ ├── options.go │ ├── partials_test.go │ ├── plain.go │ ├── plain_test.go │ ├── render.go │ ├── render_test.go │ ├── renderer.go │ ├── sse.go │ ├── string.go │ ├── string_map.go │ ├── string_map_test.go │ ├── string_test.go │ ├── template.go │ ├── template_engine.go │ ├── template_helpers.go │ ├── template_helpers_test.go │ ├── template_test.go │ ├── xml.go │ └── xml_test.go ├── request_data.go ├── request_data_test.go ├── request_logger.go ├── resource.go ├── response.go ├── response_test.go ├── route.go ├── route_info.go ├── route_info_test.go ├── route_mappings.go ├── route_mappings_test.go ├── routenamer.go ├── router_test.go ├── runtime/ │ └── build.go ├── server.go ├── server_test.go ├── servers/ │ ├── listener.go │ ├── servers.go │ ├── simple.go │ └── tls.go ├── session.go ├── session_test.go ├── worker/ │ ├── job.go │ ├── simple.go │ ├── simple_test.go │ └── worker.go ├── wrappers.go └── wrappers_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/CODEOWNERS ================================================ # Default owner * @gobuffalo/core-managers ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: markbates patreon: buffalo ================================================ FILE: .github/workflows/standard-go-test.yml ================================================ name: Standard Test on: push: branches: [main v1] pull_request: permissions: contents: read jobs: call-standard-test: name: Test uses: gobuffalo/.github/.github/workflows/go-test.yml@v1.8 secrets: inherit govulncheck: name: govulncheck runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: "1.26" - name: Install govulncheck run: go install golang.org/x/vuln/cmd/govulncheck@latest - name: Run govulncheck run: govulncheck ./... ================================================ FILE: .github/workflows/standard-stale.yml ================================================ name: Standard Autocloser on: schedule: - cron: "30 1 * * *" jobs: call-standard-autocloser: name: Autocloser uses: gobuffalo/.github/.github/workflows/stale.yml@v1 secrets: inherit ================================================ FILE: .gitignore ================================================ *.log .DS_Store doc tmp pkg *.gem *.pid coverage coverage.data *.pbxuser *.mode1v3 .svn profile .console_history .sass-cache/* .rake_tasks~ *.log.lck solr/ .jhw-cache/ jhw.* *.sublime* node_modules/ dist/ generated/ .vendor/ bin/* gin-bin .idea/ .vscode/settings.json ================================================ FILE: BACKERS.md ================================================ # Financial Backers of the Buffalo Project Buffalo is a community-driven project that is run by individuals who believe that Buffalo is the way to quickly, and easily, build high quality, scalable applications in Go. Financial contributions to the Buffalo go towards ongoing development costs, servers, swag, conferences, etc... If you, or your company, use Buffalo, please consider supporting this effort to make rapid web development in Go, simple, easy, and fun! [http://patreon.com/buffalo](http://patreon.com/buffalo) --- ## Platinum Sponsors * **[Gopher Guides](https://www.gopherguides.com)** * **[PaperCall.io](https://www.papercall.io)** * **[Wawandco](https://wawand.co)** * **[Symbolsecurity](https://symbolsecurity.com)** * [Your Company Here](http://patreon.com/buffalo) ### Gold Sponsors * [Your Company Here](http://patreon.com/buffalo) ### Premium Backers * [Your Company Here](http://patreon.com/buffalo) #### Generous Backers * **[Zhorty](https://zhorty.com)** * [Your Company Here](http://patreon.com/buffalo) ================================================ FILE: Dockerfile ================================================ FROM gobuffalo/buffalo:latest ARG CODECOV_TOKEN ENV GOPROXY https://proxy.golang.org ENV BP /src/buffalo RUN rm -rf $BP RUN mkdir -p $BP WORKDIR $BP COPY . . RUN go mod tidy RUN go test -tags "sqlite integration_test" -cover -race -v ./... ================================================ FILE: Dockerfile.build ================================================ FROM golang:1.17 EXPOSE 3000 ENV GOPROXY=https://proxy.golang.org RUN apt-get update \ && apt-get install -y -q build-essential sqlite3 libsqlite3-dev postgresql libpq-dev vim # Installing Node 12 RUN curl -sL https://deb.nodesource.com/setup_12.x | bash RUN apt-get update && apt-get install nodejs # Installing Postgres RUN sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt/ `lsb_release -cs`-pgdg main" >> /etc/apt/sources.list.d/pgdg.list' \ && wget -q https://www.postgresql.org/media/keys/ACCC4CF8.asc -O - | apt-key add - \ && apt-get update \ && apt-get install -y -q postgresql postgresql-contrib libpq-dev\ && rm -rf /var/lib/apt/lists/* \ && service postgresql start && \ # Setting up password for postgres su -c "psql -c \"ALTER USER postgres WITH PASSWORD 'postgres';\"" - postgres # Installing yarn RUN npm install -g --no-progress yarn \ && yarn config set yarn-offline-mirror /npm-packages-offline-cache \ && yarn config set yarn-offline-mirror-pruning true # Install golangci RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.24.0 # Installing buffalo binary RUN go install github.com/gobuffalo/cli/cmd/buffalo@latest RUN go get github.com/gobuffalo/buffalo-pop/v2 RUN mkdir /src WORKDIR /src ================================================ FILE: Dockerfile.slim.build ================================================ FROM golang:1.17-alpine EXPOSE 3000 ENV GOPROXY=https://proxy.golang.org RUN apk add --no-cache --upgrade apk-tools \ && apk add --no-cache bash curl openssl git build-base nodejs npm sqlite sqlite-dev mysql-client vim postgresql libpq postgresql-contrib libc6-compat # Installing linter RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh \ | sh -s -- -b $(go env GOPATH)/bin v1.24.0 # Installing Yarn RUN npm i -g --no-progress yarn \ && yarn config set yarn-offline-mirror /npm-packages-offline-cache \ && yarn config set yarn-offline-mirror-pruning true # Installing buffalo binary RUN go install github.com/gobuffalo/cli/cmd/buffalo@latest RUN go get github.com/gobuffalo/buffalo-pop/v2 RUN mkdir /src WORKDIR /src ================================================ FILE: LICENSE.txt ================================================ The MIT License (MIT) Copyright (c) 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. ================================================ FILE: README.md ================================================

PkgGoDev Go Report Card Open Source Helpers

# Buffalo A Go web development eco-system, designed to make your project easier. Buffalo helps you to generate a web project that already has everything from front-end (JavaScript, SCSS, etc.) to the back-end (database, routing, etc.) already hooked up and ready to run. From there it provides easy APIs to build your web application quickly in Go. Buffalo **isn't just a framework**; it's a holistic web development environment and project structure that **lets developers get straight to the business** of, well, building their business. > I :heart: web dev in go again - Brian Ketelsen ## Versions The current stable version of Buffalo core is v1 (`v1` branch). Versions (branches): * `main` is for the current mainstream development. * `v1` is the current stable release. ## ⚠️ Important Buffalo works only with Go [modules](https://blog.golang.org/using-go-modules). `GOPATH` mode is likely to break most of the functionality of the Buffalo eco-system. Please see [this blog post](https://blog.gobuffalo.io/the-road-to-1-0-requiring-modules-5672c6b015e5) for more information. Also, the Buffalo team actively gives support to the last 2 versions of Go, which at the moment are Go 1.23 and 1.24. While Buffalo `may` work on older versions, we encourage you to upgrade to latest 2 versions of Go for a better development experience. ## Documentation Please visit [http://gobuffalo.io](http://gobuffalo.io) for the latest documentation, examples, and more. ### Quick Start - [Installation](https://gobuffalo.io/documentation/getting_started/installation) - [Create a new project](https://gobuffalo.io/documentation/getting_started/new-project) - [Tutorials](https://gobuffalo.io/documentation/tutorials/) ## Shoulders of Giants Buffalo would not be possible if not for all of the great projects it depends on. Please see [SHOULDERS.md](SHOULDERS.md) to see a list of them. ### Templating [github.com/gobuffalo/plush](https://github.com/gobuffalo/plush) - This templating package was chosen over the standard Go `html/template` package for a variety of reasons. The biggest of which is that it is significantly more flexible and easy to work with. ### Routing [github.com/gorilla/mux](https://github.com/gorilla/mux) - This router was chosen because of its stability and flexibility. There might be faster routers out there, but this one is definitely the most powerful! ### Models/ORM (Optional) [github.com/gobuffalo/pop](https://github.com/gobuffalo/pop) - Accessing databases is nothing new in web applications. Pop, and its command line tool, Soda, were chosen because they strike a nice balance between simplifying common tasks, being idiomatic, and giving you the flexibility you need to build your app. Pop and Soda share the same core philosophies as Buffalo, so they were a natural choice. ### Sessions, Cookies, WebSockets, and more [github.com/gorilla](https://github.com/gorilla) - The Gorilla toolkit is a great set of packages designed to improve upon the standard library for a variety of web-related packages. With these high-quality packages Buffalo can keep its "core" code to a minimum and focus on its goal of gluing them all together to make your life better. ## Benchmarks Oh, yeah, everyone wants benchmarks! What would a web framework be without its benchmarks? Well, guess what? I'm not giving you any! That's right. This is Go! I assure you that it is plenty fast enough for you. If you want benchmarks you can either a) check out any benchmarks that the [GIANTS](SHOULDERS.md) Buffalo is built upon having published, or b) run your own. I have no interest in playing the benchmark game, and neither should you. ## Contributing First, thank you so much for wanting to contribute! It means so much that you care enough to want to contribute. We appreciate every PR from the smallest of typos to the be biggest of features. **Here are the core rules to respect**: - If you have any question, please consider using the [Slack channel](https://gophers.slack.com/messages/buffalo/) (-#buffalo-, *#buffalo_fr* or *#buffalo-dev* for contribution related questions) or [Stack Overflow](https://stackoverflow.com/questions/tagged/buffalo). We use GitHub issues for **bug reports and feature requests only**. - All contributors of this project are working on their free time: be patient and kind. :- - Consider opening an issue **BEFORE** creating a Pull request (PR): you won't lose your time on fixing non-existing bugs, or fixing the wrong bug. Also we can help you to produce the best PR! - Open a PR against the `main` branch if your PR is for mainstream or version specific branch e.g. `v1` if your PR is for specific version. Note that the valid branch for a new feature request PR should be `main` while a PR against a version specific branch are allowed only for bugfixes. For the full contribution guidelines, please read [CONTRIBUTING](.github/CONTRIBUTING.md). ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Supported Versions | Version | Supported | | ------- | ------------------ | | 1.x.x | :white_check_mark: | ## Reporting a Vulnerability Contact @paganotoni or @sio4 on the [Gophers Slack](https://gophers.slack.com). ================================================ FILE: SHOULDERS.md ================================================ # Buffalo Stands on the Shoulders of Giants Buffalo does not try to reinvent the wheel! Instead, it uses the already great wheels developed by the Go community and puts them all together in the best way possible. Without these giants, this project would not be possible. Please make sure to check them out and thank them for all of their hard work. Thank you to the following **GIANTS**: * [github.com/BurntSushi/toml](https://godoc.org/github.com/BurntSushi/toml) * [github.com/aymerick/douceur](https://godoc.org/github.com/aymerick/douceur) * [github.com/cpuguy83/go-md2man/v2](https://godoc.org/github.com/cpuguy83/go-md2man/v2) * [github.com/davecgh/go-spew](https://godoc.org/github.com/davecgh/go-spew) * [github.com/dustin/go-humanize](https://godoc.org/github.com/dustin/go-humanize) * [github.com/fatih/color](https://godoc.org/github.com/fatih/color) * [github.com/fatih/structs](https://godoc.org/github.com/fatih/structs) * [github.com/felixge/httpsnoop](https://godoc.org/github.com/felixge/httpsnoop) * [github.com/fsnotify/fsnotify](https://godoc.org/github.com/fsnotify/fsnotify) * [github.com/go-sql-driver/mysql](https://godoc.org/github.com/go-sql-driver/mysql) * [github.com/joho/godotenv](https://godoc.org/github.com/joho/godotenv) * [github.com/gobuffalo/events](https://godoc.org/github.com/gobuffalo/events) * [github.com/gobuffalo/flect](https://godoc.org/github.com/gobuffalo/flect) * [github.com/gobuffalo/github_flavored_markdown](https://godoc.org/github.com/gobuffalo/github_flavored_markdown) * [github.com/gobuffalo/helpers](https://godoc.org/github.com/gobuffalo/helpers) * [github.com/gobuffalo/here](https://godoc.org/github.com/gobuffalo/here) * [github.com/gobuffalo/httptest](https://godoc.org/github.com/gobuffalo/httptest) * [github.com/gobuffalo/logger](https://godoc.org/github.com/gobuffalo/logger) * [github.com/gobuffalo/meta](https://godoc.org/github.com/gobuffalo/meta) * [github.com/gobuffalo/nulls](https://godoc.org/github.com/gobuffalo/nulls) * [github.com/gobuffalo/plush/v5](https://godoc.org/github.com/gobuffalo/plush/v5) * [github.com/gobuffalo/refresh](https://godoc.org/github.com/gobuffalo/refresh) * [github.com/gobuffalo/tags/v3](https://godoc.org/github.com/gobuffalo/tags/v3) * [github.com/gobuffalo/validate/v3](https://godoc.org/github.com/gobuffalo/validate/v3) * [github.com/gofrs/uuid](https://godoc.org/github.com/gofrs/uuid) * [github.com/google/go-cmp](https://godoc.org/github.com/google/go-cmp) * [github.com/gorilla/css](https://godoc.org/github.com/gorilla/css) * [github.com/gorilla/handlers](https://godoc.org/github.com/gorilla/handlers) * [github.com/gorilla/mux](https://godoc.org/github.com/gorilla/mux) * [github.com/gorilla/securecookie](https://godoc.org/github.com/gorilla/securecookie) * [github.com/gorilla/sessions](https://godoc.org/github.com/gorilla/sessions) * [github.com/inconshreveable/mousetrap](https://godoc.org/github.com/inconshreveable/mousetrap) * [github.com/jmoiron/sqlx](https://godoc.org/github.com/jmoiron/sqlx) * [github.com/joho/godotenv](https://godoc.org/github.com/joho/godotenv) * [github.com/kr/pretty](https://godoc.org/github.com/kr/pretty) * [github.com/kr/pty](https://godoc.org/github.com/kr/pty) * [github.com/kr/text](https://godoc.org/github.com/kr/text) * [github.com/lib/pq](https://godoc.org/github.com/lib/pq) * [github.com/mattn/go-colorable](https://godoc.org/github.com/mattn/go-colorable) * [github.com/mattn/go-isatty](https://godoc.org/github.com/mattn/go-isatty) * [github.com/mattn/go-sqlite3](https://godoc.org/github.com/mattn/go-sqlite3) * [github.com/microcosm-cc/bluemonday](https://godoc.org/github.com/microcosm-cc/bluemonday) * [github.com/mitchellh/go-homedir](https://godoc.org/github.com/mitchellh/go-homedir) * [github.com/monoculum/formam](https://godoc.org/github.com/monoculum/formam) * [github.com/pkg/diff](https://godoc.org/github.com/pkg/diff) * [github.com/pmezard/go-difflib](https://godoc.org/github.com/pmezard/go-difflib) * [github.com/psanford/memfs](https://godoc.org/github.com/psanford/memfs) * [github.com/rogpeppe/go-internal](https://godoc.org/github.com/rogpeppe/go-internal) * [github.com/russross/blackfriday/v2](https://godoc.org/github.com/russross/blackfriday/v2) * [github.com/sergi/go-diff](https://godoc.org/github.com/sergi/go-diff) * [github.com/sirupsen/logrus](https://godoc.org/github.com/sirupsen/logrus) * [github.com/sourcegraph/annotate](https://godoc.org/github.com/sourcegraph/annotate) * [github.com/sourcegraph/syntaxhighlight](https://godoc.org/github.com/sourcegraph/syntaxhighlight) * [github.com/spf13/cobra](https://godoc.org/github.com/spf13/cobra) * [github.com/spf13/pflag](https://godoc.org/github.com/spf13/pflag) * [github.com/stretchr/objx](https://godoc.org/github.com/stretchr/objx) * [github.com/stretchr/testify](https://godoc.org/github.com/stretchr/testify) * [github.com/yuin/goldmark](https://godoc.org/github.com/yuin/goldmark) * [golang.org/x/crypto](https://godoc.org/golang.org/x/crypto) * [golang.org/x/mod](https://godoc.org/golang.org/x/mod) * [golang.org/x/net](https://godoc.org/golang.org/x/net) * [golang.org/x/sync](https://godoc.org/golang.org/x/sync) * [golang.org/x/sys](https://godoc.org/golang.org/x/sys) * [golang.org/x/term](https://godoc.org/golang.org/x/term) * [golang.org/x/text](https://godoc.org/golang.org/x/text) * [golang.org/x/tools](https://godoc.org/golang.org/x/tools) * [golang.org/x/xerrors](https://godoc.org/golang.org/x/xerrors) * [gopkg.in/check.v1](https://godoc.org/gopkg.in/check.v1) * [gopkg.in/yaml.v2](https://godoc.org/gopkg.in/yaml.v2) * [gopkg.in/yaml.v3](https://godoc.org/gopkg.in/yaml.v3) ================================================ FILE: app.go ================================================ package buffalo import ( "fmt" "net/http" "sync" "github.com/gobuffalo/buffalo/internal/env" "github.com/gorilla/mux" ) // App is where it all happens! It holds on to options, // the underlying router, the middleware, and more. // Without an App you can't do much! type App struct { Options // Middleware, ErrorHandlers, router, and filepaths are moved to Home. Home moot *sync.RWMutex routes RouteList // TODO: to be deprecated #road-to-v1 root *App children []*App // Routenamer for the app. This field provides the ability to override the // base route namer for something more specific to the app. RouteNamer RouteNamer } // Muxer returns the underlying mux router to allow // for advance configurations func (a *App) Muxer() *mux.Router { return a.router } // New returns a new instance of App and adds some sane, and useful, defaults. func New(opts Options) *App { LoadPlugins() env.Load() opts = optionsWithDefaults(opts) a := &App{ Options: opts, Home: Home{ name: opts.Name, host: opts.Host, prefix: opts.Prefix, ErrorHandlers: ErrorHandlers{ http.StatusNotFound: defaultErrorHandler, http.StatusInternalServerError: defaultErrorHandler, }, router: mux.NewRouter(), }, moot: &sync.RWMutex{}, routes: RouteList{}, children: []*App{}, RouteNamer: baseRouteNamer{}, } a.Home.app = a // replace root. a.Home.appSelf = a // temporary, reverse reference to the group app. notFoundHandler := func(errorf string, code int) http.HandlerFunc { return func(res http.ResponseWriter, req *http.Request) { c := a.newContext(RouteInfo{}, res, req) err := fmt.Errorf(errorf, req.Method, req.URL.Path) _ = a.ErrorHandlers.Get(code)(code, err, c) } } a.router.NotFoundHandler = notFoundHandler("path not found: %s %s", http.StatusNotFound) a.router.MethodNotAllowedHandler = notFoundHandler("method not found: %s %s", http.StatusMethodNotAllowed) if a.MethodOverride == nil { a.MethodOverride = MethodOverride } a.Middleware = newMiddlewareStack(RequestLogger) a.Use(a.defaultErrorMiddleware) a.Use(a.PanicHandler) return a } ================================================ FILE: app_test.go ================================================ package buffalo func voidHandler(c Context) error { return nil } ================================================ FILE: binding/bindable.go ================================================ package binding import "net/http" // Bindable when implemented, on a type // will override any Binders that have been // configured when using buffalo#Context.Bind type Bindable interface { Bind(*http.Request) error } ================================================ FILE: binding/bindable_test.go ================================================ package binding import ( "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/require" ) type orbison struct { bound bool } func (o *orbison) Bind(req *http.Request) error { o.bound = true return nil } func Test_Bindable(t *testing.T) { r := require.New(t) req := httptest.NewRequest("GET", "/", nil) o := &orbison{} r.False(o.bound) r.NoError(Exec(req, o)) r.True(o.bound) } ================================================ FILE: binding/binding.go ================================================ package binding import ( "net/http" "time" "github.com/gobuffalo/buffalo/binding/decoders" "github.com/gobuffalo/buffalo/internal/nulls" "github.com/monoculum/formam" ) var ( // MaxFileMemory can be used to set the maximum size, in bytes, for files to be // stored in memory during uploaded for multipart requests. // See https://golang.org/pkg/net/http/#Request.ParseMultipartForm for more // information on how this impacts file uploads. MaxFileMemory int64 = 5 * 1024 * 1024 // formDecoder (formam) that will be used across ContentTypeBinders formDecoder = buildFormDecoder() // BaseRequestBinder is an instance of the requestBinder, it comes with preconfigured // content type binders for HTML, JSON, XML and Files, as well as custom types decoders // for time.Time and nulls.Time BaseRequestBinder = NewRequestBinder( HTMLContentTypeBinder{ decoder: formDecoder, }, JSONContentTypeBinder{}, XMLRequestTypeBinder{}, FileRequestTypeBinder{ decoder: formDecoder, }, ) ) // buildFormDecoder that will be used in the package. This method adds some custom decoders for time.Time and nulls.Time. func buildFormDecoder() *formam.Decoder { decoder := formam.NewDecoder(&formam.DecoderOptions{ TagName: "form", IgnoreUnknownKeys: true, }) decoder.RegisterCustomType(decoders.TimeDecoderFn(), []any{time.Time{}}, nil) decoder.RegisterCustomType(decoders.NullTimeDecoderFn(), []any{nulls.Time{}}, nil) return decoder } // RegisterTimeFormats allows to add custom time layouts that // the binder will be able to use for decoding. func RegisterTimeFormats(layouts ...string) { decoders.RegisterTimeFormats(layouts...) } // RegisterCustomDecoder allows to define custom decoders for certain types // In the request. func RegisterCustomDecoder(fn CustomTypeDecoder, types []any, fields []any) { rawFunc := (func([]string) (any, error))(fn) formDecoder.RegisterCustomType(rawFunc, types, fields) } // Register maps a request Content-Type (application/json) // to a Binder. func Register(contentType string, fn Binder) { BaseRequestBinder.Register(contentType, fn) } // Exec will bind the interface to the request.Body. The type of binding // is dependent on the "Content-Type" for the request. If the type // is "application/json" it will use "json.NewDecoder". If the type // is "application/xml" it will use "xml.NewDecoder". The default // binder is "https://github.com/monoculum/formam". func Exec(req *http.Request, value any) error { return BaseRequestBinder.Exec(req, value) } ================================================ FILE: binding/binding_test.go ================================================ package binding import ( "net/http" "net/url" "testing" "github.com/stretchr/testify/require" ) type blogPost struct { Tags []string Dislikes int Likes int32 } func Test_Register(t *testing.T) { r := require.New(t) Register("foo/bar", func(*http.Request, any) error { return nil }) r.NotNil(BaseRequestBinder.binders["foo/bar"]) req, err := http.NewRequest("POST", "/", nil) r.NoError(err) req.Header.Set("Content-Type", "foo/bar") req.Form = url.Values{ "Tags": []string{"AAA"}, "Likes": []string{"12"}, "Dislikes": []string{"1000"}, } req.ParseForm() var post blogPost r.NoError(Exec(req, &post)) r.Equal([]string(nil), post.Tags) r.Equal(int32(0), post.Likes) r.Equal(0, post.Dislikes) } func Test_RegisterCustomDecoder(t *testing.T) { r := require.New(t) RegisterCustomDecoder(func(vals []string) (any, error) { return []string{"X"}, nil }, []any{[]string{}}, nil) RegisterCustomDecoder(func(vals []string) (any, error) { return 0, nil }, []any{int(0)}, nil) post := blogPost{} req, err := http.NewRequest("POST", "/", nil) r.NoError(err) req.Header.Set("Content-Type", "application/html") req.Form = url.Values{ "Tags": []string{"AAA"}, "Likes": []string{"12"}, "Dislikes": []string{"1000"}, } req.ParseForm() r.NoError(Exec(req, &post)) r.Equal([]string{"X"}, post.Tags) r.Equal(int32(12), post.Likes) r.Equal(0, post.Dislikes) } ================================================ FILE: binding/decoders/decoders.go ================================================ package decoders import ( "sync" "time" ) var ( lock = &sync.RWMutex{} // timeFormats are the base time formats supported by the time.Time and // nulls.Time Decoders you can prepend custom formats to this list // by using RegisterTimeFormats. timeFormats = []string{ time.RFC3339, "01/02/2006", "2006-01-02", "2006-01-02T15:04", time.ANSIC, time.UnixDate, time.RubyDate, time.RFC822, time.RFC822Z, time.RFC850, time.RFC1123, time.RFC1123Z, time.RFC3339Nano, time.Kitchen, time.Stamp, time.StampMilli, time.StampMicro, time.StampNano, } ) // RegisterTimeFormats allows to add custom time layouts that // the binder will be able to use for decoding. func RegisterTimeFormats(layouts ...string) { lock.Lock() defer lock.Unlock() timeFormats = append(layouts, timeFormats...) } ================================================ FILE: binding/decoders/null_time.go ================================================ package decoders import "github.com/gobuffalo/buffalo/internal/nulls" // NullTimeDecoderFn is a custom type decoder func for null.Time fields func NullTimeDecoderFn() func([]string) (any, error) { return func(vals []string) (any, error) { var ti nulls.Time // If vals is empty, return a nulls.Time with Valid = false (i.e. NULL). // The parseTime() function called below does this check as well, but // because it doesn't return an error in the case where vals is empty, // we have no way to determine from its response that the nulls.Time // should actually be NULL. if len(vals) == 0 || vals[0] == "" { return ti, nil } t, err := parseTime(vals) if err != nil { return ti, err } ti.Time = t ti.Valid = true return ti, nil } } ================================================ FILE: binding/decoders/null_time_test.go ================================================ package decoders import ( "testing" "time" "github.com/gobuffalo/buffalo/internal/nulls" "github.com/stretchr/testify/require" ) func Test_NullTimeCustomDecoder_Decode(t *testing.T) { r := require.New(t) testCases := []struct { input string expected time.Time expValid bool expectErr bool }{ { input: "2017-01-01", expected: time.Date(2017, time.January, 1, 0, 0, 0, 0, time.UTC), expValid: true, }, { input: "2018-07-13T15:34", expected: time.Date(2018, time.July, 13, 15, 34, 0, 0, time.UTC), expValid: true, }, { input: "2018-20-10T30:15", expected: time.Time{}, expValid: false, }, { input: "", expected: time.Time{}, expValid: false, }, } for _, testCase := range testCases { tt, err := NullTimeDecoderFn()([]string{testCase.input}) r.IsType(tt, nulls.Time{}) nt := tt.(nulls.Time) if testCase.expectErr { r.Error(err) r.Equal(nt.Valid, false) continue } r.Equal(testCase.expected, nt.Time) r.Equal(testCase.expValid, nt.Valid) } } ================================================ FILE: binding/decoders/parse_time.go ================================================ package decoders import ( "time" ) func parseTime(vals []string) (time.Time, error) { var t time.Time var err error // don't try to parse empty time values, it will raise an error if len(vals) == 0 || vals[0] == "" { return t, nil } for _, layout := range timeFormats { t, err = time.Parse(layout, vals[0]) if err == nil { return t, nil } } if err != nil { return t, err } return t, nil } ================================================ FILE: binding/decoders/time.go ================================================ package decoders // TimeDecoderFn is a custom type decoder func for Time fields func TimeDecoderFn() func([]string) (any, error) { return func(vals []string) (any, error) { return parseTime(vals) } } ================================================ FILE: binding/decoders/time_test.go ================================================ package decoders import ( "testing" "time" "github.com/stretchr/testify/require" ) func TestParseTimeErrorParsing(t *testing.T) { r := require.New(t) _, err := parseTime([]string{"this is sparta"}) r.Error(err) } func TestParseTime(t *testing.T) { r := require.New(t) testCases := []struct { input string expected time.Time expectErr bool }{ { input: "2017-01-01", expected: time.Date(2017, time.January, 1, 0, 0, 0, 0, time.UTC), expectErr: false, }, { input: "2018-07-13T15:34", expected: time.Date(2018, time.July, 13, 15, 34, 0, 0, time.UTC), expectErr: false, }, { input: "2018-20-10T30:15", expected: time.Time{}, expectErr: true, }, } for _, tc := range testCases { tt, err := parseTime([]string{tc.input}) if !tc.expectErr { r.NoError(err) } r.Equal(tc.expected, tt) } } func TestParseTimeConflicting(t *testing.T) { r := require.New(t) RegisterTimeFormats("2006-02-01") tt, err := parseTime([]string{"2017-01-10"}) r.NoError(err) expected := time.Date(2017, time.October, 1, 0, 0, 0, 0, time.UTC) r.Equal(expected, tt) } ================================================ FILE: binding/file.go ================================================ package binding import ( "mime/multipart" ) // File holds information regarding an uploaded file type File struct { multipart.File *multipart.FileHeader } // Valid if there is an actual uploaded file func (f File) Valid() bool { return f.File != nil } func (f File) String() string { if f.File == nil { return "" } return f.Filename } ================================================ FILE: binding/file_request_type_binder.go ================================================ package binding import ( "net/http" "reflect" "github.com/monoculum/formam" ) // FileRequestTypeBinder is in charge of binding File request types. type FileRequestTypeBinder struct { decoder *formam.Decoder } // ContentTypes returns the list of content types for FileRequestTypeBinder func (ht FileRequestTypeBinder) ContentTypes() []string { return []string{ "multipart/form-data", } } // BinderFunc that will take care of the HTML File binding func (ht FileRequestTypeBinder) BinderFunc() Binder { return func(req *http.Request, i any) error { err := req.ParseMultipartForm(MaxFileMemory) if err != nil { return err } if err := ht.decoder.Decode(req.Form, i); err != nil { return err } form := req.MultipartForm.File if len(form) == 0 { return nil } ri := reflect.Indirect(reflect.ValueOf(i)) rt := ri.Type() for n := range form { f := ri.FieldByName(n) if !f.IsValid() { for i := 0; i < rt.NumField(); i++ { sf := rt.Field(i) if sf.Tag.Get("form") == n { f = ri.Field(i) break } } } if !f.IsValid() { continue } if f.Kind() == reflect.Slice { for _, fh := range req.MultipartForm.File[n] { mf, err := fh.Open() if err != nil { return err } f.Set(reflect.Append(f, reflect.ValueOf(File{ File: mf, FileHeader: fh, }))) } continue } if _, ok := f.Interface().(File); ok { mf, mh, err := req.FormFile(n) if err != nil { return err } f.Set(reflect.ValueOf(File{ File: mf, FileHeader: mh, })) } } return nil } } ================================================ FILE: binding/file_test.go ================================================ package binding_test import ( "bytes" "fmt" "io" "mime/multipart" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "github.com/gobuffalo/buffalo" "github.com/gobuffalo/buffalo/binding" "github.com/gobuffalo/buffalo/render" "github.com/stretchr/testify/require" ) type WithFile struct { MyFile binding.File } type NamedFileSlice struct { MyFiles []binding.File `form:"thefiles"` } type NamedFile struct { MyFile binding.File `form:"afile"` } func App() *buffalo.App { a := buffalo.New(buffalo.Options{}) a.POST("/on-struct", func(c buffalo.Context) error { wf := &WithFile{} if err := c.Bind(wf); err != nil { return err } return c.Render(http.StatusCreated, render.String(wf.MyFile.Filename)) }) a.POST("/named-file", func(c buffalo.Context) error { wf := &NamedFile{} if err := c.Bind(wf); err != nil { return err } return c.Render(http.StatusCreated, render.String(wf.MyFile.Filename)) }) a.POST("/named-file-slice", func(c buffalo.Context) error { wmf := &NamedFileSlice{} if err := c.Bind(wmf); err != nil { return err } result := make([]string, len(wmf.MyFiles)) for i, f := range wmf.MyFiles { result[i] += fmt.Sprintf("%s", f.Filename) } return c.Render(http.StatusCreated, render.String(strings.Join(result, ","))) }) a.POST("/on-context", func(c buffalo.Context) error { f, err := c.File("MyFile") if err != nil { return err } return c.Render(http.StatusCreated, render.String(f.Filename)) }) return a } func Test_File_Upload_On_Struct(t *testing.T) { r := require.New(t) req, err := newFileUploadRequest("/on-struct", "MyFile", "file_test.go") r.NoError(err) res := httptest.NewRecorder() App().ServeHTTP(res, req) r.Equal(http.StatusCreated, res.Code) r.Equal("file_test.go", res.Body.String()) } func Test_File_Upload_On_Struct_WithTag_WithMultipleFiles(t *testing.T) { r := require.New(t) req, err := newFileUploadRequest("/named-file-slice", "thefiles", "file_test.go", "file.go", "types.go") r.NoError(err) res := httptest.NewRecorder() App().ServeHTTP(res, req) r.Equal(http.StatusCreated, res.Code) r.Equal("file_test.go,file.go,types.go", res.Body.String()) } func Test_File_Upload_On_Struct_WithTag(t *testing.T) { r := require.New(t) req, err := newFileUploadRequest("/named-file", "afile", "file_test.go") r.NoError(err) res := httptest.NewRecorder() App().ServeHTTP(res, req) r.Equal(http.StatusCreated, res.Code) r.Equal("file_test.go", res.Body.String()) } func Test_File_Upload_On_Context(t *testing.T) { r := require.New(t) req, err := newFileUploadRequest("/on-context", "MyFile", "file_test.go") r.NoError(err) res := httptest.NewRecorder() App().ServeHTTP(res, req) r.Equal(http.StatusCreated, res.Code) r.Equal("file_test.go", res.Body.String()) } // this helper method was inspired by this blog post by Matt Aimonetti: // https://matt.aimonetti.net/posts/2013/07/01/golang-multipart-file-upload-example/ func newFileUploadRequest(uri string, paramName string, paths ...string) (*http.Request, error) { body := &bytes.Buffer{} writer := multipart.NewWriter(body) for _, path := range paths { file, err := os.Open(path) if err != nil { return nil, err } defer file.Close() part, err := writer.CreateFormFile(paramName, filepath.Base(path)) if err != nil { return nil, err } if _, err = io.Copy(part, file); err != nil { return nil, err } } if err := writer.Close(); err != nil { return nil, err } req, err := http.NewRequest("POST", uri, body) req.Header.Set("Content-Type", writer.FormDataContentType()) return req, err } ================================================ FILE: binding/html_content_type_binder.go ================================================ package binding import ( "net/http" "github.com/monoculum/formam" ) // HTMLContentTypeBinder is in charge of binding HTML request types. type HTMLContentTypeBinder struct { decoder *formam.Decoder } // ContentTypes that will be used to identify HTML requests func (ht HTMLContentTypeBinder) ContentTypes() []string { return []string{ "application/html", "text/html", "application/x-www-form-urlencoded", "html", } } // BinderFunc that will take care of the HTML binding func (ht HTMLContentTypeBinder) BinderFunc() Binder { return func(req *http.Request, i any) error { err := req.ParseForm() if err != nil { return err } if err := ht.decoder.Decode(req.Form, i); err != nil { return err } return nil } } ================================================ FILE: binding/json_content_type_binder.go ================================================ package binding import ( "encoding/json" "net/http" ) // JSONContentTypeBinder is in charge of binding JSON request types. type JSONContentTypeBinder struct{} // BinderFunc returns the Binder for this JSONRequestTypeBinder func (js JSONContentTypeBinder) BinderFunc() Binder { return func(req *http.Request, value any) error { return json.NewDecoder(req.Body).Decode(value) } } // ContentTypes that will be wired to this the JSON Binder func (js JSONContentTypeBinder) ContentTypes() []string { return []string{ "application/json", "text/json", "json", } } ================================================ FILE: binding/request_binder.go ================================================ package binding import ( "errors" "fmt" "net/http" "strings" "sync" "github.com/gobuffalo/buffalo/internal/httpx" ) var ( errBlankContentType = errors.New("blank content type") ) // RequestBinder is in charge of binding multiple requests types to // struct. type RequestBinder struct { lock *sync.RWMutex binders map[string]Binder } // Register maps a request Content-Type (application/json) // to a Binder. func (rb *RequestBinder) Register(contentType string, fn Binder) { rb.lock.Lock() defer rb.lock.Unlock() rb.binders[strings.ToLower(contentType)] = fn } // Exec binds a request with a passed value, depending on the content type // It will look for the correct RequestTypeBinder and use it. func (rb *RequestBinder) Exec(req *http.Request, value any) error { rb.lock.Lock() defer rb.lock.Unlock() if ba, ok := value.(Bindable); ok { return ba.Bind(req) } ct := httpx.ContentType(req) if ct == "" { return errBlankContentType } binder := rb.binders[ct] if binder == nil { return fmt.Errorf("could not find a binder for %s", ct) } return binder(req, value) } // NewRequestBinder creates our request binder with support for // XML, JSON, HTTP and File request types. func NewRequestBinder(requestBinders ...ContenTypeBinder) *RequestBinder { result := &RequestBinder{ lock: &sync.RWMutex{}, binders: map[string]Binder{}, } for _, requestBinder := range requestBinders { for _, contentType := range requestBinder.ContentTypes() { result.Register(contentType, requestBinder.BinderFunc()) } } return result } ================================================ FILE: binding/request_binder_test.go ================================================ package binding import ( "errors" "net/http" "strings" "testing" "github.com/stretchr/testify/require" ) func Test_RequestBinder_Exec(t *testing.T) { r := require.New(t) var used bool BaseRequestBinder.Register("paganotoni/test", func(*http.Request, any) error { used = true return nil }) req, err := http.NewRequest("GET", "/home", strings.NewReader("")) req.Header.Add("content-type", "paganotoni/test") r.NoError(err) data := &struct{}{} r.NoError(BaseRequestBinder.Exec(req, data)) r.True(used) } func Test_RequestBinder_Exec_BlankContentType(t *testing.T) { r := require.New(t) req, err := http.NewRequest("GET", "/home", strings.NewReader("")) r.NoError(err) data := &struct{}{} r.Equal(BaseRequestBinder.Exec(req, data), errBlankContentType) } func Test_RequestBinder_Exec_Bindable(t *testing.T) { r := require.New(t) BaseRequestBinder.Register("paganotoni/orbison", func(req *http.Request, val any) error { switch v := val.(type) { case orbison: v.bound = false } return errors.New("this should not be called") }) req, err := http.NewRequest("GET", "/home", strings.NewReader("")) req.Header.Add("content-type", "paganotoni/orbison") r.NoError(err) data := &orbison{} r.NoError(BaseRequestBinder.Exec(req, data)) r.True(data.bound) } func Test_RequestBinder_Exec_NoBinder(t *testing.T) { r := require.New(t) req, err := http.NewRequest("GET", "/home", strings.NewReader("")) req.Header.Add("content-type", "paganotoni/other") r.NoError(err) err = BaseRequestBinder.Exec(req, &struct{}{}) r.Error(err) r.Equal(err.Error(), "could not find a binder for paganotoni/other") } ================================================ FILE: binding/types.go ================================================ package binding import ( "net/http" ) // ContenTypeBinder are those capable of handling a request type like JSON or XML type ContenTypeBinder interface { BinderFunc() Binder ContentTypes() []string } // Binder takes a request and binds it to an interface. // If there is a problem it should return an error. type Binder func(*http.Request, any) error // CustomTypeDecoder converts a custom type from the request into its exact type. type CustomTypeDecoder func([]string) (any, error) ================================================ FILE: binding/xml_request_type_binder.go ================================================ package binding import ( "encoding/xml" "net/http" ) // XMLRequestTypeBinder is in charge of binding XML request types. type XMLRequestTypeBinder struct{} // BinderFunc returns the Binder for this RequestTypeBinder func (xm XMLRequestTypeBinder) BinderFunc() Binder { return func(req *http.Request, value any) error { return xml.NewDecoder(req.Body).Decode(value) } } // ContentTypes that will be wired to this the XML Binder func (xm XMLRequestTypeBinder) ContentTypes() []string { return []string{ "application/xml", "text/xml", "xml", } } ================================================ FILE: buffalo.go ================================================ /* Package buffalo is a Go web development eco-system, designed to make your life easier. Buffalo helps you to generate a web project that already has everything from front-end (JavaScript, SCSS, etc.) to back-end (database, routing, etc.) already hooked up and ready to run. From there it provides easy APIs to build your web application quickly in Go. Buffalo **isn't just a framework**, it's a holistic web development environment and project structure that **lets developers get straight to the business** of, well, building their business. */ package buffalo ================================================ FILE: context.go ================================================ package buffalo import ( "context" "net/http" "net/url" "github.com/gobuffalo/buffalo/binding" "github.com/gobuffalo/buffalo/internal/httpx" "github.com/gobuffalo/buffalo/render" "github.com/gorilla/mux" ) // Context holds on to information as you // pass it down through middleware, Handlers, // templates, etc... It strives to make your // life a happier one. type Context interface { context.Context Response() http.ResponseWriter Request() *http.Request Session() *Session Cookies() *Cookies Params() ParamValues Param(string) string Set(string, any) LogField(string, any) LogFields(map[string]any) Logger() Logger Bind(any) error Render(int, render.Renderer) error Error(int, error) error Redirect(int, string, ...any) error Data() map[string]any Flash() *Flash File(string) (binding.File, error) } // ParamValues will most commonly be url.Values, // but isn't it great that you set your own? :) type ParamValues interface { Get(string) string } func (a *App) newContext(info RouteInfo, res http.ResponseWriter, req *http.Request) Context { if ws, ok := res.(*Response); ok { res = ws } // Parse URL Params params := url.Values{} vars := mux.Vars(req) for k, v := range vars { params.Add(k, v) } // Parse URL Query String Params // For POST, PUT, and PATCH requests, it also parse the request body as a form. // Request body parameters take precedence over URL query string values in params if err := req.ParseForm(); err == nil { for k, v := range req.Form { for _, vv := range v { params.Add(k, vv) } } } session := a.getSession(req, res) ct := httpx.ContentType(req) data := newRequestData() data.d = map[string]any{ "app": a, "env": a.Env, "routes": a.Routes(), "current_route": info, "current_path": req.URL.Path, "contentType": ct, "method": req.Method, } for _, route := range a.Routes() { cRoute := route data.d[cRoute.PathName] = cRoute.BuildPathHelper() } return &DefaultContext{ Context: req.Context(), contentType: ct, response: res, request: req, params: params, logger: a.Logger, session: session, flash: newFlash(session), data: data, } } ================================================ FILE: cookies.go ================================================ package buffalo import ( "net/http" "time" ) // Cookies allows you to easily get cookies from the request, and set cookies on the response. type Cookies struct { req *http.Request res http.ResponseWriter } // Get returns the value of the cookie with the given name. Returns http.ErrNoCookie if there's no cookie with that name in the request. func (c *Cookies) Get(name string) (string, error) { ck, err := c.req.Cookie(name) if err != nil { return "", err } return ck.Value, nil } // Set a cookie on the response, which will expire after the given duration. func (c *Cookies) Set(name, value string, maxAge time.Duration) { ck := http.Cookie{ Name: name, Value: value, MaxAge: int(maxAge.Seconds()), } http.SetCookie(c.res, &ck) } // SetWithExpirationTime sets a cookie that will expire at a specific time. // Note that the time is determined by the client's browser, so it might not expire at the expected time, // for example if the client has changed the time on their computer. func (c *Cookies) SetWithExpirationTime(name, value string, expires time.Time) { ck := http.Cookie{ Name: name, Value: value, Expires: expires, } http.SetCookie(c.res, &ck) } // SetWithPath sets a cookie path on the server in which the cookie will be available on. // If set to '/', the cookie will be available within the entire domain. // If set to '/foo/', the cookie will only be available within the /foo/ directory and // all sub-directories such as /foo/bar/ of domain. func (c *Cookies) SetWithPath(name, value, path string) { ck := http.Cookie{ Name: name, Value: value, Path: path, } http.SetCookie(c.res, &ck) } // Delete sets a header that tells the browser to remove the cookie with the given name. func (c *Cookies) Delete(name string) { ck := http.Cookie{ Name: name, Value: "v", // Setting a time in the distant past, like the unix epoch, removes the cookie, // since it has long expired. Expires: time.Unix(0, 0), } http.SetCookie(c.res, &ck) } ================================================ FILE: cookies_test.go ================================================ package buffalo import ( "net/http" "net/http/httptest" "testing" "time" "github.com/stretchr/testify/require" ) func TestCookies_Get(t *testing.T) { r := require.New(t) req := httptest.NewRequest("POST", "/", nil) req.Header.Set("Cookie", "name=Arthur Dent; answer=42") c := Cookies{req, nil} v, err := c.Get("name") r.NoError(err) r.Equal("Arthur Dent", v) v, err = c.Get("answer") r.NoError(err) r.Equal("42", v) _, err = c.Get("unknown") r.EqualError(err, http.ErrNoCookie.Error()) } func TestCookies_Set(t *testing.T) { r := require.New(t) res := httptest.NewRecorder() c := Cookies{&http.Request{}, res} c.Set("name", "Rob Pike", time.Hour*24) h := res.Header().Get("Set-Cookie") r.Equal("name=\"Rob Pike\"; Max-Age=86400", h) } func TestCookies_SetWithPath(t *testing.T) { r := require.New(t) res := httptest.NewRecorder() c := Cookies{&http.Request{}, res} c.SetWithPath("name", "Rob Pike", "/foo") h := res.Header().Get("Set-Cookie") r.Equal("name=\"Rob Pike\"; Path=/foo", h) } func TestCookies_SetWithExpirationTime(t *testing.T) { r := require.New(t) res := httptest.NewRecorder() c := Cookies{&http.Request{}, res} e := time.Date(2017, 7, 29, 19, 28, 45, 0, time.UTC) c.SetWithExpirationTime("name", "Rob Pike", e) h := res.Header().Get("Set-Cookie") r.Equal("name=\"Rob Pike\"; Expires=Sat, 29 Jul 2017 19:28:45 GMT", h) } func TestCookies_Delete(t *testing.T) { r := require.New(t) res := httptest.NewRecorder() c := Cookies{&http.Request{}, res} c.Delete("remove-me") h := res.Header().Get("Set-Cookie") r.Equal("remove-me=v; Expires=Thu, 01 Jan 1970 00:00:00 GMT", h) } ================================================ FILE: default_context.go ================================================ package buffalo import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "maps" "net/http" "net/url" "reflect" "slices" "strings" "time" "github.com/gobuffalo/buffalo/binding" "github.com/gobuffalo/buffalo/render" ) // assert that DefaultContext is implementing Context var _ Context = &DefaultContext{} var _ context.Context = &DefaultContext{} // TODO(sio4): #road-to-v1 - make DefaultContext private // and only allow it to be generated by App.newContext() or any similar. // DefaultContext is, as its name implies, a default // implementation of the Context interface. type DefaultContext struct { context.Context response http.ResponseWriter request *http.Request params url.Values logger Logger session *Session contentType string data *requestData flash *Flash } // Response returns the original Response for the request. func (d *DefaultContext) Response() http.ResponseWriter { return d.response } // Request returns the original Request. func (d *DefaultContext) Request() *http.Request { return d.request } // Params returns all of the parameters for the request, // including both named params and query string parameters. func (d *DefaultContext) Params() ParamValues { return d.params } // Logger returns the Logger for this context. func (d *DefaultContext) Logger() Logger { return d.logger } // Param returns a param, either named or query string, // based on the key. func (d *DefaultContext) Param(key string) string { return d.Params().Get(key) } // Set a value onto the Context. Any value set onto the Context // will be automatically available in templates. func (d *DefaultContext) Set(key string, value any) { if d.data == nil { d.data = newRequestData() } d.data.moot.Lock() defer d.data.moot.Unlock() d.data.d[key] = value } // Value that has previously stored on the context. func (d *DefaultContext) Value(key any) any { if k, ok := key.(string); ok && d.data != nil { d.data.moot.RLock() defer d.data.moot.RUnlock() if v, ok := d.data.d[k]; ok { return v } } if d.Context == nil { return nil } return d.Context.Value(key) } // Session for the associated Request. func (d *DefaultContext) Session() *Session { return d.session } // Cookies for the associated request and response. func (d *DefaultContext) Cookies() *Cookies { return &Cookies{d.request, d.response} } // Flash messages for the associated Request. func (d *DefaultContext) Flash() *Flash { return d.flash } type paginable interface { Paginate() string } // Render a status code and render.Renderer to the associated Response. // The request parameters will be made available to the render.Renderer // "{{.params}}". Any values set onto the Context will also automatically // be made available to the render.Renderer. To render "no content" pass // in a nil render.Renderer. func (d *DefaultContext) Render(status int, rr render.Renderer) error { start := time.Now() defer func() { d.LogField("render", time.Since(start)) }() if rr == nil { d.Response().WriteHeader(status) return nil } data := d.Data() pp := map[string]string{} for k, v := range d.params { pp[k] = v[0] } data["params"] = pp data["flash"] = d.Flash().data data["session"] = d.Session() data["request"] = d.Request() data["status"] = status bb := &bytes.Buffer{} err := rr.Render(bb, data) if err != nil { var er render.ErrRedirect if errors.As(err, &er) { return d.Redirect(er.Status, er.URL) } return HTTPError{Status: http.StatusInternalServerError, Cause: err} } if d.Session() != nil { d.Flash().Clear() d.Flash().persist(d.Session()) if err := d.Session().Save(); err != nil { return HTTPError{Status: http.StatusInternalServerError, Cause: err} } } d.Response().Header().Set("Content-Type", rr.ContentType()) if p, ok := data["pagination"].(paginable); ok { d.Response().Header().Set("X-Pagination", p.Paginate()) } d.Response().WriteHeader(status) _, err = io.Copy(d.Response(), bb) if err != nil { return HTTPError{Status: http.StatusInternalServerError, Cause: err} } return nil } // Bind the interface to the request.Body. The type of binding // is dependent on the "Content-Type" for the request. If the type // is "application/json" it will use "json.NewDecoder". If the type // is "application/xml" it will use "xml.NewDecoder". See the // github.com/gobuffalo/buffalo/binding package for more details. func (d *DefaultContext) Bind(value any) error { return binding.Exec(d.Request(), value) } // LogField adds the key/value pair onto the Logger to be printed out // as part of the request logging. This allows you to easily add things // like metrics (think DB times) to your request. func (d *DefaultContext) LogField(key string, value any) { if d.logger == nil { return } d.logger = d.logger.WithField(key, value) } // LogFields adds the key/value pairs onto the Logger to be printed out // as part of the request logging. This allows you to easily add things // like metrics (think DB times) to your request. func (d *DefaultContext) LogFields(values map[string]any) { if d.logger == nil { return } d.logger = d.logger.WithFields(values) } func (d *DefaultContext) Error(status int, err error) error { return HTTPError{Status: status, Cause: err} } var mapType = reflect.ValueOf(map[string]any{}).Type() // Redirect a request with the given status to the given URL. func (d *DefaultContext) Redirect(status int, url string, args ...any) error { if d.Session() != nil { d.Flash().persist(d.Session()) if err := d.Session().Save(); err != nil { return HTTPError{Status: http.StatusInternalServerError, Cause: err} } } if strings.HasSuffix(url, "Path()") { if len(args) > 1 { return fmt.Errorf("you must pass only a map[string]any to a route path: %T", args) } var m map[string]any if len(args) == 1 { rv := reflect.Indirect(reflect.ValueOf(args[0])) if !rv.Type().ConvertibleTo(mapType) { return fmt.Errorf("you must pass only a map[string]any to a route path: %T", args) } m = rv.Convert(mapType).Interface().(map[string]any) } h, ok := d.Value(strings.TrimSuffix(url, "()")).(RouteHelperFunc) if !ok { return fmt.Errorf("could not find a route helper named %s", url) } url, err := h(m) if err != nil { return err } http.Redirect(d.Response(), d.Request(), string(url), status) return nil } if len(args) > 0 { url = fmt.Sprintf(url, args...) } http.Redirect(d.Response(), d.Request(), url, status) return nil } // Data contains all the values set through Get/Set. func (d *DefaultContext) Data() map[string]any { m := map[string]any{} if d.data == nil { return m } d.data.moot.RLock() defer d.data.moot.RUnlock() m = maps.Clone(d.data.d) return m } func (d *DefaultContext) String() string { data := d.Data() bb := make([]string, 0, len(data)) for k, v := range data { if _, ok := v.(RouteHelperFunc); !ok { bb = append(bb, fmt.Sprintf("%s: %s", k, v)) } } slices.Sort(bb) return strings.Join(bb, "\n\n") } // File returns an uploaded file by name, or an error func (d *DefaultContext) File(name string) (binding.File, error) { req := d.Request() if err := req.ParseMultipartForm(5 * 1024 * 1024); err != nil { return binding.File{}, err } f, h, err := req.FormFile(name) bf := binding.File{ File: f, FileHeader: h, } return bf, err } // MarshalJSON implements json marshaling for the context func (d *DefaultContext) MarshalJSON() ([]byte, error) { m := map[string]any{} data := d.Data() for k, v := range data { // don't try and marshal ourself if _, ok := v.(*DefaultContext); ok { continue } if _, err := json.Marshal(v); err == nil { // it can be marshaled, so add it: m[k] = v } } return json.Marshal(m) } ================================================ FILE: default_context_test.go ================================================ package buffalo import ( "bytes" "context" "net/http" "net/url" "testing" "github.com/gobuffalo/buffalo/render" "github.com/gobuffalo/httptest" "github.com/gobuffalo/logger" "github.com/stretchr/testify/require" ) func basicContext() DefaultContext { return DefaultContext{ Context: context.Background(), logger: logger.New(logger.DebugLevel), data: newRequestData(), flash: &Flash{data: make(map[string][]string)}, } } func Test_DefaultContext_Redirect(t *testing.T) { r := require.New(t) a := New(Options{}) u := "/foo?bar=http%3A%2F%2Flocalhost%3A3000%2Flogin%2Fcallback%2Ffacebook" a.GET("/", func(c Context) error { return c.Redirect(http.StatusFound, u) }) w := httptest.New(a) res := w.HTML("/").Get() r.Equal(u, res.Location()) } func Test_DefaultContext_Redirect_Helper(t *testing.T) { r := require.New(t) table := []struct { E string I map[string]any S int }{ { E: "/foo/baz/", I: map[string]any{"bar": "baz"}, S: http.StatusPermanentRedirect, }, { S: http.StatusInternalServerError, }, } for _, tt := range table { a := New(Options{}) a.GET("/foo/{bar}", func(c Context) error { return c.Render(http.StatusOK, render.String(c.Param("bar"))) }) a.GET("/", func(c Context) error { return c.Redirect(http.StatusPermanentRedirect, "fooBarPath()", tt.I) }) a.GET("/nomap", func(c Context) error { return c.Redirect(http.StatusPermanentRedirect, "rootPath()") }) w := httptest.New(a) res := w.HTML("/").Get() r.Equal(tt.S, res.Code) r.Equal(tt.E, res.Location()) res = w.HTML("/nomap").Get() r.Equal(http.StatusPermanentRedirect, res.Code) r.Equal("/", res.Location()) } } func Test_DefaultContext_Param(t *testing.T) { r := require.New(t) c := DefaultContext{ params: url.Values{ "name": []string{"Mark"}, }, } r.Equal("Mark", c.Param("name")) } func Test_DefaultContext_Param_form(t *testing.T) { r := require.New(t) app := New(Options{}) var name string app.POST("/", func(c Context) error { name = c.Param("name") return nil }) w := httptest.New(app) res := w.HTML("/").Post(map[string]string{ "name": "Mark", }) r.Equal(http.StatusOK, res.Code) r.Equal("Mark", name) } func Test_DefaultContext_Param_Multiple(t *testing.T) { r := require.New(t) app := New(Options{}) var params ParamValues var param string app.POST("/{id}", func(c Context) error { params = c.Params() param = c.Param("id") return nil }) w := httptest.New(app) res := w.HTML("/a?id=c&y=z&id=d").Post(map[string]string{ "id": "b", }) paramsExpected := url.Values{ "id": []string{"a", "b", "c", "d"}, "y": []string{"z"}, } r.Equal(200, res.Code) r.Equal(paramsExpected, params.(url.Values)) r.Equal("a", param) } func Test_DefaultContext_GetSet(t *testing.T) { r := require.New(t) c := basicContext() r.Nil(c.Value("name")) c.Set("name", "Mark") r.NotNil(c.Value("name")) r.Equal("Mark", c.Value("name").(string)) } func Test_DefaultContext_Set_not_configured(t *testing.T) { r := require.New(t) c := DefaultContext{} c.Set("name", "Yonghwan") r.NotNil(c.Value("name")) r.Equal("Yonghwan", c.Value("name").(string)) } func Test_DefaultContext_Value(t *testing.T) { r := require.New(t) c := basicContext() r.Nil(c.Value("name")) c.Set("name", "Mark") r.NotNil(c.Value("name")) r.Equal("Mark", c.Value("name").(string)) } func Test_DefaultContext_Value_not_configured(t *testing.T) { r := require.New(t) c := DefaultContext{} r.Nil(c.Value("name")) } func Test_DefaultContext_Render(t *testing.T) { r := require.New(t) c := basicContext() res := httptest.NewRecorder() c.response = res c.params = url.Values{"name": []string{"Mark"}} c.Set("greet", "Hello") err := c.Render(http.StatusTeapot, render.String(`<%= greet %> <%= params["name"] %>!`)) r.NoError(err) r.Equal(http.StatusTeapot, res.Code) r.Equal("Hello Mark!", res.Body.String()) } func Test_DefaultContext_Bind_Default(t *testing.T) { r := require.New(t) user := struct { FirstName string `form:"first_name"` }{} a := New(Options{}) a.POST("/", func(c Context) error { err := c.Bind(&user) if err != nil { return err } return c.Render(http.StatusCreated, nil) }) w := httptest.New(a) uv := url.Values{"first_name": []string{"Mark"}} res := w.HTML("/").Post(uv) r.Equal(http.StatusCreated, res.Code) r.Equal("Mark", user.FirstName) } func Test_DefaultContext_Bind_No_ContentType(t *testing.T) { r := require.New(t) user := struct { FirstName string `form:"first_name"` }{ FirstName: "Mark", } a := New(Options{}) a.POST("/", func(c Context) error { err := c.Bind(&user) if err != nil { return c.Error(http.StatusUnprocessableEntity, err) } return c.Render(http.StatusCreated, nil) }) bb := &bytes.Buffer{} req, err := http.NewRequest("POST", "/", bb) r.NoError(err) req.Header.Del("Content-Type") res := httptest.NewRecorder() a.ServeHTTP(res, req) r.Equal(http.StatusUnprocessableEntity, res.Code) r.Contains(res.Body.String(), "blank content type") } func Test_DefaultContext_Bind_Empty_ContentType(t *testing.T) { r := require.New(t) user := struct { FirstName string `form:"first_name"` }{ FirstName: "Mark", } a := New(Options{}) a.POST("/", func(c Context) error { err := c.Bind(&user) if err != nil { return c.Error(http.StatusUnprocessableEntity, err) } return c.Render(http.StatusCreated, nil) }) bb := &bytes.Buffer{} req, err := http.NewRequest("POST", "/", bb) r.NoError(err) // Want to make sure that an empty string value does not cause an error on `split` req.Header.Set("Content-Type", "") res := httptest.NewRecorder() a.ServeHTTP(res, req) r.Equal(http.StatusUnprocessableEntity, res.Code) r.Contains(res.Body.String(), "blank content type") } func Test_DefaultContext_Bind_Default_BlankFields(t *testing.T) { r := require.New(t) user := struct { FirstName string `form:"first_name"` }{ FirstName: "Mark", } a := New(Options{}) a.POST("/", func(c Context) error { err := c.Bind(&user) if err != nil { return err } return c.Render(http.StatusCreated, nil) }) w := httptest.New(a) uv := url.Values{"first_name": []string{""}} res := w.HTML("/").Post(uv) r.Equal(http.StatusCreated, res.Code) r.Equal("", user.FirstName) } func Test_DefaultContext_Bind_JSON(t *testing.T) { r := require.New(t) user := struct { FirstName string `json:"first_name"` }{} a := New(Options{}) a.POST("/", func(c Context) error { err := c.Bind(&user) if err != nil { return err } return c.Render(http.StatusCreated, nil) }) w := httptest.New(a) res := w.JSON("/").Post(map[string]string{ "first_name": "Mark", }) r.Equal(http.StatusCreated, res.Code) r.Equal("Mark", user.FirstName) } func Test_DefaultContext_Data(t *testing.T) { r := require.New(t) c := basicContext() r.EqualValues(map[string]any{}, c.Data()) } func Test_DefaultContext_Data_not_configured(t *testing.T) { r := require.New(t) c := DefaultContext{} r.EqualValues(map[string]any{}, c.Data()) } func Test_DefaultContext_String(t *testing.T) { r := require.New(t) c := basicContext() c.Set("name", "Buffalo") c.Set("language", "go") r.EqualValues("language: go\n\nname: Buffalo", c.String()) } func Test_DefaultContext_String_EmptyData(t *testing.T) { r := require.New(t) c := basicContext() r.EqualValues("", c.String()) } func Test_DefaultContext_String_EmptyData_not_configured(t *testing.T) { r := require.New(t) c := DefaultContext{} r.EqualValues("", c.String()) } func Test_DefaultContext_MarshalJSON(t *testing.T) { r := require.New(t) c := basicContext() c.Set("name", "Buffalo") c.Set("language", "go") jb, err := c.MarshalJSON() r.NoError(err) r.EqualValues(`{"language":"go","name":"Buffalo"}`, string(jb)) } func Test_DefaultContext_MarshalJSON_EmptyData(t *testing.T) { r := require.New(t) c := basicContext() jb, err := c.MarshalJSON() r.NoError(err) r.EqualValues(`{}`, string(jb)) } func Test_DefaultContext_MarshalJSON_EmptyData_not_configured(t *testing.T) { r := require.New(t) c := DefaultContext{} jb, err := c.MarshalJSON() r.NoError(err) r.EqualValues(`{}`, string(jb)) } ================================================ FILE: errors.go ================================================ package buffalo import ( "database/sql" _ "embed" "encoding/json" "encoding/xml" "errors" "fmt" "net/http" "runtime/debug" "slices" "strings" "github.com/gobuffalo/buffalo/internal/defaults" "github.com/gobuffalo/buffalo/internal/httpx" "github.com/gobuffalo/events" "github.com/gobuffalo/plush/v5" ) var ( //go:embed internal/templates/error.dev.html devErrorTmpl string //go:embed internal/templates/error.prod.html prodErrorTmpl string //go:embed internal/templates/notfound.prod.html prodNotFoundTmpl string ) // HTTPError a typed error returned by http Handlers and used for choosing error handlers type HTTPError struct { Status int `json:"status"` Cause error `json:"error"` } // Unwrap allows the error to be unwrapped. func (h HTTPError) Unwrap() error { return h.Cause } // Error returns the cause of the error as string. func (h HTTPError) Error() string { if h.Cause != nil { return h.Cause.Error() } return "unknown cause" } // ErrorHandler interface for handling an error for a // specific status code. type ErrorHandler func(int, error, Context) error // ErrorHandlers is used to hold a list of ErrorHandler // types that can be used to handle specific status codes. /* a.ErrorHandlers[http.StatusInternalServerError] = func(status int, err error, c buffalo.Context) error { res := c.Response() res.WriteHeader(status) res.Write([]byte(err.Error())) return nil } */ type ErrorHandlers map[int]ErrorHandler // Get a registered ErrorHandler for this status code. If // no ErrorHandler has been registered, a default one will // be returned. func (e ErrorHandlers) Get(status int) ErrorHandler { if eh, ok := e[status]; ok { return eh } if eh, ok := e[0]; ok { return eh } return defaultErrorHandler } // Default sets an error handler should a status // code not already be mapped. This will replace // the original default error handler. // This is a *catch-all* handler. func (e ErrorHandlers) Default(eh ErrorHandler) { if eh != nil { e[0] = eh } } // PanicHandler recovers from panics gracefully and calls // the error handling code for a 500 error. func (a *App) PanicHandler(next Handler) Handler { return func(c Context) error { defer func() { //catch or finally r := recover() var err error if r != nil { //catch switch t := r.(type) { case error: err = t case string: err = fmt.Errorf("%s", t) default: err = fmt.Errorf("%s", fmt.Sprint(t)) } payload := events.Payload{ "context": c, "app": a, "stacktrace": string(debug.Stack()), "error": err, } events.EmitError(events.ErrPanic, err, payload) eh := a.ErrorHandlers.Get(http.StatusInternalServerError) eh(http.StatusInternalServerError, err, c) } }() return next(c) } } func (a *App) defaultErrorMiddleware(next Handler) Handler { return func(c Context) error { err := next(c) if err == nil { return nil } // 500 Internal Server Error by default status := http.StatusInternalServerError // unpack root err and check for HTTPError if errors.Is(err, sql.ErrNoRows) { status = http.StatusNotFound } var h HTTPError if errors.As(err, &h) { status = h.Status } payload := events.Payload{ "context": c, "app": a, "status": status, "error": err, } if status >= http.StatusInternalServerError { // we need the details (or stack trace) only for 5xx errors. // pkg/errors supports '%+v' for stack trace. // the other type of errors that support '%+v' is also supported. payload["stacktrace"] = fmt.Sprintf("%+v", err) } events.EmitError(events.ErrGeneral, err, payload) eh := a.ErrorHandlers.Get(status) err = eh(status, err, c) if err != nil { events.Emit(events.Event{ Kind: EvtFailureErr, Message: "unable to handle error and giving up", Error: err, Payload: payload, }) // things have really hit the fan if we're here!! a.Logger.Error(err) c.Response().WriteHeader(http.StatusInternalServerError) c.Response().Write([]byte(err.Error())) } return nil } } func productionErrorResponseFor(status int) []byte { if status == http.StatusNotFound { return []byte(prodNotFoundTmpl) } return []byte(prodErrorTmpl) } // ErrorResponse is a used to display errors as JSON or XML type ErrorResponse struct { XMLName xml.Name `json:"-" xml:"response"` Error string `json:"error" xml:"error"` Trace string `json:"trace,omitempty" xml:"trace,omitempty"` Code int `json:"code" xml:"code,attr"` } const defaultErrorCT = "text/html; charset=utf-8" func defaultErrorHandler(status int, origErr error, c Context) error { env := c.Value("env") requestCT := defaults.String(httpx.ContentType(c.Request()), defaultErrorCT) var defaultErrorResponse *ErrorResponse c.LogField("status", status) c.Logger().Error(origErr) c.Response().WriteHeader(status) if env != nil && env.(string) != "development" { switch strings.ToLower(requestCT) { case "application/json", "text/json", "json", "application/xml", "text/xml", "xml": defaultErrorResponse = &ErrorResponse{ Code: status, Error: http.StatusText(status), } default: c.Response().Header().Set("content-type", defaultErrorCT) responseBody := productionErrorResponseFor(status) c.Response().Write(responseBody) return nil } } trace := fmt.Sprintf("%+v", origErr) if cause := errors.Unwrap(origErr); cause != nil { origErr = cause } errResponse := errorResponseDefault(defaultErrorResponse, &ErrorResponse{ Error: origErr.Error(), Trace: trace, Code: status, }) switch strings.ToLower(requestCT) { case "application/json", "text/json", "json": c.Response().Header().Set("content-type", "application/json") err := json.NewEncoder(c.Response()).Encode(errResponse) if err != nil { return err } case "application/xml", "text/xml", "xml": c.Response().Header().Set("content-type", "text/xml") err := xml.NewEncoder(c.Response()).Encode(errResponse) if err != nil { return err } default: c.Response().Header().Set("content-type", defaultErrorCT) if err := c.Request().ParseForm(); err != nil { trace = fmt.Sprintf("%s\n%s", err.Error(), trace) } routes := c.Value("routes") cd := c.Data() delete(cd, "app") delete(cd, "routes") data := map[string]any{ "routes": routes, "error": trace, "status": status, "data": cd, "params": c.Params(), "posted_form": c.Request().Form, "context": c, "headers": inspectHeaders(c.Request().Header), "inspect": func(v any) string { return fmt.Sprintf("%+v", v) }, } ctx := plush.NewContextWith(data) t, err := plush.Render(devErrorTmpl, ctx) if err != nil { return err } _, err = c.Response().Write([]byte(t)) return err } return nil } func errorResponseDefault(defaultResponse, alternativeResponse *ErrorResponse) *ErrorResponse { if defaultResponse != nil { return defaultResponse } return alternativeResponse } type inspectHeaders http.Header func (i inspectHeaders) String() string { bb := make([]string, 0, len(i)) for k, v := range i { bb = append(bb, fmt.Sprintf("%s: %s", k, v)) } slices.Sort(bb) return strings.Join(bb, "\n") } ================================================ FILE: errors_test.go ================================================ package buffalo import ( "fmt" "net/http" "os" "testing" "github.com/gobuffalo/httptest" "github.com/gobuffalo/logger" "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" ) // testLoggerHook is useful to test whats being logged. type testLoggerHook struct { errors []*logrus.Entry } func (lh *testLoggerHook) Fire(entry *logrus.Entry) error { lh.errors = append(lh.errors, entry) return nil } func (lh *testLoggerHook) Levels() []logrus.Level { return []logrus.Level{ logrus.ErrorLevel, } } func Test_defaultErrorHandler_SetsContentType(t *testing.T) { r := require.New(t) app := New(Options{}) app.GET("/", func(c Context) error { return c.Error(http.StatusUnauthorized, fmt.Errorf("boom")) }) w := httptest.New(app) res := w.HTML("/").Get() r.Equal(http.StatusUnauthorized, res.Code) ct := res.Header().Get("content-type") r.Equal("text/html; charset=utf-8", ct) } func Test_defaultErrorHandler_Logger(t *testing.T) { r := require.New(t) app := New(Options{}) app.GET("/", func(c Context) error { return c.Error(http.StatusUnauthorized, fmt.Errorf("boom")) }) testHook := &testLoggerHook{} l := logrus.New() l.SetOutput(os.Stdout) l.AddHook(testHook) log := logger.Logrus{ FieldLogger: l, } app.Logger = log w := httptest.New(app) res := w.HTML("/").Get() r.Equal(http.StatusUnauthorized, res.Code) r.Equal(http.StatusUnauthorized, testHook.errors[0].Data["status"]) } func Test_defaultErrorHandler_JSON_development(t *testing.T) { testDefaultErrorHandler(t, "application/json", "development") } func Test_defaultErrorHandler_XML_development(t *testing.T) { testDefaultErrorHandler(t, "text/xml", "development") } func Test_defaultErrorHandler_JSON_staging(t *testing.T) { testDefaultErrorHandler(t, "application/json", "staging") } func Test_defaultErrorHandler_XML_staging(t *testing.T) { testDefaultErrorHandler(t, "text/xml", "staging") } func Test_defaultErrorHandler_JSON_production(t *testing.T) { testDefaultErrorHandler(t, "application/json", "production") } func Test_defaultErrorHandler_XML_production(t *testing.T) { testDefaultErrorHandler(t, "text/xml", "production") } func testDefaultErrorHandler(t *testing.T, contentType, env string) { r := require.New(t) app := New(Options{}) app.Env = env app.GET("/", func(c Context) error { return c.Error(http.StatusUnauthorized, fmt.Errorf("boom")) }) w := httptest.New(app) var res *httptest.Response if contentType == "application/json" { res = w.JSON("/").Get().Response } else { res = w.XML("/").Get().Response } r.Equal(http.StatusUnauthorized, res.Code) ct := res.Header().Get("content-type") r.Equal(contentType, ct) b := res.Body.String() if env == "development" { if contentType == "text/xml" { r.Contains(b, ``) r.Contains(b, `boom`) r.Contains(b, ``) r.Contains(b, ``) r.Contains(b, ``) } else { r.Contains(b, `"code":401`) r.Contains(b, `"error":"boom"`) r.Contains(b, `"trace":"`) } } else { if contentType == "text/xml" { r.Contains(b, ``) r.Contains(b, fmt.Sprintf(`%s`, http.StatusText(http.StatusUnauthorized))) r.NotContains(b, ``) r.NotContains(b, ``) r.Contains(b, ``) } else { r.Contains(b, `"code":401`) r.Contains(b, fmt.Sprintf(`"error":"%s"`, http.StatusText(http.StatusUnauthorized))) r.NotContains(b, `"trace":"`) } } } func Test_defaultErrorHandler_nil_error(t *testing.T) { r := require.New(t) app := New(Options{}) app.GET("/", func(c Context) error { return c.Error(http.StatusInternalServerError, nil) }) w := httptest.New(app) res := w.JSON("/").Get() r.Equal(http.StatusInternalServerError, res.Code) } func Test_PanicHandler(t *testing.T) { app := New(Options{}) app.GET("/string", func(c Context) error { panic("string boom") }) app.GET("/error", func(c Context) error { panic(fmt.Errorf("error boom")) }) table := []struct { path string expected string }{ {"/string", "string boom"}, {"/error", "error boom"}, } const stack = `github.com/gobuffalo/buffalo.Test_PanicHandler` w := httptest.New(app) for _, tt := range table { t.Run(tt.path, func(st *testing.T) { r := require.New(st) res := w.HTML("%s", tt.path).Get() r.Equal(http.StatusInternalServerError, res.Code) body := res.Body.String() r.Contains(body, tt.expected) r.Contains(body, stack) }) } } func Test_defaultErrorMiddleware(t *testing.T) { r := require.New(t) app := New(Options{}) var x string var ok bool app.ErrorHandlers[http.StatusUnprocessableEntity] = func(code int, err error, c Context) error { x, ok = c.Value("T").(string) c.Response().WriteHeader(code) c.Response().Write([]byte(err.Error())) return nil } app.Use(func(next Handler) Handler { return func(c Context) error { c.Set("T", "t") return c.Error(http.StatusUnprocessableEntity, fmt.Errorf("boom")) } }) app.GET("/", func(c Context) error { return nil }) w := httptest.New(app) res := w.HTML("/").Get() r.Equal(http.StatusUnprocessableEntity, res.Code) r.True(ok) r.Equal("t", x) } func Test_SetErrorMiddleware(t *testing.T) { r := require.New(t) app := New(Options{}) app.ErrorHandlers.Default(func(code int, err error, c Context) error { res := c.Response() res.WriteHeader(http.StatusTeapot) res.Write([]byte("i'm a teapot")) return nil }) app.GET("/", func(c Context) error { return c.Error(http.StatusUnprocessableEntity, fmt.Errorf("boom")) }) w := httptest.New(app) res := w.HTML("/").Get() r.Equal(http.StatusTeapot, res.Code) r.Equal("i'm a teapot", res.Body.String()) } ================================================ FILE: events.go ================================================ package buffalo // TODO: TODO-v1 check if they are really need to be exported. /* The event id should be unique across packages as the format of "::" as documented. They should not be used by another packages to keep it informational. To make it sure, they need to be internal. Especially for plugable conponents like servers or workers, they can have their own event definition if they need but the buffalo runtime can emit generalize events when e.g. the runtime calls configured worker. */ const ( // EvtAppStart is emitted when buffalo.App#Serve is called EvtAppStart = "buffalo:app:start" // EvtAppStartErr is emitted when an error occurs calling buffalo.App#Serve EvtAppStartErr = "buffalo:app:start:err" // EvtAppStop is emitted when buffalo.App#Stop is called EvtAppStop = "buffalo:app:stop" // EvtAppStopErr is emitted when an error occurs calling buffalo.App#Stop EvtAppStopErr = "buffalo:app:stop:err" // EvtRouteStarted is emitted when a requested route is being processed EvtRouteStarted = "buffalo:route:started" // EvtRouteFinished is emitted when a requested route is completed EvtRouteFinished = "buffalo:route:finished" // EvtRouteErr is emitted when there is a problem handling processing a route EvtRouteErr = "buffalo:route:err" // EvtServerStart is emitted when buffalo is about to start servers EvtServerStart = "buffalo:server:start" // EvtServerStartErr is emitted when an error occurs when starting servers EvtServerStartErr = "buffalo:server:start:err" // EvtServerStop is emitted when buffalo is about to stop servers EvtServerStop = "buffalo:server:stop" // EvtServerStopErr is emitted when an error occurs when stopping servers EvtServerStopErr = "buffalo:server:stop:err" // EvtWorkerStart is emitted when buffalo is about to start workers EvtWorkerStart = "buffalo:worker:start" // EvtWorkerStartErr is emitted when an error occurs when starting workers EvtWorkerStartErr = "buffalo:worker:start:err" // EvtWorkerStop is emitted when buffalo is about to stop workers EvtWorkerStop = "buffalo:worker:stop" // EvtWorkerStopErr is emitted when an error occurs when stopping workers EvtWorkerStopErr = "buffalo:worker:stop:err" // EvtFailureErr is emitted when something can't be processed at all. it is a bad thing EvtFailureErr = "buffalo:failure:err" ) ================================================ FILE: flash.go ================================================ package buffalo import "encoding/json" // flashKey is the prefix inside the Session. const flashKey = "_flash_" // Flash is a struct that helps with the operations over flash messages. type Flash struct { data map[string][]string } // Delete removes a particular key from the Flash. func (f Flash) Delete(key string) { delete(f.data, key) } // Clear removes all keys from the Flash. func (f *Flash) Clear() { f.data = map[string][]string{} } // Set allows to set a list of values into a particular key. func (f Flash) Set(key string, values []string) { f.data[key] = values } // Add adds a flash value for a flash key, if the key already has values the list for that value grows. func (f Flash) Add(key, value string) { if len(f.data[key]) == 0 { f.data[key] = []string{value} return } f.data[key] = append(f.data[key], value) } // Persist the flash inside the session. func (f Flash) persist(session *Session) { b, _ := json.Marshal(f.data) session.Set(flashKey, b) } // newFlash creates a new Flash and loads the session data inside its data. func newFlash(session *Session) *Flash { result := &Flash{ data: map[string][]string{}, } if session.Session != nil { if f := session.Get(flashKey); f != nil { json.Unmarshal(f.([]byte), &result.data) } } return result } ================================================ FILE: flash_test.go ================================================ package buffalo import ( "net/http" "testing" "text/template" "github.com/gobuffalo/buffalo/render" "github.com/gobuffalo/httptest" "github.com/stretchr/testify/require" ) func Test_FlashAdd(t *testing.T) { r := require.New(t) f := newFlash(&Session{}) r.Equal(f.data, map[string][]string{}) f.Add("error", "something") r.Equal(f.data, map[string][]string{ "error": {"something"}, }) f.Add("error", "other") r.Equal(f.data, map[string][]string{ "error": {"something", "other"}, }) } func Test_FlashRender(t *testing.T) { r := require.New(t) a := New(Options{}) rr := render.New(render.Options{}) a.GET("/", func(c Context) error { c.Flash().Add("errors", "Error AJ set") c.Flash().Add("errors", "Error DAL set") return c.Render(http.StatusCreated, rr.String(errorsTPL)) }) w := httptest.New(a) res := w.HTML("/").Get() r.Contains(res.Body.String(), "Error AJ set") r.Contains(res.Body.String(), "Error DAL set") } func Test_FlashRenderEmpty(t *testing.T) { r := require.New(t) a := New(Options{}) rr := render.New(render.Options{}) a.GET("/", func(c Context) error { return c.Render(http.StatusCreated, rr.String(errorsTPL)) }) w := httptest.New(a) res := w.HTML("/").Get() r.NotContains(res.Body.String(), "Flash:") } const errorsTPL = ` <%= for (k, v) in flash["errors"] { %> Flash: <%= k %>:<%= v %> <% } %> ` func Test_FlashRenderEntireFlash(t *testing.T) { r := require.New(t) a := New(Options{}) rr := render.New(render.Options{}) a.GET("/", func(c Context) error { c.Flash().Add("something", "something to say!") return c.Render(http.StatusCreated, rr.String(keyTPL)) }) w := httptest.New(a) res := w.HTML("/").Get() r.Contains(res.Body.String(), "something to say!") } const keyTPL = `<%= for (k, v) in flash { %> Flash: <%= k %>:<%= v %> <% } %> ` func Test_FlashRenderCustomKey(t *testing.T) { r := require.New(t) a := New(Options{}) rr := render.New(render.Options{}) a.GET("/", func(c Context) error { c.Flash().Add("something", "something to say!") return c.Render(http.StatusCreated, rr.String(keyTPL)) }) w := httptest.New(a) res := w.HTML("/").Get() r.Contains(res.Body.String(), "something to say!") } func Test_FlashRenderCustomKeyNotDefined(t *testing.T) { r := require.New(t) a := New(Options{}) rr := render.New(render.Options{}) a.GET("/", func(c Context) error { return c.Render(http.StatusCreated, rr.String(customKeyTPL)) }) w := httptest.New(a) res := w.HTML("/").Get() r.NotContains(res.Body.String(), "something to say!") } const customKeyTPL = ` {{#each flash.other as |k value|}} {{value}} {{/each}} ` func Test_FlashNotClearedOnRedirect(t *testing.T) { r := require.New(t) a := New(Options{}) rr := render.New(render.Options{}) a.GET("/flash", func(c Context) error { c.Flash().Add("success", "Antonio, you're welcome!") return c.Redirect(http.StatusSeeOther, "/") }) a.GET("/", func(c Context) error { template := `Message: <%= flash["success"] %>` return c.Render(http.StatusCreated, rr.String(template)) }) w := httptest.New(a) res := w.HTML("/flash").Get() r.Equal(res.Code, http.StatusSeeOther) r.Equal(res.Location(), "/") res = w.HTML("/").Get() r.Contains(res.Body.String(), template.HTMLEscapeString("Antonio, you're welcome!")) } ================================================ FILE: fs.go ================================================ package buffalo import ( "fmt" "io/fs" "os" ) // FS wraps a directory and an embed FS that are expected to have the same contents. // it prioritizes the directory FS and falls back to the embedded FS if the file cannot // be found on disk. This is useful during development or when deploying with // assets not embedded in the binary. // // Additionally FS hiddes any file named embed.go from the FS. type FS struct { embed fs.FS dir fs.FS } // NewFS returns a new FS that wraps the given directory and embedded FS. // the embed.FS is expected to embed the same files as the directory FS. func NewFS(embed fs.ReadDirFS, dir string) FS { return FS{ embed: embed, dir: os.DirFS(dir), } } // Open opens the named file. // // When Open returns an error, it should be of type *PathError with the Op // field set to "open", the Path field set to name, and the Err field // describing the problem. // // Open should reject attempts to open names that do not satisfy // ValidPath(name), returning a *PathError with Err set to ErrInvalid or // ErrNotExist. func (f FS) Open(name string) (fs.File, error) { if name == "embed.go" { return nil, &fs.PathError{ Op: "open", Path: name, Err: fs.ErrNotExist, } } file, err := f.getFile(name) if name == "." { // NOTE: It always returns the root from the "disk" instead // "embed". However, it could be fine since the the purpose // of buffalo.FS isn't supporting full featured filesystem. return rootFile{file}, err } return file, err } func (f FS) getFile(name string) (fs.File, error) { file, err := f.dir.Open(name) if err == nil { return file, nil } return f.embed.Open(name) } // rootFile wraps the "." directory for hidding the embed.go file. type rootFile struct { fs.File } // ReadDir implements the fs.ReadDirFile interface. func (f rootFile) ReadDir(n int) (entries []fs.DirEntry, err error) { dir, ok := f.File.(fs.ReadDirFile) if !ok { return nil, fmt.Errorf("%T is not a directory", f.File) } entries, err = dir.ReadDir(n) entries = hideEmbedFile(entries) return entries, err } func hideEmbedFile(entries []fs.DirEntry) []fs.DirEntry { result := make([]fs.DirEntry, 0, len(entries)) for _, entry := range entries { if entry.Name() != "embed.go" { result = append(result, entry) } } return result } ================================================ FILE: fs_test.go ================================================ package buffalo import ( "io" "io/fs" "testing" "github.com/gobuffalo/buffalo/internal/testdata/embedded" "github.com/stretchr/testify/require" ) func Test_FS_Disallows_Parent_Folders(t *testing.T) { r := require.New(t) fsys := NewFS(embedded.FS(), "internal/testdata/disk") r.NotNil(fsys) f, err := fsys.Open("../panic.txt") r.ErrorIs(err, fs.ErrNotExist) r.Nil(f) f, err = fsys.Open("try/../to/../trick/../panic.txt") r.ErrorIs(err, fs.ErrNotExist) r.Nil(f) } func Test_FS_Hides_embed_go(t *testing.T) { r := require.New(t) fsys := NewFS(embedded.FS(), "internal/testdata/disk") r.NotNil(fsys) f, err := fsys.Open("embed.go") r.ErrorIs(err, fs.ErrNotExist) r.Nil(f) } func Test_FS_Prioritizes_Disk(t *testing.T) { r := require.New(t) fsys := NewFS(embedded.FS(), "internal/testdata/disk") r.NotNil(fsys) f, err := fsys.Open("file.txt") r.NoError(err) b, err := io.ReadAll(f) r.NoError(err) r.Equal("This file is on disk.", string(b)) // should handle slash-separated path for all systems including Windows f, err = fsys.Open("under/sub/subfile") r.NoError(err) b, err = io.ReadAll(f) r.NoError(err) r.Equal("This file is on disk/sub.", string(b)) } func Test_FS_Uses_Embed_If_No_Disk(t *testing.T) { r := require.New(t) fsys := NewFS(embedded.FS(), "internal/testdata/empty") r.NotNil(fsys) f, err := fsys.Open("file.txt") r.NoError(err) b, err := io.ReadAll(f) r.NoError(err) r.Equal("This file is embedded.", string(b)) // should handle slash-separated path for all systems including Windows f, err = fsys.Open("under/sub/subfile") r.NoError(err) b, err = io.ReadAll(f) r.NoError(err) r.Equal("This file is on embedded/sub.", string(b)) } func Test_FS_ReadDirFile(t *testing.T) { r := require.New(t) fsys := NewFS(embedded.FS(), "internal/testdata/disk") r.NotNil(fsys) f, err := fsys.Open(".") r.NoError(err) dir, ok := f.(fs.ReadDirFile) r.True(ok, "folder does not implement fs.ReadDirFile interface") // First read should return at most 1 file entries, err := dir.ReadDir(1) r.NoError(err) // The actual len will be 0 because the first file read is the embed.go file // this is counter-intuitive, but it's how the fs.ReadDirFile interface is specified; // if err == nil, just continue to call ReadDir until io.EOF is returned. r.LessOrEqual(len(entries), 1, "a call to ReadDir must at most return n entries") // Second read should return at most 2 files entries, err = dir.ReadDir(3) r.NoError(err) // The actual len will be 2 (file.txt & file2.txt + under/) r.LessOrEqual(len(entries), 3, "a call to ReadDir must at most return n entries") // trying to read next 2 files (none left) entries, err = dir.ReadDir(2) r.ErrorIs(err, io.EOF) r.Empty(entries) } ================================================ FILE: go.mod ================================================ module github.com/gobuffalo/buffalo go 1.25.0 require ( github.com/BurntSushi/toml v1.2.1 github.com/gobuffalo/events v1.4.3 github.com/gobuffalo/flect v1.0.3 github.com/gobuffalo/github_flavored_markdown v1.1.4 github.com/gobuffalo/helpers v0.6.10 github.com/gobuffalo/httptest v1.5.2 github.com/gobuffalo/logger v1.0.7 github.com/gobuffalo/plush/v5 v5.0.11 github.com/gobuffalo/refresh v1.13.3 github.com/gobuffalo/tags/v3 v3.1.4 github.com/gorilla/handlers v1.5.1 github.com/gorilla/mux v1.8.0 github.com/gorilla/sessions v1.2.1 github.com/joho/godotenv v1.4.0 github.com/monoculum/formam v3.5.5+incompatible github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.6.1 github.com/stretchr/testify v1.9.0 golang.org/x/text v0.29.0 ) require ( github.com/aymerick/douceur v0.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fatih/color v1.13.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/felixge/httpsnoop v1.0.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/gobuffalo/validate/v3 v3.3.3 // indirect github.com/gofrs/uuid v4.2.0+incompatible // indirect github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/securecookie v1.1.1 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/mattn/go-colorable v0.1.9 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/sergi/go-diff v1.3.1 // indirect github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d // indirect github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e // indirect github.com/spf13/pflag v1.0.5 // indirect golang.org/x/net v0.45.0 // indirect golang.org/x/sys v0.36.0 // indirect golang.org/x/term v0.35.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: go.sum ================================================ github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/gobuffalo/events v1.4.3 h1:JYDq7NbozP10zaN9Ijfem6Ozox2KacU2fU38RyquXM8= github.com/gobuffalo/events v1.4.3/go.mod h1:2BwfpV5X63t8xkUcVqIv4IbyAobJazRSVu1F1pgf3rc= github.com/gobuffalo/flect v0.3.0/go.mod h1:5pf3aGnsvqvCj50AVni7mJJF8ICxGZ8HomberC3pXLE= github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4= github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= github.com/gobuffalo/github_flavored_markdown v1.1.4 h1:WacrEGPXUDX+BpU1GM/Y0ADgMzESKNWls9hOTG1MHVs= github.com/gobuffalo/github_flavored_markdown v1.1.4/go.mod h1:Vl9686qrVVQou4GrHRK/KOG3jCZOKLUqV8MMOAYtlso= github.com/gobuffalo/helpers v0.6.10 h1:puKDCOrJ0EIq5ScnTRgKyvEZ05xQa+gwRGCpgoh6Ek8= github.com/gobuffalo/helpers v0.6.10/go.mod h1:r52L6VSnByLJFOmURp1irvzgSakk7RodChi1YbGwk8I= github.com/gobuffalo/httptest v1.5.2 h1:GpGy520SfY1QEmyPvaqmznTpG4gEQqQ82HtHqyNEreM= github.com/gobuffalo/httptest v1.5.2/go.mod h1:FA23yjsWLGj92mVV74Qtc8eqluc11VqcWr8/C1vxt4g= github.com/gobuffalo/logger v1.0.7 h1:LTLwWelETXDYyqF/ASf0nxaIcdEOIJNxRokPcfI/xbU= github.com/gobuffalo/logger v1.0.7/go.mod h1:u40u6Bq3VVvaMcy5sRBclD8SXhBYPS0Qk95ubt+1xJM= github.com/gobuffalo/plush/v5 v5.0.11 h1:FlThobIUreYx8fM4pH2Sug8TLXfNtmhqj6JO1Qs5jT8= github.com/gobuffalo/plush/v5 v5.0.11/go.mod h1:C08u/VEqzzPBXFF/yqs40P/5Cvc/zlZsMzhCxXyWJmU= github.com/gobuffalo/refresh v1.13.3 h1:HYQlI6RiqWUf2yzCXvUHAYqm9M9/teVnox+mjzo/9rQ= github.com/gobuffalo/refresh v1.13.3/go.mod h1:NkzgLKZGk5suOvgvOD0/VALog0fH29Ib7fwym9JmRxA= github.com/gobuffalo/tags/v3 v3.1.4 h1:X/ydLLPhgXV4h04Hp2xlbI2oc5MDaa7eub6zw8oHjsM= github.com/gobuffalo/tags/v3 v3.1.4/go.mod h1:ArRNo3ErlHO8BtdA0REaZxijuWnWzF6PUXngmMXd2I0= github.com/gobuffalo/validate/v3 v3.3.3 h1:o7wkIGSvZBYBd6ChQoLxkz2y1pfmhbI4jNJYh6PuNJ4= github.com/gobuffalo/validate/v3 v3.3.3/go.mod h1:YC7FsbJ/9hW/VjQdmXPvFqvRis4vrRYFxr69WiNZw6g= github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/monoculum/formam v3.5.5+incompatible h1:iPl5csfEN96G2N2mGu8V/ZB62XLf9ySTpC8KRH6qXec= github.com/monoculum/formam v3.5.5+incompatible/go.mod h1:RKgILGEJq24YyJ2ban8EO0RUVSJlF1pGsEvoLEACr/Q= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d h1:yKm7XZV6j9Ev6lojP2XaIshpT4ymkqhMeSghO5Ps00E= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e h1:qpG93cPwA5f7s/ZPBJnGOYQNK/vKsaDaseuKT5Asee8= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= github.com/spf13/cobra v1.6.0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: handler.go ================================================ package buffalo // Handler is the basis for all of Buffalo. A Handler // will be given a Context interface that represents the // give request/response. It is the responsibility of the // Handler to handle the request/response correctly. This // could mean rendering a template, JSON, etc... or it could // mean returning an error. /* func (c Context) error { return c.Render(http.StatusOK, render.String("Hello World!")) } func (c Context) error { return c.Redirect(http.StatusMovedPermanently, "http://github.com/gobuffalo/buffalo") } func (c Context) error { return c.Error(http.StatusUnprocessableEntity, fmt.Errorf("oops!!")) } */ type Handler func(Context) error ================================================ FILE: home.go ================================================ package buffalo import ( "github.com/gorilla/mux" ) /* TODO: consider to split out Home (or Router, whatever) from App #road-to-v1 Group and Domain based multi-homing are actually not an App if the concept of the App represents the application. The App should be only one for whole application. For an extreme example, App.Group().Stop() or even App.Group().Serve() are still valid function calls while they should not be allowed and the result could be strage. */ // Home is a container for Domains and Groups that independently serves a // group of pages with its own Middleware and ErrorHandlers. It is usually // a multi-homed server domain or group of paths under a certain prefix. // // While the App is for managing whole application life cycle along with its // default Home, including initializing and stopping its all components such // as listeners and long-running jobs, Home is only for a specific group of // services to serve its service logic efficiently. type Home struct { app *App // will replace App.root appSelf *App // temporary while the App is in action. // replace Options' Name, Host, and Prefix name string host string prefix string // moved from App // Middleware returns the current MiddlewareStack for the App/Group. Middleware *MiddlewareStack `json:"-"` ErrorHandlers ErrorHandlers `json:"-"` router *mux.Router filepaths []string } ================================================ FILE: internal/defaults/defaults.go ================================================ package defaults func String(s1, s2 string) string { if s1 == "" { return s2 } return s1 } func Int(i1, i2 int) int { if i1 == 0 { return i2 } return i1 } func Int64(i1, i2 int64) int64 { if i1 == 0 { return i2 } return i1 } func Float32(i1, i2 float32) float32 { if i1 == 0.0 { return i2 } return i1 } func Float64(i1, i2 float64) float64 { if i1 == 0.0 { return i2 } return i1 } ================================================ FILE: internal/defaults/defaults_test.go ================================================ package defaults import ( "testing" "github.com/stretchr/testify/assert" ) func Test_String(t *testing.T) { a := assert.New(t) a.Equal(String("", "foo"), "foo") a.Equal(String("bar", "foo"), "bar") var s string a.Equal(String(s, "foo"), "foo") } func Test_Int(t *testing.T) { a := assert.New(t) a.Equal(Int(0, 1), 1) a.Equal(Int(2, 1), 2) var s int a.Equal(Int(s, 1), 1) } func Test_Int64(t *testing.T) { a := assert.New(t) a.Equal(Int64(0, 1), int64(1)) a.Equal(Int64(2, 1), int64(2)) var s int64 a.Equal(Int64(s, 1), int64(1)) } func Test_Float32(t *testing.T) { a := assert.New(t) a.Equal(Float32(0, 1), float32(1)) a.Equal(Float32(2, 1), float32(2)) var s float32 a.Equal(Float32(s, 1), float32(1)) } func Test_Float64(t *testing.T) { a := assert.New(t) a.Equal(Float64(0, 1), float64(1)) a.Equal(Float64(2, 1), float64(2)) var s float64 a.Equal(Float64(s, 1), float64(1)) } ================================================ FILE: internal/env/env.go ================================================ // Package env provides environment variable utilities for Buffalo. // This package replaces the github.com/gobuffalo/envy dependency. package env import ( "fmt" "os" "path/filepath" "strings" "github.com/joho/godotenv" ) // Load loads .env file(s) into environment. // If no filenames are provided, it loads the .env file in the current directory. func Load(filenames ...string) error { return godotenv.Load(filenames...) } // Get returns the value of the environment variable named by key. // If the variable is not set or is empty, it returns the fallback value. func Get(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback } // MustGet returns the value of the environment variable named by key. // It returns an error if the variable is not set or is empty. func MustGet(key string) (string, error) { v := os.Getenv(key) if v == "" { return "", fmt.Errorf("environment variable %s is not set", key) } return v, nil } // Environ returns a copy of the environment variables. func Environ() []string { return os.Environ() } // GoPath returns the GOPATH environment variable. // If GOPATH is not set, it returns the default GOPATH: $HOME/go func GoPath() string { if gp := os.Getenv("GOPATH"); gp != "" { return gp } home, _ := os.UserHomeDir() return filepath.Join(home, "go") } // Set sets the environment variable named by key to value. func Set(key, value string) { os.Setenv(key, value) } // Temp executes f with temporarily modified environment variables. // After f returns, the environment is restored to its original state. func Temp(f func()) { oldEnv := os.Environ() defer func() { os.Clearenv() for _, e := range oldEnv { if i := strings.IndexByte(e, '='); i > 0 { os.Setenv(e[:i], e[i+1:]) } } }() os.Clearenv() f() } ================================================ FILE: internal/fakesmtp/connection.go ================================================ package fakesmtp import ( "bufio" "fmt" "net" ) // Connection of a client with our server type Connection struct { conn net.Conn address string time int64 bufin *bufio.Reader bufout *bufio.Writer } // write something to the client on the connection func (c *Connection) write(s string) { c.bufout.WriteString(s + "\r\n") c.bufout.Flush() } // read a string from the connected client func (c *Connection) read() string { reply, err := c.bufin.ReadString('\n') if err != nil { fmt.Println("e ", err) } return reply } ================================================ FILE: internal/fakesmtp/server.go ================================================ package fakesmtp // This server is inspired by https://github.com/andrewarrow/jungle_smtp // and most of its functionality have been taken from the original repo and updated to // work better for buffalo. import ( "bufio" "net" "strings" "sync" "time" ) // Server is our fake server that will be listening for SMTP connections. type Server struct { Listener net.Listener messages []string mutex sync.Mutex } // Start listens for connections on the given port func (s *Server) Start(port string) error { for { conn, err := s.Listener.Accept() if err != nil { return err } s.Handle(&Connection{ conn: conn, address: conn.RemoteAddr().String(), time: time.Now().Unix(), bufin: bufio.NewReader(conn), bufout: bufio.NewWriter(conn), }) } } // Handle a connection from a client func (s *Server) Handle(c *Connection) { s.mutex.Lock() defer s.mutex.Unlock() s.messages = append(s.messages, "") s.readHello(c) s.readSender(c) s.readRecipients(c) s.readData(c) c.conn.Close() } // Requests and notifies readed the Hello func (s *Server) readHello(c *Connection) { c.write("220 Welcome") text := c.read() s.addMessageLine(text) c.write("250 Received") } // readSender reads the Sender from the connection func (s *Server) readSender(c *Connection) { text := c.read() s.addMessageLine(text) c.write("250 Sender") } // readRecipients reads recipients from the connection func (s *Server) readRecipients(c *Connection) { text := c.read() s.addMessageLine(text) c.write("250 Recipient") text = c.read() for strings.Contains(text, "RCPT") { s.addMessageLine(text) c.write("250 Recipient") text = c.read() } } // readData reads the message data. func (s *Server) readData(c *Connection) { c.write("354 Ok Send data ending with .") for { text := c.read() bytes := []byte(text) s.addMessageLine(text) // 46 13 10 if bytes[0] == 46 && bytes[1] == 13 && bytes[2] == 10 { break } } c.write("250 server has transmitted the message") } // addMessageLine ads a line to the last message func (s *Server) addMessageLine(text string) { s.messages[len(s.Messages())-1] = s.LastMessage() + text } // LastMessage returns the last message on the server func (s *Server) LastMessage() string { if len(s.Messages()) == 0 { return "" } return s.Messages()[len(s.Messages())-1] } // Messages returns the list of messages on the server func (s *Server) Messages() []string { return s.messages } // Clear the server messages func (s *Server) Clear() { s.mutex.Lock() defer s.mutex.Unlock() s.messages = []string{} } // New returns a pointer to a new Server instance listening on the given port. func New(port string) (*Server, error) { s := &Server{messages: []string{}} listener, err := net.Listen("tcp", "0.0.0.0:"+port) if err != nil { return s, err } s.Listener = listener return s, nil } ================================================ FILE: internal/httpx/content_type.go ================================================ package httpx import ( "net/http" "strings" "github.com/gobuffalo/buffalo/internal/defaults" ) func ContentType(req *http.Request) string { ct := defaults.String(req.Header.Get("Content-Type"), req.Header.Get("Accept")) ct = strings.TrimSpace(ct) var cts []string if strings.Contains(ct, ",") { cts = strings.Split(ct, ",") } else { cts = strings.Split(ct, ";") } for _, c := range cts { c = strings.TrimSpace(c) if strings.HasPrefix(c, "*/*") { continue } return strings.ToLower(c) } if ct == "*/*" { return "" } return ct } ================================================ FILE: internal/httpx/content_type_test.go ================================================ package httpx import ( "net/http/httptest" "testing" "github.com/stretchr/testify/require" ) func Test_ContentType(t *testing.T) { r := require.New(t) table := []struct { Header string Value string Expected string }{ {"content-type", "a", "a"}, {"Content-Type", "c,d", "c"}, {"Content-Type", "e;f", "e"}, {"Content-Type", "", ""}, {"Accept", "", ""}, {"Accept", "*/*", ""}, {"Accept", "*/*;q=0.5, text/javascript, application/javascript, application/ecmascript, application/x-ecmascript", "text/javascript"}, {"accept", "text/javascript,application/javascript,application/ecmascript,application/x-ecmascript", "text/javascript"}, } for _, tt := range table { req := httptest.NewRequest("GET", "/", nil) req.Header.Set(tt.Header, tt.Value) r.Equal(tt.Expected, ContentType(req)) } } ================================================ FILE: internal/meta/meta.go ================================================ // Package meta provides application metadata for Buffalo's plugin system. package meta import ( "os" "path/filepath" "github.com/BurntSushi/toml" ) // BuildTags is a type alias for build tags used by plugins. type BuildTags []string // App holds metadata about the Buffalo application. type App struct { Root string `toml:"-"` WithPop bool `toml:"with_pop"` } // New creates App metadata for the given root path. // If root is "." or empty, uses current working directory. // First tries to load WithPop from config/buffalo-app.toml, // then falls back to detecting database.yml. func New(root string) App { if root == "." || root == "" { if pwd, err := os.Getwd(); err == nil { root = pwd } } app := App{Root: root} tomlPath := filepath.Join(root, "config", "buffalo-app.toml") if _, err := os.Stat(tomlPath); err == nil { // TOML config exists, use it and skip auto-detection toml.DecodeFile(tomlPath, &app) return app } // No TOML config, auto-detect from filesystem if _, err := os.Stat(filepath.Join(root, "database.yml")); err == nil { app.WithPop = true } return app } ================================================ FILE: internal/meta/meta_test.go ================================================ package meta import ( "os" "path/filepath" "testing" "github.com/stretchr/testify/require" ) func Test_New_Defaults(t *testing.T) { r := require.New(t) app := New("") r.NotEmpty(app.Root) r.False(app.WithPop) app = New(".") r.NotEmpty(app.Root) } func Test_New_With_DatabaseYML(t *testing.T) { r := require.New(t) tmp := t.TempDir() dbYML := filepath.Join(tmp, "database.yml") r.NoError(os.WriteFile(dbYML, []byte("test"), 0644)) app := New(tmp) r.Equal(tmp, app.Root) r.True(app.WithPop) } func Test_New_With_TOML(t *testing.T) { r := require.New(t) tmp := t.TempDir() configDir := filepath.Join(tmp, "config") r.NoError(os.MkdirAll(configDir, 0755)) tomlContent := `with_pop = true` tomlPath := filepath.Join(configDir, "buffalo-app.toml") r.NoError(os.WriteFile(tomlPath, []byte(tomlContent), 0644)) app := New(tmp) r.Equal(tmp, app.Root) r.True(app.WithPop) } func Test_New_TOML_Priority_Over_DatabaseYML(t *testing.T) { r := require.New(t) tmp := t.TempDir() // Create both files configDir := filepath.Join(tmp, "config") r.NoError(os.MkdirAll(configDir, 0755)) tomlContent := `with_pop = false` tomlPath := filepath.Join(configDir, "buffalo-app.toml") r.NoError(os.WriteFile(tomlPath, []byte(tomlContent), 0644)) dbYML := filepath.Join(tmp, "database.yml") r.NoError(os.WriteFile(dbYML, []byte("test"), 0644)) // TOML should take priority app := New(tmp) r.False(app.WithPop) } func Test_New_No_Files(t *testing.T) { r := require.New(t) tmp := t.TempDir() app := New(tmp) r.Equal(tmp, app.Root) r.False(app.WithPop) } ================================================ FILE: internal/nulls/nulls.go ================================================ package nulls import ( "database/sql/driver" "encoding/json" "time" ) // Time represents a time.Time that may be null. // It implements sql.Scanner and driver.Valuer interfaces. type Time struct { Time time.Time Valid bool // Valid is true if Time is not NULL } // Scan implements the sql.Scanner interface. func (t *Time) Scan(value any) error { if value == nil { t.Time, t.Valid = time.Time{}, false return nil } t.Valid = true switch v := value.(type) { case time.Time: t.Time = v case []byte: return t.Parse(string(v)) case string: return t.Parse(v) } return nil } // Parse tries to parse the string as a time using multiple formats. func (t *Time) Parse(s string) error { formats := []string{ time.RFC3339, "2006-01-02 15:04:05", "2006-01-02", "01/02/2006", "01/02/2006 15:04:05", "2006-01-02T15:04:05", time.RFC3339Nano, } for _, format := range formats { if tt, err := time.Parse(format, s); err == nil { t.Time = tt return nil } } return nil } // Value implements the driver.Valuer interface. func (t Time) Value() (driver.Value, error) { if !t.Valid { return nil, nil } return t.Time, nil } // UnmarshalJSON implements json.Unmarshaler. func (t *Time) UnmarshalJSON(data []byte) error { if string(data) == "null" { t.Time, t.Valid = time.Time{}, false return nil } if len(data) >= 2 && data[0] == '"' && data[len(data)-1] == '"' { data = data[1 : len(data)-1] } return t.Parse(string(data)) } // MarshalJSON implements json.Marshaler. func (t Time) MarshalJSON() ([]byte, error) { if !t.Valid { return []byte("null"), nil } return json.Marshal(t.Time) } // String implements fmt.Stringer. func (t Time) String() string { if !t.Valid { return "" } return t.Time.String() } // NewTime returns a new, properly initialized // Time object. func NewTime(t time.Time) Time { return Time{ Time: t, Valid: true, } } ================================================ FILE: internal/templates/error.dev.html ================================================ <%= status %> - ERROR!

<%= status %> - ERROR!

Error Trace

<%= error %>

Context

<%= inspect(context) %>

Parameters

<%= inspect(params) %>

Headers

<%= inspect(headers) %>

Form

<%= inspect(posted_form) %>

Routes

<%= for (r) in routes { %> <% } %>
METHOD PATH NAME HANDLER
<%= r.Method %> <%= if (r.Method !="GET" || r.Path ~="{" ) { %> <%= r.Path %> <% } else { %> <%= r.Path %> <% } %> <%= r.PathName %> <%= r.HandlerName %>
Powered by gobuffalo.io
================================================ FILE: internal/templates/error.prod.html ================================================

We're Sorry!


It looks like something went wrong! Don't worry, we are aware of the problem and are looking into it.

Sorry if this has caused you any problems. Please check back again later.

powered by gobuffalo.io

================================================ FILE: internal/templates/notfound.prod.html ================================================

Not Found


The page you're looking for does not exist, you may have mistyped the address or the page may have been moved.

powered by gobuffalo.io

================================================ FILE: internal/testdata/disk/file.txt ================================================ This file is on disk. ================================================ FILE: internal/testdata/disk/file2.txt ================================================ ================================================ FILE: internal/testdata/disk/under/sub/subfile ================================================ This file is on disk/sub. ================================================ FILE: internal/testdata/embedded/embed.go ================================================ package embedded import ( "embed" ) //go:embed * var files embed.FS func FS() embed.FS { return files } ================================================ FILE: internal/testdata/embedded/file.txt ================================================ This file is embedded. ================================================ FILE: internal/testdata/embedded/under/sub/subfile ================================================ This file is on embedded/sub. ================================================ FILE: internal/testdata/panic.txt ================================================ This file must not be accessible from buffalo.FS. ================================================ FILE: logger.go ================================================ package buffalo import ( "github.com/gobuffalo/logger" ) // Logger interface is used throughout Buffalo // apps to log a whole manner of things. type Logger = logger.FieldLogger ================================================ FILE: mail/README.md ================================================ # github.com/gobuffalo/buffalo/mail This package is intended to allow easy Email sending with Buffalo, it allows you to define your custom `mail.Sender` for the provider you would like to use. ## Generator ```bash buffalo generate mailer welcome_email ``` ## Example Usage ```go //actions/mail.go package x import ( "log" "net/http" "github.com/gobuffalo/buffalo/render" "github.com/gobuffalo/buffalo/internal/env" "github.com/gobuffalo/plush" "github.com/gobuffalo/buffalo/mail" "errors" "gitlab.com/wawandco/app/models" ) var smtp mail.Sender var r *render.Engine func init() { //Pulling config from the env. port := env.Get("SMTP_PORT", "1025") host := env.Get("SMTP_HOST", "localhost") user := env.Get("SMTP_USER", "") password := env.Get("SMTP_PASSWORD", "") var err error smtp, err = mail.NewSMTPSender(host, port, user, password) if err != nil { log.Fatal(err) } //The rendering engine, this is usually generated inside actions/render.go in your buffalo app. r = render.New(render.Options{ TemplatesFS: mailTemplates, }) } //SendContactMessage Sends contact message to contact@myapp.com func SendContactMessage(c *models.Contact) error { //Creates a new message m := mail.NewMessage() m.From = "sender@myapp.com" m.Subject = "New Contact" m.To = []string{"contact@myapp.com"} // Data that will be used inside the templates when rendering. data := map[string]interface{}{ "contact": c, } // You can add multiple bodies to the message you're creating to have content-types alternatives. err := m.AddBodies(data, r.HTML("mail/contact.html"), r.Plain("mail/contact.txt")) if err != nil { return err } err = smtp.Send(m) if err != nil { return err } return nil } ``` This `SendContactMessage` could be called by one of your actions, p.e. the action that handles your contact form submission. ```go //actions/contact.go ... func ContactFormHandler(c buffalo.Context) error { contact := &models.Contact{} c.Bind(contact) //Calling to send the message SendContactMessage(contact) return c.Redirect(http.StatusFound, "contact/thanks") } ... ``` If you're using Gmail or need to configure your SMTP connection you can use the Dialer property on the SMTPSender, p.e: (for Gmail) ```go ... var smtp mail.Sender func init() { port := env.Get("SMTP_PORT", "465") // or 587 with TLS host := env.Get("SMTP_HOST", "smtp.gmail.com") user := env.Get("SMTP_USER", "your@email.com") password := env.Get("SMTP_PASSWORD", "yourp4ssw0rd") var err error sender, err := mail.NewSMTPSender(host, port, user, password) sender.Dialer.SSL = true //or if TLS sender.Dialer.TLSConfig = &tls.Config{...} smtp = sender } ... ``` ================================================ FILE: mail/attachment.go ================================================ package mail import "io" // Attachment are files added into a email message type Attachment struct { Name string Reader io.Reader ContentType string Embedded bool } ================================================ FILE: mail/body.go ================================================ package mail // Body represents one of the bodies in the Message could be main or alternative type Body struct { Content string ContentType string } ================================================ FILE: mail/dialer.go ================================================ // Portions of this code are derived from the go-mail/mail project. // https://github.com/go-mail/mail (MIT License) package mail import ( "crypto/tls" "fmt" "io" "net" "net/smtp" "strings" "time" ) // Dialer connects to an SMTP server and sends emails. type Dialer struct { Host string Port int Username string Password string Auth smtp.Auth // SSL defines whether an SSL connection is used. It should be false in // most cases since the authentication mechanism should use the STARTTLS // extension instead. SSL bool // TLSConfig represents the TLS configuration used for the TLS (when the // STARTTLS extension is used) or SSL connection. TLSConfig *tls.Config // StartTLSPolicy represents the TLS security level required to communicate // with the SMTP server. Defaults to opportunistic STARTTLS. StartTLSPolicy StartTLSPolicy // LocalName is the hostname sent to the SMTP server with the HELO command. // By default, "localhost" is sent. LocalName string // Timeout to use for read/write operations. Defaults to 10 seconds, can // be set to 0 to disable timeouts. Timeout time.Duration // Whether we should retry mailing if the connection returned an error. RetryFailure bool } func newDialer(host string, port int, username, password string) *Dialer { return &Dialer{ Host: host, Port: port, Username: username, Password: password, SSL: port == 465, Timeout: 10 * time.Second, RetryFailure: true, } } // NetDialTimeout specifies the DialTimeout function to establish a connection // to the SMTP server. This can be used to override dialing in the case that a // proxy or other special behavior is needed. var NetDialTimeout = net.DialTimeout func (d *Dialer) dial() (sendCloser, error) { conn, err := NetDialTimeout("tcp", addr(d.Host, d.Port), d.Timeout) if err != nil { return nil, err } if d.SSL { conn = tlsClient(conn, d.tlsConfig()) } c, err := smtpNewClient(conn, d.Host) if err != nil { return nil, err } if d.Timeout > 0 { conn.SetDeadline(time.Now().Add(d.Timeout)) } if d.LocalName != "" { if err := c.Hello(d.LocalName); err != nil { return nil, err } } if !d.SSL && d.StartTLSPolicy != noStartTLS { ok, _ := c.Extension("STARTTLS") if !ok && d.StartTLSPolicy == mandatoryStartTLS { err := startTLSUnsupportedError{Policy: d.StartTLSPolicy} return nil, err } if ok { if err := c.StartTLS(d.tlsConfig()); err != nil { c.Close() return nil, err } } } if d.Auth == nil && d.Username != "" { if ok, auths := c.Extension("AUTH"); ok { if strings.Contains(auths, "CRAM-MD5") { d.Auth = smtp.CRAMMD5Auth(d.Username, d.Password) } else if strings.Contains(auths, "LOGIN") && !strings.Contains(auths, "PLAIN") { d.Auth = &loginAuth{ username: d.Username, password: d.Password, host: d.Host, } } else { d.Auth = smtp.PlainAuth("", d.Username, d.Password, d.Host) } } } if d.Auth != nil { if err = c.Auth(d.Auth); err != nil { c.Close() return nil, err } } return &smtpSender{c, conn, d}, nil } func (d *Dialer) tlsConfig() *tls.Config { if d.TLSConfig == nil { return &tls.Config{ServerName: d.Host} } return d.TLSConfig } // StartTLSPolicy represents the TLS security level required to communicate // with an SMTP server. type StartTLSPolicy int const ( opportunisticStartTLS StartTLSPolicy = iota mandatoryStartTLS noStartTLS = -1 ) func (policy *StartTLSPolicy) String() string { switch *policy { case opportunisticStartTLS: return "OpportunisticStartTLS" case mandatoryStartTLS: return "MandatoryStartTLS" case noStartTLS: return "NoStartTLS" default: return fmt.Sprintf("StartTLSPolicy:%v", *policy) } } type startTLSUnsupportedError struct { Policy StartTLSPolicy } func (e startTLSUnsupportedError) Error() string { return "gomail: " + e.Policy.String() + " required, but SMTP server does not support STARTTLS" } func addr(host string, port int) string { return fmt.Sprintf("%s:%d", host, port) } func (d *Dialer) dialAndSend(m ...*smtpMessage) error { s, err := d.dial() if err != nil { return err } defer s.Close() for _, err := range sendSMTP(s, m...) { if err != nil { return err } } return nil } type smtpSender struct { smtpClient conn net.Conn d *Dialer } func (c *smtpSender) retryError(err error) bool { if !c.d.RetryFailure { return false } if nerr, ok := err.(net.Error); ok && nerr.Timeout() { return true } return err == io.EOF } func (c *smtpSender) Send(from string, to []string, msg io.WriterTo) error { if c.d.Timeout > 0 { c.conn.SetDeadline(time.Now().Add(c.d.Timeout)) } if err := c.Mail(from); err != nil { if c.retryError(err) { sc, derr := c.d.dial() if derr == nil { if s, ok := sc.(*smtpSender); ok { *c = *s return c.Send(from, to, msg) } } } return err } for _, addr := range to { if err := c.Rcpt(addr); err != nil { return err } } w, err := c.Data() if err != nil { return err } if _, err = msg.WriteTo(w); err != nil { w.Close() return err } return w.Close() } func (c *smtpSender) Close() error { return c.Quit() } // Stubbed out for tests. var ( tlsClient = tls.Client smtpNewClient = func(conn net.Conn, host string) (smtpClient, error) { return smtp.NewClient(conn, host) } ) type smtpClient interface { Hello(string) error Extension(string) (bool, string) StartTLS(*tls.Config) error Auth(smtp.Auth) error Mail(string) error Rcpt(string) error Data() (io.WriteCloser, error) Quit() error Close() error } type sendCloser interface { sender Close() error } type sender interface { Send(from string, to []string, msg io.WriterTo) error } ================================================ FILE: mail/mail.go ================================================ // Package mail provides email sending functionality for Buffalo applications. // It supports SMTP delivery with customizable configuration including TLS/SSL, // authentication, and batch sending capabilities. // // Portions of the SMTP implementation are derived from the go-mail/mail project // (https://github.com/go-mail/mail) under the MIT License. // // TODO: Properly encode filenames for non-ASCII characters. // TODO: Properly encode email addresses for non-ASCII characters. // TODO: Test embedded files and attachments for their existence before sending. // TODO: Allow supplying an io.Reader when embedding and attaching files. package mail import ( "context" "maps" "sync" "github.com/gobuffalo/buffalo" "github.com/gobuffalo/buffalo/render" ) // NewMessage builds a new message. func NewMessage() Message { return Message{ Context: context.Background(), Headers: map[string]string{}, Data: render.Data{}, moot: &sync.RWMutex{}, } } // NewFromData builds a new message with raw template data given func NewFromData(data render.Data) Message { m := NewMessage() m.Data = maps.Clone(data) return m } // New builds a new message with the current buffalo.Context func New(c buffalo.Context) Message { m := NewFromData(c.Data()) m.Context = c return m } ================================================ FILE: mail/mail_test.go ================================================ package mail import ( "html/template" "net/http" "testing" "github.com/gobuffalo/buffalo" "github.com/gobuffalo/buffalo/render" "github.com/gobuffalo/httptest" "github.com/stretchr/testify/require" ) func Test_NewFromData(t *testing.T) { r := require.New(t) m := NewFromData(map[string]any{ "foo": "bar", }) r.Equal("bar", m.Data["foo"]) } func Test_New(t *testing.T) { r := require.New(t) var m Message app := buffalo.New(buffalo.Options{}) app.GET("/", func(c buffalo.Context) error { c.Set("foo", "bar") m = New(c) return c.Render(http.StatusOK, render.String("")) }) w := httptest.New(app) w.HTML("/").Get() r.NotNil(m) r.Equal("bar", m.Data["foo"]) rp, ok := m.Data["rootPath"].(buffalo.RouteHelperFunc) r.True(ok) x, err := rp(map[string]any{}) r.NoError(err) r.Equal(template.HTML("/"), x) } ================================================ FILE: mail/message.go ================================================ package mail import ( "bytes" "context" "io" "maps" "sync" "github.com/gobuffalo/buffalo/render" ) // Message represents an Email message type Message struct { Context context.Context From string To []string CC []string Bcc []string Subject string Headers map[string]string Data render.Data Bodies []Body Attachments []Attachment moot *sync.RWMutex } func (m *Message) merge(data render.Data) render.Data { m.moot.Lock() d := maps.Clone(m.Data) m.moot.Unlock() maps.Copy(d, data) return d } // AddBody the message by receiving a renderer and rendering data, first message will be // used as the main message Body rest of them will be passed as alternative bodies on the // email message func (m *Message) AddBody(r render.Renderer, data render.Data) error { buf := bytes.NewBuffer([]byte{}) err := r.Render(buf, m.merge(data)) if err != nil { return err } m.Bodies = append(m.Bodies, Body{ Content: buf.String(), ContentType: r.ContentType(), }) return nil } // AddBodies Allows to add multiple bodies to the message, it returns errors that // could happen in the rendering. func (m *Message) AddBodies(data render.Data, renderers ...render.Renderer) error { for _, r := range renderers { err := m.AddBody(r, data) if err != nil { return err } } return nil } // AddAttachment adds the attachment to the list of attachments the Message has. func (m *Message) AddAttachment(name, contentType string, r io.Reader) error { m.Attachments = append(m.Attachments, Attachment{ Name: name, ContentType: contentType, Reader: r, Embedded: false, }) return nil } // AddEmbedded adds the attachment to the list of attachments // the Message has and uses inline instead of attachement property. func (m *Message) AddEmbedded(name string, r io.Reader) error { m.Attachments = append(m.Attachments, Attachment{ Name: name, Reader: r, Embedded: true, }) return nil } // SetHeader sets the heder field and value for the message func (m *Message) SetHeader(field, value string) { m.Headers[field] = value } ================================================ FILE: mail/sender.go ================================================ package mail // Sender defines the interface for sending individual email messages. type Sender interface { // Send delivers a single email message. Send(Message) error } // BatchSender defines the interface for sending multiple email messages. type BatchSender interface { Sender // SendBatch delivers multiple messages. It returns per-message errors // and any general error that prevented sending entirely. SendBatch(messages ...Message) (errorsByMessages []error, generalError error) } ================================================ FILE: mail/smtp_auth.go ================================================ // Portions of this code are derived from the go-mail/mail project. // https://github.com/go-mail/mail (MIT License) package mail import ( "bytes" "fmt" "net/smtp" "slices" ) // loginAuth is an smtp.Auth that implements the LOGIN authentication mechanism. type loginAuth struct { username string password string host string } func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) { if !server.TLS { if !slices.Contains(server.Auth, "LOGIN") { return "", nil, fmt.Errorf("gomail: unencrypted connection") } } if server.Name != a.host { return "", nil, fmt.Errorf("gomail: wrong host name") } return "LOGIN", nil, nil } func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) { if !more { return nil, nil } switch { case bytes.Equal(fromServer, []byte("Username:")): return []byte(a.username), nil case bytes.Equal(fromServer, []byte("Password:")): return []byte(a.password), nil default: return nil, fmt.Errorf("gomail: unexpected server challenge: %s", fromServer) } } ================================================ FILE: mail/smtp_errors.go ================================================ // Portions of this code are derived from the go-mail/mail project. // https://github.com/go-mail/mail (MIT License) package mail import "fmt" // sendError represents the failure to transmit a Message, detailing the cause // of the failure and index of the Message within a batch. type sendError struct { // Index specifies the index of the Message within a batch. Index uint Cause error } func (err *sendError) Error() string { return fmt.Sprintf("gomail: could not send email %d: %v", err.Index+1, err.Cause) } ================================================ FILE: mail/smtp_message.go ================================================ // Portions of this code are derived from the go-mail/mail project. // https://github.com/go-mail/mail (MIT License) package mail import ( "bytes" "io" "os" "path/filepath" "time" ) // smtpMessage represents an email for SMTP transmission. type smtpMessage struct { header smtpHeader parts []*part attachments []*file embedded []*file charset string encoding encoding hEncoder mimeEncoder buf bytes.Buffer boundary string } type smtpHeader map[string][]string type part struct { contentType string copier func(io.Writer) error encoding encoding } func newSMTPMessage(settings ...messageSetting) *smtpMessage { m := &smtpMessage{ header: make(smtpHeader), charset: "UTF-8", encoding: encodingQuotedPrintable, } m.applySettings(settings) if m.encoding == encodingBase64 { m.hEncoder = bEncoding } else { m.hEncoder = qEncoding } return m } func (m *smtpMessage) applySettings(settings []messageSetting) { for _, s := range settings { s(m) } } type messageSetting func(m *smtpMessage) type encoding string const ( encodingQuotedPrintable encoding = "quoted-printable" encodingBase64 encoding = "base64" encodingUnencoded encoding = "8bit" ) func (m *smtpMessage) setHeader(field string, value ...string) { m.encodeHeader(value) m.header[field] = value } func (m *smtpMessage) encodeHeader(values []string) { for i := range values { values[i] = m.encodeString(values[i]) } } func (m *smtpMessage) encodeString(value string) string { return m.hEncoder.Encode(m.charset, value) } func (m *smtpMessage) setBody(contentType, body string, settings ...partSetting) { m.setBodyWriter(contentType, newCopier(body), settings...) } func (m *smtpMessage) setBodyWriter(contentType string, f func(io.Writer) error, settings ...partSetting) { m.parts = []*part{m.newPart(contentType, f, settings)} } func (m *smtpMessage) addAlternative(contentType, body string, settings ...partSetting) { m.addAlternativeWriter(contentType, newCopier(body), settings...) } func newCopier(s string) func(io.Writer) error { return func(w io.Writer) error { _, err := io.WriteString(w, s) return err } } func (m *smtpMessage) addAlternativeWriter(contentType string, f func(io.Writer) error, settings ...partSetting) { m.parts = append(m.parts, m.newPart(contentType, f, settings)) } func (m *smtpMessage) newPart(contentType string, f func(io.Writer) error, settings []partSetting) *part { p := &part{ contentType: contentType, copier: f, encoding: m.encoding, } for _, s := range settings { s(p) } return p } type partSetting func(*part) func setPartEncoding(e encoding) partSetting { return partSetting(func(p *part) { p.encoding = e }) } type file struct { Name string Header map[string][]string CopyFunc func(w io.Writer) error } func (f *file) setHeader(field, value string) { f.Header[field] = []string{value} } type fileSetting func(*file) func setCopyFunc(f func(io.Writer) error) fileSetting { return func(fi *file) { fi.CopyFunc = f } } func (m *smtpMessage) attach(filename string, settings ...fileSetting) { m.attachments = m.appendFile(m.attachments, fileFromFilename(filename), settings) } func (m *smtpMessage) embed(filename string, settings ...fileSetting) { m.embedded = m.appendFile(m.embedded, fileFromFilename(filename), settings) } func fileFromFilename(name string) *file { return &file{ Name: filepath.Base(name), Header: make(map[string][]string), CopyFunc: func(w io.Writer) error { h, err := os.Open(name) if err != nil { return err } if _, err := io.Copy(w, h); err != nil { h.Close() return err } return h.Close() }, } } func (m *smtpMessage) formatDate(date time.Time) string { return date.Format(time.RFC1123Z) } func (m *smtpMessage) appendFile(list []*file, f *file, settings []fileSetting) []*file { for _, s := range settings { s(f) } if list == nil { return []*file{f} } return append(list, f) } ================================================ FILE: mail/smtp_mime.go ================================================ // Portions of this code are derived from the go-mail/mail project. // https://github.com/go-mail/mail (MIT License) package mail import ( "mime" "mime/quotedprintable" "strings" ) var newQPWriter = quotedprintable.NewWriter type mimeEncoder struct { mime.WordEncoder } var ( bEncoding = mimeEncoder{mime.BEncoding} qEncoding = mimeEncoder{mime.QEncoding} lastIndexByte = strings.LastIndexByte ) ================================================ FILE: mail/smtp_send.go ================================================ // Portions of this code are derived from the go-mail/mail project. // https://github.com/go-mail/mail (MIT License) package mail import ( "fmt" stdmail "net/mail" "slices" ) // sendSMTP sends emails using the given Sender. func sendSMTP(s sender, msg ...*smtpMessage) []error { errors := make([]error, len(msg)) for i, m := range msg { if err := sendSingle(s, m); err != nil { errors[i] = &sendError{Cause: err, Index: uint(i)} } } return errors } func sendSingle(s sender, m *smtpMessage) error { from, err := m.getFrom() if err != nil { return err } to, err := m.getRecipients() if err != nil { return err } if err := s.Send(from, to, m); err != nil { return err } return nil } func (m *smtpMessage) getFrom() (string, error) { from := m.header["Sender"] if len(from) == 0 { from = m.header["From"] if len(from) == 0 { return "", fmt.Errorf(`gomail: invalid message, "From" field is absent`) } } return parseAddress(from[0]) } func (m *smtpMessage) getRecipients() ([]string, error) { n := 0 for _, field := range []string{"To", "Cc", "Bcc"} { if addresses, ok := m.header[field]; ok { n += len(addresses) } } list := make([]string, 0, n) for _, field := range []string{"To", "Cc", "Bcc"} { if addresses, ok := m.header[field]; ok { for _, a := range addresses { addr, err := parseAddress(a) if err != nil { return nil, err } list = addAddress(list, addr) } } } return list, nil } func addAddress(list []string, addr string) []string { if slices.Contains(list, addr) { return list } return append(list, addr) } func parseAddress(field string) (string, error) { addr, err := stdmail.ParseAddress(field) if err != nil { return "", fmt.Errorf("gomail: invalid address %q: %v", field, err) } return addr.Address, nil } ================================================ FILE: mail/smtp_sender.go ================================================ package mail import ( "fmt" "io" "strconv" ) // SMTPSender delivers emails via an SMTP server. type SMTPSender struct { // Dialer configures the connection to the SMTP server. Dialer *Dialer } // Send delivers a single message via SMTP. func (sm SMTPSender) Send(message Message) error { return sm.Dialer.dialAndSend(sm.prepareMessage(message)) } // SendBatch delivers multiple messages using a single SMTP connection. // Returns per-message errors and any general connection error. func (sm SMTPSender) SendBatch(messages ...Message) (errorsByMessages []error, generalError error) { preparedMessages := make([]*smtpMessage, len(messages)) for i, message := range messages { preparedMessages[i] = sm.prepareMessage(message) } s, err := sm.Dialer.dial() if err != nil { return nil, err } defer s.Close() return sendSMTP(s, preparedMessages...), nil } func (sm SMTPSender) prepareMessage(message Message) *smtpMessage { gm := newSMTPMessage() gm.setHeader("From", message.From) gm.setHeader("To", message.To...) gm.setHeader("Subject", message.Subject) gm.setHeader("Cc", message.CC...) gm.setHeader("Bcc", message.Bcc...) sm.addBodies(message, gm) sm.addAttachments(message, gm) for field, value := range message.Headers { gm.setHeader(field, value) } return gm } func (sm SMTPSender) addBodies(message Message, gm *smtpMessage) { if len(message.Bodies) == 0 { return } mainBody := message.Bodies[0] gm.setBody(mainBody.ContentType, mainBody.Content, setPartEncoding(encodingUnencoded)) for i := 1; i < len(message.Bodies); i++ { alt := message.Bodies[i] gm.addAlternative(alt.ContentType, alt.Content, setPartEncoding(encodingUnencoded)) } } func (sm SMTPSender) addAttachments(message Message, gm *smtpMessage) { for _, at := range message.Attachments { currentAttachement := at settings := setCopyFunc(func(w io.Writer) error { _, err := io.Copy(w, currentAttachement.Reader) return err }) if currentAttachement.Embedded { gm.embed(currentAttachement.Name, settings) } else { gm.attach(currentAttachement.Name, settings) } } } // NewSMTPSender builds a SMTP mail based in passed config. func NewSMTPSender(host string, port string, user string, password string) (SMTPSender, error) { iport, err := strconv.Atoi(port) if err != nil { return SMTPSender{}, fmt.Errorf("invalid port for the SMTP mail") } dialer := &Dialer{ Host: host, Port: iport, } if user != "" { dialer.Username = user dialer.Password = password } return SMTPSender{ Dialer: dialer, }, nil } ================================================ FILE: mail/smtp_sender_test.go ================================================ package mail_test import ( "bytes" "testing" "github.com/gobuffalo/buffalo/internal/fakesmtp" "github.com/gobuffalo/buffalo/mail" "github.com/gobuffalo/buffalo/render" "github.com/stretchr/testify/require" ) var sender mail.Sender var rend *render.Engine var smtpServer *fakesmtp.Server const smtpPort = "2002" func init() { rend = render.New(render.Options{}) smtpServer, _ = fakesmtp.New(smtpPort) sender, _ = mail.NewSMTPSender("127.0.0.1", smtpPort, "username", "password") go smtpServer.Start(smtpPort) } func TestSendPlain(t *testing.T) { smtpServer.Clear() r := require.New(t) m := mail.NewMessage() m.From = "mark@example.com" m.To = []string{"something@something.com"} m.Subject = "Cool Message" m.CC = []string{"other@other.com", "my@other.com"} m.Bcc = []string{"secret@other.com"} m.AddAttachment("someFile.txt", "text/plain", bytes.NewBuffer([]byte("hello"))) m.AddAttachment("otherFile.txt", "text/plain", bytes.NewBuffer([]byte("bye"))) m.AddEmbedded("test.jpg", bytes.NewBuffer([]byte("not a real image"))) m.AddBody(rend.String("Hello <%= Name %>"), render.Data{"Name": "Antonio"}) r.Equal(m.Bodies[0].Content, "Hello Antonio") m.SetHeader("X-SMTPAPI", `{"send_at": 1409348513}`) err := sender.Send(m) r.Nil(err) lastMessage := smtpServer.LastMessage() r.Contains(lastMessage, "FROM:") r.Contains(lastMessage, "RCPT TO:") r.Contains(lastMessage, "RCPT TO:") r.Contains(lastMessage, "RCPT TO:") r.Contains(lastMessage, "Subject: Cool Message") r.Contains(lastMessage, "Cc: other@other.com, my@other.com") r.Contains(lastMessage, "Content-Type: text/plain") r.Contains(lastMessage, "Hello Antonio") r.Contains(lastMessage, "Content-Disposition: attachment; filename=\"someFile.txt\"") r.Contains(lastMessage, "aGVsbG8=") //base64 of the file content r.Contains(lastMessage, "Content-Disposition: attachment; filename=\"otherFile.txt\"") r.Contains(lastMessage, "Ynll") //base64 of the file content r.Contains(lastMessage, "Content-Disposition: inline; filename=\"test.jpg\"") r.Contains(lastMessage, "bm90IGEgcmVhbCBpbWFnZQ==") //base64 of the file content r.Contains(lastMessage, `X-SMTPAPI: {"send_at": 1409348513}`) } ================================================ FILE: mail/smtp_writeto.go ================================================ // Portions of this code are derived from the go-mail/mail project. // https://github.com/go-mail/mail (MIT License) package mail import ( "encoding/base64" "fmt" "io" "mime" "mime/multipart" "path/filepath" "strings" "time" ) func (m *smtpMessage) WriteTo(w io.Writer) (int64, error) { mw := &messageWriter{w: w} mw.writeMessage(m) return mw.n, mw.err } func (w *messageWriter) writeMessage(m *smtpMessage) { if _, ok := m.header["MIME-Version"]; !ok { w.writeString("MIME-Version: 1.0\r\n") } if _, ok := m.header["Date"]; !ok { w.writeHeader("Date", m.formatDate(now())) } w.writeHeaders(m.header) if m.hasMixedPart() { w.openMultipart("mixed", m.boundary) } if m.hasRelatedPart() { w.openMultipart("related", m.boundary) } if m.hasAlternativePart() { w.openMultipart("alternative", m.boundary) } for _, part := range m.parts { w.writePart(part, m.charset) } if m.hasAlternativePart() { w.closeMultipart() } w.addFiles(m.embedded, false) if m.hasRelatedPart() { w.closeMultipart() } w.addFiles(m.attachments, true) if m.hasMixedPart() { w.closeMultipart() } } func (m *smtpMessage) hasMixedPart() bool { return (len(m.parts) > 0 && len(m.attachments) > 0) || len(m.attachments) > 1 } func (m *smtpMessage) hasRelatedPart() bool { return (len(m.parts) > 0 && len(m.embedded) > 0) || len(m.embedded) > 1 } func (m *smtpMessage) hasAlternativePart() bool { return len(m.parts) > 1 } type messageWriter struct { w io.Writer n int64 writers [3]*multipart.Writer partWriter io.Writer depth uint8 err error } func (w *messageWriter) openMultipart(mimeType, boundary string) { mw := multipart.NewWriter(w) if boundary != "" { mw.SetBoundary(boundary) } contentType := "multipart/" + mimeType + ";\r\n boundary=" + mw.Boundary() w.writers[w.depth] = mw if w.depth == 0 { w.writeHeader("Content-Type", contentType) w.writeString("\r\n") } else { w.createPart(map[string][]string{ "Content-Type": {contentType}, }) } w.depth++ } func (w *messageWriter) createPart(h map[string][]string) { w.partWriter, w.err = w.writers[w.depth-1].CreatePart(h) } func (w *messageWriter) closeMultipart() { if w.depth > 0 { w.writers[w.depth-1].Close() w.depth-- } } func (w *messageWriter) writePart(p *part, charset string) { w.writeHeaders(map[string][]string{ "Content-Type": {p.contentType + "; charset=" + charset}, "Content-Transfer-Encoding": {string(p.encoding)}, }) w.writeBody(p.copier, p.encoding) } func (w *messageWriter) addFiles(files []*file, isAttachment bool) { for _, f := range files { if _, ok := f.Header["Content-Type"]; !ok { mediaType := mime.TypeByExtension(filepath.Ext(f.Name)) if mediaType == "" { mediaType = "application/octet-stream" } f.setHeader("Content-Type", mediaType+`; name="`+f.Name+`"`) } if _, ok := f.Header["Content-Transfer-Encoding"]; !ok { f.setHeader("Content-Transfer-Encoding", string(encodingBase64)) } if _, ok := f.Header["Content-Disposition"]; !ok { var disp string if isAttachment { disp = "attachment" } else { disp = "inline" } f.setHeader("Content-Disposition", disp+`; filename="`+f.Name+`"`) } if !isAttachment { if _, ok := f.Header["Content-ID"]; !ok { f.setHeader("Content-ID", "<"+f.Name+">") } } w.writeHeaders(f.Header) w.writeBody(f.CopyFunc, encodingBase64) } } func (w *messageWriter) Write(p []byte) (int, error) { if w.err != nil { return 0, fmt.Errorf("gomail: cannot write as writer is in error") } var n int n, w.err = w.w.Write(p) w.n += int64(n) return n, w.err } func (w *messageWriter) writeString(s string) { if w.err != nil { return } var n int n, w.err = io.WriteString(w.w, s) w.n += int64(n) } func (w *messageWriter) writeHeader(k string, v ...string) { w.writeString(k) if len(v) == 0 { w.writeString(":\r\n") return } w.writeString(": ") charsLeft := 76 - len(k) - len(": ") for i, s := range v { if charsLeft < 1 { if i == 0 { w.writeString("\r\n ") } else { w.writeString(",\r\n ") } charsLeft = 75 } else if i != 0 { w.writeString(", ") charsLeft -= 2 } for len(s) > charsLeft { s = w.writeLine(s, charsLeft) charsLeft = 75 } w.writeString(s) if i := lastIndexByte(s, '\n'); i != -1 { charsLeft = 75 - (len(s) - i - 1) } else { charsLeft -= len(s) } } w.writeString("\r\n") } func (w *messageWriter) writeLine(s string, charsLeft int) string { if i := strings.IndexByte(s, '\n'); i != -1 && i < charsLeft { w.writeString(s[:i+1]) return s[i+1:] } for i := charsLeft - 1; i >= 0; i-- { if s[i] == ' ' { w.writeString(s[:i]) w.writeString("\r\n ") return s[i+1:] } } for i := 75; i < len(s); i++ { if s[i] == ' ' { w.writeString(s[:i]) w.writeString("\r\n ") return s[i+1:] } if s[i] == '\n' { w.writeString(s[:i+1]) return s[i+1:] } } w.writeString(s) return "" } func (w *messageWriter) writeHeaders(h map[string][]string) { if w.depth == 0 { for k, v := range h { if k != "Bcc" { w.writeHeader(k, v...) } } } else { w.createPart(h) } } func (w *messageWriter) writeBody(f func(io.Writer) error, enc encoding) { var subWriter io.Writer if w.depth == 0 { w.writeString("\r\n") subWriter = w.w } else { subWriter = w.partWriter } if enc == encodingBase64 { wc := base64.NewEncoder(base64.StdEncoding, newBase64LineWriter(subWriter)) w.err = f(wc) wc.Close() } else if enc == encodingUnencoded { w.err = f(subWriter) } else { wc := newQPWriter(subWriter) w.err = f(wc) wc.Close() } } const maxLineLen = 76 type base64LineWriter struct { w io.Writer lineLen int } func newBase64LineWriter(w io.Writer) *base64LineWriter { return &base64LineWriter{w: w} } func (w *base64LineWriter) Write(p []byte) (int, error) { n := 0 for len(p)+w.lineLen > maxLineLen { w.w.Write(p[:maxLineLen-w.lineLen]) w.w.Write([]byte("\r\n")) p = p[maxLineLen-w.lineLen:] n += maxLineLen - w.lineLen w.lineLen = 0 } w.w.Write(p) w.lineLen += len(p) return n + len(p), nil } var now = time.Now ================================================ FILE: method_override.go ================================================ package buffalo import ( "net/http" "github.com/gobuffalo/buffalo/internal/defaults" ) // MethodOverride is the default implementation for the // Options#MethodOverride. By default it will look for a form value // name `_method` and change the request method if that is // present and the original request is of type "POST". This is // added automatically when using `New` Buffalo, unless // an alternative is defined in the Options. func MethodOverride(res http.ResponseWriter, req *http.Request) { if req.Method == "POST" { req.Method = defaults.String(req.FormValue("_method"), "POST") req.Form.Del("_method") req.PostForm.Del("_method") } } ================================================ FILE: method_override_test.go ================================================ package buffalo import ( "net/http" "net/url" "testing" "github.com/gobuffalo/buffalo/render" "github.com/gobuffalo/httptest" "github.com/stretchr/testify/require" ) func Test_MethodOverride(t *testing.T) { r := require.New(t) a := New(Options{}) a.PUT("/", func(c Context) error { return c.Render(http.StatusOK, render.String("you put me!")) }) w := httptest.New(a) res := w.HTML("/").Post(url.Values{"_method": []string{"PUT"}}) r.Equal(http.StatusOK, res.Code) r.Equal("you put me!", res.Body.String()) } ================================================ FILE: middleware.go ================================================ package buffalo import ( "maps" "net/http" "reflect" "runtime" "strings" "sync" ) // MiddlewareFunc defines the interface for a piece of Buffalo // Middleware. /* func DoSomething(next Handler) Handler { return func(c Context) error { // do something before calling the next handler err := next(c) // do something after call the handler return err } } */ type MiddlewareFunc func(Handler) Handler const funcKeyDelimeter = ":" // Use the specified Middleware for the App. // When defined on an `*App` the specified middleware will be // inherited by any `Group` calls that are made on that on // the App. func (a *App) Use(mw ...MiddlewareFunc) { a.Middleware.Use(mw...) } // MiddlewareStack manages the middleware stack for an App/Group. type MiddlewareStack struct { stack []MiddlewareFunc skips map[string]bool } func (ms MiddlewareStack) String() string { s := []string{} for _, m := range ms.stack { s = append(s, funcKey(m)) } return strings.Join(s, "\n") } func (ms *MiddlewareStack) clone() *MiddlewareStack { n := newMiddlewareStack() n.stack = append(n.stack, ms.stack...) maps.Copy(n.skips, ms.skips) 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. func (ms *MiddlewareStack) Clear() { ms.stack = []MiddlewareFunc{} ms.skips = map[string]bool{} } // Use the specified Middleware for the App. // When defined on an `*App` the specified middleware will be // inherited by any `Group` calls that are made on that on // the App. func (ms *MiddlewareStack) Use(mw ...MiddlewareFunc) { ms.stack = append(ms.stack, mw...) } // Remove the specified Middleware(s) for the App/group. This is useful when // the middleware will be skipped by the entire group. /* a.Middleware.Remove(Authorization) */ func (ms *MiddlewareStack) Remove(mws ...MiddlewareFunc) { result := []MiddlewareFunc{} base: for _, existing := range ms.stack { for _, banned := range mws { if funcKey(existing) == funcKey(banned) { continue base } } result = append(result, existing) } ms.stack = result } // Skip a specified piece of middleware the specified Handlers. // This is useful for things like wrapping your application in an // authorization middleware, but skipping it for things the home // page, the login page, etc... /* a.Middleware.Skip(Authorization, HomeHandler, LoginHandler, RegistrationHandler) */ func (ms *MiddlewareStack) Skip(mw MiddlewareFunc, handlers ...Handler) { for _, h := range handlers { key := funcKey(mw, h) ms.skips[key] = true } } // Replace a piece of middleware with another piece of middleware. Great for // testing. func (ms *MiddlewareStack) Replace(mw1 MiddlewareFunc, mw2 MiddlewareFunc) { m1k := funcKey(mw1) stack := []MiddlewareFunc{} for _, mw := range ms.stack { if funcKey(mw) == m1k { stack = append(stack, mw2) } else { stack = append(stack, mw) } } ms.stack = stack } // assertMiddleware is a hidden middleware that works just befor and after the // actual handler runs to make it sure everything is OK with the Handler // specification. // // It writes response header with `http.StatusOK` if the request handler exited // without error but the response status is still zero. Setting response is the // responsibility of handler but this middleware make it sure the response // should be compatible with middleware specification. // // See also: https://github.com/gobuffalo/buffalo/issues/2339 func assertMiddleware(handler Handler) Handler { return func(c Context) error { err := handler(c) if err != nil { return err } if res, ok := c.Response().(*Response); ok { if res.Status == 0 { res.WriteHeader(http.StatusOK) c.Logger().Debug("warning: handler exited without setting the response status. 200 OK will be used.") } } return err } } func (ms *MiddlewareStack) handler(info RouteInfo) Handler { tstack := []MiddlewareFunc{assertMiddleware} if len(ms.stack) > 0 { sl := len(ms.stack) - 1 for i := sl; i >= 0; i-- { mw := ms.stack[i] key := funcKey(mw, info) if !ms.skips[key] { tstack = append(tstack, mw) } } } h := info.Handler for _, mw := range tstack { h = mw(h) } return h } func newMiddlewareStack(mws ...MiddlewareFunc) *MiddlewareStack { return &MiddlewareStack{ stack: mws, skips: map[string]bool{}, } } func funcKey(funcs ...any) string { names := []string{} for _, f := range funcs { if n, ok := f.(RouteInfo); ok { names = append(names, n.HandlerName) continue } rv := reflect.ValueOf(f) ptr := rv.Pointer() keyMapMutex.Lock() if n, ok := keyMap[ptr]; ok { keyMapMutex.Unlock() names = append(names, n) continue } keyMapMutex.Unlock() n := ptrName(ptr) keyMapMutex.Lock() keyMap[ptr] = n keyMapMutex.Unlock() names = append(names, n) } return strings.Join(names, funcKeyDelimeter) } func ptrName(ptr uintptr) string { fnc := runtime.FuncForPC(ptr) n := fnc.Name() n = strings.Replace(n, "-fm", "", 1) n = strings.Replace(n, "(", "", 1) n = strings.Replace(n, ")", "", 1) return n } func setFuncKey(f any, name string) { rv := reflect.ValueOf(f) if rv.Kind() == reflect.Ptr { rv = rv.Elem() } ptr := rv.Pointer() keyMapMutex.Lock() keyMap[ptr] = name keyMapMutex.Unlock() } var keyMap = map[uintptr]string{} var keyMapMutex = sync.Mutex{} ================================================ FILE: middleware_test.go ================================================ package buffalo import ( "fmt" "net/http" "testing" "github.com/gobuffalo/buffalo/render" "github.com/gobuffalo/httptest" "github.com/stretchr/testify/require" ) // Test_App_Use tests that middleware gets added func Test_App_Use(t *testing.T) { r := require.New(t) log := []string{} a := New(Options{}) a.Use(func(h Handler) Handler { return func(c Context) error { log = append(log, "start") err := h(c) log = append(log, "end") return err } }) a.GET("/", func(c Context) error { log = append(log, "handler") return nil }) w := httptest.New(a) w.HTML("/").Get() r.Len(log, 3) r.Equal([]string{"start", "handler", "end"}, log) } // Test_Middleware_Replace tests that middleware gets added func Test_Middleware_Replace(t *testing.T) { r := require.New(t) log := []string{} a := New(Options{}) mw1 := func(h Handler) Handler { return func(c Context) error { log = append(log, "m1 start") err := h(c) log = append(log, "m1 end") return err } } mw2 := func(h Handler) Handler { return func(c Context) error { log = append(log, "m2 start") err := h(c) log = append(log, "m2 end") return err } } a.Use(mw1) a.Middleware.Replace(mw1, mw2) a.GET("/", func(c Context) error { log = append(log, "handler") return nil }) w := httptest.New(a) w.HTML("/").Get() r.Len(log, 3) r.Equal([]string{"m2 start", "handler", "m2 end"}, log) } // Test_Middleware_Skip tests that middleware gets skipped func Test_Middleware_Skip(t *testing.T) { r := require.New(t) log := []string{} a := New(Options{}) 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 } } mw2 := func(h Handler) Handler { return func(c Context) error { log = append(log, "mw2 start") err := h(c) log = append(log, "mw2 end") return err } } a.Use(mw1) a.Use(mw2) h1 := func(c Context) error { log = append(log, "h1") return nil } h2 := func(c Context) error { log = append(log, "h2") return nil } a.GET("/h1", h1) a.GET("/h2", h2) a.Middleware.Skip(mw2, h2) w := httptest.New(a) w.HTML("/h2").Get() r.Len(log, 3) r.Equal([]string{"mw1 start", "h2", "mw1 end"}, log) log = []string{} w.HTML("/h1").Get() r.Len(log, 5) 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(http.StatusOK, render.String("show")) } func (ur *carsResource) List(c Context) error { return c.Render(http.StatusOK, 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 = &carsResource{} g := a.Resource("/autos", cr) g.Use(mw1) var ur Resource = &carsResource{} g = a.Resource("/cars", ur) g.Use(mw1) // fmt.Println("set up skip") g.Middleware.Skip(mw1, ur.Show) w := httptest.New(a) // fmt.Println("make autos call") log = []string{} res := w.HTML("/autos/1").Get() r.Len(log, 2) r.Equal("show", res.Body.String()) // fmt.Println("make list call") log = []string{} res = w.HTML("/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.HTML("/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) mws := newMiddlewareStack() mw := func(h Handler) Handler { return h } mws.Use(mw) mws.Skip(mw, voidHandler) r.Len(mws.stack, 1) r.Len(mws.skips, 1) mws.Clear() r.Len(mws.stack, 0) r.Len(mws.skips, 0) } func Test_Middleware_Remove(t *testing.T) { r := require.New(t) log := []string{} mw1 := func(h Handler) Handler { log = append(log, "mw1") return h } mw2 := func(h Handler) Handler { log = append(log, "mw2") return h } a := New(Options{}) a.Use(mw2) a.Use(mw1) var cr Resource = &carsResource{} g := a.Resource("/autos", cr) g.Middleware.Remove(mw2) a.Resource("/all_log_autos", cr) w := httptest.New(a) ng := a.Resource("/no_log_autos", cr) ng.Middleware.Remove(mw1, mw2) _ = w.HTML("/autos/1").Get() r.Len(log, 1) r.Equal("mw1", log[0]) log = []string{} _ = w.HTML("/all_log_autos/1").Get() r.Len(log, 2) r.Contains(log, "mw2") r.Contains(log, "mw1") log = []string{} _ = w.HTML("/no_log_autos/1").Get() r.Len(log, 0) } func Test_AssertMiddleware_NilStatus200(t *testing.T) { r := require.New(t) var status int a := New(Options{}) a.Use(func(h Handler) Handler { return func(c Context) error { err := h(c) res, ok := c.Response().(*Response) r.True(ok) status = res.Status return err } }) a.GET("/200", func(c Context) error { c.Response().WriteHeader(http.StatusOK) // explicitly set return nil }) a.GET("/404", func(c Context) error { c.Response().WriteHeader(http.StatusNotFound) //explicitly set return nil }) a.GET("/nil", func(c Context) error { return nil // return nil without setting response status. should be OK }) a.GET("/500", func(c Context) error { return fmt.Errorf("error") // return error }) a.GET("/502", func(c Context) error { return HTTPError{Status: http.StatusBadGateway} // return HTTPError }) a.GET("/panic", func(c Context) error { panic("hoy hoy") }) tests := []struct { path string code int status int }{ {"/200", http.StatusOK, http.StatusOK}, // when the handler set response code explicitly (e.g. 200, 404) {"/404", http.StatusNotFound, http.StatusNotFound}, {"/nil", http.StatusOK, http.StatusOK}, // when the handler returns nil without setting status code {"/502", http.StatusBadGateway, 0}, // set by defaultErrorHandler, when the handler just returns error {"/500", http.StatusInternalServerError, 0}, // set by defaultErrorHandler, when the handler returns HTTPError {"/panic", http.StatusInternalServerError, 0}, // set by PanicHandler } w := httptest.New(a) for _, tc := range tests { res := w.HTML("%s", tc.path).Get() r.Equal(tc.status, status) r.Equal(tc.code, res.Code) } } ================================================ FILE: not_found_test.go ================================================ package buffalo import ( "encoding/json" "net/http" "testing" "github.com/gobuffalo/httptest" "github.com/stretchr/testify/require" ) func Test_App_Dev_NotFound(t *testing.T) { r := require.New(t) a := New(Options{}) a.Env = "development" a.GET("/foo", func(c Context) error { return nil }) w := httptest.New(a) res := w.HTML("/bad").Get() body := res.Body.String() r.Contains(body, "404 - ERROR!") r.Contains(body, "/foo") r.Equal(http.StatusNotFound, res.Code) } func Test_App_Dev_NotFound_JSON(t *testing.T) { r := require.New(t) a := New(Options{}) a.Env = "development" a.GET("/foo", func(c Context) error { return nil }) w := httptest.New(a) res := w.JSON("/bad").Get() r.Equal(http.StatusNotFound, res.Code) jb := map[string]any{} err := json.NewDecoder(res.Body).Decode(&jb) r.NoError(err) r.Equal(float64(http.StatusNotFound), jb["code"]) } func Test_App_Override_NotFound(t *testing.T) { r := require.New(t) a := New(Options{}) a.ErrorHandlers[http.StatusNotFound] = func(status int, err error, c Context) error { c.Response().WriteHeader(http.StatusNotFound) c.Response().Write([]byte("oops!!!")) return nil } a.GET("/foo", func(c Context) error { return nil }) w := httptest.New(a) res := w.HTML("/bad").Get() r.Equal(http.StatusNotFound, res.Code) body := res.Body.String() r.Equal(body, "oops!!!") r.NotContains(body, "/foo") } ================================================ FILE: options.go ================================================ package buffalo import ( "context" "fmt" "net/http" "strings" "github.com/gobuffalo/buffalo/internal/defaults" "github.com/gobuffalo/buffalo/internal/env" "github.com/gobuffalo/buffalo/worker" "github.com/gobuffalo/logger" "github.com/gorilla/sessions" ) // Options are used to configure and define how your application should run. type Options struct { Name string `json:"name"` // Addr is the bind address provided to http.Server. Default is "127.0.0.1:3000" // Can be set using ENV vars "ADDR" and "PORT". Addr string `json:"addr"` // Host that this application will be available at. Default is "http://127.0.0.1:[$PORT|3000]". Host string `json:"host"` // Env is the "environment" in which the App is running. Default is "development". Env string `json:"env"` // LogLvl defaults to logger.DebugLvl. LogLvl logger.Level `json:"log_lvl"` // Logger to be used with the application. A default one is provided. Logger Logger `json:"-"` // MethodOverride allows for changing of the request method type. See the default // implementation at buffalo.MethodOverride MethodOverride http.HandlerFunc `json:"-"` // SessionStore is the `github.com/gorilla/sessions` store used to back // the session. It defaults to use a cookie store and the ENV variable // `SESSION_SECRET`. SessionStore sessions.Store `json:"-"` // SessionName is the name of the session cookie that is set. This defaults // to "_buffalo_session". SessionName string `json:"session_name"` // Timeout in second for ongoing requests when shutdown the server. // The default value is 60. TimeoutSecondShutdown int `json:"timeout_second_shutdown"` // Worker implements the Worker interface and can process tasks in the background. // Default is "github.com/gobuffalo/worker.Simple. Worker worker.Worker `json:"-"` // WorkerOff tells App.Start() whether to start the Worker process or not. Default is "false". WorkerOff bool `json:"worker_off"` // PreHandlers are http.Handlers that are called between the http.Server // and the buffalo Application. PreHandlers []http.Handler `json:"-"` // PreWare takes an http.Handler and returns an http.Handler // and acts as a pseudo-middleware between the http.Server and // a Buffalo application. PreWares []PreWare `json:"-"` // CompressFiles enables gzip compression of static files served by ServeFiles using // gorilla's CompressHandler (https://godoc.org/github.com/gorilla/handlers#CompressHandler). // Default is "false". CompressFiles bool `json:"compress_files"` Prefix string `json:"prefix"` Context context.Context `json:"-"` cancel context.CancelFunc } // PreWare takes an http.Handler and returns an http.Handler // and acts as a pseudo-middleware between the http.Server and // a Buffalo application. type PreWare func(http.Handler) http.Handler // NewOptions returns a new Options instance with sensible defaults func NewOptions() Options { return optionsWithDefaults(Options{}) } func optionsWithDefaults(opts Options) Options { opts.Env = defaults.String(opts.Env, env.Get("GO_ENV", "development")) opts.Name = defaults.String(opts.Name, "/") addr := "0.0.0.0" if opts.Env == "development" { addr = "127.0.0.1" } envAddr := env.Get("ADDR", addr) if strings.HasPrefix(envAddr, "unix:") { // UNIX domain socket doesn't have a port opts.Addr = envAddr } else { // TCP case opts.Addr = defaults.String(opts.Addr, fmt.Sprintf("%s:%s", envAddr, env.Get("PORT", "3000"))) } opts.Host = defaults.String(opts.Host, env.Get("HOST", fmt.Sprintf("http://127.0.0.1:%s", env.Get("PORT", "3000")))) if opts.PreWares == nil { opts.PreWares = []PreWare{} } if opts.PreHandlers == nil { opts.PreHandlers = []http.Handler{} } if opts.Context == nil { opts.Context = context.Background() } opts.Context, opts.cancel = context.WithCancel(opts.Context) if opts.Logger == nil { if lvl, err := env.MustGet("LOG_LEVEL"); err == nil { opts.LogLvl, err = logger.ParseLevel(lvl) if err != nil { opts.LogLvl = logger.DebugLevel } } if opts.LogLvl == 0 { opts.LogLvl = logger.DebugLevel } opts.Logger = logger.New(opts.LogLvl) } if opts.SessionStore == nil { secret := env.Get("SESSION_SECRET", "") if secret == "" && (opts.Env == "development" || opts.Env == "test") { secret = "buffalo-secret" } // In production a SESSION_SECRET must be set! if secret == "" { opts.Logger.Warn("Unless you set SESSION_SECRET env variable, your session storage is not protected!") } cookieStore := sessions.NewCookieStore([]byte(secret)) //Cookie secure attributes, see: https://www.owasp.org/index.php/Testing_for_cookies_attributes_(OTG-SESS-002) cookieStore.Options.HttpOnly = true if opts.Env == "production" { cookieStore.Options.Secure = true } opts.SessionStore = cookieStore } opts.SessionName = defaults.String(opts.SessionName, "_buffalo_session") if opts.Worker == nil { w := worker.NewSimpleWithContext(opts.Context) w.Logger = opts.Logger opts.Worker = w } opts.TimeoutSecondShutdown = defaults.Int(opts.TimeoutSecondShutdown, 60) return opts } ================================================ FILE: options_test.go ================================================ package buffalo import ( "net/http" "strings" "testing" "github.com/gobuffalo/buffalo/internal/env" "github.com/stretchr/testify/require" ) func TestOptions_NewOptions(t *testing.T) { tests := []struct { name string env string secret string expectErr string }{ {name: "Development doesn't fail with no secret", env: "development", secret: "", expectErr: "securecookie:"}, {name: "Development doesn't fail with secret set", env: "development", secret: "secrets", expectErr: "securecookie:"}, {name: "Test doesn't fail with secret set", env: "test", secret: "", expectErr: "securecookie:"}, {name: "Test doesn't fail with secret set", env: "test", secret: "secrets", expectErr: "securecookie:"}, {name: "Production fails with no secret", env: "production", secret: "", expectErr: "securecookie:"}, {name: "Production doesn't fail with secret set", env: "production", secret: "secrets", expectErr: "securecookie:"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { r := require.New(t) env.Temp(func() { env.Set("GO_ENV", test.env) env.Set("SESSION_SECRET", test.secret) opts := NewOptions() req, _ := http.NewRequest("GET", "/", strings.NewReader("")) req.AddCookie(&http.Cookie{Name: "_buffalo_session"}) _, err := opts.SessionStore.New(req, "_buffalo_session") r.Error(err) r.Contains(err.Error(), test.expectErr) }) }) } } ================================================ FILE: plugins/cache.go ================================================ package plugins import ( "crypto/sha256" "encoding/json" "fmt" "io" "os" "os/user" "path/filepath" "sync" "github.com/gobuffalo/buffalo/internal/env" ) type cachedPlugin struct { Commands Commands `json:"commands"` CheckSum string `json:"check_sum"` } type cachedPlugins map[string]cachedPlugin // CachePath returns the path to the plugins cache var CachePath = func() string { home := "." if usr, err := user.Current(); err == nil { home = usr.HomeDir } return filepath.Join(home, ".buffalo", "plugin.cache") }() var cacheMoot sync.RWMutex var cacheOn = env.Get("BUFFALO_PLUGIN_CACHE", "on") var cache = func() cachedPlugins { m := cachedPlugins{} if cacheOn != "on" { return m } f, err := os.Open(CachePath) if err != nil { return m } defer f.Close() if err := json.NewDecoder(f).Decode(&m); err != nil { f.Close() os.Remove(f.Name()) } return m }() func findInCache(path string) (cachedPlugin, bool) { cacheMoot.RLock() defer cacheMoot.RUnlock() cp, ok := cache[path] return cp, ok } func saveCache() error { if cacheOn != "on" { return nil } cacheMoot.Lock() defer cacheMoot.Unlock() os.MkdirAll(filepath.Dir(CachePath), 0744) f, err := os.Create(CachePath) if err != nil { return err } return json.NewEncoder(f).Encode(cache) } func sum(path string) string { f, err := os.Open(path) if err != nil { return "" } defer f.Close() hash := sha256.New() if _, err := io.Copy(hash, f); err != nil { return "" } sum := hash.Sum(nil) s := fmt.Sprintf("%x", sum) return s } func addToCache(path string, cp cachedPlugin) { if cp.CheckSum == "" { cp.CheckSum = sum(path) } cacheMoot.Lock() defer cacheMoot.Unlock() cache[path] = cp } ================================================ FILE: plugins/command.go ================================================ package plugins // Command that the plugin supplies type Command struct { // Name "foo" Name string `json:"name"` // UseCommand "bar" UseCommand string `json:"use_command"` // BuffaloCommand "generate" BuffaloCommand string `json:"buffalo_command"` // Description "generates a foo" Description string `json:"description,omitempty"` Aliases []string `json:"aliases,omitempty"` Binary string `json:"-"` Flags []string `json:"flags,omitempty"` // Filters events to listen to ("" or "*") is all events ListenFor string `json:"listen_for,omitempty"` } // Commands is a slice of Command type Commands []Command ================================================ FILE: plugins/decorate.go ================================================ package plugins import ( "fmt" "os" "os/exec" "path/filepath" "runtime" "strings" "github.com/gobuffalo/buffalo/internal/env" "github.com/spf13/cobra" ) // ErrPlugMissing error for when a plugin is missing var ErrPlugMissing = fmt.Errorf("plugin missing") // Decorate setup cobra Commands for plugins func Decorate(c Command) *cobra.Command { var flags []string if len(c.Flags) > 0 { flags = append(flags, c.Flags...) } cc := &cobra.Command{ Use: c.Name, Short: fmt.Sprintf("[PLUGIN] %s", c.Description), Aliases: c.Aliases, RunE: func(cmd *cobra.Command, args []string) error { plugCmd := c.Name if c.UseCommand != "" { plugCmd = c.UseCommand } ax := []string{plugCmd} if plugCmd == "-" { ax = []string{} } ax = append(ax, args...) ax = append(ax, flags...) bin, err := LookPath(c.Binary) if err != nil { return err } ex := exec.Command(bin, ax...) if runtime.GOOS != "windows" { ex.Env = append(env.Environ(), "BUFFALO_PLUGIN=1") } ex.Stdin = os.Stdin ex.Stdout = os.Stdout ex.Stderr = os.Stderr return log(strings.Join(ex.Args, " "), ex.Run) }, } cc.DisableFlagParsing = true return cc } // LookPath for plugin func LookPath(s string) (string, error) { if _, err := os.Stat(s); err == nil { return s, nil } if lp, err := exec.LookPath(s); err == nil { return lp, err } var bin string pwd, err := os.Getwd() if err != nil { return "", err } var looks []string if from, err := env.MustGet("BUFFALO_PLUGIN_PATH"); err == nil { looks = append(looks, from) } else { looks = []string{filepath.Join(pwd, "plugins"), filepath.Join(env.GoPath(), "bin"), env.Get("PATH", "")} } for _, p := range looks { lp := filepath.Join(p, s) if lp, err = filepath.EvalSymlinks(lp); err == nil { bin = lp break } } if len(bin) == 0 { return "", ErrPlugMissing } return bin, nil } ================================================ FILE: plugins/events.go ================================================ package plugins const ( EvtSetupStarted = "buffalo-plugins:setup:started" EvtSetupErr = "buffalo-plugins:setup:err" EvtSetupFinished = "buffalo-plugins:setup:finished" ) ================================================ FILE: plugins/log.go ================================================ //go:build !debug package plugins func log(_ string, fn func() error) error { return fn() } ================================================ FILE: plugins/log_debug.go ================================================ //go:build debug package plugins import ( "fmt" "time" ) func log(name string, fn func() error) error { start := time.Now() defer fmt.Println(name, time.Now().Sub(start)) return fn() } ================================================ FILE: plugins/plugcmds/available.go ================================================ package plugcmds import ( "encoding/json" "fmt" "io" "os" "github.com/gobuffalo/buffalo/plugins" "github.com/gobuffalo/events" "github.com/spf13/cobra" ) // NewAvailable returns a fully formed Available type func NewAvailable() *Available { return &Available{ plugs: plugMap{}, } } // Available used to manage all of the available commands // for the plugin type Available struct { plugs plugMap } type plug struct { BuffaloCommand string Cmd *cobra.Command Plugin plugins.Command } func (p plug) String() string { b, _ := json.Marshal(p.Plugin) return string(b) } // Cmd returns the "available" command func (a *Available) Cmd() *cobra.Command { return &cobra.Command{ Use: "available", Short: "a list of available buffalo plugins", RunE: func(cmd *cobra.Command, args []string) error { return a.Encode(os.Stdout) }, } } // Commands returns all of the commands that are available func (a *Available) Commands() []*cobra.Command { cmds := []*cobra.Command{a.Cmd()} a.plugs.Range(func(_ string, p plug) bool { cmds = append(cmds, p.Cmd) return true }) return cmds } // Add a new command to this list of available ones. // The bufCmd should corresponding buffalo command that // command should live below. // // Special "commands": // // "root" - is the `buffalo` command // "events" - listens for emitted events func (a *Available) Add(bufCmd string, cmd *cobra.Command) error { if len(cmd.Aliases) == 0 { cmd.Aliases = []string{} } p := plug{ BuffaloCommand: bufCmd, Cmd: cmd, Plugin: plugins.Command{ Name: cmd.Use, BuffaloCommand: bufCmd, Description: cmd.Short, Aliases: cmd.Aliases, UseCommand: cmd.Use, }, } a.plugs.Store(p.String(), p) return nil } // Mount all of the commands that are available // on to the other command. This is the recommended // approach for using Available. // // a.Mount(rootCmd) func (a *Available) Mount(cmd *cobra.Command) { // mount all the cmds on to the cobra command cmd.AddCommand(a.Cmd()) a.plugs.Range(func(_ string, p plug) bool { cmd.AddCommand(p.Cmd) return true }) } // Encode into the required Buffalo plugins available // format func (a *Available) Encode(w io.Writer) error { var plugs plugins.Commands a.plugs.Range(func(_ string, p plug) bool { plugs = append(plugs, p.Plugin) return true }) return json.NewEncoder(w).Encode(plugs) } // Listen adds a command for github.com/gobuffalo/events. // This will listen for ALL events. Use ListenFor to // listen to a regex of events. func (a *Available) Listen(fn func(e events.Event) error) error { return a.Add("events", buildListen(fn)) } // ListenFor adds a command for github.com/gobuffalo/events. // This will only listen for events that match the regex provided. func (a *Available) ListenFor(rx string, fn func(e events.Event) error) error { cmd := buildListen(fn) p := plug{ BuffaloCommand: "events", Cmd: cmd, Plugin: plugins.Command{ Name: cmd.Use, BuffaloCommand: "events", Description: cmd.Short, Aliases: cmd.Aliases, UseCommand: cmd.Use, ListenFor: rx, }, } a.plugs.Store(p.String(), p) return nil } func buildListen(fn func(e events.Event) error) *cobra.Command { listenCmd := &cobra.Command{ Use: "listen", Short: "listens to github.com/gobuffalo/events", Aliases: []string{}, RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { return fmt.Errorf("must pass a payload") } e := events.Event{} err := json.Unmarshal([]byte(args[0]), &e) if err != nil { return err } return fn(e) }, } return listenCmd } ================================================ FILE: plugins/plugcmds/available_test.go ================================================ package plugcmds import ( "bytes" "strings" "testing" "github.com/gobuffalo/events" "github.com/spf13/cobra" "github.com/stretchr/testify/require" ) func Test_Available_Add(t *testing.T) { r := require.New(t) a := NewAvailable() err := a.Add("generate", &cobra.Command{ Use: "foo", Short: "generates foo", Aliases: []string{"f"}, }) r.NoError(err) r.Len(a.Commands(), 2) } func Test_Available_Encode(t *testing.T) { r := require.New(t) bb := &bytes.Buffer{} a := NewAvailable() err := a.Add("generate", &cobra.Command{ Use: "foo", Short: "generates foo", Aliases: []string{"f"}, }) r.NoError(err) r.NoError(a.Encode(bb)) const exp = `[{"name":"foo","use_command":"foo","buffalo_command":"generate","description":"generates foo","aliases":["f"]}]` r.Equal(exp, strings.TrimSpace(bb.String())) } func Test_Available_Listen(t *testing.T) { r := require.New(t) a := NewAvailable() err := a.Listen(func(e events.Event) error { return nil }) r.NoError(err) r.Len(a.Commands(), 2) } ================================================ FILE: plugins/plugcmds/plug_map.go ================================================ //go:generate mapgen -name "plug" -zero "plug{}" -go-type "plug" -pkg "" -a "nil" -b "nil" -c "nil" -bb "nil" -destination "plugcmds" // Code generated by github.com/gobuffalo/mapgen. DO NOT EDIT. package plugcmds import ( "slices" "sync" ) // plugMap wraps sync.Map and uses the following types: // key: string // value: plug type plugMap struct { data sync.Map } // Delete the key from the map func (m *plugMap) Delete(key string) { m.data.Delete(key) } // Load the key from the map. // Returns plug or bool. // A false return indicates either the key was not found // or the value is not of type plug func (m *plugMap) Load(key string) (plug, bool) { i, ok := m.data.Load(key) if !ok { return plug{}, false } s, ok := i.(plug) return s, ok } // LoadOrStore will return an existing key or // store the value if not already in the map func (m *plugMap) LoadOrStore(key string, value plug) (plug, bool) { i, _ := m.data.LoadOrStore(key, value) s, ok := i.(plug) return s, ok } // Range over the plug values in the map func (m *plugMap) Range(f func(key string, value plug) bool) { m.data.Range(func(k, v any) bool { key, ok := k.(string) if !ok { return false } value, ok := v.(plug) if !ok { return false } return f(key, value) }) } // Store a plug in the map func (m *plugMap) Store(key string, value plug) { m.data.Store(key, value) } // Keys returns a list of keys in the map func (m *plugMap) Keys() []string { var keys []string m.Range(func(key string, value plug) bool { keys = append(keys, key) return true }) slices.Sort(keys) return keys } ================================================ FILE: plugins/plugcmds/plug_map_test.go ================================================ // Code generated by github.com/gobuffalo/mapgen. DO NOT EDIT. package plugcmds import ( "slices" "testing" "github.com/stretchr/testify/require" ) func Test_plugMap(t *testing.T) { r := require.New(t) sm := &plugMap{} sm.Store("a", plug{}) s, ok := sm.Load("a") r.True(ok) r.Equal(plug{}, s) s, ok = sm.LoadOrStore("b", plug{}) r.True(ok) r.Equal(plug{}, s) s, ok = sm.LoadOrStore("b", plug{}) r.True(ok) r.Equal(plug{}, s) var keys []string sm.Range(func(key string, value plug) bool { keys = append(keys, key) return true }) slices.Sort(keys) r.Equal(sm.Keys(), keys) sm.Delete("b") r.Equal([]string{"a", "b"}, keys) sm.Delete("b") _, ok = sm.Load("b") r.False(ok) p := plug{ BuffaloCommand: "foo", } func(m *plugMap) { m.Store("c", p) }(sm) s, ok = sm.Load("c") r.True(ok) r.Equal(p, s) } ================================================ FILE: plugins/plugdeps/command.go ================================================ package plugdeps import "encoding/json" // Command is the plugin command you want to control further type Command struct { Name string `toml:"name" json:"name"` Flags []string `toml:"flags,omitempty" json:"flags,omitempty"` Commands []Command `toml:"command,omitempty" json:"commands,omitempty"` } // String implementation of fmt.Stringer func (p Command) String() string { b, _ := json.Marshal(p) return string(b) } ================================================ FILE: plugins/plugdeps/plugdeps.go ================================================ package plugdeps import ( "fmt" "io/fs" "os" "path/filepath" "strings" "github.com/gobuffalo/buffalo/internal/meta" ) // ErrMissingConfig is if config/buffalo-plugins.toml file is not found. Use plugdeps#On(app) to test if plugdeps are being used var ErrMissingConfig = fmt.Errorf("could not find a buffalo-plugins config file at %s", ConfigPath(meta.New("."))) // List all of the plugins the application depends on. Will return ErrMissingConfig // if the app is not using config/buffalo-plugins.toml to manage their plugins. // Use plugdeps#On(app) to test if plugdeps are being used. func List(app meta.App) (*Plugins, error) { plugs := New() if app.WithPop { plugs.Add(pop) } lp, err := listLocal(app) if err != nil { return plugs, err } plugs.Add(lp.List()...) if !On(app) { return plugs, ErrMissingConfig } p := ConfigPath(app) tf, err := os.Open(p) if err != nil { return plugs, err } if err := plugs.Decode(tf); err != nil { return plugs, err } return plugs, nil } func listLocal(app meta.App) (*Plugins, error) { plugs := New() pRoot := filepath.Join(app.Root, "plugins") if _, err := os.Stat(pRoot); err != nil { return plugs, nil } err := filepath.WalkDir(pRoot, func(path string, d fs.DirEntry, err error) error { if d.IsDir() { return nil } if !strings.HasPrefix(d.Name(), "buffalo-") { return nil } plugs.Add(Plugin{ Binary: d.Name(), Local: "." + strings.TrimPrefix(path, app.Root), }) return nil }) if err != nil { return plugs, err } return plugs, nil } // ConfigPath returns the path to the config/buffalo-plugins.toml file // relative to the app func ConfigPath(app meta.App) string { return filepath.Join(app.Root, "config", "buffalo-plugins.toml") } // On checks for the existence of config/buffalo-plugins.toml if this // file exists its contents will be used to list plugins. If the file is not // found, then the BUFFALO_PLUGIN_PATH and ./plugins folders are consulted. func On(app meta.App) bool { _, err := os.Stat(ConfigPath(app)) return err == nil } ================================================ FILE: plugins/plugdeps/plugdeps_test.go ================================================ package plugdeps import ( "errors" "os" "path/filepath" "testing" "github.com/gobuffalo/buffalo/internal/meta" "github.com/stretchr/testify/require" ) var heroku = Plugin{ Binary: "buffalo-heroku", GoGet: "github.com/gobuffalo/buffalo-heroku", Commands: []Command{ {Name: "deploy", Flags: []string{"-v"}}, }, Tags: []string{"foo", "bar"}, } var local = Plugin{ Binary: "buffalo-hello.rb", Local: "./plugins/buffalo-hello.rb", } func Test_ConfigPath(t *testing.T) { r := require.New(t) x := ConfigPath(meta.App{Root: "foo"}) r.Equal(x, filepath.Join("foo", "config", "buffalo-plugins.toml")) } func Test_List_Off(t *testing.T) { r := require.New(t) app := meta.App{} plugs, err := List(app) r.Error(err) r.True(errors.Is(err, ErrMissingConfig)) r.Len(plugs.List(), 0) } func Test_List_On(t *testing.T) { r := require.New(t) app := meta.New(os.TempDir()) p := ConfigPath(app) r.NoError(os.MkdirAll(filepath.Dir(p), 0755)) f, err := os.Create(p) r.NoError(err) f.WriteString(eToml) r.NoError(f.Close()) plugs, err := List(app) r.NoError(err) r.Len(plugs.List(), 3) } const eToml = `[[plugin]] binary = "buffalo-hello.rb" local = "./plugins/buffalo-hello.rb" [[plugin]] binary = "buffalo-heroku" go_get = "github.com/gobuffalo/buffalo-heroku" tags = ["foo", "bar"] [[plugin.command]] name = "deploy" flags = ["-v"] [[plugin]] binary = "buffalo-pop" go_get = "github.com/gobuffalo/buffalo-pop/v2" ` ================================================ FILE: plugins/plugdeps/plugin.go ================================================ package plugdeps import ( "encoding/json" "github.com/gobuffalo/buffalo/internal/meta" ) // Plugin represents a Go plugin for Buffalo applications type Plugin struct { Binary string `toml:"binary" json:"binary"` GoGet string `toml:"go_get,omitempty" json:"go_get,omitempty"` Local string `toml:"local,omitempty" json:"local,omitempty"` Commands []Command `toml:"command,omitempty" json:"commands,omitempty"` Tags meta.BuildTags `toml:"tags,omitempty" json:"tags,omitempty"` } // String implementation of fmt.Stringer func (p Plugin) String() string { b, _ := json.Marshal(p) return string(b) } func (p Plugin) key() string { return p.Binary + p.GoGet + p.Local } ================================================ FILE: plugins/plugdeps/plugin_test.go ================================================ package plugdeps ================================================ FILE: plugins/plugdeps/plugins.go ================================================ package plugdeps import ( "io" "sort" "sync" "github.com/BurntSushi/toml" ) // Plugins manages the config/buffalo-plugins.toml file // as well as the plugins available from the file. type Plugins struct { plugins map[string]Plugin moot *sync.RWMutex } // Encode the list of plugins, in TOML format, to the reader func (plugs *Plugins) Encode(w io.Writer) error { tp := tomlPlugins{ Plugins: plugs.List(), } if err := toml.NewEncoder(w).Encode(tp); err != nil { return err } return nil } // Decode the list of plugins, in TOML format, from the reader func (plugs *Plugins) Decode(r io.Reader) error { tp := &tomlPlugins{ Plugins: []Plugin{}, } if _, err := toml.NewDecoder(r).Decode(tp); err != nil { return err } for _, p := range tp.Plugins { plugs.Add(p) } return nil } // List of dependent plugins listed in order of Plugin.String() func (plugs *Plugins) List() []Plugin { m := map[string]Plugin{} plugs.moot.RLock() for _, p := range plugs.plugins { m[p.key()] = p } plugs.moot.RUnlock() var pp []Plugin for _, v := range m { pp = append(pp, v) } sort.Slice(pp, func(a, b int) bool { return pp[a].Binary < pp[b].Binary }) return pp } // Add plugin(s) to the list of dependencies func (plugs *Plugins) Add(pp ...Plugin) { plugs.moot.Lock() for _, p := range pp { plugs.plugins[p.key()] = p } plugs.moot.Unlock() } // Remove plugin(s) from the list of dependencies func (plugs *Plugins) Remove(pp ...Plugin) { plugs.moot.Lock() for _, p := range pp { delete(plugs.plugins, p.key()) } plugs.moot.Unlock() } // New returns a configured *Plugins value func New() *Plugins { plugs := &Plugins{ plugins: map[string]Plugin{}, moot: &sync.RWMutex{}, } return plugs } type tomlPlugins struct { Plugins []Plugin `toml:"plugin"` } ================================================ FILE: plugins/plugdeps/plugins_test.go ================================================ package plugdeps import ( "bytes" "fmt" "strings" "testing" "github.com/stretchr/testify/require" ) func Test_Plugins_Encode(t *testing.T) { r := require.New(t) bb := &bytes.Buffer{} plugs := New() plugs.Add(pop, heroku, local) r.NoError(plugs.Encode(bb)) fmt.Println(bb.String()) act := strings.TrimSpace(bb.String()) exp := strings.TrimSpace(eToml) r.Equal(exp, act) } func Test_Plugins_Decode(t *testing.T) { r := require.New(t) plugs := New() r.NoError(plugs.Decode(strings.NewReader(eToml))) names := []string{"buffalo-hello.rb", "buffalo-heroku", "buffalo-pop"} list := plugs.List() r.Len(list, len(names)) for i, p := range list { r.Equal(names[i], p.Binary) } } func Test_Plugins_Remove(t *testing.T) { r := require.New(t) plugs := New() plugs.Add(pop, heroku) r.Len(plugs.List(), 2) plugs.Remove(pop) r.Len(plugs.List(), 1) } ================================================ FILE: plugins/plugdeps/pop.go ================================================ package plugdeps var pop = Plugin{ Binary: "buffalo-pop", GoGet: "github.com/gobuffalo/buffalo-pop/v2", } ================================================ FILE: plugins/plugins.go ================================================ package plugins import ( "bytes" "context" "encoding/json" "errors" "io/fs" "os" "os/exec" "path/filepath" "strings" "sync" "time" "github.com/gobuffalo/buffalo/internal/env" "github.com/gobuffalo/buffalo/internal/meta" "github.com/gobuffalo/buffalo/plugins/plugdeps" "github.com/sirupsen/logrus" ) const timeoutEnv = "BUFFALO_PLUGIN_TIMEOUT" var availableOnce sync.Once var timeout = sync.OnceValue(func() time.Duration { t := time.Second * 2 rawTimeout, err := env.MustGet(timeoutEnv) if err == nil { if parsed, err := time.ParseDuration(rawTimeout); err == nil { t = parsed } else { logrus.Errorf("%q value is malformed assuming default %q: %v", timeoutEnv, t, err) } } else { logrus.Debugf("%q not set, assuming default of %v", timeoutEnv, t) } return t }) // List maps a Buffalo command to a slice of Command type List map[string]Commands var _list List // Available plugins for the `buffalo` command. // It will look in $GOPATH/bin and the `./plugins` directory. // This can be changed by setting the $BUFFALO_PLUGIN_PATH // environment variable. // // Requirements: // - file/command must be executable // - file/command must start with `buffalo-` // - file/command must respond to `available` and return JSON of // plugins.Commands{} // // # Limit full path scan with direct plugin path // // If a file/command doesn't respond to being invoked with `available` // within one second, buffalo will assume that it is unable to load. This // can be changed by setting the $BUFFALO_PLUGIN_TIMEOUT environment // variable. It must be set to a duration that `time.ParseDuration` can // process. func Available() (List, error) { var err error availableOnce.Do(func() { defer func() { if err := saveCache(); err != nil { logrus.Error(err) } }() app := meta.New(".") if plugdeps.On(app) { _list, err = listPlugDeps(app) return } paths := []string{"plugins"} from, err := env.MustGet("BUFFALO_PLUGIN_PATH") if err != nil { from, err = env.MustGet("GOPATH") if err != nil { return } from = filepath.Join(from, "bin") } paths = append(paths, strings.Split(from, string(os.PathListSeparator))...) list := List{} for _, p := range paths { if ignorePath(p) { continue } if _, err := os.Stat(p); err != nil { continue } err := filepath.WalkDir(p, func(path string, d fs.DirEntry, err error) error { if err != nil { // May indicate a permissions problem with the path, skip it return nil } if d.IsDir() { return nil } if !strings.HasPrefix(d.Name(), "buffalo-") { return nil } if strings.HasPrefix(d.Name(), "buffalo-plugins") { return nil } ctx, cancel := context.WithTimeout(context.Background(), timeout()) commands := askBin(ctx, path) cancel() for _, c := range commands { bc := c.BuffaloCommand if _, ok := list[bc]; !ok { list[bc] = Commands{} } c.Binary = path list[bc] = append(list[bc], c) } return nil }) if err != nil { return } } _list = list }) return _list, err } func askBin(ctx context.Context, path string) Commands { start := time.Now() defer func() { logrus.Debugf("askBin %s=%.4f s", path, time.Since(start).Seconds()) }() commands := Commands{} if cp, ok := findInCache(path); ok { s := sum(path) if s == cp.CheckSum { logrus.Debugf("cache hit: %s", path) commands = cp.Commands return commands } } logrus.Debugf("cache miss: %s", path) if strings.HasPrefix(filepath.Base(path), "buffalo-no-sqlite") { return commands } cmd := exec.CommandContext(ctx, path, "available") bb := &bytes.Buffer{} cmd.Stdout = bb err := cmd.Run() if err != nil { logrus.Errorf("[PLUGIN] error loading plugin %s: %s\n", path, err) return commands } msg := bb.String() for len(msg) > 0 { err = json.NewDecoder(strings.NewReader(msg)).Decode(&commands) if err == nil { addToCache(path, cachedPlugin{ Commands: commands, }) return commands } msg = msg[1:] } logrus.Errorf("[PLUGIN] error decoding plugin %s: %s\n%s\n", path, err, msg) return commands } func ignorePath(p string) bool { p = strings.ToLower(p) for _, x := range []string{`c:\windows`, `c:\program`} { if strings.HasPrefix(p, x) { return true } } return false } func listPlugDeps(app meta.App) (List, error) { list := List{} plugs, err := plugdeps.List(app) if err != nil { return list, err } for _, p := range plugs.List() { ctx, cancel := context.WithTimeout(context.Background(), timeout()) defer cancel() bin := p.Binary if len(p.Local) != 0 { bin = p.Local } bin, err := LookPath(bin) if err != nil { if !errors.Is(err, ErrPlugMissing) { return list, err } continue } commands := askBin(ctx, bin) cancel() for _, c := range commands { bc := c.BuffaloCommand if _, ok := list[bc]; !ok { list[bc] = Commands{} } c.Binary = p.Binary for _, pc := range p.Commands { if c.Name == pc.Name { c.Flags = pc.Flags break } } list[bc] = append(list[bc], c) } } return list, nil } ================================================ FILE: plugins/plugins_test.go ================================================ package plugins import ( "context" "os" "strings" "testing" "time" "github.com/gobuffalo/buffalo/internal/env" "github.com/stretchr/testify/require" ) func TestAskBin_respectsTimeout(t *testing.T) { r := require.New(t) from, err := env.MustGet("BUFFALO_PLUGIN_PATH") if err != nil { t.Skipf("BUFFALO_PLUGIN_PATH not set.") return } if fileEntries, err := os.ReadDir(from); err == nil { found := false for _, e := range fileEntries { if strings.HasPrefix(e.Name(), "buffalo-") { from = e.Name() found = true break } } if !found { t.Skipf("no plugins found") return } } else { r.Error(err, "plugin path not able to be read") return } const tooShort = time.Millisecond impossible, cancel := context.WithTimeout(context.Background(), tooShort) defer cancel() done := make(chan struct{}) go func() { askBin(impossible, from) close(done) }() select { case <-time.After(tooShort + 80*time.Millisecond): r.Fail("did not time-out quickly enough") case <-done: t.Log("timed-out successfully") } if _, ok := findInCache(from); ok { r.Fail("expected plugin not to be added to cache on failure, but it was in cache") } } ================================================ FILE: plugins.go ================================================ package buffalo import ( "encoding/json" "fmt" "os" "os/exec" "strings" "sync" "github.com/gobuffalo/buffalo/internal/env" "github.com/gobuffalo/buffalo/plugins" "github.com/gobuffalo/events" ) // LoadPlugins will add listeners for any plugins that support "events" var LoadPlugins = sync.OnceValue(func() error { // don't send plugins events during testing if env.Get("GO_ENV", "development") == "test" { return nil } plugs, err := plugins.Available() if err != nil { return err } for _, cmds := range plugs { for _, c := range cmds { if c.BuffaloCommand != "events" { continue } err := func(c plugins.Command) error { n := fmt.Sprintf("[PLUGIN] %s %s", c.Binary, c.Name) fn := func(e events.Event) { b, err := json.Marshal(e) if err != nil { fmt.Println("error trying to marshal event", e, err) return } cmd := exec.Command(c.Binary, c.UseCommand, string(b)) cmd.Stderr = os.Stderr cmd.Stdout = os.Stdout cmd.Stdin = os.Stdin if err := cmd.Run(); err != nil { fmt.Println("error trying to send event", strings.Join(cmd.Args, " "), err) } } _, err := events.NamedListen(n, events.Filter(c.ListenFor, fn)) return err }(c) if err != nil { return err } } } return nil }) ================================================ FILE: render/auto.go ================================================ package render import ( "context" "errors" "fmt" "io" "net/http" "path" "reflect" "strings" "github.com/gobuffalo/flect/name" ) var errNoID = fmt.Errorf("no ID on model") // ErrRedirect indicates to Context#Render that this is a // redirect and a template shouldn't be rendered. type ErrRedirect struct { Status int URL string } func (ErrRedirect) Error() string { return "" } // Auto figures out how to render the model based information // about the request and the name of the model. Auto supports // automatic rendering of HTML, JSON, and XML. Any status code // give to Context#Render between 300 - 400 will be respected // by Auto. Other status codes are not. /* # Rules for HTML template lookup: GET /users - users/index.html GET /users/id - users/show.html GET /users/new - users/new.html GET /users/id/edit - users/edit.html POST /users - (redirect to /users/id or render user/new.html) PUT /users/edit - (redirect to /users/id or render user/edit.html) DELETE /users/id - redirect to /users */ func Auto(ctx context.Context, i any) Renderer { e := New(Options{}) return e.Auto(ctx, i) } // Auto figures out how to render the model based information // about the request and the name of the model. Auto supports // automatic rendering of HTML, JSON, and XML. Any status code // give to Context#Render between 300 - 400 will be respected // by Auto. Other status codes are not. /* # Rules for HTML template lookup: GET /users - users/index.html GET /users/id - users/show.html GET /users/new - users/new.html GET /users/id/edit - users/edit.html POST /users - (redirect to /users/id or render user/new.html) PUT /users/edit - (redirect to /users/id or render user/edit.html) DELETE /users/id - redirect to /users */ func (e *Engine) Auto(ctx context.Context, i any) Renderer { ct, _ := ctx.Value("contentType").(string) if ct == "" { ct = e.DefaultContentType } ct = strings.TrimSpace(strings.ToLower(ct)) if strings.Contains(ct, "json") { return e.JSON(i) } if strings.Contains(ct, "xml") { return e.XML(i) } return htmlAutoRenderer{ Engine: e, model: i, } } type htmlAutoRenderer struct { *Engine model any } func (htmlAutoRenderer) ContentType() string { return "text/html" } func (ir htmlAutoRenderer) Render(w io.Writer, data Data) error { n := name.New(ir.typeName()) pname := name.New(n.Pluralize().String()) isPlural := ir.isPlural() if isPlural { data[pname.VarCasePlural().String()] = ir.model } else { data[n.VarCaseSingle().String()] = ir.model } templatePrefix := pname.File() if pf, ok := data["template_prefix"].(string); ok { templatePrefix = name.New(pf) } switch data["method"] { case "PUT", "POST", "DELETE": if err := ir.redirect(pname, w, data); err != nil { var er ErrRedirect if errors.As(err, &er) && er.Status >= http.StatusMultipleChoices && er.Status < http.StatusBadRequest { return err } if data["method"] == "PUT" { return ir.HTML(path.Join(templatePrefix.String(), "edit.html")).Render(w, data) } return ir.HTML(path.Join(templatePrefix.String(), "new.html")).Render(w, data) } return nil } cp, ok := data["current_path"].(string) defCase := func() error { return ir.HTML(path.Join(templatePrefix.String(), "index.html")).Render(w, data) } if !ok { return defCase() } if strings.HasSuffix(cp, "/edit/") { return ir.HTML(path.Join(templatePrefix.String(), "edit.html")).Render(w, data) } if strings.HasSuffix(cp, "/new/") { return ir.HTML(path.Join(templatePrefix.String(), "new.html")).Render(w, data) } if !isPlural { return ir.HTML(path.Join(templatePrefix.String(), "show.html")).Render(w, data) } return defCase() } func (ir htmlAutoRenderer) redirect(name name.Ident, w io.Writer, data Data) error { rv := reflect.Indirect(reflect.ValueOf(ir.model)) f := rv.FieldByName("ID") if !f.IsValid() { return errNoID } fi := f.Interface() rt := reflect.TypeOf(fi) zero := reflect.Zero(rt) if fi != zero.Interface() { m, ok := data["method"].(string) if !ok { m = "GET" } url := fmt.Sprint(data["current_path"]) id := fmt.Sprint(f.Interface()) url = strings.TrimSuffix(url, "/") switch m { case "DELETE": url = strings.TrimSuffix(url, id) default: if !strings.HasSuffix(url, id) { url = path.Join(url, id) } } code := http.StatusFound if i, ok := data["status"].(int); ok { if i >= http.StatusMultipleChoices { code = i } } return ErrRedirect{ Status: code, URL: url, } } return errNoID } func (ir htmlAutoRenderer) typeName() string { rv := reflect.Indirect(reflect.ValueOf(ir.model)) rt := rv.Type() switch rt.Kind() { case reflect.Slice, reflect.Array: el := rt.Elem() return el.Name() default: return rt.Name() } } func (ir htmlAutoRenderer) isPlural() bool { rv := reflect.Indirect(reflect.ValueOf(ir.model)) rt := rv.Type() switch rt.Kind() { case reflect.Slice, reflect.Array, reflect.Map: return true } return false } ================================================ FILE: render/auto_test.go ================================================ package render_test import ( "net/http" "strings" "testing" "testing/fstest" "github.com/gobuffalo/buffalo" "github.com/gobuffalo/buffalo/render" "github.com/gobuffalo/httptest" "github.com/stretchr/testify/require" ) type Car struct { ID int Name string } type Cars []Car func Test_Auto_DefaultContentType(t *testing.T) { r := require.New(t) re := render.New(render.Options{ DefaultContentType: "application/json", }) app := buffalo.New(buffalo.Options{}) app.GET("/cars", func(c buffalo.Context) error { return c.Render(http.StatusOK, re.Auto(c, []string{"Honda", "Toyota", "Ford", "Chevy"})) }) res := httptest.NewRecorder() req := httptest.NewRequest("GET", "/cars", nil) app.ServeHTTP(res, req) r.Equal(`["Honda","Toyota","Ford","Chevy"]`, strings.TrimSpace(res.Body.String())) } func Test_Auto_JSON(t *testing.T) { r := require.New(t) re := render.New(render.Options{}) app := buffalo.New(buffalo.Options{}) app.GET("/cars", func(c buffalo.Context) error { return c.Render(http.StatusOK, re.Auto(c, []string{"Honda", "Toyota", "Ford", "Chevy"})) }) w := httptest.New(app) res := w.JSON("/cars").Get() r.Equal(`["Honda","Toyota","Ford","Chevy"]`, strings.TrimSpace(res.Body.String())) } func Test_Auto_XML(t *testing.T) { r := require.New(t) re := render.New(render.Options{}) app := buffalo.New(buffalo.Options{}) app.GET("/cars", func(c buffalo.Context) error { return c.Render(http.StatusOK, re.Auto(c, []string{"Honda", "Toyota", "Ford", "Chevy"})) }) w := httptest.New(app) res := w.XML("/cars").Get() r.Equal("\nHonda\nToyota\nFord\nChevy", strings.TrimSpace(res.Body.String())) } func Test_Auto_HTML_List(t *testing.T) { r := require.New(t) rootFS := fstest.MapFS{ "cars/index.html": &fstest.MapFile{ Data: []byte("INDEX: <%= len(cars) %>"), Mode: 0644, }, } re := render.NewEngine() re.TemplatesFS = rootFS app := buffalo.New(buffalo.Options{}) app.GET("/cars", func(c buffalo.Context) error { return c.Render(http.StatusOK, re.Auto(c, Cars{ {Name: "Ford"}, {Name: "Chevy"}, })) }) w := httptest.New(app) res := w.HTML("/cars").Get() r.Contains(res.Body.String(), "INDEX: 2") } func Test_Auto_HTML_List_Plural(t *testing.T) { r := require.New(t) type Person struct { Name string } type People []Person rootFS := fstest.MapFS{ "people/index.html": &fstest.MapFile{ Data: []byte("INDEX: <%= len(people) %>"), Mode: 0644, }, } re := render.New(render.Options{ TemplatesFS: rootFS, }) app := buffalo.New(buffalo.Options{}) app.GET("/people", func(c buffalo.Context) error { return c.Render(http.StatusOK, re.Auto(c, People{ Person{Name: "Ford"}, Person{Name: "Chevy"}, })) }) w := httptest.New(app) res := w.HTML("/people").Get() r.Contains(res.Body.String(), "INDEX: 2") } func Test_Auto_HTML_Show(t *testing.T) { r := require.New(t) rootFS := fstest.MapFS{ "cars/show.html": &fstest.MapFile{ Data: []byte("Show: <%= car.Name %>"), Mode: 0644, }, } re := render.New(render.Options{ TemplatesFS: rootFS, }) app := buffalo.New(buffalo.Options{}) app.GET("/cars/{id}", func(c buffalo.Context) error { return c.Render(http.StatusOK, re.Auto(c, Car{Name: "Honda"})) }) w := httptest.New(app) res := w.HTML("/cars/1").Get() r.Contains(res.Body.String(), "Show: Honda") } func Test_Auto_HTML_New(t *testing.T) { r := require.New(t) rootFS := fstest.MapFS{ "cars/new.html": &fstest.MapFile{ Data: []byte("New: <%= car.Name %>"), Mode: 0644, }, } re := render.New(render.Options{ TemplatesFS: rootFS, }) app := buffalo.New(buffalo.Options{}) app.GET("/cars/new", func(c buffalo.Context) error { return c.Render(http.StatusOK, re.Auto(c, Car{Name: "Honda"})) }) w := httptest.New(app) res := w.HTML("/cars/new").Get() r.Contains(res.Body.String(), "New: Honda") } func Test_Auto_HTML_Create(t *testing.T) { r := require.New(t) rootFS := fstest.MapFS{ "cars/new.html": &fstest.MapFile{ Data: []byte("New: <%= car.Name %>"), Mode: 0644, }, } re := render.New(render.Options{ TemplatesFS: rootFS, }) app := buffalo.New(buffalo.Options{}) app.POST("/cars", func(c buffalo.Context) error { return c.Render(http.StatusCreated, re.Auto(c, Car{Name: "Honda"})) }) w := httptest.New(app) res := w.HTML("/cars").Post(nil) r.Contains(res.Body.String(), "New: Honda") } func Test_Auto_HTML_Create_Redirect(t *testing.T) { r := require.New(t) app := buffalo.New(buffalo.Options{}) app.POST("/cars", func(c buffalo.Context) error { return c.Render(http.StatusCreated, render.Auto(c, Car{ ID: 1, Name: "Honda", })) }) w := httptest.New(app) res := w.HTML("/cars").Post(nil) r.Equal("/cars/1", res.Location()) r.Equal(http.StatusFound, res.Code) } func Test_Auto_HTML_Create_Redirect_Error(t *testing.T) { r := require.New(t) rootFS := fstest.MapFS{ "cars/new.html": &fstest.MapFile{ Data: []byte("Create: <%= car.Name %>"), Mode: 0644, }, } re := render.New(render.Options{ TemplatesFS: rootFS, }) app := buffalo.New(buffalo.Options{}) app.POST("/cars", func(c buffalo.Context) error { b := Car{ Name: "Honda", } return c.Render(http.StatusUnprocessableEntity, re.Auto(c, b)) }) w := httptest.New(app) res := w.HTML("/cars").Post(nil) r.Equal(http.StatusUnprocessableEntity, res.Code) r.Contains(res.Body.String(), "Create: Honda") } func Test_Auto_HTML_Create_Nested_Redirect(t *testing.T) { r := require.New(t) app := buffalo.New(buffalo.Options{}) admin := app.Group("/admin") admin.POST("/cars", func(c buffalo.Context) error { return c.Render(http.StatusCreated, render.Auto(c, Car{ ID: 1, Name: "Honda", })) }) w := httptest.New(app) res := w.HTML("/admin/cars").Post(nil) r.Equal("/admin/cars/1", res.Location()) r.Equal(http.StatusFound, res.Code) } func Test_Auto_HTML_Destroy_Nested_Redirect(t *testing.T) { r := require.New(t) app := buffalo.New(buffalo.Options{}) admin := app.Group("/admin") admin.DELETE("/cars", func(c buffalo.Context) error { return c.Render(http.StatusOK, render.Auto(c, Car{ ID: 1, Name: "Honda", })) }) w := httptest.New(app) res := w.HTML("/admin/cars").Delete() r.Equal("/admin/cars", res.Location()) r.Equal(http.StatusFound, res.Code) } func Test_Auto_HTML_Edit(t *testing.T) { r := require.New(t) rootFS := fstest.MapFS{ "cars/edit.html": &fstest.MapFile{ Data: []byte("Edit: <%= car.Name %>"), Mode: 0644, }, } re := render.New(render.Options{ TemplatesFS: rootFS, }) app := buffalo.New(buffalo.Options{}) app.GET("/cars/{id}/edit", func(c buffalo.Context) error { return c.Render(http.StatusOK, re.Auto(c, Car{Name: "Honda"})) }) w := httptest.New(app) res := w.HTML("/cars/1/edit").Get() r.Contains(res.Body.String(), "Edit: Honda") } func Test_Auto_HTML_Update(t *testing.T) { r := require.New(t) rootFS := fstest.MapFS{ "cars/edit.html": &fstest.MapFile{ Data: []byte("Update: <%= car.Name %>"), Mode: 0644, }, } re := render.New(render.Options{ TemplatesFS: rootFS, }) app := buffalo.New(buffalo.Options{}) app.PUT("/cars/{id}", func(c buffalo.Context) error { return c.Render(http.StatusOK, re.Auto(c, Car{Name: "Honda"})) }) w := httptest.New(app) res := w.HTML("/cars/1").Put(nil) r.Contains(res.Body.String(), "Update: Honda") } func Test_Auto_HTML_Update_Redirect(t *testing.T) { r := require.New(t) app := buffalo.New(buffalo.Options{}) app.PUT("/cars/{id}", func(c buffalo.Context) error { b := Car{ ID: 1, Name: "Honda", } return c.Render(http.StatusOK, render.Auto(c, b)) }) w := httptest.New(app) res := w.HTML("/cars/1").Put(nil) r.Equal("/cars/1", res.Location()) r.Equal(http.StatusFound, res.Code) } func Test_Auto_HTML_Update_Redirect_Error(t *testing.T) { r := require.New(t) rootFS := fstest.MapFS{ "cars/edit.html": &fstest.MapFile{ Data: []byte("Update: <%= car.Name %>"), Mode: 0644, }, } re := render.New(render.Options{ TemplatesFS: rootFS, }) app := buffalo.New(buffalo.Options{}) app.PUT("/cars/{id}", func(c buffalo.Context) error { b := Car{ ID: 1, Name: "Honda", } return c.Render(http.StatusUnprocessableEntity, re.Auto(c, b)) }) w := httptest.New(app) res := w.HTML("/cars/1").Put(nil) r.Equal(http.StatusUnprocessableEntity, res.Code) r.Contains(res.Body.String(), "Update: Honda") } func Test_Auto_HTML_Destroy_Redirect(t *testing.T) { r := require.New(t) app := buffalo.New(buffalo.Options{}) app.DELETE("/cars/{id}", func(c buffalo.Context) error { b := Car{ ID: 1, Name: "Honda", } return c.Render(http.StatusOK, render.Auto(c, b)) }) w := httptest.New(app) res := w.HTML("/cars/1").Delete() r.Equal("/cars/", res.Location()) r.Equal(http.StatusFound, res.Code) } func Test_Auto_HTML_List_Plural_MultiWord(t *testing.T) { r := require.New(t) type RoomProvider struct { Name string } type RoomProviders []RoomProvider rootFS := fstest.MapFS{ "room_providers/index.html": &fstest.MapFile{ Data: []byte("INDEX: <%= len(roomProviders) %>"), Mode: 0644, }, } re := render.New(render.Options{ TemplatesFS: rootFS, }) app := buffalo.New(buffalo.Options{}) app.GET("/room_providers", func(c buffalo.Context) error { return c.Render(http.StatusOK, re.Auto(c, RoomProviders{ RoomProvider{Name: "Ford"}, RoomProvider{Name: "Chevy"}, })) }) w := httptest.New(app) res := w.HTML("/room_providers").Get() r.Contains(res.Body.String(), "INDEX: 2") } func Test_Auto_HTML_List_Plural_MultiWord_Dashed(t *testing.T) { r := require.New(t) type RoomProvider struct { Name string } type RoomProviders []RoomProvider rootFS := fstest.MapFS{ "room_providers/index.html": &fstest.MapFile{ Data: []byte("INDEX: <%= len(roomProviders) %>"), Mode: 0644, }, } re := render.New(render.Options{ TemplatesFS: rootFS, }) app := buffalo.New(buffalo.Options{}) app.GET("/room-providers", func(c buffalo.Context) error { return c.Render(http.StatusOK, re.Auto(c, RoomProviders{ RoomProvider{Name: "Ford"}, RoomProvider{Name: "Chevy"}, })) }) w := httptest.New(app) res := w.HTML("/room-providers").Get() r.Contains(res.Body.String(), "INDEX: 2") } func Test_Auto_HTML_Show_MultiWord_Dashed(t *testing.T) { r := require.New(t) type RoomProvider struct { ID int Name string } rootFS := fstest.MapFS{ "room_providers/show.html": &fstest.MapFile{ Data: []byte("SHOW: <%= roomProvider.Name %>"), Mode: 0644, }, } re := render.New(render.Options{ TemplatesFS: rootFS, }) app := buffalo.New(buffalo.Options{}) app.GET("/room-providers/{id}", func(c buffalo.Context) error { return c.Render(http.StatusOK, re.Auto(c, RoomProvider{ID: 1, Name: "Ford"})) }) w := httptest.New(app) res := w.HTML("/room-providers/1").Get() r.Contains(res.Body.String(), "SHOW: Ford") } ================================================ FILE: render/download.go ================================================ package render import ( "context" "fmt" "io" "mime" "net/http" "path/filepath" "strconv" ) type downloadRenderer struct { ctx context.Context name string reader io.Reader } func (r downloadRenderer) ContentType() string { ext := filepath.Ext(r.name) t := mime.TypeByExtension(ext) if t == "" { t = "application/octet-stream" } return t } func (r downloadRenderer) Render(w io.Writer, d Data) error { written, err := io.Copy(w, r.reader) if err != nil { return err } ctx, ok := r.ctx.(responsible) if !ok { return fmt.Errorf("context has no response writer") } header := ctx.Response().Header() disposition := fmt.Sprintf("attachment; filename=%s", r.name) header.Add("Content-Disposition", disposition) contentLength := strconv.Itoa(int(written)) header.Add("Content-Length", contentLength) return nil } // Download renders a file attachment automatically setting following headers: // // Content-Type // Content-Length // Content-Disposition // // Content-Type is set using mime#TypeByExtension with the filename's extension. Content-Type will default to // application/octet-stream if using a filename with an unknown extension. // // Note: the purpose of this function is not serving static files but to support // downloading of dynamically genrated data as a file. For example, you can use // this function when you implement CSV file download feature for the result of // a database query. // // Do not use this function for large io.Reader. It could cause out of memory if // the size of io.Reader is too big. func Download(ctx context.Context, name string, r io.Reader) Renderer { return downloadRenderer{ ctx: ctx, name: name, reader: r, } } // Download renders a file attachment automatically setting following headers: // // Content-Type // Content-Length // Content-Disposition // // Content-Type is set using mime#TypeByExtension with the filename's extension. Content-Type will default to // application/octet-stream if using a filename with an unknown extension. // // Note: the purpose of this method is not serving static files but to support // downloading of dynamically genrated data as a file. For example, you can use // this method when you implement CSV file download feature for the result of // a database query. // // Do not use this method for large io.Reader. It could cause out of memory if // the size of io.Reader is too big. func (e *Engine) Download(ctx context.Context, name string, r io.Reader) Renderer { return Download(ctx, name, r) } type responsible interface { Response() http.ResponseWriter } ================================================ FILE: render/download_test.go ================================================ package render import ( "bytes" "context" "io" "net/http" "net/http/httptest" "strconv" "testing" "github.com/stretchr/testify/require" ) type dlRenderer func(context.Context, string, io.Reader) Renderer type dlContext struct { context.Context rw http.ResponseWriter } func (c dlContext) Response() http.ResponseWriter { return c.rw } var data = []byte("data") func Test_Download_KnownExtension(t *testing.T) { r := require.New(t) table := []dlRenderer{ Download, New(Options{}).Download, } for _, dl := range table { ctx := dlContext{rw: httptest.NewRecorder()} re := dl(ctx, "filename.pdf", bytes.NewReader(data)) bb := &bytes.Buffer{} r.NoError(re.Render(bb, nil)) r.Equal(data, bb.Bytes()) r.Equal(strconv.Itoa(len(data)), ctx.Response().Header().Get("Content-Length")) r.Equal("attachment; filename=filename.pdf", ctx.Response().Header().Get("Content-Disposition")) r.Equal("application/pdf", re.ContentType()) } } func Test_Download_UnknownExtension(t *testing.T) { r := require.New(t) table := []dlRenderer{ Download, New(Options{}).Download, } for _, dl := range table { ctx := dlContext{rw: httptest.NewRecorder()} re := dl(ctx, "filename", bytes.NewReader(data)) bb := &bytes.Buffer{} r.NoError(re.Render(bb, nil)) r.Equal(data, bb.Bytes()) r.Equal(strconv.Itoa(len(data)), ctx.Response().Header().Get("Content-Length")) r.Equal("attachment; filename=filename", ctx.Response().Header().Get("Content-Disposition")) r.Equal("application/octet-stream", re.ContentType()) } } func Test_InvalidContext(t *testing.T) { r := require.New(t) table := []dlRenderer{ Download, New(Options{}).Download, } for _, dl := range table { re := dl(context.TODO(), "filename", bytes.NewReader(data)) bb := &bytes.Buffer{} r.Error(re.Render(bb, nil)) } } ================================================ FILE: render/func.go ================================================ package render import "io" // RendererFunc is the interface for the the function // needed by the Func renderer. type RendererFunc func(io.Writer, Data) error type funcRenderer struct { contentType string renderFunc RendererFunc } // ContentType returns the content type for this render. // Examples would be "text/html" or "application/json". func (s funcRenderer) ContentType() string { return s.contentType } // Render the provided Data to the provider Writer using the // RendererFunc provide. func (s funcRenderer) Render(w io.Writer, data Data) error { return s.renderFunc(w, data) } // Func renderer allows for easily building one of renderers // using just a RendererFunc and not having to build a whole // implementation of the Render interface. func Func(s string, fn RendererFunc) Renderer { return funcRenderer{ contentType: s, renderFunc: fn, } } // Func renderer allows for easily building one of renderers // using just a RendererFunc and not having to build a whole // implementation of the Render interface. func (e *Engine) Func(s string, fn RendererFunc) Renderer { return Func(s, fn) } ================================================ FILE: render/func_test.go ================================================ package render import ( "bytes" "io" "testing" "github.com/stretchr/testify/require" ) func Test_Func(t *testing.T) { r := require.New(t) table := []rendFriend{ Func, New(Options{}).Func, } for _, tt := range table { bb := &bytes.Buffer{} re := tt("foo/bar", func(w io.Writer, data Data) error { _, err := w.Write([]byte(data["name"].(string))) return err }) r.Equal("foo/bar", re.ContentType()) err := re.Render(bb, Data{"name": "Mark"}) r.NoError(err) r.Equal("Mark", bb.String()) } } ================================================ FILE: render/helpers.go ================================================ package render import ( "html/template" "net/http" "github.com/gobuffalo/helpers/forms" "github.com/gobuffalo/helpers/forms/bootstrap" "github.com/gobuffalo/plush/v5" "github.com/gobuffalo/tags/v3" ) func init() { plush.Helpers.Add("paginator", func(pagination any, opts map[string]any, help plush.HelperContext) (template.HTML, error) { if opts["path"] == nil { if req, ok := help.Value("request").(*http.Request); ok { opts["path"] = req.URL.String() } } t, err := tags.Pagination(pagination, opts) if err != nil { return "", err } return t.HTML(), nil }) plush.Helpers.Add(forms.RemoteFormKey, bootstrap.RemoteForm) plush.Helpers.Add(forms.RemoteFormForKey, bootstrap.RemoteFormFor) } ================================================ FILE: render/html.go ================================================ package render import ( "html" "strings" "github.com/gobuffalo/github_flavored_markdown" "github.com/gobuffalo/plush/v5" ) // HTML renders the named files using the 'text/html' // content type and the github.com/gobuffalo/plush/v5 // package for templating. If more than 1 file is provided // the second file will be considered a "layout" file // and the first file will be the "content" file which will // be placed into the "layout" using "<%= yield %>". func HTML(names ...string) Renderer { e := New(Options{}) return e.HTML(names...) } // HTML renders the named files using the 'text/html' // content type and the github.com/gobuffalo/plush/v5 // package for templating. If more than 1 file is provided // the second file will be considered a "layout" file // and the first file will be the "content" file which will // be placed into the "layout" using "<%= yield %>". If no // second file is provided and an `HTMLLayout` is specified // in the options, then that layout file will be used // automatically. func (e *Engine) HTML(names ...string) Renderer { // just allow leading slash and remove them here. // generated actions were various by buffalo versions. tmp := []string{} for _, name := range names { tmp = append(tmp, strings.TrimPrefix(name, "/")) } names = tmp if e.HTMLLayout != "" && len(names) == 1 { names = append(names, e.HTMLLayout) } hr := &templateRenderer{ Engine: e, contentType: "text/html; charset=utf-8", names: names, } return hr } // MDTemplateEngine runs the input through github flavored markdown before sending it to the Plush engine. func MDTemplateEngine(input string, data map[string]any, helpers map[string]any) (string, error) { if ct, ok := data["contentType"].(string); ok && ct == "text/plain" { return plush.BuffaloRenderer(input, data, helpers) } source := github_flavored_markdown.Markdown([]byte(input)) source = []byte(html.UnescapeString(string(source))) return plush.BuffaloRenderer(string(source), data, helpers) } ================================================ FILE: render/html_test.go ================================================ package render import ( "bytes" "strings" "testing" "testing/fstest" "github.com/stretchr/testify/require" ) const htmlLayout = "layout.html" const htmlAltLayout = "alt_layout.plush.html" const htmlTemplate = "my-template.html" func Test_HTML_WithoutLayout(t *testing.T) { r := require.New(t) rootFS := fstest.MapFS{ htmlTemplate: &fstest.MapFile{ Data: []byte("<%= name %>"), Mode: 0644, }, } e := NewEngine() e.TemplatesFS = rootFS h := e.HTML(htmlTemplate) r.Equal("text/html; charset=utf-8", h.ContentType()) bb := &bytes.Buffer{} r.NoError(h.Render(bb, Data{"name": "Mark"})) r.Equal("Mark", strings.TrimSpace(bb.String())) } func Test_HTML_WithLayout(t *testing.T) { r := require.New(t) rootFS := fstest.MapFS{ htmlTemplate: &fstest.MapFile{ Data: []byte("<%= name %>"), Mode: 0644, }, htmlLayout: &fstest.MapFile{ Data: []byte("<%= yield %>"), Mode: 0644, }, } e := NewEngine() e.TemplatesFS = rootFS e.HTMLLayout = htmlLayout h := e.HTML(htmlTemplate) r.Equal("text/html; charset=utf-8", h.ContentType()) bb := &bytes.Buffer{} r.NoError(h.Render(bb, Data{"name": "Mark"})) r.Equal("Mark", strings.TrimSpace(bb.String())) } func Test_HTML_WithLayout_Override(t *testing.T) { r := require.New(t) rootFS := fstest.MapFS{ htmlTemplate: &fstest.MapFile{ Data: []byte("<%= name %>"), Mode: 0644, }, htmlLayout: &fstest.MapFile{ Data: []byte("<%= yield %>"), Mode: 0644, }, htmlAltLayout: &fstest.MapFile{ Data: []byte("<%= yield %>"), Mode: 0644, }, } e := NewEngine() e.TemplatesFS = rootFS e.HTMLLayout = htmlLayout h := e.HTML(htmlTemplate, htmlAltLayout) r.Equal("text/html; charset=utf-8", h.ContentType()) bb := &bytes.Buffer{} r.NoError(h.Render(bb, Data{"name": "Mark"})) r.Equal("Mark", strings.TrimSpace(bb.String())) } func Test_HTML_LeadingSlash(t *testing.T) { r := require.New(t) rootFS := fstest.MapFS{ htmlTemplate: &fstest.MapFile{ Data: []byte("<%= name %>"), Mode: 0644, }, htmlLayout: &fstest.MapFile{ Data: []byte("<%= yield %>"), Mode: 0644, }, } e := NewEngine() e.TemplatesFS = rootFS e.HTMLLayout = htmlLayout h := e.HTML("/my-template.html") // instead of "my-template.html" r.Equal("text/html; charset=utf-8", h.ContentType()) bb := &bytes.Buffer{} r.NoError(h.Render(bb, Data{"name": "Mark"})) r.Equal("Mark", strings.TrimSpace(bb.String())) } ================================================ FILE: render/js.go ================================================ package render // JavaScript renders the named files using the 'application/javascript' // content type and the github.com/gobuffalo/plush // package for templating. If more than 1 file is provided // the second file will be considered a "layout" file // and the first file will be the "content" file which will // be placed into the "layout" using "<%= yield %>". func JavaScript(names ...string) Renderer { e := New(Options{}) return e.JavaScript(names...) } // JavaScript renders the named files using the 'application/javascript' // content type and the github.com/gobuffalo/plush // package for templating. If more than 1 file is provided // the second file will be considered a "layout" file // and the first file will be the "content" file which will // be placed into the "layout" using "<%= yield %>". If no // second file is provided and an `JavaScriptLayout` is specified // in the options, then that layout file will be used // automatically. func (e *Engine) JavaScript(names ...string) Renderer { if e.JavaScriptLayout != "" && len(names) == 1 { names = append(names, e.JavaScriptLayout) } hr := &templateRenderer{ Engine: e, contentType: "application/javascript", names: names, } return hr } ================================================ FILE: render/js_test.go ================================================ package render import ( "bytes" "strings" "testing" "testing/fstest" "github.com/stretchr/testify/require" ) const jsLayout = "layout.js" const jsAltLayout = "alt_layout.plush.js" const jsTemplate = "my-template.js" func Test_JavaScript_WithoutLayout(t *testing.T) { r := require.New(t) rootFS := fstest.MapFS{ jsTemplate: &fstest.MapFile{ Data: []byte("alert(<%= name %>)"), Mode: 0644, }, } e := NewEngine() e.TemplatesFS = rootFS h := e.JavaScript(jsTemplate) r.Equal("application/javascript", h.ContentType()) bb := &bytes.Buffer{} r.NoError(h.Render(bb, Data{"name": "Mark"})) r.Equal("alert(Mark)", strings.TrimSpace(bb.String())) } func Test_JavaScript_WithLayout(t *testing.T) { r := require.New(t) rootFS := fstest.MapFS{ jsTemplate: &fstest.MapFile{ Data: []byte("alert(<%= name %>)"), Mode: 0644, }, jsLayout: &fstest.MapFile{ Data: []byte("$(<%= yield %>)"), Mode: 0644, }, } e := NewEngine() e.TemplatesFS = rootFS e.JavaScriptLayout = jsLayout h := e.JavaScript(jsTemplate) r.Equal("application/javascript", h.ContentType()) bb := &bytes.Buffer{} r.NoError(h.Render(bb, Data{"name": "Mark"})) r.Equal("$(alert(Mark))", strings.TrimSpace(bb.String())) } func Test_JavaScript_WithLayout_Override(t *testing.T) { r := require.New(t) rootFS := fstest.MapFS{ jsTemplate: &fstest.MapFile{ Data: []byte("alert(<%= name %>)"), Mode: 0644, }, jsLayout: &fstest.MapFile{ Data: []byte("$(<%= yield %>)"), Mode: 0644, }, jsAltLayout: &fstest.MapFile{ Data: []byte("_(<%= yield %>)"), Mode: 0644, }, } e := NewEngine() e.TemplatesFS = rootFS e.JavaScriptLayout = jsLayout h := e.JavaScript(jsTemplate, jsAltLayout) r.Equal("application/javascript", h.ContentType()) bb := &bytes.Buffer{} r.NoError(h.Render(bb, Data{"name": "Mark"})) r.Equal("_(alert(Mark))", strings.TrimSpace(bb.String())) } func Test_JavaScript_Partial_Without_Extension(t *testing.T) { const tmpl = "let a = 1;\n<%= partial(\"part\") %>" const part = "alert('Hi <%= name %>!');" r := require.New(t) rootFS := fstest.MapFS{ jsTemplate: &fstest.MapFile{ Data: []byte(tmpl), Mode: 0644, }, "_part.js": &fstest.MapFile{ Data: []byte(part), Mode: 0644, }, } e := NewEngine() e.TemplatesFS = rootFS h := e.JavaScript(jsTemplate) r.Equal("application/javascript", h.ContentType()) bb := &bytes.Buffer{} r.NoError(h.Render(bb, Data{"name": "Yonghwan"})) r.Equal("let a = 1;\nalert('Hi Yonghwan!');", bb.String()) } func Test_JavaScript_Partial(t *testing.T) { const tmpl = "let a = 1;\n<%= partial(\"part.js\") %>" const part = "alert('Hi <%= name %>!');" r := require.New(t) rootFS := fstest.MapFS{ jsTemplate: &fstest.MapFile{ Data: []byte(tmpl), Mode: 0644, }, "_part.js": &fstest.MapFile{ Data: []byte(part), Mode: 0644, }, } e := NewEngine() e.TemplatesFS = rootFS h := e.JavaScript(jsTemplate) r.Equal("application/javascript", h.ContentType()) bb := &bytes.Buffer{} r.NoError(h.Render(bb, Data{"name": "Yonghwan"})) r.Equal("let a = 1;\nalert('Hi Yonghwan!');", bb.String()) } func Test_JavaScript_HTML_Partial(t *testing.T) { const tmpl = "let a = \"<%= partial(\"part.html\") %>\"" const part = `

hi

` r := require.New(t) rootFS := fstest.MapFS{ jsTemplate: &fstest.MapFile{ Data: []byte(tmpl), Mode: 0644, }, "_part.html": &fstest.MapFile{ Data: []byte(part), Mode: 0644, }, } e := NewEngine() e.TemplatesFS = rootFS h := e.JavaScript(jsTemplate) r.Equal("application/javascript", h.ContentType()) bb := &bytes.Buffer{} r.NoError(h.Render(bb, Data{})) r.Contains(bb.String(), `id`) r.Contains(bb.String(), `foo`) // To check it has escaped the partial r.NotContains(bb.String(), `
`) } ================================================ FILE: render/json.go ================================================ package render import ( "encoding/json" "io" ) type jsonRenderer struct { value any } func (s jsonRenderer) ContentType() string { return "application/json; charset=utf-8" } func (s jsonRenderer) Render(w io.Writer, data Data) error { return json.NewEncoder(w).Encode(s.value) } // JSON renders the value using the "application/json" // content type. func JSON(v any) Renderer { return jsonRenderer{value: v} } // JSON renders the value using the "application/json" // content type. func (e *Engine) JSON(v any) Renderer { return JSON(v) } ================================================ FILE: render/json_test.go ================================================ package render import ( "bytes" "strings" "testing" "github.com/stretchr/testify/require" ) func Test_JSON(t *testing.T) { r := require.New(t) e := NewEngine() re := e.JSON(Data{"hello": "world"}) r.Equal("application/json; charset=utf-8", re.ContentType()) bb := &bytes.Buffer{} r.NoError(re.Render(bb, nil)) r.Equal(`{"hello":"world"}`, strings.TrimSpace(bb.String())) } ================================================ FILE: render/markdown_test.go ================================================ package render import ( "bytes" "strings" "testing" "testing/fstest" "github.com/stretchr/testify/require" ) const mdTemplate = "my-template.md" func Test_MD_WithoutLayout(t *testing.T) { r := require.New(t) rootFS := fstest.MapFS{ mdTemplate: &fstest.MapFile{ Data: []byte("<%= name %>"), Mode: 0644, }, } e := NewEngine() e.TemplatesFS = rootFS h := e.HTML(mdTemplate) r.Equal("text/html; charset=utf-8", h.ContentType()) bb := &bytes.Buffer{} r.NoError(h.Render(bb, Data{"name": "Mark"})) r.Equal("

Mark

", strings.TrimSpace(bb.String())) } func Test_MD_WithLayout(t *testing.T) { r := require.New(t) rootFS := fstest.MapFS{ mdTemplate: &fstest.MapFile{ Data: []byte("<%= name %>"), Mode: 0644, }, htmlLayout: &fstest.MapFile{ Data: []byte("<%= yield %>"), Mode: 0644, }, } e := NewEngine() e.TemplatesFS = rootFS e.HTMLLayout = htmlLayout h := e.HTML(mdTemplate) r.Equal("text/html; charset=utf-8", h.ContentType()) bb := &bytes.Buffer{} r.NoError(h.Render(bb, Data{"name": "Mark"})) r.Equal("

Mark

\n", strings.TrimSpace(bb.String())) } ================================================ FILE: render/options.go ================================================ package render import ( "io/fs" "github.com/gobuffalo/plush/v5/helpers/hctx" ) // Helpers to be included in all templates type Helpers hctx.Map // Options for render.Engine type Options struct { // HTMLLayout is the default layout to be used with all HTML renders. HTMLLayout string // JavaScriptLayout is the default layout to be used with all JavaScript renders. JavaScriptLayout string // TemplateFS is the fs.FS that holds the templates TemplatesFS fs.FS // AssetsFS is the fs.FS that holds the of the public assets the app will serve. AssetsFS fs.FS // Helpers to be rendered with the templates Helpers Helpers // TemplateEngine to be used for rendering HTML templates TemplateEngines map[string]TemplateEngine // DefaultContentType instructs the engine what it should fall back to if // the "content-type" is unknown DefaultContentType string // TemplateMetadataKeys allows users to specify custom keys for template metadata // If nil, uses default Buffalo metadata approach TemplateMetadataKeys map[string]string TemplateBaseDir string } // Default metadata keys var defaultTemplateMetadataKeys = map[string]string{ "template_file": "_buffalo_template_file", "base_name": "_buffalo_base_name", "extension": "_buffalo_extension", "last_modified": "_buffalo_last_modification", } ================================================ FILE: render/partials_test.go ================================================ package render import ( "bytes" "strings" "testing" "testing/fstest" "github.com/stretchr/testify/require" ) func Test_Template_Partial(t *testing.T) { r := require.New(t) const part = `<%= partial("foo.html") %>` const tmpl = "Foo > <%= name %>" rootFS := fstest.MapFS{ htmlTemplate: &fstest.MapFile{ Data: []byte(tmpl), Mode: 0644, }, "_foo.html": &fstest.MapFile{ Data: []byte(part), Mode: 0644, }, } e := NewEngine() e.TemplatesFS = rootFS bb := &bytes.Buffer{} re := e.Template("foo/bar", htmlTemplate) r.NoError(re.Render(bb, Data{"name": "Mark"})) r.Equal("Foo > Mark", strings.TrimSpace(bb.String())) } func Test_Template_PartialCustomFeeder(t *testing.T) { r := require.New(t) rootFS := fstest.MapFS{ "base.plush.html": &fstest.MapFile{ Data: []byte(`<%= partial("foo.plush.html") %>`), Mode: 0644, }, "_foo.plush.html": &fstest.MapFile{ Data: []byte("other"), Mode: 0644, }, } e := NewEngine() e.TemplatesFS = rootFS t.Run("Custom Feeder", func(t *testing.T) { e.Helpers["partialFeeder"] = func(path string) (string, error) { return "custom", nil } bb := &bytes.Buffer{} re := e.HTML("base.plush.html") r.NoError(re.Render(bb, Data{})) r.Equal("custom", strings.TrimSpace(bb.String())) }) t.Run("Default Feeder", func(t *testing.T) { e.Helpers["partialFeeder"] = nil bb := &bytes.Buffer{} re := e.HTML("base.plush.html") r.NoError(re.Render(bb, Data{})) r.Equal("other", strings.TrimSpace(bb.String())) }) } func Test_Template_Partial_WithoutExtension(t *testing.T) { r := require.New(t) const part = `<%= partial("foo") %>` const tmpl = "Foo > <%= name %>" rootFS := fstest.MapFS{ htmlTemplate: &fstest.MapFile{ Data: []byte(tmpl), Mode: 0644, }, "_foo.html": &fstest.MapFile{ Data: []byte(part), Mode: 0644, }, } e := NewEngine() e.TemplatesFS = rootFS bb := &bytes.Buffer{} re := e.Template("foo/bar", htmlTemplate) r.NoError(re.Render(bb, Data{"name": "Mark"})) r.Equal("Foo > Mark", strings.TrimSpace(bb.String())) } func Test_Template_Partial_Form(t *testing.T) { r := require.New(t) const newHTML = `<%= form_for(user, {}) { return partial("form.html") } %>` const formHTML = `<%= f.InputTag("Name") %>` const result = `
` rootFS := fstest.MapFS{ "new.html": &fstest.MapFile{ Data: []byte(newHTML), Mode: 0644, }, "_form.html": &fstest.MapFile{ Data: []byte(formHTML), Mode: 0644, }, } e := NewEngine() e.TemplatesFS = rootFS u := Widget{Name: "Mark"} bb := &bytes.Buffer{} re := e.HTML("new.html") r.NoError(re.Render(bb, Data{"user": u})) r.Equal(result, strings.TrimSpace(bb.String())) } func Test_Template_Partial_With_For(t *testing.T) { r := require.New(t) const forHTML = `<%= for(user) in users { %><%= partial("row") %><% } %>` const rowHTML = `Hi <%= user.Name %>, ` const result = `Hi Mark, Hi Yonghwan,` rootFS := fstest.MapFS{ "for.html": &fstest.MapFile{ Data: []byte(forHTML), Mode: 0644, }, "_row.html": &fstest.MapFile{ Data: []byte(rowHTML), Mode: 0644, }, } e := NewEngine() e.TemplatesFS = rootFS bb := &bytes.Buffer{} re := e.Template("text/html; charset=utf-8", "for.html") r.Equal("text/html; charset=utf-8", re.ContentType()) err := re.Render(bb, Data{"users": []Widget{ {Name: "Mark"}, {Name: "Yonghwan"}, }}) r.NoError(err) r.Equal(result, strings.TrimSpace(bb.String())) } func Test_Template_Partial_With_For_And_Local(t *testing.T) { r := require.New(t) const forHTML = `<%= for(user) in users { %><%= partial("row", {say:"Hi"}) %><% } %>` const rowHTML = `<%= say %> <%= user.Name %>, ` const result = `Hi Mark, Hi Yonghwan,` rootFS := fstest.MapFS{ "for.html": &fstest.MapFile{ Data: []byte(forHTML), Mode: 0644, }, "_row.html": &fstest.MapFile{ Data: []byte(rowHTML), Mode: 0644, }, } e := NewEngine() e.TemplatesFS = rootFS bb := &bytes.Buffer{} re := e.Template("text/html; charset=utf-8", "for.html") r.Equal("text/html; charset=utf-8", re.ContentType()) err := re.Render(bb, Data{"users": []Widget{ {Name: "Mark"}, {Name: "Yonghwan"}, }}) r.NoError(err) r.Equal(result, strings.TrimSpace(bb.String())) } func Test_Template_Partial_Recursive_With_Global_And_Local_Context(t *testing.T) { r := require.New(t) const indexHTML = `<%= partial("foo.html", {other: "Other"}) %>` const fooHTML = `<%= other %>|<%= name %>` const result = `Other|Mark` rootFS := fstest.MapFS{ "index.html": &fstest.MapFile{ Data: []byte(indexHTML), Mode: 0644, }, "_foo.html": &fstest.MapFile{ Data: []byte(fooHTML), Mode: 0644, }, } e := NewEngine() e.TemplatesFS = rootFS bb := &bytes.Buffer{} re := e.Template("foo/bar", "index.html") r.NoError(re.Render(bb, Data{"name": "Mark"})) r.Equal(result, strings.TrimSpace(bb.String())) } func Test_Template_Partial_With_Layout(t *testing.T) { r := require.New(t) const indexHTML = `<%= partial("foo.html",{layout:"layout.html"}) %>` const layoutHTML = `Layout > <%= yield %>` const fooHTML = "Foo > <%= name %>" const result = `Layout > Foo > Mark` rootFS := fstest.MapFS{ "index.html": &fstest.MapFile{ Data: []byte(indexHTML), Mode: 0644, }, "_layout.html": &fstest.MapFile{ Data: []byte(layoutHTML), Mode: 0644, }, "_foo.html": &fstest.MapFile{ Data: []byte(fooHTML), Mode: 0644, }, } e := NewEngine() e.TemplatesFS = rootFS bb := &bytes.Buffer{} re := e.Template("foo/bar", "index.html") r.NoError(re.Render(bb, Data{"name": "Mark"})) r.Equal(result, strings.TrimSpace(bb.String())) } ================================================ FILE: render/plain.go ================================================ package render // Plain renders the named files using the 'text/html' // content type and the github.com/gobuffalo/plush // package for templating. If more than 1 file is provided // the second file will be considered a "layout" file // and the first file will be the "content" file which will // be placed into the "layout" using "<%= yield %>". func Plain(names ...string) Renderer { e := New(Options{}) return e.Plain(names...) } // Plain renders the named files using the 'text/plain' // content type and the github.com/gobuffalo/plush // package for templating. If more than 1 file is provided // the second file will be considered a "layout" file // and the first file will be the "content" file which will // be placed into the "layout" using "<%= yield %>". func (e *Engine) Plain(names ...string) Renderer { hr := &templateRenderer{ Engine: e, contentType: "text/plain; charset=utf-8", names: names, } return hr } ================================================ FILE: render/plain_test.go ================================================ package render import ( "bytes" "testing" "testing/fstest" "github.com/stretchr/testify/require" ) func Test_Plain(t *testing.T) { r := require.New(t) rootFS := fstest.MapFS{ "test.txt": &fstest.MapFile{ Data: []byte("<%= name %>"), Mode: 0644, }, } e := NewEngine() e.TemplatesFS = rootFS re := e.Plain("test.txt") r.Equal("text/plain; charset=utf-8", re.ContentType()) var examples = []string{"Mark", "Jém"} for _, example := range examples { bb := &bytes.Buffer{} r.NoError(re.Render(bb, Data{"name": example})) r.Equal(example, bb.String()) } } ================================================ FILE: render/render.go ================================================ package render import ( "github.com/gobuffalo/helpers" "github.com/gobuffalo/helpers/forms" "github.com/gobuffalo/helpers/forms/bootstrap" "github.com/gobuffalo/plush/v5" ) // Engine used to power all defined renderers. // This allows you to configure the system to your // preferred settings, instead of just getting // the defaults. type Engine struct { Options } // New render.Engine ready to go with your Options // and some defaults we think you might like. func New(opts Options) *Engine { if opts.Helpers == nil { opts.Helpers = Helpers{} } if len(opts.Helpers) == 0 { opts.Helpers = defaultHelpers() } if opts.TemplateEngines == nil { opts.TemplateEngines = map[string]TemplateEngine{} } if _, ok := opts.TemplateEngines["html"]; !ok { opts.TemplateEngines["html"] = plush.BuffaloRenderer } if _, ok := opts.TemplateEngines["plush"]; !ok { opts.TemplateEngines["plush"] = plush.BuffaloRenderer } if _, ok := opts.TemplateEngines["text"]; !ok { opts.TemplateEngines["text"] = plush.BuffaloRenderer } if _, ok := opts.TemplateEngines["txt"]; !ok { opts.TemplateEngines["txt"] = plush.BuffaloRenderer } if _, ok := opts.TemplateEngines["js"]; !ok { opts.TemplateEngines["js"] = plush.BuffaloRenderer } if _, ok := opts.TemplateEngines["md"]; !ok { opts.TemplateEngines["md"] = MDTemplateEngine } if _, ok := opts.TemplateEngines["tmpl"]; !ok { opts.TemplateEngines["tmpl"] = GoTemplateEngine } if opts.DefaultContentType == "" { opts.DefaultContentType = "text/html; charset=utf-8" } if opts.TemplateMetadataKeys == nil { opts.TemplateMetadataKeys = defaultTemplateMetadataKeys } e := &Engine{ Options: opts, } return e } func defaultHelpers() Helpers { h := Helpers(helpers.ALL()) h[forms.FormKey] = bootstrap.Form h[forms.FormForKey] = bootstrap.FormFor h["form_for"] = bootstrap.FormFor return h } ================================================ FILE: render/render_test.go ================================================ package render import ( "os" "testing" "testing/fstest" ) type Widget struct { Name string } func (w Widget) ToPath() string { return w.Name } func NewEngine() *Engine { return New(Options{ TemplatesFS: fstest.MapFS{}, AssetsFS: fstest.MapFS{}, }) } type rendFriend func(string, RendererFunc) Renderer func TestMain(m *testing.M) { code := m.Run() os.Exit(code) } func init() { assetMap.Range(func(key, value string) bool { assetMap.Delete(key) return true }) } ================================================ FILE: render/renderer.go ================================================ package render import "io" // Renderer interface that must be satisfied to be used with // buffalo.Context.Render type Renderer interface { ContentType() string Render(io.Writer, Data) error } // Data type to be provided to the Render function on the // Renderer interface. type Data map[string]any ================================================ FILE: render/sse.go ================================================ package render import ( "encoding/json" "fmt" "net/http" ) type sse struct { Data any `json:"data"` Type string `json:"type"` } func (s *sse) String() string { b, _ := json.Marshal(s) return fmt.Sprintf("data: %s\n\n", string(b)) } func (s *sse) Bytes() []byte { return []byte(s.String()) } // EventSource is designed to work with JavaScript EventSource objects. // see https://developer.mozilla.org/en-US/docs/Web/API/EventSource for // more details type EventSource struct { w http.ResponseWriter fl http.Flusher } func (es *EventSource) Write(t string, d any) error { s := &sse{Type: t, Data: d} _, err := es.w.Write(s.Bytes()) if err != nil { return err } es.Flush() return nil } // Flush messages down the pipe. If messages aren't flushed they // won't be sent. func (es *EventSource) Flush() { es.fl.Flush() } type closeNotifier interface { CloseNotify() <-chan bool } // CloseNotify return true across the channel when the connection // in the browser has been severed. func (es *EventSource) CloseNotify() <-chan bool { if cn, ok := es.w.(closeNotifier); ok { return cn.CloseNotify() } return nil } // NewEventSource returns a new EventSource instance while ensuring // that the http.ResponseWriter is able to handle EventSource messages. // It also makes sure to set the proper response heads. func NewEventSource(w http.ResponseWriter) (*EventSource, error) { es := &EventSource{w: w} var ok bool es.fl, ok = w.(http.Flusher) if !ok { return es, fmt.Errorf("streaming is not supported") } es.w.Header().Set("Content-Type", "text/event-stream") es.w.Header().Set("Cache-Control", "no-cache") es.w.Header().Set("Connection", "keep-alive") es.w.Header().Set("Access-Control-Allow-Origin", "*") return es, nil } ================================================ FILE: render/string.go ================================================ package render import ( "fmt" "io" ) type stringRenderer struct { *Engine body string } func (s stringRenderer) ContentType() string { return "text/plain; charset=utf-8" } func (s stringRenderer) Render(w io.Writer, data Data) error { te, ok := s.TemplateEngines["text"] if !ok { return fmt.Errorf("could not find a template engine for text") } t, err := te(s.body, data, s.Helpers) if err != nil { return err } _, err = w.Write([]byte(t)) return err } // String renderer that will run the string through // the github.com/gobuffalo/plush package and return // "text/plain" as the content type. func String(s string, args ...any) Renderer { e := New(Options{}) return e.String(s, args...) } // String renderer that will run the string through // the github.com/gobuffalo/plush package and return // "text/plain" as the content type. func (e *Engine) String(s string, args ...any) Renderer { if len(args) > 0 { s = fmt.Sprintf(s, args...) } return stringRenderer{ Engine: e, body: s, } } ================================================ FILE: render/string_map.go ================================================ package render import ( "slices" "sync" ) // stringMap wraps sync.Map and uses the following types: // key: string // value: string type stringMap struct { data sync.Map } // Delete the key from the map func (m *stringMap) Delete(key string) { m.data.Delete(key) } // Load the key from the map. // Returns string or bool. // A false return indicates either the key was not found // or the value is not of type string func (m *stringMap) Load(key string) (string, bool) { i, ok := m.data.Load(key) if !ok { return ``, false } s, ok := i.(string) return s, ok } // LoadOrStore will return an existing key or // store the value if not already in the map func (m *stringMap) LoadOrStore(key string, value string) (string, bool) { i, _ := m.data.LoadOrStore(key, value) s, ok := i.(string) return s, ok } // Range over the string values in the map func (m *stringMap) Range(f func(key string, value string) bool) { m.data.Range(func(k, v any) bool { key, ok := k.(string) if !ok { return false } value, ok := v.(string) if !ok { return false } return f(key, value) }) } // Store a string in the map func (m *stringMap) Store(key string, value string) { m.data.Store(key, value) } // Keys returns a list of keys in the map func (m *stringMap) Keys() []string { var keys []string m.Range(func(key string, value string) bool { keys = append(keys, key) return true }) slices.Sort(keys) return keys } // Clear removes all entries from the map func (m *stringMap) Clear() { m.data.Clear() } ================================================ FILE: render/string_map_test.go ================================================ package render import ( "slices" "testing" "github.com/stretchr/testify/require" ) func Test_stringMap(t *testing.T) { r := require.New(t) sm := &stringMap{} sm.Store("a", `A`) s, ok := sm.Load("a") r.True(ok) r.Equal(`A`, s) s, ok = sm.LoadOrStore("b", `B`) r.True(ok) r.Equal(`B`, s) s, ok = sm.LoadOrStore("b", `BB`) r.True(ok) r.Equal(`B`, s) var keys []string sm.Range(func(key string, value string) bool { keys = append(keys, key) return true }) slices.Sort(keys) r.Equal(sm.Keys(), keys) sm.Delete("b") r.Equal([]string{"a", "b"}, keys) sm.Delete("b") _, ok = sm.Load("b") r.False(ok) func(m *stringMap) { m.Store("c", `C`) }(sm) s, ok = sm.Load("c") r.True(ok) r.Equal(`C`, s) } ================================================ FILE: render/string_test.go ================================================ package render_test import ( "bytes" "testing" "github.com/gobuffalo/buffalo/render" "github.com/stretchr/testify/require" ) func Test_String(t *testing.T) { r := require.New(t) j := render.New(render.Options{}).String re := j("<%= name %>") r.Equal("text/plain; charset=utf-8", re.ContentType()) var examples = []string{"Mark", "Jém"} for _, example := range examples { bb := &bytes.Buffer{} err := re.Render(bb, map[string]any{"name": example}) r.NoError(err) r.Equal(example, bb.String()) } } ================================================ FILE: render/template.go ================================================ package render import ( "fmt" "html/template" "io" "io/fs" "maps" "os" "path/filepath" "sort" "strings" "sync" "unsafe" "github.com/sirupsen/logrus" "golang.org/x/text/language" ) type templateRenderer struct { *Engine contentType string names []string aliases sync.Map } func (s *templateRenderer) ContentType() string { return s.contentType } func (s *templateRenderer) resolve(name string) ([]byte, fs.FileInfo, error) { if s.TemplatesFS == nil { return nil, nil, fmt.Errorf("no templates fs defined") } f, err := s.TemplatesFS.Open(name) if err == nil { contents, err := io.ReadAll(f) ff, _ := f.Stat() return contents, ff, err } v, ok := s.aliases.Load(name) if !ok { return nil, nil, fmt.Errorf("could not find template %s", name) } f, err = s.TemplatesFS.Open(v.(string)) if err != nil { return nil, nil, err } contents, err := io.ReadAll(f) ff, _ := f.Stat() return contents, ff, err } func (s *templateRenderer) Render(w io.Writer, data Data) error { if err := s.updateAliases(); err != nil { return err } var body template.HTML for _, name := range s.names { var err error body, err = s.exec(name, data) if err != nil { return fmt.Errorf("%s: %w", name, err) } data["yield"] = body } _, err := w.Write(unsafe.Slice(unsafe.StringData(string(body)), len(body))) return err } func (s *templateRenderer) updateAliases() error { if s.TemplatesFS == nil { return nil } return fs.WalkDir(s.TemplatesFS, ".", func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { return nil } // index.plush.ko-kr.html as index.ko-kr.html shortcut := strings.Replace(path, ".plush.", ".", 1) s.aliases.Store(shortcut, path) // register short version (lang only) of shortcut words := strings.Split(filepath.Base(shortcut), ".") if len(words) > 2 { for _, w := range words[1 : len(words)-1] { if tag, err := language.Parse(w); err == nil { base, confidence := tag.Base() if confidence == language.Exact || confidence == language.High { // index.plush.ko-kr.html as index.ko.html shortcut := strings.Replace(shortcut, w, base.String(), 1) s.aliases.Store(shortcut, path) // index.plush.ko-kr.html as index.plush.ko.html shortcut = strings.Replace(path, w, base.String(), 1) s.aliases.Store(shortcut, path) } } } } return nil }) } func fixExtension(name string, ct string) string { if filepath.Ext(name) == "" { switch { case strings.Contains(ct, "html"): name += ".html" case strings.Contains(ct, "javascript"): name += ".js" case strings.Contains(ct, "markdown"): name += ".md" } } return name } // partialFeeder returns template string for the name from `TemplateBox`. // It should be registered as helper named `partialFeeder` so plush can // find it with the name. func (s *templateRenderer) partialFeeder(name string) (string, error) { ct := strings.ToLower(s.contentType) d, f := filepath.Split(name) name = filepath.Join(d, "_"+f) name = fixExtension(name, ct) b, _, err := s.resolve(name) return string(b), err } func (s *templateRenderer) exec(name string, data Data) (template.HTML, error) { ct := strings.ToLower(s.contentType) data["contentType"] = ct name = fixExtension(name, ct) source, file, err := s.localizedResolve(name, data) if err != nil { return "", err } helpers := maps.Clone(s.Helpers) // Allows to specify custom partialFeeder if helpers["partialFeeder"] == nil { helpers["partialFeeder"] = s.partialFeeder } helpers = s.addAssetsHelpers(helpers) exts, fileName := s.extsAndBase(name) body := string(source) for _, ext := range exts { s.addTemplateMetadata(data, fileName, ext, file) te, ok := s.TemplateEngines[ext] if !ok { logrus.Errorf("could not find a template engine for %s", ext) continue } body, err = te(body, data, helpers) if err != nil { return "", err } } return template.HTML(body), nil } // next step, deprecate if this is no longer required. func (s *templateRenderer) localizedName(name string, data Data) string { templateName := name languages, ok := data["languages"].([]string) if !ok || len(languages) == 0 { return templateName } ll := len(languages) // Default language is the last in the list defaultLanguage := languages[ll-1] ext := filepath.Ext(name) rawName := strings.TrimSuffix(name, ext) for _, l := range languages { var candidateName string if l == defaultLanguage { break } candidateName = rawName + "." + strings.ToLower(l) + ext if _, _, err := s.resolve(candidateName); err == nil { // Replace name with the existing suffixed version templateName = candidateName break } } return templateName } func (s *templateRenderer) localizedResolve(name string, data Data) ([]byte, fs.FileInfo, error) { languages, ok := data["languages"].([]string) if !ok || len(languages) == 0 { return s.resolve(name) } defaultLang := languages[len(languages)-1] // default language ext := filepath.Ext(name) rawName := strings.TrimSuffix(name, ext) for _, lang := range languages { if lang == defaultLang { break } fullLower := strings.ToLower(lang) // forms of ko-kr or ko short := strings.Split(fullLower, "-")[0] // form of ko fullLocale := rawName + "." + fullLower + ext if source, file, err := s.resolve(fullLocale); err == nil { return source, file, nil } if fullLower != short { langOnly := rawName + "." + short + ext if source, file, err := s.resolve(langOnly); err == nil { return source, file, nil } } } return s.resolve(name) } func (s *templateRenderer) addTemplateMetadata(data Data, fileName, ext string, info fs.FileInfo) { // Get metadata keys from options, fallback to defaults metaKeys := s.TemplateMetadataKeys if metaKeys == nil { metaKeys = defaultTemplateMetadataKeys } // Map each metadata type to user-defined key if templateFileKey, exists := metaKeys["template_file"]; exists && templateFileKey != "" { data[templateFileKey] = filepath.Join(s.TemplateBaseDir, fileName) + "." + ext } // base_name and extension are derived from the name passed to exec // last_modified is derived from the fs.FileInfo passed to exec // if available if baseNameKey, exists := metaKeys["base_name"]; exists && baseNameKey != "" { data[baseNameKey] = fileName } if extensionKey, exists := metaKeys["extension"]; exists && extensionKey != "" { data[extensionKey] = ext } if lastModKey, exists := metaKeys["last_modified"]; exists && lastModKey != "" { data[lastModKey] = info.ModTime() } } func (s *templateRenderer) extsAndBase(name string) ([]string, string) { exts := []string{} baseName := name for { ext := filepath.Ext(baseName) if ext == "" { break } baseName = strings.TrimSuffix(baseName, ext) exts = append(exts, strings.ToLower(ext[1:])) } if len(exts) == 0 { return []string{"html"}, baseName } sort.Sort(sort.Reverse(sort.StringSlice(exts))) return exts, baseName } func (s *templateRenderer) exts(name string) []string { exts := []string{} for { ext := filepath.Ext(name) if ext == "" { break } name = strings.TrimSuffix(name, ext) exts = append(exts, strings.ToLower(ext[1:])) } if len(exts) == 0 { return []string{"html"} } sort.Sort(sort.Reverse(sort.StringSlice(exts))) return exts } func (s *templateRenderer) assetPath(file string) (string, error) { if len(assetMap.Keys()) == 0 || os.Getenv("GO_ENV") != "production" { manifest, err := s.AssetsFS.Open("manifest.json") if err != nil { manifest, err = s.AssetsFS.Open("assets/manifest.json") if err != nil { return assetPathFor(file), nil } } defer manifest.Close() err = loadManifest(manifest) if err != nil { return assetPathFor(file), fmt.Errorf("your manifest.json is not correct: %s", err) } } return assetPathFor(file), nil } // Template renders the named files using the specified // content type and the github.com/gobuffalo/plush // package for templating. If more than 1 file is provided // the second file will be considered a "layout" file // and the first file will be the "content" file which will // be placed into the "layout" using "{{yield}}". func Template(c string, names ...string) Renderer { e := New(Options{}) return e.Template(c, names...) } // Template renders the named files using the specified // content type and the github.com/gobuffalo/plush // package for templating. If more than 1 file is provided // the second file will be considered a "layout" file // and the first file will be the "content" file which will // be placed into the "layout" using "{{yield}}". func (e *Engine) Template(c string, names ...string) Renderer { return &templateRenderer{ Engine: e, contentType: c, names: names, } } ================================================ FILE: render/template_engine.go ================================================ package render import ( "bytes" "html/template" ) // TemplateEngine needs to be implemented for a template system to be able to be used with Buffalo. type TemplateEngine func(input string, data map[string]any, helpers map[string]any) (string, error) // GoTemplateEngine implements the TemplateEngine interface for using standard Go templates func GoTemplateEngine(input string, data map[string]any, helpers map[string]any) (string, error) { t := template.New(input) t, err := t.Parse(input) if err != nil { return "", err } bb := &bytes.Buffer{} err = t.Execute(bb, data) return bb.String(), err } ================================================ FILE: render/template_helpers.go ================================================ package render import ( "encoding/json" "html/template" "io" "path" "path/filepath" ht "github.com/gobuffalo/helpers/tags" "github.com/gobuffalo/tags/v3" ) type helperTag struct { name string fn func(string, tags.Options) template.HTML } func (s *templateRenderer) addAssetsHelpers(helpers Helpers) Helpers { helpers["assetPath"] = s.assetPath ah := []helperTag{ {"javascriptTag", ht.JS}, {"stylesheetTag", ht.CSS}, {"imgTag", ht.Img}, } for _, h := range ah { func(h helperTag) { helpers[h.name] = func(file string, options tags.Options) (template.HTML, error) { if options == nil { options = tags.Options{} } f, err := s.assetPath(file) if err != nil { return "", err } return h.fn(f, options), nil } }(h) } return helpers } var assetMap = stringMap{} func assetPathFor(file string) string { filePath, ok := assetMap.Load(file) if filePath == "" || !ok { filePath = file } return path.Join("/assets", filePath) } func loadManifest(manifest io.Reader) error { m := map[string]string{} if err := json.NewDecoder(manifest).Decode(&m); err != nil { return err } for k, v := range m { // I don't think v has backslash but if so, correct them when // creating the map instead using the value in `assetPathFor()`. assetMap.Store(k, filepath.ToSlash(v)) } return nil } ================================================ FILE: render/template_helpers_test.go ================================================ package render import ( "fmt" "html/template" "testing" "testing/fstest" "github.com/gobuffalo/tags/v3" "github.com/stretchr/testify/require" ) type tagHelper = func(string, tags.Options) (template.HTML, error) func tag(name string) (tagHelper, error) { // Clear assetMap to ensure test isolation from other tests that may have loaded manifests assetMap.Clear() e := NewEngine() // Use empty AssetsFS for these tests to avoid manifest lookup e.AssetsFS = fstest.MapFS{} tr := e.Template("").(*templateRenderer) h := tr.addAssetsHelpers(Helpers{}) jt := h[name] f, ok := jt.(func(string, tags.Options) (template.HTML, error)) if !ok { return f, fmt.Errorf("expected tagHelper got %T", jt) } return f, nil } func Test_javascriptTag(t *testing.T) { r := require.New(t) f, err := tag("javascriptTag") r.NoError(err) s, err := f("application.js", nil) r.NoError(err) r.Equal(template.HTML(``), s) } func Test_javascriptTag_Options(t *testing.T) { r := require.New(t) f, err := tag("javascriptTag") r.NoError(err) s, err := f("application.js", tags.Options{"class": "foo"}) r.NoError(err) r.Equal(template.HTML(``), s) } func Test_stylesheetTag(t *testing.T) { r := require.New(t) f, err := tag("stylesheetTag") r.NoError(err) s, err := f("application.css", nil) r.NoError(err) r.Equal(template.HTML(``), s) } func Test_stylesheetTag_Options(t *testing.T) { r := require.New(t) f, err := tag("stylesheetTag") r.NoError(err) s, err := f("application.css", tags.Options{"class": "foo"}) r.NoError(err) r.Equal(template.HTML(``), s) } func Test_imgTag(t *testing.T) { r := require.New(t) f, err := tag("imgTag") r.NoError(err) s, err := f("foo.png", nil) r.NoError(err) r.Equal(template.HTML(``), s) } func Test_imgTag_Options(t *testing.T) { r := require.New(t) f, err := tag("imgTag") r.NoError(err) s, err := f("foo.png", tags.Options{"class": "foo"}) r.NoError(err) r.Equal(template.HTML(``), s) } ================================================ FILE: render/template_test.go ================================================ package render import ( "bytes" "strings" "testing" "testing/fstest" "github.com/stretchr/testify/require" ) func Test_Template(t *testing.T) { r := require.New(t) rootFS := fstest.MapFS{ htmlTemplate: &fstest.MapFile{ Data: []byte("<%= name %>"), Mode: 0644, }, } e := NewEngine() e.TemplatesFS = rootFS re := e.Template("foo/bar", htmlTemplate) r.Equal("foo/bar", re.ContentType()) bb := &bytes.Buffer{} r.NoError(re.Render(bb, Data{"name": "Mark"})) } func Test_AssetPath(t *testing.T) { r := require.New(t) rootFS := fstest.MapFS{ "manifest.json": &fstest.MapFile{ Data: []byte(`{ "application.css": "application.aabbc123.css" }`), Mode: 0644, }, } e := NewEngine() e.AssetsFS = rootFS cases := map[string]string{ "something.txt": "/assets/something.txt", "images/something.png": "/assets/images/something.png", "/images/something.png": "/assets/images/something.png", "application.css": "/assets/application.aabbc123.css", } for original, expected := range cases { rootFS := fstest.MapFS{ htmlTemplate: &fstest.MapFile{ Data: []byte("<%= assetPath(\"" + original + "\") %>"), Mode: 0644, }, } e.TemplatesFS = rootFS re := e.Template("text/html; charset=utf-8", htmlTemplate) bb := &bytes.Buffer{} r.NoError(re.Render(bb, Data{})) r.Equal(expected, strings.TrimSpace(bb.String())) } } func Test_AssetPathNoManifest(t *testing.T) { r := require.New(t) e := NewEngine() cases := map[string]string{ "something.txt": "/assets/something.txt", } for original, expected := range cases { rootFS := fstest.MapFS{ htmlTemplate: &fstest.MapFile{ Data: []byte("<%= assetPath(\"" + original + "\") %>"), Mode: 0644, }, } e.TemplatesFS = rootFS re := e.Template("text/html; charset=utf-8", htmlTemplate) bb := &bytes.Buffer{} r.NoError(re.Render(bb, Data{})) r.Equal(expected, strings.TrimSpace(bb.String())) } } func Test_AssetPathNoManifestCorrupt(t *testing.T) { r := require.New(t) rootFS := fstest.MapFS{ "manifest.json": &fstest.MapFile{ Data: []byte("//shdnn Corrupt!"), Mode: 0644, }, } e := NewEngine() e.AssetsFS = rootFS cases := map[string]string{ "something.txt": "manifest.json is not correct", "other.txt": "manifest.json is not correct", } for original, expected := range cases { rootFS := fstest.MapFS{ htmlTemplate: &fstest.MapFile{ Data: []byte("<%= assetPath(\"" + original + "\") %>"), Mode: 0644, }, } e.TemplatesFS = rootFS re := e.Template("text/html; charset=utf-8", htmlTemplate) bb := &bytes.Buffer{} r.Error(re.Render(bb, Data{})) r.NotEqual(expected, strings.TrimSpace(bb.String())) } } /* test if i18n files (both with plush mid-extension and latecy) proceeded correctly. */ func Test_Template_resolve_DefaultLang_Plush(t *testing.T) { r := require.New(t) rootFS := fstest.MapFS{ "index.plush.html": &fstest.MapFile{ Data: []byte("default <%= name %>"), Mode: 0644, }, "index.plush.ko-kr.html": &fstest.MapFile{ Data: []byte("korean <%= name %>"), Mode: 0644, }, } e := NewEngine() e.TemplatesFS = rootFS re := e.Template("foo/bar", "index.plush.html") r.Equal("foo/bar", re.ContentType()) bb := &bytes.Buffer{} r.NoError(re.Render(bb, Data{"name": "Paul", "languages": []string{"es", "en"}})) r.Equal("default Paul", strings.TrimSpace(bb.String())) } func Test_Template_resolve_UserLang_Plush(t *testing.T) { r := require.New(t) rootFS := fstest.MapFS{ "index.plush.html": &fstest.MapFile{ Data: []byte("default <%= name %>"), Mode: 0644, }, "index.plush.ko-kr.html": &fstest.MapFile{ Data: []byte("korean <%= name %>"), Mode: 0644, }, } e := NewEngine() e.TemplatesFS = rootFS re := e.Template("foo/bar", "index.plush.html") r.Equal("foo/bar", re.ContentType()) bb := &bytes.Buffer{} r.NoError(re.Render(bb, Data{"name": "Paul", "languages": []string{"ko-KR", "en"}})) r.Equal("korean Paul", strings.TrimSpace(bb.String())) } func Test_Template_resolve_DefaultLang_Legacy(t *testing.T) { r := require.New(t) rootFS := fstest.MapFS{ "index.html": &fstest.MapFile{ Data: []byte("default <%= name %>"), Mode: 0644, }, "index.ko-kr.html": &fstest.MapFile{ Data: []byte("korean <%= name %>"), Mode: 0644, }, } e := NewEngine() e.TemplatesFS = rootFS re := e.Template("foo/bar", "index.html") r.Equal("foo/bar", re.ContentType()) bb := &bytes.Buffer{} r.NoError(re.Render(bb, Data{"name": "Paul", "languages": []string{"es", "en"}})) r.Equal("default Paul", strings.TrimSpace(bb.String())) } func Test_Template_resolve_UserLang_Legacy(t *testing.T) { r := require.New(t) rootFS := fstest.MapFS{ "index.html": &fstest.MapFile{ Data: []byte("default <%= name %>"), Mode: 0644, }, "index.ko-kr.html": &fstest.MapFile{ Data: []byte("korean <%= name %>"), Mode: 0644, }, } e := NewEngine() e.TemplatesFS = rootFS re := e.Template("foo/bar", "index.html") r.Equal("foo/bar", re.ContentType()) bb := &bytes.Buffer{} r.NoError(re.Render(bb, Data{"name": "Paul", "languages": []string{"ko-KR", "en"}})) r.Equal("korean Paul", strings.TrimSpace(bb.String())) } func Test_Template_resolve_DefaultLang_Mixed(t *testing.T) { r := require.New(t) rootFS := fstest.MapFS{ "index.plush.html": &fstest.MapFile{ Data: []byte("default <%= name %>"), Mode: 0644, }, "index.plush.ko-kr.html": &fstest.MapFile{ Data: []byte("korean <%= name %>"), Mode: 0644, }, } e := NewEngine() e.TemplatesFS = rootFS // `buffalo fix` renames templates but does not fix actions // in this case, aliases will be used for template matching re := e.Template("foo/bar", "index.html") r.Equal("foo/bar", re.ContentType()) bb := &bytes.Buffer{} r.NoError(re.Render(bb, Data{"name": "Paul", "languages": []string{"es", "en"}})) r.Equal("default Paul", strings.TrimSpace(bb.String())) } func Test_Template_resolve_UserLang_Mixed(t *testing.T) { r := require.New(t) rootFS := fstest.MapFS{ "index.plush.html": &fstest.MapFile{ Data: []byte("default <%= name %>"), Mode: 0644, }, "index.plush.ko-kr.html": &fstest.MapFile{ Data: []byte("korean <%= name %>"), Mode: 0644, }, } e := NewEngine() e.TemplatesFS = rootFS // `buffalo fix` renames templates but does not fix actions // in this case, aliases will be used for template matching re := e.Template("foo/bar", "index.html") r.Equal("foo/bar", re.ContentType()) bb := &bytes.Buffer{} r.NoError(re.Render(bb, Data{"name": "Paul", "languages": []string{"ko-KR", "en"}})) r.Equal("korean Paul", strings.TrimSpace(bb.String())) } // support short language-only version of template e.g. index.plush.ko.html func Test_Template_resolve_FullLocale_ShortFile(t *testing.T) { r := require.New(t) rootFS := fstest.MapFS{ "index.plush.html": &fstest.MapFile{ Data: []byte("default <%= name %>"), Mode: 0644, }, "index.plush.ko.html": &fstest.MapFile{ Data: []byte("korean <%= name %>"), Mode: 0644, }, } e := NewEngine() e.TemplatesFS = rootFS re := e.Template("foo/bar", "index.plush.html") r.Equal("foo/bar", re.ContentType()) bb := &bytes.Buffer{} r.NoError(re.Render(bb, Data{"name": "Paul", "languages": []string{"ko-KR", "en"}})) r.Equal("korean Paul", strings.TrimSpace(bb.String())) } func Test_Template_resolve_LangOnly_FullFile(t *testing.T) { r := require.New(t) rootFS := fstest.MapFS{ "index.plush.html": &fstest.MapFile{ Data: []byte("default <%= name %>"), Mode: 0644, }, "index.plush.ko-kr.html": &fstest.MapFile{ Data: []byte("korean <%= name %>"), Mode: 0644, }, } e := NewEngine() e.TemplatesFS = rootFS re := e.Template("foo/bar", "index.plush.html") r.Equal("foo/bar", re.ContentType()) bb := &bytes.Buffer{} r.NoError(re.Render(bb, Data{"name": "Paul", "languages": []string{"ko", "en"}})) r.Equal("korean Paul", strings.TrimSpace(bb.String())) } func Test_Template_resolve_FullLocale_ShortFile_Legacy(t *testing.T) { r := require.New(t) rootFS := fstest.MapFS{ "index.html": &fstest.MapFile{ Data: []byte("default <%= name %>"), Mode: 0644, }, "index.ko.html": &fstest.MapFile{ Data: []byte("korean <%= name %>"), Mode: 0644, }, } e := NewEngine() e.TemplatesFS = rootFS re := e.Template("foo/bar", "index.html") r.Equal("foo/bar", re.ContentType()) bb := &bytes.Buffer{} r.NoError(re.Render(bb, Data{"name": "Paul", "languages": []string{"ko-KR", "en"}})) r.Equal("korean Paul", strings.TrimSpace(bb.String())) } func Test_Template_resolve_LangOnly_FullFile_Legacy(t *testing.T) { r := require.New(t) rootFS := fstest.MapFS{ "index.html": &fstest.MapFile{ Data: []byte("default <%= name %>"), Mode: 0644, }, "index.ko-kr.html": &fstest.MapFile{ Data: []byte("korean <%= name %>"), Mode: 0644, }, } e := NewEngine() e.TemplatesFS = rootFS re := e.Template("foo/bar", "index.html") r.Equal("foo/bar", re.ContentType()) bb := &bytes.Buffer{} r.NoError(re.Render(bb, Data{"name": "Paul", "languages": []string{"ko", "en"}})) r.Equal("korean Paul", strings.TrimSpace(bb.String())) } func Test_Template_resolve_FullLocale_ShortFile_Mixed(t *testing.T) { r := require.New(t) rootFS := fstest.MapFS{ "index.plush.html": &fstest.MapFile{ Data: []byte("default <%= name %>"), Mode: 0644, }, "index.plush.ko.html": &fstest.MapFile{ Data: []byte("korean <%= name %>"), Mode: 0644, }, } e := NewEngine() e.TemplatesFS = rootFS re := e.Template("foo/bar", "index.html") r.Equal("foo/bar", re.ContentType()) bb := &bytes.Buffer{} r.NoError(re.Render(bb, Data{"name": "Paul", "languages": []string{"ko-KR", "en"}})) r.Equal("korean Paul", strings.TrimSpace(bb.String())) } func Test_Template_resolve_LangOnly_FullFile_Mixed(t *testing.T) { r := require.New(t) rootFS := fstest.MapFS{ "index.plush.html": &fstest.MapFile{ Data: []byte("default <%= name %>"), Mode: 0644, }, "index.plush.ko-kr.html": &fstest.MapFile{ Data: []byte("korean <%= name %>"), Mode: 0644, }, } e := NewEngine() e.TemplatesFS = rootFS re := e.Template("foo/bar", "index.html") r.Equal("foo/bar", re.ContentType()) bb := &bytes.Buffer{} r.NoError(re.Render(bb, Data{"name": "Paul", "languages": []string{"ko", "en"}})) r.Equal("korean Paul", strings.TrimSpace(bb.String())) } func Test_Template_extsAndBase(t *testing.T) { r := require.New(t) tests := []struct { name string input string expectedExts []string expectedBase string }{ { name: "single extension", input: "index.html", expectedExts: []string{"html"}, expectedBase: "index", }, { name: "multiple extensions", input: "template.html.plush", expectedExts: []string{"plush", "html"}, expectedBase: "template", }, { name: "three extensions", input: "layout.md.html.plush", expectedExts: []string{"plush", "md", "html"}, expectedBase: "layout", }, { name: "no extension", input: "template", expectedExts: []string{"html"}, expectedBase: "template", }, { name: "empty string", input: "", expectedExts: []string{"html"}, expectedBase: "", }, { name: "extension with uppercase", input: "index.HTML", expectedExts: []string{"html"}, expectedBase: "index", }, { name: "mixed case extensions", input: "template.MD.HTML.PLUSH", expectedExts: []string{"plush", "md", "html"}, expectedBase: "template", }, { name: "path with directories", input: "layouts/application.html.plush", expectedExts: []string{"plush", "html"}, expectedBase: "layouts/application", }, { name: "nested path no extension", input: "views/users/index", expectedExts: []string{"html"}, expectedBase: "views/users/index", }, { name: "dotfile", input: ".gitignore", expectedExts: []string{"gitignore"}, expectedBase: "", }, { name: "dotfile with extension", input: ".env.local", expectedExts: []string{"local", "env"}, expectedBase: "", }, { name: "complex filename", input: "user-profile.en-US.html.plush", expectedExts: []string{"plush", "html", "en-us"}, expectedBase: "user-profile", }, { name: "only extension", input: ".html", expectedExts: []string{"html"}, expectedBase: "", }, { name: "locale and template extensions", input: "welcome.fr-FR.html.plush", expectedExts: []string{"plush", "html", "fr-fr"}, expectedBase: "welcome", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { renderer := &templateRenderer{} gotExts, gotBase := renderer.extsAndBase(tt.input) r.Equal(tt.expectedExts, gotExts, "extensions should match") r.Equal(tt.expectedBase, gotBase, "base name should match") }) } } ================================================ FILE: render/xml.go ================================================ package render import ( "encoding/xml" "io" ) type xmlRenderer struct { value any } func (s xmlRenderer) ContentType() string { return "application/xml; charset=utf-8" } func (s xmlRenderer) Render(w io.Writer, data Data) error { io.WriteString(w, xml.Header) enc := xml.NewEncoder(w) enc.Indent("", " ") return enc.Encode(s.value) } // XML renders the value using the "application/xml" // content type. func XML(v any) Renderer { return xmlRenderer{value: v} } // XML renders the value using the "application/xml" // content type. func (e *Engine) XML(v any) Renderer { return XML(v) } ================================================ FILE: render/xml_test.go ================================================ package render import ( "bytes" "strings" "testing" "github.com/stretchr/testify/require" ) func Test_XML(t *testing.T) { r := require.New(t) type user struct { Name string } e := NewEngine() re := e.XML(user{Name: "Mark"}) r.Equal("application/xml; charset=utf-8", re.ContentType()) bb := &bytes.Buffer{} r.NoError(re.Render(bb, nil)) r.Equal("\n\n Mark\n", strings.TrimSpace(bb.String())) } ================================================ FILE: request_data.go ================================================ package buffalo import "sync" type requestData struct { d map[string]any moot *sync.RWMutex } func newRequestData() *requestData { return &requestData{ d: make(map[string]any), moot: &sync.RWMutex{}, } } ================================================ FILE: request_data_test.go ================================================ package buffalo import ( "testing" "github.com/stretchr/testify/require" ) func Test_newRequestData(t *testing.T) { r := require.New(t) ts := newRequestData() r.NotNil(ts) r.NotNil(ts.moot) r.NotNil(ts.d) } ================================================ FILE: request_logger.go ================================================ package buffalo import ( "crypto/rand" "encoding/hex" "fmt" "net/http" "time" "github.com/gobuffalo/buffalo/internal/httpx" ) // RequestLogger can be be overridden to a user specified // function that can be used to log the request. var RequestLogger = RequestLoggerFunc func randString(i int) (string, error) { if i == 0 { i = 64 } b := make([]byte, i) _, err := rand.Read(b) return hex.EncodeToString(b), err } // formatBytes converts bytes to human-readable format (e.g., "1.5 MB") func formatBytes(b uint64) string { const unit = 1024 if b < unit { return fmt.Sprintf("%d B", b) } div, exp := uint64(unit), 0 for n := b / unit; n >= unit; n /= unit { div *= unit exp++ } return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "KMGTPE"[exp]) } // RequestLoggerFunc is the default implementation of the RequestLogger. // By default it will log a uniq "request_id", the HTTP Method of the request, // the path that was requested, the duration (time) it took to process the // request, the size of the response (and the "human" size), and the status // code of the response. func RequestLoggerFunc(h Handler) Handler { return func(c Context) error { rs, err := randString(10) if err != nil { return err } var irid any if irid = c.Session().Get("requestor_id"); irid == nil { rs, err := randString(10) if err != nil { return err } irid = rs c.Session().Set("requestor_id", irid) } rid := irid.(string) + "-" + rs c.Set("request_id", rid) c.LogField("request_id", rid) start := time.Now() defer func() { ws, ok := c.Response().(*Response) if !ok { ws = &Response{ResponseWriter: c.Response()} ws.Status = http.StatusOK } req := c.Request() ct := httpx.ContentType(req) if ct != "" { c.LogField("content_type", ct) } c.LogFields(map[string]any{ "method": req.Method, "path": req.URL.String(), "duration": time.Since(start), "size": ws.Size, "human_size": formatBytes(uint64(ws.Size)), "status": ws.Status, }) c.Logger().Info(req.URL.String()) }() return h(c) } } ================================================ FILE: resource.go ================================================ package buffalo import ( "fmt" "net/http" ) // 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 cr 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 Create(Context) error Update(Context) error Destroy(Context) error } // Middler can be implemented to specify additional // middleware specific to the resource type Middler interface { Use() []MiddlewareFunc } // BaseResource fills in the gaps for any Resource interface // functions you don't want/need to implement. /* type UsersResource struct { Resource } func (ur *UsersResource) List(c Context) error { return c.Render(http.StatusOK, render.String("hello") } // This will fulfill the Resource interface, despite only having // one of the functions defined. &UsersResource{&BaseResource{}) */ type BaseResource struct{} // List default implementation. Returns a 404 func (v BaseResource) List(c Context) error { return c.Error(http.StatusNotFound, fmt.Errorf("resource not implemented")) } // Show default implementation. Returns a 404 func (v BaseResource) Show(c Context) error { return c.Error(http.StatusNotFound, fmt.Errorf("resource not implemented")) } // Create default implementation. Returns a 404 func (v BaseResource) Create(c Context) error { return c.Error(http.StatusNotFound, fmt.Errorf("resource not implemented")) } // Update default implementation. Returns a 404 func (v BaseResource) Update(c Context) error { return c.Error(http.StatusNotFound, fmt.Errorf("resource not implemented")) } // Destroy default implementation. Returns a 404 func (v BaseResource) Destroy(c Context) error { return c.Error(http.StatusNotFound, fmt.Errorf("resource not implemented")) } ================================================ FILE: response.go ================================================ package buffalo import ( "bufio" "encoding/binary" "fmt" "net" "net/http" ) // Response implements the http.ResponseWriter interface and allows // for the capture of the response status and size to be used for things // like logging requests. type Response struct { Status int Size int http.ResponseWriter } // WriteHeader sets the status code for a response func (w *Response) WriteHeader(code int) { if code == w.Status { return } if w.Status > 0 { fmt.Printf("[WARNING] Headers were already written. Wanted to override status code %d with %d", w.Status, code) return } w.Status = code w.ResponseWriter.WriteHeader(code) } // Write the body of the response func (w *Response) Write(b []byte) (int, error) { w.Size = binary.Size(b) return w.ResponseWriter.Write(b) } // Hijack implements the http.Hijacker interface to allow for things like websockets. func (w *Response) Hijack() (net.Conn, *bufio.ReadWriter, error) { if hj, ok := w.ResponseWriter.(http.Hijacker); ok { return hj.Hijack() } return nil, nil, fmt.Errorf("does not implement http.Hijack") } // Flush the response func (w *Response) Flush() { if f, ok := w.ResponseWriter.(http.Flusher); ok { f.Flush() } } type closeNotifier interface { CloseNotify() <-chan bool } // CloseNotify implements the http.CloseNotifier interface func (w *Response) CloseNotify() <-chan bool { if cn, ok := w.ResponseWriter.(closeNotifier); ok { return cn.CloseNotify() } return nil } ================================================ FILE: response_test.go ================================================ package buffalo import ( "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/require" ) func Test_Response_MultipleWrite(t *testing.T) { r := require.New(t) resWr := httptest.NewRecorder() res := Response{ ResponseWriter: resWr, } res.WriteHeader(http.StatusOK) res.WriteHeader(http.StatusInternalServerError) r.Equal(res.Status, http.StatusOK) } ================================================ FILE: route.go ================================================ package buffalo import ( "fmt" "html/template" "net/url" "slices" "strings" ) // Routes returns a list of all of the routes defined // in this application. func (a *App) Routes() RouteList { // CHKME: why this function is exported? can we deprecate it? if a.root != nil { return a.root.routes } return a.routes } func addExtraParamsTo(path string, opts map[string]any) string { pendingParams := map[string]string{} keys := []string{} for k, v := range opts { if strings.Contains(path, fmt.Sprintf("%v", v)) { continue } keys = append(keys, k) pendingParams[k] = fmt.Sprintf("%v", v) } if len(keys) == 0 { return path } if !strings.Contains(path, "?") { path = path + "?" } else { if !strings.HasSuffix(path, "?") { path = path + "&" } } slices.Sort(keys) for index, k := range keys { format := "%v=%v" if index > 0 { format = "&%v=%v" } path = path + fmt.Sprintf(format, url.QueryEscape(k), url.QueryEscape(pendingParams[k])) } return path } // RouteHelperFunc represents the function that takes the route and the opts and build the path type RouteHelperFunc func(opts map[string]any) (template.HTML, error) // RouteList contains a mapping of the routes defined // in the application. This listing contains, Method, Path, // and the name of the Handler defined to process that route. type RouteList []*RouteInfo var methodOrder = map[string]string{ "GET": "1", "POST": "2", "PUT": "3", "DELETE": "4", } func (a RouteList) Len() int { return len(a) } func (a RouteList) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a RouteList) Less(i, j int) bool { // NOTE: it was used for sorting of app.routes but we don't sort the routes anymore. // keep it for compatibility but could be deprecated. x := a[i].App.host + a[i].Path + methodOrder[a[i].Method] y := a[j].App.host + a[j].Path + methodOrder[a[j].Method] return x < y } // Lookup search a specific PathName in the RouteList and return the *RouteInfo func (a RouteList) Lookup(name string) (*RouteInfo, error) { for _, ri := range a { if ri.PathName == name { return ri, nil } } return nil, fmt.Errorf("path name not found") } ================================================ FILE: route_info.go ================================================ package buffalo import ( "encoding/json" "errors" "fmt" "html/template" "net/http" "reflect" "strings" "github.com/gobuffalo/flect" "github.com/gobuffalo/events" "github.com/gorilla/mux" ) // RouteInfo provides information about the underlying route that // was built. type RouteInfo struct { Method string `json:"method"` Path string `json:"path"` HandlerName string `json:"handler"` ResourceName string `json:"resourceName,omitempty"` PathName string `json:"pathName"` Aliases []string `json:"aliases"` MuxRoute *mux.Route `json:"-"` Handler Handler `json:"-"` App *App `json:"-"` } // String returns a JSON representation of the RouteInfo func (ri RouteInfo) String() string { b, _ := json.MarshalIndent(ri, "", " ") return string(b) } // Alias path patterns to the this route. This is not the // same as a redirect. func (ri *RouteInfo) Alias(aliases ...string) *RouteInfo { ri.Aliases = append(ri.Aliases, aliases...) for _, a := range aliases { ri.App.router.Handle(a, ri).Methods(ri.Method) } return ri } // Name allows users to set custom names for the routes. func (ri *RouteInfo) Name(name string) *RouteInfo { routeIndex := -1 for index, route := range ri.App.Routes() { if route.App.host == ri.App.host && route.Path == ri.Path && route.Method == ri.Method { routeIndex = index break } } name = flect.Camelize(name) if !strings.HasSuffix(name, "Path") { name = name + "Path" } ri.PathName = name if routeIndex != -1 { ri.App.Routes()[routeIndex] = reflect.ValueOf(ri).Interface().(*RouteInfo) } return ri } // BuildPathHelper Builds a routeHelperfunc for a particular RouteInfo func (ri *RouteInfo) BuildPathHelper() RouteHelperFunc { cRoute := ri return func(opts map[string]any) (template.HTML, error) { pairs := []string{} for k, v := range opts { pairs = append(pairs, k) pairs = append(pairs, fmt.Sprintf("%v", v)) } url, err := cRoute.MuxRoute.URL(pairs...) if err != nil { return "", fmt.Errorf("missing parameters for %v: %s", cRoute.Path, err) } result := url.String() result = addExtraParamsTo(result, opts) return template.HTML(result), nil } } func (ri RouteInfo) ServeHTTP(res http.ResponseWriter, req *http.Request) { a := ri.App c := a.newContext(ri, res, req) payload := events.Payload{ "route": ri, "app": a, "context": c, } events.EmitPayload(EvtRouteStarted, payload) err := a.Middleware.handler(ri)(c) if err != nil { status := http.StatusInternalServerError var he HTTPError if errors.As(err, &he) { status = he.Status } events.EmitError(EvtRouteErr, err, payload) // things have really hit the fan if we're here!! a.Logger.Error(err) c.Response().WriteHeader(status) c.Response().Write([]byte(err.Error())) } events.EmitPayload(EvtRouteFinished, payload) } ================================================ FILE: route_info_test.go ================================================ package buffalo import ( "database/sql" "fmt" "net/http" "testing" "github.com/gobuffalo/buffalo/render" "github.com/gobuffalo/httptest" "github.com/stretchr/testify/require" ) func Test_RouteInfo_ServeHTTP_SQL_Error(t *testing.T) { r := require.New(t) app := New(Options{}) app.GET("/good", func(c Context) error { return c.Render(http.StatusOK, render.String("hi")) }) app.GET("/bad", func(c Context) error { return sql.ErrNoRows }) app.GET("/bad-2", func(c Context) error { return sql.ErrTxDone }) app.GET("/gone-unwrap", func(c Context) error { return c.Error(http.StatusGone, sql.ErrTxDone) }) app.GET("/gone-wrap", func(c Context) error { return c.Error(http.StatusGone, fmt.Errorf("some error wrapping here: %w", sql.ErrNoRows)) }) w := httptest.New(app) res := w.HTML("/good").Get() r.Equal(http.StatusOK, res.Code) res = w.HTML("/bad").Get() r.Equal(http.StatusNotFound, res.Code) res = w.HTML("/gone-wrap").Get() r.Equal(http.StatusGone, res.Code) res = w.HTML("/gone-unwrap").Get() r.Equal(http.StatusGone, res.Code) } ================================================ FILE: route_mappings.go ================================================ package buffalo import ( "errors" "fmt" "net/http" "net/url" "os" "path" "reflect" "strings" "github.com/gobuffalo/buffalo/internal/env" "github.com/gobuffalo/flect/name" "github.com/gorilla/handlers" ) const ( // AssetsAgeVarName is the ENV variable used to specify max age when ServeFiles is used. AssetsAgeVarName = "ASSETS_MAX_AGE" ) // These method functions will be moved to Home structure. // GET maps an HTTP "GET" request to the path and the specified handler. func (a *App) GET(p string, h Handler) *RouteInfo { return a.addRoute("GET", p, h) } // POST maps an HTTP "POST" request to the path and the specified handler. func (a *App) POST(p string, h Handler) *RouteInfo { return a.addRoute("POST", p, h) } // PUT maps an HTTP "PUT" request to the path and the specified handler. func (a *App) PUT(p string, h Handler) *RouteInfo { return a.addRoute("PUT", p, h) } // DELETE maps an HTTP "DELETE" request to the path and the specified handler. func (a *App) DELETE(p string, h Handler) *RouteInfo { return a.addRoute("DELETE", p, h) } // HEAD maps an HTTP "HEAD" request to the path and the specified handler. func (a *App) HEAD(p string, h Handler) *RouteInfo { return a.addRoute("HEAD", p, h) } // OPTIONS maps an HTTP "OPTIONS" request to the path and the specified handler. func (a *App) OPTIONS(p string, h Handler) *RouteInfo { return a.addRoute("OPTIONS", p, h) } // PATCH maps an HTTP "PATCH" request to the path and the specified handler. func (a *App) PATCH(p string, h Handler) *RouteInfo { return a.addRoute("PATCH", p, h) } // Redirect from one URL to another URL. Only works for "GET" requests. func (a *App) Redirect(status int, from, to string) *RouteInfo { return a.GET(from, func(c Context) error { return c.Redirect(status, to) }) } // Mount mounts a http.Handler (or Buffalo app) and passes through all requests to it. // // func muxer() http.Handler { // f := func(res http.ResponseWriter, req *http.Request) { // fmt.Fprintf(res, "%s - %s", req.Method, req.URL.String()) // } // mux := mux.NewRouter() // mux.HandleFunc("/foo", f).Methods("GET") // mux.HandleFunc("/bar", f).Methods("POST") // mux.HandleFunc("/baz/baz", f).Methods("DELETE") // return mux // } // // a.Mount("/admin", muxer()) // // $ curl -X DELETE http://localhost:3000/admin/baz/baz func (a *App) Mount(p string, h http.Handler) { prefix := path.Join(a.Prefix, p) path := path.Join(p, "{path:.+}") a.ANY(path, WrapHandler(http.StripPrefix(prefix, h))) } // ServeFiles maps an path to a directory on disk to serve static files. // Useful for JavaScript, images, CSS, etc... /* a.ServeFiles("/assets", http.Dir("path/to/assets")) */ func (a *App) ServeFiles(p string, root http.FileSystem) { path := path.Join(a.Prefix, p) a.filepaths = append(a.filepaths, path) h := stripAsset(path, a.fileServer(root), a) a.router.PathPrefix(path).Handler(h) } func (a *App) fileServer(fs http.FileSystem) http.Handler { fsh := http.FileServer(fs) baseHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { f, err := fs.Open(path.Clean(r.URL.Path)) if errors.Is(err, os.ErrNotExist) { eh := a.ErrorHandlers.Get(http.StatusNotFound) eh(http.StatusNotFound, fmt.Errorf("could not find %s", r.URL.Path), a.newContext(RouteInfo{}, w, r)) return } stat, _ := f.Stat() maxAge := env.Get(AssetsAgeVarName, "31536000") w.Header().Add("ETag", fmt.Sprintf("%x", stat.ModTime().UnixNano())) w.Header().Add("Cache-Control", fmt.Sprintf("max-age=%s", maxAge)) fsh.ServeHTTP(w, r) }) if a.CompressFiles { return handlers.CompressHandler(baseHandler) } return baseHandler } type newable interface { New(Context) error } type editable interface { Edit(Context) error } // Resource maps an implementation of the Resource interface // to the appropriate RESTful mappings. Resource returns the *App // associated with this group of mappings so you can set middleware, etc... // on that group, just as if you had used the a.Group functionality. // // Resource automatically creates a URL `/resources/new` if the resource // has a function `New()`. So it could act as a restriction for the value // of `resource_id`. URL `/resources/new` will always show the resource // creation page instead of showing the resource called `new`. /* a.Resource("/users", &UsersResource{}) // Is equal to this: ur := &UsersResource{} g := a.Group("/users") g.GET("/", ur.List) // GET /users => ur.List g.POST("/", ur.Create) // POST /users => ur.Create g.GET("/new", ur.New) // GET /users/new => ur.New g.GET("/{user_id}", ur.Show) // GET /users/{user_id} => ur.Show g.PUT("/{user_id}", ur.Update) // PUT /users/{user_id} => ur.Update g.DELETE("/{user_id}", ur.Destroy) // DELETE /users/{user_id} => ur.Destroy g.GET("/{user_id}/edit", ur.Edit) // GET /users/{user_id}/edit => ur.Edit */ func (a *App) Resource(p string, r Resource) *App { g := a.Group(p) if mw, ok := r.(Middler); ok { g.Use(mw.Use()...) } p = "/" rv := reflect.ValueOf(r) if rv.Kind() == reflect.Ptr { rv = rv.Elem() } rt := rv.Type() resourceName := rt.Name() handlerName := fmt.Sprintf("%s.%s", rt.PkgPath(), resourceName) + ".%s" n := strings.TrimSuffix(rt.Name(), "Resource") paramName := name.New(n).ParamID().String() type paramKeyable interface { ParamKey() string } if pk, ok := r.(paramKeyable); ok { paramName = pk.ParamKey() } spath := path.Join(p, "{"+paramName+"}") // This order will become the order of route evaluation too. setFuncKey(r.List, fmt.Sprintf(handlerName, "List")) g.GET(p, r.List).ResourceName = resourceName setFuncKey(r.Create, fmt.Sprintf(handlerName, "Create")) g.POST(p, r.Create).ResourceName = resourceName // NOTE: it makes restriction that resource id cannot be 'new'. if n, ok := r.(newable); ok { setFuncKey(n.New, fmt.Sprintf(handlerName, "New")) g.GET(path.Join(p, "new"), n.New).ResourceName = resourceName } setFuncKey(r.Show, fmt.Sprintf(handlerName, "Show")) g.GET(path.Join(spath), r.Show).ResourceName = resourceName setFuncKey(r.Update, fmt.Sprintf(handlerName, "Update")) g.PUT(path.Join(spath), r.Update).ResourceName = resourceName setFuncKey(r.Destroy, fmt.Sprintf(handlerName, "Destroy")) g.DELETE(path.Join(spath), r.Destroy).ResourceName = resourceName if n, ok := r.(editable); ok { setFuncKey(n.Edit, fmt.Sprintf(handlerName, "Edit")) g.GET(path.Join(spath, "edit"), n.Edit).ResourceName = resourceName } g.Prefix = path.Join(g.Prefix, spath) g.prefix = g.Prefix return g } // ANY accepts a request across any HTTP method for the specified path // and routes it to the specified Handler. func (a *App) ANY(p string, h Handler) { a.GET(p, h) a.POST(p, h) a.PUT(p, h) a.PATCH(p, h) a.HEAD(p, h) a.OPTIONS(p, h) a.DELETE(p, h) } // Group creates a new `*App` that inherits from it's parent `*App`. // This is useful for creating groups of end-points that need to share // common functionality, like middleware. /* g := a.Group("/api/v1") g.Use(AuthorizeAPIMiddleware) g.GET("/users, APIUsersHandler) g.GET("/users/:user_id, APIUserShowHandler) */ func (a *App) Group(groupPath string) *App { // TODO: move this function to app.go or home.go eventually. g := New(a.Options) // keep them for v0 compatibility g.Prefix = path.Join(a.Prefix, groupPath) g.Name = g.Prefix // for Home structure g.prefix = path.Join(a.prefix, groupPath) g.host = a.host g.name = g.prefix g.router = a.router g.RouteNamer = a.RouteNamer g.Middleware = a.Middleware.clone() g.ErrorHandlers = a.ErrorHandlers g.app = a.app // will replace g.root g.root = g.app // will be deprecated // to be replaced with child Homes. currently, only used in grifts. a.children = append(a.children, g) return g } // VirtualHost creates a new `*App` that inherits from it's parent `*App`. // All pre-configured things on the parent App such as middlewares will be // applied, and can be modified only for this child App. // // This is a multi-homing feature similar to the `VirtualHost` in Apache // or multiple `server`s in nginx. One important different behavior is that // there is no concept of the `default` host in buffalo (at least for now) // and the routing decision will be made with the "first match" manner. // (e.g. if you have already set the route for '/' for the root App before // setting up a virualhost, the route of the root App will be picked up // even if the client makes a request to the specified domain.) /* a.VirtualHost("www.example.com") a.VirtualHost("{subdomain}.example.com") a.VirtualHost("{subdomain:[a-z]+}.example.com") */ func (a *App) VirtualHost(h string) *App { g := a.Group("/") g.host = h g.router = a.router.Host(h).Subrouter() return g } // RouteHelpers returns a map of BuildPathHelper() for each route available in the app. func (a *App) RouteHelpers() map[string]RouteHelperFunc { rh := map[string]RouteHelperFunc{} for _, route := range a.Routes() { cRoute := route rh[cRoute.PathName] = cRoute.BuildPathHelper() } return rh } func (e *Home) addRoute(method string, url string, h Handler) *RouteInfo { // NOTE: lock the root app (not this app). only the root has the affective // routes list. e.app.moot.Lock() defer e.app.moot.Unlock() url = path.Join(e.prefix, url) url = e.app.normalizePath(url) name := e.app.RouteNamer.NameRoute(url) hs := funcKey(h) r := &RouteInfo{ Method: method, Path: url, HandlerName: hs, Handler: h, App: e.appSelf, // CHKME: to be replaced with Home Aliases: []string{}, } r.MuxRoute = e.router.Handle(url, r).Methods(method) r.Name(name) routes := e.app.Routes() routes = append(routes, r) // NOTE: sorting is fancy but we lose the evaluation order information // of routing decision. Let's keep the routes as registered order so // developers can easily evaluate the order with `buffalo routes` and // can debug any routing priority issue. (just keep the original line // as history reference) //sort.Sort(routes) e.app.routes = routes return r } func stripAsset(path string, h http.Handler, a *App) http.Handler { if path == "" { return h } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { up := r.URL.Path up = strings.TrimPrefix(up, path) up = strings.TrimSuffix(up, "/") u, err := url.Parse(up) if err != nil { eh := a.ErrorHandlers.Get(http.StatusBadRequest) eh(http.StatusBadRequest, err, a.newContext(RouteInfo{}, w, r)) return } r.URL = u h.ServeHTTP(w, r) }) } ================================================ FILE: route_mappings_test.go ================================================ package buffalo import ( "testing" "github.com/stretchr/testify/require" ) func Test_App_Routes_without_Root(t *testing.T) { r := require.New(t) a := New(Options{}) r.Nil(a.root) a.GET("/foo", voidHandler) routes := a.Routes() r.Len(routes, 1) route := routes[0] r.Equal("GET", route.Method) r.Equal("/foo/", route.Path) r.NotZero(route.HandlerName) } func Test_App_Routes_with_Root(t *testing.T) { r := require.New(t) a := New(Options{}) r.Nil(a.root) g := a.Group("/api/v1") g.GET("/foo", voidHandler) routes := a.Routes() r.Len(routes, 1) route := routes[0] r.Equal("GET", route.Method) r.Equal("/api/v1/foo/", route.Path) r.NotZero(route.HandlerName) r.Equal(a.Routes(), g.Routes()) } func Test_App_RouteName(t *testing.T) { r := require.New(t) a := New(Options{}) cases := map[string]string{ "cool": "coolPath", "coolPath": "coolPath", "coco_path": "cocoPath", "ouch_something_cool": "ouchSomethingCoolPath", } ri := a.GET("/something", voidHandler) for k, v := range cases { ri.Name(k) r.Equal(ri.PathName, v) } } func Test_RouteList_Lookup(t *testing.T) { r := require.New(t) a := New(Options{}) r.Nil(a.root) a.GET("/foo", voidHandler) a.GET("/test", voidHandler) routes := a.Routes() for _, route := range routes { lRoute, err := routes.Lookup(route.PathName) r.NoError(err) r.Equal(lRoute, route) } lRoute, err := routes.Lookup("a") r.Error(err) r.Nil(lRoute) } func Test_App_RouteHelpers(t *testing.T) { r := require.New(t) a := New(Options{}) r.Nil(a.root) a.GET("/foo", voidHandler) a.GET("/test/{id}", voidHandler) rh := a.RouteHelpers() r.Len(rh, 2) f, ok := rh["fooPath"] r.True(ok) x, err := f(map[string]any{}) r.NoError(err) r.Equal("/foo/", string(x)) f, ok = rh["testPath"] r.True(ok) x, err = f(map[string]any{ "id": 1, }) r.NoError(err) r.Equal("/test/1/", string(x)) } type resourceHandler struct{} func (r resourceHandler) List(Context) error { return nil } func (r resourceHandler) Show(Context) error { return nil } func (r resourceHandler) Create(Context) error { return nil } func (r resourceHandler) Update(Context) error { return nil } func (r resourceHandler) Destroy(Context) error { return nil } func Test_App_Routes_Resource(t *testing.T) { r := require.New(t) a := New(Options{}) r.Nil(a.root) a.GET("/foo", voidHandler) a.Resource("/r", resourceHandler{}) routes := a.Routes() r.Len(routes, 6) route := routes[0] r.Equal("GET", route.Method) r.Equal("/foo/", route.Path) r.NotZero(route.HandlerName) for k, v := range routes { if k > 0 { r.Equal("resourceHandler", v.ResourceName) } } } func Test_App_VirtualHost(t *testing.T) { r := require.New(t) a1 := New(Options{}) r.Nil(a1.root) h1 := a1.VirtualHost("www.example.com") h1.GET("/foo", voidHandler) routes := h1.Routes() r.Len(routes, 1) route := routes[0] r.Equal("GET", route.Method) r.Equal("/foo/", route.Path) r.NotZero(route.HandlerName) // With Regular Expressions a2 := New(Options{}) r.Nil(a1.root) h2 := a2.VirtualHost("{subdomain}.example.com") h2.GET("/foo", voidHandler) h2.GET("/foo/{id}", voidHandler).Name("fooID") rh := h2.RouteHelpers() routes = h2.Routes() r.Len(routes, 2) r.Equal("GET", routes[0].Method) r.Equal("/foo/", routes[0].Path) r.NotZero(routes[0].HandlerName) r.Equal("GET", routes[1].Method) r.Equal("/foo/{id}/", routes[1].Path) r.NotZero(routes[1].HandlerName) f, ok := rh["fooPath"] r.True(ok) x, err := f(map[string]any{ "subdomain": "test", }) r.NoError(err) r.Equal("http://test.example.com/foo/", string(x)) f, ok = rh["fooIDPath"] r.True(ok) x, err = f(map[string]any{ "subdomain": "test", "id": 1, }) r.NoError(err) r.Equal("http://test.example.com/foo/1/", string(x)) } ================================================ FILE: routenamer.go ================================================ package buffalo import ( "strings" "github.com/gobuffalo/flect" "github.com/gobuffalo/flect/name" ) // RouteNamer is in charge of naming a route from the // path assigned, this name typically will be used if no // name is assined with .Name(...). type RouteNamer interface { // NameRoute receives the path and returns the name // for the route. NameRoute(string) string } // BaseRouteNamer is the default route namer used by apps. type baseRouteNamer struct{} func (drn baseRouteNamer) NameRoute(p string) string { if p == "/" || p == "" { return "root" } resultParts := []string{} parts := strings.Split(p, "/") for index, part := range parts { originalPart := parts[index] var previousPart string if index > 0 { previousPart = parts[index-1] } var nextPart string if len(parts) > index+1 { nextPart = parts[index+1] } isIdentifierPart := strings.Contains(part, "{") && (strings.Contains(part, flect.Singularize(previousPart))) isSimplifiedID := part == `{id}` if isIdentifierPart || isSimplifiedID || part == "" { continue } if strings.Contains(nextPart, "{") { part = flect.Singularize(part) } if originalPart == "new" || originalPart == "edit" { resultParts = append([]string{part}, resultParts...) continue } if strings.Contains(previousPart, "}") { resultParts = append(resultParts, part) continue } resultParts = append(resultParts, part) } if len(resultParts) == 0 { return "unnamed" } underscore := strings.TrimSpace(strings.Join(resultParts, "_")) return name.VarCase(underscore) } ================================================ FILE: router_test.go ================================================ package buffalo import ( "fmt" "io" "net/http" "os" "path" "strings" "testing" "testing/fstest" "github.com/gobuffalo/buffalo/internal/env" "github.com/gobuffalo/buffalo/render" "github.com/gobuffalo/httptest" "github.com/gorilla/mux" "github.com/stretchr/testify/require" ) func testApp() *App { a := New(Options{}) a.Redirect(http.StatusMovedPermanently, "/foo", "/bar") a.GET("/bar", func(c Context) error { return c.Render(http.StatusOK, render.String("bar")) }) rt := a.Group("/router/tests") h := func(c Context) error { x := c.Request().Method + "|" x += strings.TrimSuffix(c.Value("current_path").(string), "/") return c.Render(http.StatusOK, render.String(x)) } rt.GET("/", h) rt.POST("/", h) rt.PUT("/", h) rt.DELETE("/", h) rt.OPTIONS("/", h) rt.PATCH("/", h) a.ErrorHandlers[http.StatusMethodNotAllowed] = func(status int, err error, c Context) error { res := c.Response() res.WriteHeader(status) res.Write([]byte("my custom 405")) return nil } return a } func otherTestApp() *App { a := New(Options{}) f := func(c Context) error { req := c.Request() return c.Render(http.StatusOK, render.String(req.Method+" - "+req.URL.String())) } a.GET("/foo", f) a.POST("/bar", f) a.DELETE("/baz/baz", f) return a } func Test_MethodNotFoundError(t *testing.T) { r := require.New(t) a := New(Options{}) a.GET("/bar", func(c Context) error { return c.Render(http.StatusOK, render.String("bar")) }) a.ErrorHandlers[http.StatusMethodNotAllowed] = func(status int, err error, c Context) error { res := c.Response() res.WriteHeader(status) res.Write([]byte("my custom 405")) return nil } w := httptest.New(a) res := w.HTML("/bar").Post(nil) r.Equal(http.StatusMethodNotAllowed, res.Code) r.Contains(res.Body.String(), "my custom 405") } func Test_Mount_Buffalo(t *testing.T) { r := require.New(t) a := testApp() a.Mount("/admin", otherTestApp()) table := map[string]string{ "/foo": "GET", "/bar": "POST", "/baz/baz": "DELETE", } ts := httptest.NewServer(a) defer ts.Close() for u, m := range table { p := fmt.Sprintf("%s/%s", ts.URL, path.Join("admin", u)) req, err := http.NewRequest(m, p, nil) r.NoError(err) res, err := http.DefaultClient.Do(req) r.NoError(err) b, _ := io.ReadAll(res.Body) r.Equal(fmt.Sprintf("%s - %s/", m, u), string(b)) } } func Test_Mount_Buffalo_on_Group(t *testing.T) { r := require.New(t) a := testApp() g := a.Group("/users") g.Mount("/admin", otherTestApp()) table := map[string]string{ "/foo": "GET", "/bar": "POST", "/baz/baz": "DELETE", } ts := httptest.NewServer(a) defer ts.Close() for u, m := range table { p := fmt.Sprintf("%s/%s", ts.URL, path.Join("users", "admin", u)) req, err := http.NewRequest(m, p, nil) r.NoError(err) res, err := http.DefaultClient.Do(req) r.NoError(err) b, _ := io.ReadAll(res.Body) r.Equal(fmt.Sprintf("%s - %s/", m, u), string(b)) } } func muxer() http.Handler { f := func(res http.ResponseWriter, req *http.Request) { fmt.Fprintf(res, "%s - %s", req.Method, req.URL.String()) } mux := mux.NewRouter() mux.HandleFunc("/foo/", f).Methods("GET") mux.HandleFunc("/bar/", f).Methods("POST") mux.HandleFunc("/baz/baz/", f).Methods("DELETE") return mux } func Test_Mount_Handler(t *testing.T) { r := require.New(t) a := testApp() a.Mount("/admin", muxer()) table := map[string]string{ "/foo": "GET", "/bar": "POST", "/baz/baz": "DELETE", } ts := httptest.NewServer(a) defer ts.Close() for u, m := range table { p := fmt.Sprintf("%s/%s", ts.URL, path.Join("admin", u)) req, err := http.NewRequest(m, p, nil) r.NoError(err) res, err := http.DefaultClient.Do(req) r.NoError(err) b, _ := io.ReadAll(res.Body) r.Equal(fmt.Sprintf("%s - %s/", m, u), string(b)) } } func Test_PreHandlers(t *testing.T) { r := require.New(t) a := testApp() bh := func(c Context) error { req := c.Request() return c.Render(http.StatusOK, render.String(req.Method+"-"+req.URL.String())) } a.GET("/ph", bh) a.POST("/ph", bh) mh := func(res http.ResponseWriter, req *http.Request) { if req.Method == "GET" { res.WriteHeader(http.StatusTeapot) res.Write([]byte("boo")) } } a.PreHandlers = append(a.PreHandlers, http.HandlerFunc(mh)) ts := httptest.NewServer(a) defer ts.Close() table := []struct { Code int Method string Result string }{ {Code: http.StatusTeapot, Method: "GET", Result: "boo"}, {Code: http.StatusOK, Method: "POST", Result: "POST-/ph/"}, } for _, v := range table { req, err := http.NewRequest(v.Method, ts.URL+"/ph", nil) r.NoError(err) res, err := http.DefaultClient.Do(req) r.NoError(err) b, err := io.ReadAll(res.Body) r.NoError(err) r.Equal(v.Code, res.StatusCode) r.Equal(v.Result, string(b)) } } func Test_PreWares(t *testing.T) { r := require.New(t) a := testApp() bh := func(c Context) error { req := c.Request() return c.Render(http.StatusOK, render.String(req.Method+"-"+req.URL.String())) } a.GET("/ph", bh) a.POST("/ph", bh) mh := func(h http.Handler) http.Handler { return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { if req.Method == "GET" { res.WriteHeader(http.StatusTeapot) res.Write([]byte("boo")) } }) } a.PreWares = append(a.PreWares, mh) ts := httptest.NewServer(a) defer ts.Close() table := []struct { Code int Method string Result string }{ {Code: http.StatusTeapot, Method: "GET", Result: "boo"}, {Code: http.StatusOK, Method: "POST", Result: "POST-/ph/"}, } for _, v := range table { req, err := http.NewRequest(v.Method, ts.URL+"/ph", nil) r.NoError(err) res, err := http.DefaultClient.Do(req) r.NoError(err) b, err := io.ReadAll(res.Body) r.NoError(err) r.Equal(v.Code, res.StatusCode) r.Equal(v.Result, string(b)) } } func Test_Router(t *testing.T) { r := require.New(t) table := []string{ "GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH", } ts := httptest.NewServer(testApp()) defer ts.Close() for _, v := range table { req, err := http.NewRequest(v, fmt.Sprintf("%s/router/tests", ts.URL), nil) r.NoError(err) res, err := http.DefaultClient.Do(req) r.NoError(err) b, _ := io.ReadAll(res.Body) r.Equal(fmt.Sprintf("%s|/router/tests", v), string(b)) } } func Test_Router_Group(t *testing.T) { r := require.New(t) a := testApp() g := a.Group("/api/v1") g.GET("/users", func(c Context) error { return c.Render(http.StatusCreated, nil) }) w := httptest.New(a) res := w.HTML("/api/v1/users").Get() r.Equal(http.StatusCreated, res.Code) } func Test_Router_Group_on_Group(t *testing.T) { r := require.New(t) a := testApp() g := a.Group("/api/v1") g.GET("/users", func(c Context) error { return c.Render(http.StatusCreated, nil) }) f := g.Group("/foo") f.GET("/bar", func(c Context) error { return c.Render(http.StatusTeapot, nil) }) w := httptest.New(a) res := w.HTML("/api/v1/foo/bar").Get() r.Equal(http.StatusTeapot, res.Code) } func Test_Router_Group_Middleware(t *testing.T) { r := require.New(t) a := testApp() a.Use(func(h Handler) Handler { return h }) r.Len(a.Middleware.stack, 4) g := a.Group("/api/v1") r.Len(a.Middleware.stack, 4) r.Len(g.Middleware.stack, 4) g.Use(func(h Handler) Handler { return h }) r.Len(a.Middleware.stack, 4) r.Len(g.Middleware.stack, 5) } func Test_Router_Redirect(t *testing.T) { r := require.New(t) w := httptest.New(testApp()) res := w.HTML("/foo").Get() r.Equal(http.StatusMovedPermanently, res.Code) r.Equal("/bar", res.Location()) } func Test_Router_ServeFiles(t *testing.T) { r := require.New(t) rootFS := fstest.MapFS{ "foo.png": &fstest.MapFile{ Data: []byte("foo"), Mode: 0644, }, } a := New(Options{}) a.ServeFiles("/assets", http.FS(rootFS)) w := httptest.New(a) res := w.HTML("/assets/foo.png").Get() r.Equal(http.StatusOK, res.Code) r.Equal("foo", res.Body.String()) r.NotEqual(res.Header().Get("ETag"), "") r.Equal(res.Header().Get("Cache-Control"), "max-age=31536000") env.Set(AssetsAgeVarName, "3600") w = httptest.New(a) res = w.HTML("/assets/foo.png").Get() r.Equal(http.StatusOK, res.Code) r.Equal("foo", res.Body.String()) r.NotEqual(res.Header().Get("ETag"), "") r.Equal(res.Header().Get("Cache-Control"), "max-age=3600") } func Test_Router_InvalidURL(t *testing.T) { r := require.New(t) rootFS := fstest.MapFS{ "foo.png": &fstest.MapFile{ Data: []byte("foo"), Mode: 0644, }, } a := New(Options{}) a.ServeFiles("/", http.FS(rootFS)) w := httptest.New(a) s := "/%25%7dn2zq0%3cscript%3ealert(1)%3c\\/script%3evea7f" request, _ := http.NewRequest("GET", s, nil) response := httptest.NewRecorder() w.ServeHTTP(response, request) r.Equal(http.StatusBadRequest, response.Code, "(400) BadRequest response is expected") } type WebResource struct { BaseResource } // Edit default implementation. Returns a 404 func (v WebResource) Edit(c Context) error { return c.Error(http.StatusNotFound, fmt.Errorf("resource not implemented")) } // New default implementation. Returns a 404 func (v WebResource) New(c Context) error { return c.Error(http.StatusNotFound, fmt.Errorf("resource not implemented")) } func Test_App_NamedRoutes(t *testing.T) { type CarsResource struct { WebResource } type ResourcesResource struct { WebResource } r := require.New(t) a := New(Options{}) var carsResource Resource = CarsResource{} var resourcesResource Resource = ResourcesResource{} rr := render.New(render.Options{ HTMLLayout: "application.plush.html", TemplatesFS: os.DirFS("../templates"), Helpers: map[string]any{}, }) sampleHandler := func(c Context) error { c.Set("opts", map[string]any{}) return c.Render(http.StatusOK, rr.String(` 1. <%= rootPath() %> 2. <%= userPath({user_id: 1}) %> 3. <%= myPeepsPath() %> 5. <%= carPath({car_id: 1}) %> 6. <%= newCarPath() %> 7. <%= editCarPath({car_id: 1}) %> 8. <%= editCarPath({car_id: 1, other: 12}) %> 9. <%= rootPath({"some":"variable","other": 12}) %> 10. <%= rootPath() %> 11. <%= rootPath({"special/":"12=ss"}) %> 12. <%= resourcePath({resource_id: 1}) %> 13. <%= editResourcePath({resource_id: 1}) %> 14. <%= testPath() %> 15. <%= testNamePath({name: "myTest"}) %> 16. <%= paganoPath({id: 1}) %> `)) } a.GET("/", sampleHandler) a.GET("/users", sampleHandler) a.GET("/users/{user_id}", sampleHandler) a.GET("/peeps", sampleHandler).Name("myPeeps") a.Resource("/car", carsResource) a.Resource("/resources", resourcesResource) a.GET("/test", sampleHandler) a.GET("/test/{name}", sampleHandler) a.GET("/pagano/{id}", sampleHandler) w := httptest.New(a) res := w.HTML("/").Get() r.Equal(http.StatusOK, res.Code) r.Contains(res.Body.String(), "1. /") r.Contains(res.Body.String(), "2. /users/1") r.Contains(res.Body.String(), "3. /peeps") r.Contains(res.Body.String(), "5. /car/1") r.Contains(res.Body.String(), "6. /car/new") r.Contains(res.Body.String(), "7. /car/1/edit") r.Contains(res.Body.String(), "8. /car/1/edit/?other=12") r.Contains(res.Body.String(), "9. /?other=12&some=variable") r.Contains(res.Body.String(), "10. /") r.Contains(res.Body.String(), "11. /?special%2F=12%3Dss") r.Contains(res.Body.String(), "12. /resources/1") r.Contains(res.Body.String(), "13. /resources/1/edit") r.Contains(res.Body.String(), "14. /test") r.Contains(res.Body.String(), "15. /test/myTest") r.Contains(res.Body.String(), "16. /pagano/1") } func Test_App_NamedRoutes_MissingParameter(t *testing.T) { r := require.New(t) a := New(Options{}) rr := render.New(render.Options{ HTMLLayout: "application.plush.html", TemplatesFS: os.DirFS("../templates"), Helpers: map[string]any{}, }) sampleHandler := func(c Context) error { c.Set("opts", map[string]any{}) return c.Render(http.StatusOK, rr.String(` <%= userPath(opts) %> `)) } a.GET("/users/{user_id}", sampleHandler) w := httptest.New(a) res := w.HTML("/users/1").Get() r.Equal(http.StatusInternalServerError, res.Code) r.Contains(res.Body.String(), "missing parameters for /users/{user_id}") } func Test_Resource(t *testing.T) { r := require.New(t) type trs struct { Method string Path string Result string } tests := []trs{ { Method: "GET", Path: "", Result: "list", }, { Method: "GET", Path: "/new", Result: "new", }, { Method: "GET", Path: "/1", Result: "show 1", }, { Method: "GET", Path: "/1/edit", Result: "edit 1", }, { Method: "POST", Path: "", Result: "create", }, { Method: "PUT", Path: "/1", Result: "update 1", }, { Method: "DELETE", Path: "/1", Result: "destroy 1", }, } a := New(Options{}) a.Resource("/users", &userResource{}) a.Resource("/api/v1/users", &userResource{}) ts := httptest.NewServer(a) defer ts.Close() c := http.Client{} for _, path := range []string{"/users", "/api/v1/users"} { for _, test := range tests { u := ts.URL + path + test.Path req, err := http.NewRequest(test.Method, u, nil) r.NoError(err) res, err := c.Do(req) r.NoError(err) b, err := io.ReadAll(res.Body) r.NoError(err) r.Equal(test.Result, string(b)) } } } type paramKeyResource struct { *userResource } func (paramKeyResource) ParamKey() string { return "bazKey" } func Test_Resource_ParamKey(t *testing.T) { r := require.New(t) fr := ¶mKeyResource{&userResource{}} a := New(Options{}) a.Resource("/foo", fr) rt := a.Routes() paths := []string{} for _, rr := range rt { paths = append(paths, rr.Path) } r.Contains(paths, "/foo/{bazKey}/edit/") } type mwResource struct { WebResource } func (mwResource) Use() []MiddlewareFunc { var mw []MiddlewareFunc mw = append(mw, func(next Handler) Handler { return func(c Context) error { if c.Param("good") == "" { return fmt.Errorf("not good") } return next(c) } }) return mw } func (m mwResource) List(c Context) error { return c.Render(http.StatusOK, render.String("southern harmony and the musical companion")) } func Test_Resource_MW(t *testing.T) { r := require.New(t) fr := mwResource{} a := New(Options{}) a.Resource("/foo", fr) w := httptest.New(a) res := w.HTML("/foo?good=true").Get() r.Equal(http.StatusOK, res.Code) r.Contains(res.Body.String(), "southern harmony") res = w.HTML("/foo").Get() r.Equal(http.StatusInternalServerError, res.Code) r.NotContains(res.Body.String(), "southern harmony") } type userResource struct{} func (u *userResource) List(c Context) error { return c.Render(http.StatusOK, render.String("list")) } func (u *userResource) Show(c Context) error { return c.Render(http.StatusOK, render.String(`show <%=params["user_id"] %>`)) } func (u *userResource) New(c Context) error { return c.Render(http.StatusOK, render.String("new")) } func (u *userResource) Create(c Context) error { return c.Render(http.StatusOK, render.String("create")) } func (u *userResource) Edit(c Context) error { return c.Render(http.StatusOK, render.String(`edit <%=params["user_id"] %>`)) } func (u *userResource) Update(c Context) error { return c.Render(http.StatusOK, render.String(`update <%=params["user_id"] %>`)) } func (u *userResource) Destroy(c Context) error { return c.Render(http.StatusOK, render.String(`destroy <%=params["user_id"] %>`)) } func Test_ResourceOnResource(t *testing.T) { r := require.New(t) a := New(Options{}) ur := a.Resource("/users", &userResource{}) ur.Resource("/people", &userResource{}) ts := httptest.NewServer(a) defer ts.Close() type trs struct { Method string Path string Result string } tests := []trs{ { Method: "GET", Path: "/people", Result: "list", }, { Method: "GET", Path: "/people/new", Result: "new", }, { Method: "GET", Path: "/people/1", Result: "show 1", }, { Method: "GET", Path: "/people/1/edit", Result: "edit 1", }, { Method: "POST", Path: "/people", Result: "create", }, { Method: "PUT", Path: "/people/1", Result: "update 1", }, { Method: "DELETE", Path: "/people/1", Result: "destroy 1", }, } c := http.Client{} for _, test := range tests { u := ts.URL + path.Join("/users/42", test.Path) req, err := http.NewRequest(test.Method, u, nil) r.NoError(err) res, err := c.Do(req) r.NoError(err) b, err := io.ReadAll(res.Body) r.NoError(err) r.Equal(test.Result, string(b)) } } func Test_buildRouteName(t *testing.T) { r := require.New(t) cases := map[string]string{ "/": "root", "/users": "users", "/users/new": "newUsers", "/users/{user_id}": "user", "/users/{user_id}/children": "userChildren", "/users/{user_id}/children/{child_id}": "userChild", "/users/{user_id}/children/new": "newUserChildren", "/users/{user_id}/children/{child_id}/build": "userChildBuild", "/admin/planes": "adminPlanes", "/admin/planes/{plane_id}": "adminPlane", "/admin/planes/{plane_id}/edit": "editAdminPlane", "/test": "test", "/tests/{name}": "testName", "/tests/{name_id}/cases/{case_id}": "testNameIdCase", "/tests/{name_id}/cases/{case_id}/edit": "editTestNameIdCase", } a := New(Options{}) for input, result := range cases { fResult := a.RouteNamer.NameRoute(input) r.Equal(result, fResult, input) } a = New(Options{Prefix: "/test"}) cases = map[string]string{ "/test": "test", "/test/users": "testUsers", } for input, result := range cases { fResult := a.RouteNamer.NameRoute(input) r.Equal(result, fResult, input) } } func Test_CatchAll_Route(t *testing.T) { r := require.New(t) rr := render.New(render.Options{}) a := New(Options{}) a.GET("/{name:.+}", func(c Context) error { name := c.Param("name") return c.Render(http.StatusOK, rr.String(name)) }) w := httptest.New(a) res := w.HTML("/john").Get() r.Contains(res.Body.String(), "john") } func Test_Router_Matches_Trailing_Slash(t *testing.T) { table := []struct { mapped string browser string expected string }{ {"/foo", "/foo", "/foo/"}, {"/foo", "/foo/", "/foo/"}, {"/foo/", "/foo", "/foo/"}, {"/foo/", "/foo/", "/foo/"}, {"/index.html", "/index.html", "/index.html/"}, {"/foo.gif", "/foo.gif", "/foo.gif/"}, {"/{img}", "/foo.png", "/foo.png/"}, } for _, tt := range table { t.Run(tt.mapped+"|"+tt.browser, func(st *testing.T) { r := require.New(st) app := New(Options{ PreWares: []PreWare{ func(h http.Handler) http.Handler { var f http.HandlerFunc = func(res http.ResponseWriter, req *http.Request) { path := req.URL.Path req.URL.Path = strings.TrimSuffix(path, "/") r.False(strings.HasSuffix(req.URL.Path, "/")) h.ServeHTTP(res, req) } return f }, }, }) app.GET(tt.mapped, func(c Context) error { return c.Render(http.StatusOK, render.String(c.Request().URL.Path)) }) w := httptest.New(app) res := w.HTML("%s", tt.browser).Get() r.Equal(http.StatusOK, res.Code) r.Equal(tt.expected, res.Body.String()) }) } } ================================================ FILE: runtime/build.go ================================================ // Deprecated: This package is deprecated and will be removed in a future version. // Use runtime/debug.ReadBuildInfo() directly instead. // // This package previously provided build information for buffalo applications. // Starting with Go 1.18, the standard library's runtime/debug package provides // equivalent functionality through ReadBuildInfo(), which includes VCS information // (commit hash, time) and module version. // // Migration example: // // import "runtime/debug" // // info, ok := debug.ReadBuildInfo() // if ok { // // Use info.Main.Version for version // // Use info.Settings for VCS info (vcs.revision, vcs.time) // } // // For applications built with "buffalo build", build information is now // automatically embedded by Go's build system and can be accessed via // runtime/debug.ReadBuildInfo(). package runtime import ( "runtime/debug" "sync" "time" ) // Version is the current version of the buffalo binary. // Deprecated: Use runtime/debug.ReadBuildInfo().Main.Version instead. var Version = "dev" // BuildInfo holds information about the build. // Deprecated: Use runtime/debug.BuildInfo instead. type BuildInfo struct { Version string `json:"version"` Time time.Time `json:"-"` } // String implements fmt.Stringer func (b BuildInfo) String() string { if b.Time.IsZero() { return b.Version } return b.Version + " (" + b.Time.Format(time.RFC3339) + ")" } var ( build BuildInfo buildOnce sync.Once ) // Build returns the information about the current build of the application. // In development mode this will almost always return zero values for BuildInfo. // // Deprecated: Use runtime/debug.ReadBuildInfo() instead. This function now // returns information derived from the standard library's build info. // // For backward compatibility, this function caches the build info after first call. func Build() BuildInfo { buildOnce.Do(func() { build = loadBuildInfo() }) return build } // loadBuildInfo reads build information from runtime/debug and converts it // to the legacy BuildInfo format for backward compatibility. func loadBuildInfo() BuildInfo { info, ok := debug.ReadBuildInfo() if !ok { return BuildInfo{ Version: "dev", Time: time.Time{}, } } bi := BuildInfo{ Version: info.Main.Version, Time: time.Time{}, } // Handle development builds if bi.Version == "" || bi.Version == "(devel)" { bi.Version = "dev" } // Try to extract build time from VCS info for _, setting := range info.Settings { if setting.Key == "vcs.time" { if t, err := time.Parse(time.RFC3339, setting.Value); err == nil { bi.Time = t } break } } return bi } var so sync.Once // SetBuild allows the setting of build information only once. // This is typically managed by the binary built by `buffalo build`. // // Deprecated: This function is no longer necessary. Build information is now // automatically embedded by the Go toolchain and can be accessed via // runtime/debug.ReadBuildInfo(). This function is kept for backward compatibility // but has no effect when called after Build() has been called. func SetBuild(b BuildInfo) { so.Do(func() { build = b }) } ================================================ FILE: server.go ================================================ package buffalo import ( "context" "errors" "net/http" "os" "os/signal" "strings" "sync" "syscall" "time" "github.com/gobuffalo/buffalo/servers" "github.com/gobuffalo/events" "github.com/gobuffalo/refresh/refresh/web" ) // Serve the application at the specified address/port and listen for OS // interrupt and kill signals and will attempt to stop the application // gracefully. This will also start the Worker process, unless WorkerOff is enabled. func (a *App) Serve(srvs ...servers.Server) error { var wg sync.WaitGroup a.Logger.Debug("starting application") payload := events.Payload{ "app": a, } if err := events.EmitPayload(EvtAppStart, payload); err != nil { // just to make sure if events work properly? a.Logger.Error("unable to emit event. something went wrong internally") return err } if len(srvs) == 0 { if strings.HasPrefix(a.Options.Addr, "unix:") { tcp, err := servers.UnixSocket(a.Options.Addr[5:]) if err != nil { return err } srvs = append(srvs, tcp) } else { srvs = append(srvs, servers.New()) } } ctx, cancel := signal.NotifyContext(a.Context, syscall.SIGTERM, os.Interrupt) defer cancel() wg.Go(func() { // gracefully shut down the application when the context is cancelled // channel waiter should not be called any other place <-ctx.Done() a.Logger.Info("shutting down application") // shutting down listeners first, to make sure no more new request a.Logger.Info("shutting down servers") for _, s := range srvs { timeout := time.Duration(a.Options.TimeoutSecondShutdown) * time.Second ctx, cfn := context.WithTimeout(context.Background(), timeout) defer cfn() events.EmitPayload(EvtServerStop, payload) if err := s.Shutdown(ctx); err != nil { events.EmitError(EvtServerStopErr, err, payload) a.Logger.Error("shutting down server: ", err) } cfn() } if !a.WorkerOff { a.Logger.Info("shutting down worker") events.EmitPayload(EvtWorkerStop, payload) if err := a.Worker.Stop(); err != nil { events.EmitError(EvtWorkerStopErr, err, payload) a.Logger.Error("error while shutting down worker: ", err) } } }) // if configured to do so, start the workers if !a.WorkerOff { wg.Go(func() { events.EmitPayload(EvtWorkerStart, payload) if err := a.Worker.Start(ctx); err != nil { events.EmitError(EvtWorkerStartErr, err, payload) a.Stop(err) } }) } for _, s := range srvs { s.SetAddr(a.Addr) a.Logger.Infof("starting %s", s) server := s // capture loop variable wg.Go(func() { events.EmitPayload(EvtServerStart, payload) // server.Start always returns non-nil error a.Stop(server.Start(ctx, a)) }) } wg.Wait() a.Logger.Info("shutdown completed") err := ctx.Err() if errors.Is(err, context.Canceled) { return nil } return err } // Stop the application and attempt to gracefully shutdown func (a *App) Stop(err error) error { events.EmitError(EvtAppStop, err, events.Payload{"app": a}) ce := a.Context.Err() if ce != nil { a.Logger.Warn("application context has already been canceled: ", ce) return errors.New("application has already been canceled") } a.Logger.Warn("stopping application: ", err) a.cancel() return nil } // ServeHTTP implements http.Handler func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) { ws := &Response{ ResponseWriter: w, } if a.MethodOverride != nil { a.MethodOverride(w, r) } if ok := a.processPreHandlers(ws, r); !ok { return } r.URL.Path = a.normalizePath(r.URL.Path) var h http.Handler = a.router if a.Env == "development" { h = web.ErrorChecker(h) } h.ServeHTTP(ws, r) } func (a *App) processPreHandlers(res http.ResponseWriter, req *http.Request) bool { sh := func(h http.Handler) bool { h.ServeHTTP(res, req) if br, ok := res.(*Response); ok { if br.Status > 0 || br.Size > 0 { return false } } return true } for _, ph := range a.PreHandlers { if ok := sh(ph); !ok { return false } } last := http.Handler(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {})) for _, ph := range a.PreWares { last = ph(last) if ok := sh(last); !ok { return false } } return true } func (a *App) normalizePath(path string) string { if strings.HasSuffix(path, "/") { return path } for _, p := range a.filepaths { if p == "/" { continue } if strings.HasPrefix(path, p) { return path } } return path + "/" } ================================================ FILE: server_test.go ================================================ package buffalo import ( "fmt" "net/http" "sync" "testing" "time" "github.com/gobuffalo/buffalo/render" "github.com/gobuffalo/buffalo/worker" "github.com/stretchr/testify/require" ) // All tests in this file requires certain amount of waiting and they are // timing sensitive. Adjust this timing values if they are failing due to // timing issue. const ( waitStart = 2 waitRun = 2 consumerRun = 8 ) // startApp starts given buffalo app and check its exit status. // The go routine emulates a buffalo app process. func startApp(app *App, wg *sync.WaitGroup, r *require.Assertions) { wg.Go(func() { err := app.Serve() r.NoError(err) }) // wait until the server started. // could be improved with connection test but that's too much... time.Sleep(waitStart * time.Second) } func Test_Server_Simple(t *testing.T) { // This testcase explains the minimum/basic workflow of buffalo app. // Setup and execute the app, wait until startup, then stop it. // The other testcases use this structure with additional actions. r := require.New(t) var wg sync.WaitGroup // Setup a new buffalo.App to be used as a testing buffalo app. app := New(Options{}) startApp(app, &wg, r) // starts buffalo app routine. app.cancel() wg.Wait() } var handlerDone = false // timeConsumer consumes about 10 minutes for processing its request func timeConsumer(c Context) error { for range consumerRun { fmt.Println("#") time.Sleep(1 * time.Second) } handlerDone = true return c.Render(http.StatusOK, render.String("Hey!")) } func Test_Server_GracefulShutdownOngoingRequest(t *testing.T) { // This test case explain the minimum/basic workflow of buffalo app. r := require.New(t) var wg sync.WaitGroup // Setup a new buffalo.App with a simple time consuming handler. app := New(Options{}) app.GET("/", timeConsumer) startApp(app, &wg, r) // starts buffalo app routine. firstQuery := false secondQuery := false // This routine is the 1st client that GETs before Stop it // The result should be successful even though the server shutting down. wg.Go(func() { resp, err := http.Get("http://127.0.0.1:3000") r.NoError(err) defer resp.Body.Close() r.Equal(http.StatusOK, resp.StatusCode) fmt.Println("the first query should be OK:", resp.Status) firstQuery = true }) // make sure the request sent time.Sleep(waitRun * time.Second) app.cancel() time.Sleep(1 * time.Second) // make sure the server started shutdown. // This routine is the 2nd client that GETs after Stop it // The result should be connection refused even though app is still on. wg.Go(func() { _, err := http.Get("http://127.0.0.1:3000") r.Contains(err.Error(), "refused") fmt.Println("the second query should be refused:", err) secondQuery = true }) wg.Wait() r.Equal(true, handlerDone) r.Equal(true, firstQuery) r.Equal(true, secondQuery) } var timerDone = false func timerWorker(args worker.Args) error { for range consumerRun { fmt.Println("%") time.Sleep(1 * time.Second) } timerDone = true return nil } func Test_Server_GracefulShutdownOngoingWorker(t *testing.T) { // This test case explain the minimum/basic workflow of buffalo app. r := require.New(t) var wg sync.WaitGroup // Setup a new buffalo.App with a simple time consuming handler. app := New(Options{}) app.Worker.Register("timer", timerWorker) app.Worker.PerformIn(worker.Job{ Handler: "timer", }, 1*time.Second) startApp(app, &wg, r) // starts buffalo app routine. time.Sleep(1 * time.Second) // make sure just 1 second app.cancel() time.Sleep(1 * time.Second) // make sure the server started shutdown. // This routine is the 2nd client that GETs after Stop it // The result should be connection refused even though app is still on. wg.Go(func() { _, err := http.Get("http://127.0.0.1:3000") r.Contains(err.Error(), "refused") }) wg.Wait() r.Equal(true, timerDone) } ================================================ FILE: servers/listener.go ================================================ package servers import ( "context" "fmt" "net" "net/http" ) // Listener server for using a pre-defined net.Listener type Listener struct { *http.Server Listener net.Listener } // SetAddr sets the servers address, if it hasn't already been set func (s *Listener) SetAddr(addr string) { if s.Server.Addr == "" { s.Server.Addr = addr } } // String returns a string representation of a Listener func (s *Listener) String() string { return fmt.Sprintf("listener on %s", s.Server.Addr) } // Start the server func (s *Listener) Start(c context.Context, h http.Handler) error { s.Handler = h return s.Serve(s.Listener) } // UnixSocket returns a new Listener on that address func UnixSocket(addr string) (*Listener, error) { listener, err := net.Listen("unix", addr) if err != nil { return nil, err } return &Listener{ Server: &http.Server{}, Listener: listener, }, nil } ================================================ FILE: servers/servers.go ================================================ package servers import ( "context" "net" "net/http" ) // Server allows for custom server implementations type Server interface { Shutdown(context.Context) error Start(context.Context, http.Handler) error SetAddr(string) } // Wrap converts a standard *http.Server to a buffalo.Server func Wrap(s *http.Server) Server { return &Simple{Server: s} } // WrapTLS Server converts a standard *http.Server to a buffalo.Server // but makes sure it is run with TLS. func WrapTLS(s *http.Server, certFile string, keyFile string) Server { return &TLS{ Server: s, CertFile: certFile, KeyFile: keyFile, } } // WrapListener wraps an *http.Server and a net.Listener func WrapListener(s *http.Server, l net.Listener) Server { return &Listener{ Server: s, Listener: l, } } ================================================ FILE: servers/simple.go ================================================ package servers import ( "context" "fmt" "net/http" ) // Simple server type Simple struct { *http.Server } // SetAddr sets the servers address, if it hasn't already been set func (s *Simple) SetAddr(addr string) { if s.Server.Addr == "" { s.Server.Addr = addr } } // String returns a string representation of a Simple server func (s *Simple) String() string { return fmt.Sprintf("simple server on %s", s.Server.Addr) } // Start the server func (s *Simple) Start(c context.Context, h http.Handler) error { s.Handler = h return s.ListenAndServe() } // New Simple server func New() *Simple { return &Simple{ Server: &http.Server{}, } } ================================================ FILE: servers/tls.go ================================================ package servers import ( "context" "fmt" "net/http" ) // TLS server type TLS struct { *http.Server CertFile string KeyFile string } // SetAddr sets the servers address, if it hasn't already been set func (s *TLS) SetAddr(addr string) { if s.Server.Addr == "" { s.Server.Addr = addr } } // String returns a string representation of a Listener func (s *TLS) String() string { return fmt.Sprintf("TLS server on %s", s.Server.Addr) } // Start the server func (s *TLS) Start(c context.Context, h http.Handler) error { s.Handler = h return s.ListenAndServeTLS(s.CertFile, s.KeyFile) } ================================================ FILE: session.go ================================================ package buffalo import ( "net/http" "github.com/gorilla/sessions" ) // Session wraps the "github.com/gorilla/sessions" API // in something a little cleaner and a bit more useable. type Session struct { Session *sessions.Session req *http.Request res http.ResponseWriter } // Save the current session. func (s *Session) Save() error { return s.Session.Save(s.req, s.res) } // Get a value from the current session. func (s *Session) Get(name any) any { return s.Session.Values[name] } // GetOnce gets a value from the current session and then deletes it. func (s *Session) GetOnce(name any) any { if x, ok := s.Session.Values[name]; ok { s.Delete(name) return x } return nil } // Set a value onto the current session. If a value with that name // already exists it will be overridden with the new value. func (s *Session) Set(name, value any) { s.Session.Values[name] = value } // Delete a value from the current session. func (s *Session) Delete(name any) { delete(s.Session.Values, name) } // Clear the current session func (s *Session) Clear() { for k := range s.Session.Values { s.Delete(k) } } // Get a session using a request and response. func (a *App) getSession(r *http.Request, w http.ResponseWriter) *Session { if a.root != nil { return a.root.getSession(r, w) } session, _ := a.SessionStore.Get(r, a.SessionName) return &Session{ Session: session, req: r, res: w, } } ================================================ FILE: session_test.go ================================================ package buffalo import ( "fmt" "net/http" "strings" "testing" "github.com/gobuffalo/buffalo/render" "github.com/gobuffalo/httptest" "github.com/stretchr/testify/require" ) func Test_Session_SingleCookie(t *testing.T) { r := require.New(t) sessionName := "_test_session" a := New(Options{SessionName: sessionName}) rr := render.New(render.Options{}) a.GET("/", func(c Context) error { return c.Render(http.StatusCreated, rr.String("")) }) w := httptest.New(a) res := w.HTML("/").Get() var sessionCookies []string for _, c := range res.Header().Values("Set-Cookie") { if strings.HasPrefix(c, sessionName) { sessionCookies = append(sessionCookies, c) } } r.Equal(1, len(sessionCookies)) } func Test_Session_CustomValue(t *testing.T) { r := require.New(t) a := New(Options{}) rr := render.New(render.Options{}) // Root path sets a custom session value a.GET("/", func(c Context) error { c.Session().Set("example", "test") return c.Render(http.StatusCreated, rr.String("")) }) // /session path prints custom session value as response a.GET("/session", func(c Context) error { sessionValue := c.Session().Get("example") return c.Render(http.StatusCreated, rr.String(fmt.Sprintf("%s", sessionValue))) }) w := httptest.New(a) _ = w.HTML("/").Get() // Create second request that should contain the cookie from the first response reqGetSession := w.HTML("/session") resGetSession := reqGetSession.Get() r.Equal(resGetSession.Body.String(), "test") } ================================================ FILE: worker/job.go ================================================ package worker import "encoding/json" // Args are the arguments passed into a job type Args map[string]any func (a Args) String() string { b, _ := json.Marshal(a) return string(b) } // Job to be processed by a Worker type Job struct { // Queue the job should be placed into Queue string // Args that will be passed to the Handler when run Args Args // Handler that will be run by the worker Handler string } func (j Job) String() string { b, _ := json.Marshal(j) return string(b) } ================================================ FILE: worker/simple.go ================================================ package worker import ( "context" "errors" "fmt" "sync" "time" "github.com/sirupsen/logrus" ) var _ Worker = &Simple{} // NewSimple creates a basic implementation of the Worker interface // that is backed using just the standard library and goroutines. func NewSimple() *Simple { // TODO(sio4): #road-to-v1 - how to check if the worker is ready to work // when worker should be initialized? how to check if worker is ready? // and purpose of the context return NewSimpleWithContext(context.Background()) } // NewSimpleWithContext creates a basic implementation of the Worker interface // that is backed using just the standard library and goroutines. func NewSimpleWithContext(ctx context.Context) *Simple { ctx, cancel := context.WithCancel(ctx) l := logrus.New() l.Level = logrus.InfoLevel l.Formatter = &logrus.TextFormatter{} return &Simple{ Logger: l, ctx: ctx, cancel: cancel, handlers: map[string]Handler{}, moot: &sync.Mutex{}, started: false, } } // Simple is a basic implementation of the Worker interface // that is backed using just the standard library and goroutines. type Simple struct { Logger SimpleLogger ctx context.Context cancel context.CancelFunc handlers map[string]Handler moot *sync.Mutex wg sync.WaitGroup started bool } // Register Handler with the worker func (w *Simple) Register(name string, h Handler) error { if name == "" || h == nil { return fmt.Errorf("name or handler cannot be empty/nil") } w.moot.Lock() defer w.moot.Unlock() if _, ok := w.handlers[name]; ok { return fmt.Errorf("handler already mapped for name %s", name) } w.handlers[name] = h return nil } // Start the worker func (w *Simple) Start(ctx context.Context) error { // TODO(sio4): #road-to-v1 - define the purpose of Start clearly w.Logger.Info("starting Simple background worker") w.moot.Lock() defer w.moot.Unlock() w.ctx, w.cancel = context.WithCancel(ctx) w.started = true return nil } // Stop the worker func (w *Simple) Stop() error { // prevent job submission when stopping w.moot.Lock() defer w.moot.Unlock() w.Logger.Info("stopping Simple background worker") w.cancel() w.wg.Wait() w.Logger.Info("all background jobs stopped completely") return nil } // Perform a job as soon as possibly using a goroutine. func (w *Simple) Perform(job Job) error { w.moot.Lock() defer w.moot.Unlock() if !w.started { return fmt.Errorf("worker is not yet started") } // Perform should not allow a job submission if the worker is not running if err := w.ctx.Err(); err != nil { return fmt.Errorf("worker is not ready to perform a job: %v", err) } w.Logger.Debugf("performing job %s", job) if job.Handler == "" { err := fmt.Errorf("no handler name given: %s", job) w.Logger.Error(err) return err } if h, ok := w.handlers[job.Handler]; ok { // TODO(sio4): #road-to-v1 - consider timeout and/or cancellation w.wg.Go(func() { err := safeRun(func() error { return h(job.Args) }) if err != nil { w.Logger.Error(err) } w.Logger.Debugf("completed job %s", job) }) return nil } err := fmt.Errorf("no handler mapped for name %s", job.Handler) w.Logger.Error(err) return err } // safeRun the function safely knowing that if it panics // the panic will be caught and returned as an error func safeRun(fn func() error) (err error) { defer func() { if ex := recover(); ex != nil { if e, ok := ex.(error); ok { err = e return } err = errors.New(fmt.Sprint(ex)) } }() return fn() } // PerformAt performs a job at a particular time using a goroutine. func (w *Simple) PerformAt(job Job, t time.Time) error { return w.PerformIn(job, time.Until(t)) } // PerformIn performs a job after waiting for a specified amount // using a goroutine. func (w *Simple) PerformIn(job Job, d time.Duration) error { // Perform should not allow a job submission if the worker is not running if err := w.ctx.Err(); err != nil { return fmt.Errorf("worker is not ready to perform a job: %v", err) } w.wg.Go(func() { // waiting job also should be counted for { w.moot.Lock() if w.started { w.moot.Unlock() break } w.moot.Unlock() waiting := 100 * time.Millisecond time.Sleep(waiting) d = d - waiting } select { case <-time.After(d): w.Perform(job) case <-w.ctx.Done(): // TODO(sio4): #road-to-v1 - it should be guaranteed to be performed w.cancel() } }) return nil } // SimpleLogger is used by the Simple worker to write logs type SimpleLogger interface { Debugf(string, ...any) Infof(string, ...any) Errorf(string, ...any) Debug(...any) Info(...any) Error(...any) } ================================================ FILE: worker/simple_test.go ================================================ package worker import ( "context" "sync" "testing" "time" "github.com/stretchr/testify/require" ) func sampleHandler(Args) error { return nil } func Test_Simple_RegisterEmpty(t *testing.T) { r := require.New(t) w := NewSimple() err := w.Register("", sampleHandler) r.Error(err) } func Test_Simple_RegisterNil(t *testing.T) { r := require.New(t) w := NewSimple() err := w.Register("sample", nil) r.Error(err) } func Test_Simple_RegisterEmptyNil(t *testing.T) { r := require.New(t) w := NewSimple() err := w.Register("", nil) r.Error(err) } func Test_Simple_RegisterExisting(t *testing.T) { r := require.New(t) w := NewSimple() err := w.Register("sample", sampleHandler) r.NoError(err) err = w.Register("sample", sampleHandler) r.Error(err) } func Test_Simple_StartStop(t *testing.T) { r := require.New(t) w := NewSimple() ctx := context.Background() err := w.Start(ctx) r.NoError(err) r.NotNil(w.ctx) r.Nil(w.ctx.Err()) err = w.Stop() r.NoError(err) r.NotNil(w.ctx) r.NotNil(w.ctx.Err()) } func Test_Simple_Perform(t *testing.T) { r := require.New(t) var hit bool w := NewSimple() r.NoError(w.Start(context.Background())) w.Register("x", func(Args) error { hit = true return nil }) w.Perform(Job{ Handler: "x", }) // the worker should guarantee the job is finished before the worker stopped r.NoError(w.Stop()) r.True(hit) } func Test_Simple_PerformBroken(t *testing.T) { r := require.New(t) var hit bool w := NewSimple() r.NoError(w.Start(context.Background())) w.Register("x", func(Args) error { hit = true //Index out of bounds on purpose println([]string{}[0]) return nil }) w.Perform(Job{ Handler: "x", }) r.NoError(w.Stop()) r.True(hit) } func Test_Simple_PerformWithEmptyJob(t *testing.T) { r := require.New(t) w := NewSimple() r.NoError(w.Start(context.Background())) defer w.Stop() err := w.Perform(Job{}) r.Error(err) } func Test_Simple_PerformWithUnknownJob(t *testing.T) { r := require.New(t) w := NewSimple() r.NoError(w.Start(context.Background())) defer w.Stop() err := w.Perform(Job{Handler: "unknown"}) r.Error(err) } /* TODO(sio4): #road-to-v1 - define the purpose of Start clearly consider to make Perform to work only when the worker is started. func Test_Simple_PerformBeforeStart(t *testing.T) { r := require.New(t) w := NewSimple() r.NoError(w.Register("sample", sampleHandler)) err := w.Perform(Job{Handler: "sample"}) r.Error(err) } */ func Test_Simple_PerformAfterStop(t *testing.T) { r := require.New(t) w := NewSimple() r.NoError(w.Register("sample", sampleHandler)) r.NoError(w.Start(context.Background())) r.NoError(w.Stop()) err := w.Perform(Job{Handler: "sample"}) r.Error(err) } func Test_Simple_PerformAt(t *testing.T) { r := require.New(t) var hit bool w := NewSimple() r.NoError(w.Start(context.Background())) wg := &sync.WaitGroup{} wg.Add(1) w.Register("x", func(Args) error { hit = true wg.Done() return nil }) w.PerformAt(Job{ Handler: "x", }, time.Now().Add(5*time.Millisecond)) // how long does the handler take for assignment? hmm, time.Sleep(100 * time.Millisecond) wg.Wait() r.True(hit) r.NoError(w.Stop()) } func Test_Simple_PerformIn(t *testing.T) { r := require.New(t) var hit bool w := NewSimple() r.NoError(w.Start(context.Background())) wg := &sync.WaitGroup{} wg.Add(1) w.Register("x", func(Args) error { hit = true wg.Done() return nil }) w.PerformIn(Job{ Handler: "x", }, 5*time.Millisecond) // how long does the handler take for assignment? hmm, time.Sleep(100 * time.Millisecond) wg.Wait() r.True(hit) r.NoError(w.Stop()) } /* TODO(sio4): #road-to-v1 - define the purpose of Start clearly consider to make Perform to work only when the worker is started. func Test_Simple_PerformInBeforeStart(t *testing.T) { r := require.New(t) w := NewSimple() r.NoError(w.Register("sample", sampleHandler)) err := w.PerformIn(Job{Handler: "sample"}, 5*time.Millisecond) r.Error(err) } */ func Test_Simple_PerformInAfterStop(t *testing.T) { r := require.New(t) w := NewSimple() r.NoError(w.Register("sample", sampleHandler)) r.NoError(w.Start(context.Background())) r.NoError(w.Stop()) err := w.PerformIn(Job{Handler: "sample"}, 5*time.Millisecond) r.Error(err) } /* TODO(sio4): #road-to-v1 - it should be guaranteed to be performed consider to make PerformIn to guarantee the job execution func Test_Simple_PerformInFollowedByStop(t *testing.T) { r := require.New(t) var hit bool w := NewSimple() r.NoError(w.Start(context.Background())) w.Register("x", func(Args) error { hit = true return nil }) err := w.PerformIn(Job{ Handler: "x", }, 5*time.Millisecond) r.NoError(err) // stop the worker immediately after PerformIn r.NoError(w.Stop()) r.True(hit) } */ ================================================ FILE: worker/worker.go ================================================ package worker import ( "context" "time" ) // Handler function that will be run by the worker and given // a slice of arguments type Handler func(Args) error // Worker interface that needs to be implemented to be considered // a "worker" type Worker interface { // Start the worker with the given context Start(context.Context) error // Stop the worker Stop() error // Perform a job as soon as possibly Perform(Job) error // PerformAt performs a job at a particular time PerformAt(Job, time.Time) error // PerformIn performs a job after waiting for a specified amount of time PerformIn(Job, time.Duration) error // Register a Handler Register(string, Handler) error } /* TODO(sio4): #road-to-v1 - redefine Worker interface clearer 1. The Start() functions of current implementations including Simple, Gocraft Work Adapter do not block and immediately return the error. However, App.Serve() calls them within a go routine. 2. The Perform() family of functions can be called before the worker was started once the worker configured. Could be fine but there should be some guidiance for its usage. 3. The Perform() function could be interpreted as "Do it" by its name but their actual job is "Enqueue it" even though Simple worker has no clear boundary between them. It could make confusion. */ ================================================ FILE: wrappers.go ================================================ package buffalo import ( "net/http" "net/url" "github.com/gobuffalo/buffalo/internal/httpx" "github.com/gorilla/mux" ) // WrapHandler wraps a standard http.Handler and transforms it // into a buffalo.Handler. func WrapHandler(h http.Handler) Handler { return func(c Context) error { h.ServeHTTP(c.Response(), c.Request()) return nil } } // WrapHandlerFunc wraps a standard http.HandlerFunc and // transforms it into a buffalo.Handler. func WrapHandlerFunc(h http.HandlerFunc) Handler { return WrapHandler(h) } // WrapBuffaloHandler wraps a buffalo.Handler to a standard http.Handler // // NOTE: A buffalo Handler expects a buffalo Context. WrapBuffaloHandler uses // the same logic as DefaultContext where possible, but some functionality // (e.g. sessions and logging) WILL NOT work with this unwrap function. If // those features are needed a custom UnwrapHandlerFunc needs to be // implemented that provides a Context implementing those features. func WrapBuffaloHandler(h Handler) http.Handler { return WrapBuffaloHandlerFunc(h) } // WrapBuffaloHandlerFunc wraps a buffalo.Handler to a standard http.HandlerFunc // // NOTE: A buffalo Handler expects a buffalo Context. WrapBuffaloHandlerFunc uses // the same logic as DefaultContext where possible, but some functionality // (e.g. sessions and logging) WILL NOT work with this unwrap function. If // those features are needed a custom WrapBuffaloHandlerFunc needs to be // implemented that provides a Context implementing those features. func WrapBuffaloHandlerFunc(h Handler) http.HandlerFunc { return func(res http.ResponseWriter, req *http.Request) { if ws, ok := res.(*Response); ok { res = ws } // Parse URL Params params := url.Values{} vars := mux.Vars(req) for k, v := range vars { params.Add(k, v) } // Parse URL Query String Params // For POST, PUT, and PATCH requests, it also parse the request body as a form. // Request body parameters take precedence over URL query string values in params if err := req.ParseForm(); err == nil { for k, v := range req.Form { for _, vv := range v { params.Add(k, vv) } } } ct := httpx.ContentType(req) data := newRequestData() data.d = map[string]any{ "current_path": req.URL.Path, "contentType": ct, "method": req.Method, } c := &DefaultContext{ Context: req.Context(), contentType: ct, response: res, request: req, params: params, flash: &Flash{data: map[string][]string{}}, data: data, } h(c) } } ================================================ FILE: wrappers_test.go ================================================ package buffalo import ( "net/http" "testing" "github.com/gobuffalo/buffalo/render" "github.com/gobuffalo/httptest" "github.com/stretchr/testify/require" ) func Test_WrapHandlerFunc(t *testing.T) { r := require.New(t) a := New(Options{}) a.GET("/foo", WrapHandlerFunc(func(res http.ResponseWriter, req *http.Request) { res.Write([]byte("hello")) })) w := httptest.New(a) res := w.HTML("/foo").Get() r.Equal("hello", res.Body.String()) } func Test_WrapHandler(t *testing.T) { r := require.New(t) a := New(Options{}) a.GET("/foo", WrapHandler(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { res.Write([]byte("hello")) }))) w := httptest.New(a) res := w.HTML("/foo").Get() r.Equal("hello", res.Body.String()) } func Test_WrapBuffaloHandler(t *testing.T) { r := require.New(t) tt := []struct { verb string path string status int }{ {"GET", "/", 200}, {"GET", "/foo", 201}, {"POST", "/", 300}, {"POST", "/foo", 400}, } for _, x := range tt { bf := func(c Context) error { req := c.Request() return c.Render(x.status, render.String(req.Method+req.URL.Path)) } h := WrapBuffaloHandler(bf) r.NotNil(h) req := httptest.NewRequest(x.verb, x.path, nil) res := httptest.NewRecorder() h.ServeHTTP(res, req) r.Equal(x.status, res.Code) r.Contains(res.Body.String(), x.verb+x.path) } } func Test_WrapBuffaloHandlerFunc(t *testing.T) { r := require.New(t) tt := []struct { verb string path string status int }{ {"GET", "/", 200}, {"GET", "/foo", 201}, {"POST", "/", 300}, {"POST", "/foo", 400}, } for _, x := range tt { bf := func(c Context) error { req := c.Request() return c.Render(x.status, render.String(req.Method+req.URL.Path)) } h := WrapBuffaloHandlerFunc(bf) r.NotNil(h) req := httptest.NewRequest(x.verb, x.path, nil) res := httptest.NewRecorder() h(res, req) r.Equal(x.status, res.Code) r.Contains(res.Body.String(), x.verb+x.path) } } func Benchmark_WrapBuffaloHandler(b *testing.B) { r := require.New(b) status := http.StatusOK bf := func(c Context) error { return c.Render(status, render.String(http.StatusText(status))) } req := httptest.NewRequest(http.MethodGet, "/foo", nil) res := httptest.NewRecorder() b.StartTimer() for b.Loop() { h := WrapBuffaloHandler(bf) r.NotNil(h) h.ServeHTTP(res, req) r.Equal(status, res.Code) r.Contains(res.Body.String(), http.StatusText(status)) } b.StopTimer() }