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
================================================
# 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!