A smallish web framework for Go
Gadget is a smallish web application framework with a soft spot for content negotiation. To install Gadget, just use the go tool.
$ go get github.com/redneckbeard/gadget
$ go get github.com/redneckbeard/gadget/templates
The gdgt package will install a command that will help you generate Gadget projects. You don't have to use it, but it does mean mashing fewer buttons. You need Go 1.2 to use it.
$ go get github.com/redneckbeard/gadget/gdgt
The README is a narrated terminal session of having a first play with Gadget. If that's your style, it may be more helpful than the summaries below. All the concepts here are documented more granularly on godoc.org.
For the most part, you can lay out your Gadget projects however you want. There
is, however, a convention, and you can conform to it most easily by installing
the github.com/redneckbeard/gadget/gdgt
subpackage. Using the "new" command,
you can create a ready-to-compile program with the following directory/file
structure:
.
├── app
│ └── conf.go
├── controllers
│ └── home.go
├── main.go
├── static
│ ├── css
│ ├── img
│ └── js
└── templates
├── base.html
└── home
└── index.html
The app package is where you actually have your Gadget configuration and a pointer to the app object, so you will end up importing that package in files in the controllers package. main.go also imports app, and actually runs the thing.
Since your Gadget application is just a Go package, we can build this with go
install <appname>
, and voilà -- we have a single-file web application / HTTP
server waiting for as $GOPATH/bin/<appname>
.
Because there are some files that don't go into the build, and the build is
just an executable, Gadget needs an absolute path that it can assume as the
root that all relative filepaths branch off of. In development, this will often
simply be the current working directory, and that's the default. However, in
production, you might have your binary and your frontend files in completely
different locations. For this reason, we can call the <appname>
executable
with a -root
flag and point it at whatever path we please.
The command invoked in an upstart job might then look like:
/usr/local/bin/inspector serve -static="/media/" -root=/home/penny/files/ -debug=false
Gadget assumes that the file root will contain a static
directory and that
you want it to serve the contents thereof as files. By default, it will do so
at /static/
. You can, however, change this to accommodate whatever you have
against the word "static"... with the -static
flag.
Routes in Gadget are just code. They go in the Configure method of your app in app/conf.go.
People love HandlerFuncs. So if you want, you can just route stuff to HandlerFuncs.
app.Routes(
app.HandleFunc("robots.txt", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "templates/robots.txt") }),
)
If you have lots of routes with a common URL segment, you can factor it out
with Prefixed
.
app.Routes(
app.Prefixed("users",
app.HandleFunc("friends", FriendsIndex),
app.HandleFunc("frenemies", FrenemiesIndex),
),
)
In practice, though being able to use HandlerFuncs is very handy, you'll more
commonly route to RESTful controllers with the Resource
method.
app.Routes(
app.Resource("users",
app.Resource("friends"),
app.Resource("frenemies"),
),
)
In this example, "users", "friends", and "frenemies" all reference controllers
that we've registered with the app. To mount at route at the root of the site,
there's a special SetIndex
method, which is set up for you automatically if
you use the gdgt project generator.
app.Routes(
app.SetIndex("home"),
app.Resource("users",
app.Resource("friends"),
app.Resource("frenemies"),
),
)
The strings that are fed to the gadget.Resource
calls correspond to the names
of controllers that we defined in our controllers
package. The files in the
controllers package all declare a gadget.Controller type, embed a pointer to
gadget.DefaultController` to make it simpler to implement the controller
interface, and explicitly register that controller with the framework.
package controllers
import (
"github.com/redneckbeard/gadget"
"example/app"
)
type MissionController struct {
*gadget.DefaultController
}
func (c *MissionController) Index(r *gadget.Request) (int, interface{}) {
return 200, []&struct{Mission string}{{"Dr. Claw"},{"M.A.D. Cat"}}
}
func (c *MissionController) Show(r *gadget.Request) (int, interface{}) {
missionId := r.UrlParams["mission_id"]
return 200, "Mission #" + missionId + ": this message will self-destruct."
}
func (c *MissionController) ChiefQuimby(r *gadget.Request) (int, interface{}) {
return 200, "You've done it again, Gadget! Don't know how you do it!"
}
func init() {
app.Register(&MissionController{})
}
Controller methods have access to a gadget.Request
object and return simply
an HTTP status code and any value at all for the body (more on why in a bit).
The controller interface requires Index
, Show
, Create
, Update
, and
Destroy
methods. Embedding a pointer to a DefaultController
means that
these are all implemented for you. However, this doesn't provide you with
anything but 404s. If you want to take action in response to a particular verb,
override the method.
The Gadget router will hit controller methods based on the HTTP verbs that you would expect:
GET /missions
routes to Index
GET /missions/\d+
routes to Show
POST /missions
routes Create
PUT /missions/\d+
routes to Update
PATCH /missions/\d+
routes to Update
DELETE /missions/\d+
routes to Destroy
Numeric ids are the default, but if you want something else in your URLs, just
override func IdPattern() string
on your controller.
In addition, any exported method on the controller will be routed to for all
HTTP verbs. ChiefQuimby
above would be called for any verb when the requested
path was /missions/chief-quimby
.
You make a Controller available to the router by passing it to app.Register
.
This is best done in the init function. Gadget doesn't pretend to speak
perfect English, so it takes the dumbest possible guess at pluralizing your
controller's name and just tacks an "s" on the end. If inflecting is more
complicated, define a Plural() string
method on your controller.
When developing a web application, you frequently have a short-circuit pattern common to a number of controller methods -- "404 if the user isn't logged in", "Redirect if the user isn't authorized", etc. To accommodate code reuse, Gadget controllers allow you to define filters on certain actions. Setting one up in the example above might look like this:
func init() {
c := &MissionController{gadget.New()}
c.Filter([]string{"create", "update", "destroy"}, UserIsPenny)
gadget.Register(c)
}
UserIsPenny
is just a function with the signature func(r *requests.Request)
(int, interface{})
just like a controller method. If this function returns a
non-zero status code, the controller method that was filtered will never be
called. If the filter returns a status code of zero, Gadget will move on to the
next filter for that action until they are exhausted, and then call the
controller method.
gadget.Request
embeds http.Request
and provides a few convenience facilities:
Request.UrlParams
is a map[string]string
of any ids plucked from route URLsRequest.Params
is a map[string]interface{}
of GET/POST parameters, or the deserialized POST body of a request sent with Content-Type: application/json
Request.User
provides a hook into Gadget's lightweight authentication systemRequest.Debug()
gives you per-request debug statusIn most cases, returning a status code and a response body are all you need to
do to respond to a request. When you do need to set cookies or response
headers, you can wrap the response body value in gadget.NewResponse
and set
cookies and headers on the value returned.
resp := gadget.NewResponse(responseMap)
resp.AddCookie(&http.Cookie{
Name: "lastVisited",
Value: r.Path,
Expires: time.Now().Add(time.Duration(1) * time.Hour),
})
return 200, resp
The interface{}
value you that you return from a controller method is by
default piped through fmt.Sprint
. Strings are predictable, as are numbers;
other types look more like debugging output. However, Gadget has a mechanism
for transforming those values based on Content-Type
or Accept
headers. By
defining Broker functions and assigning them to MIME types, you can make the
same controller methods speak HTML and JSON.
app.Accept("application/json").Via(gadget.JsonBroker)
JSON and XML processors are included with Gadget. Placing the line above in
your app's Configure
method will make Gadget serialize the body values
returned from your controller methods when the appropriate headers are found in
the request.
Subpackage gadget/templates
implements an HTML Broker that wraps the
html/template
package. templates.TemplateBroker
attempts to render
the body value returned from a controller method as the context of an
html/template.Template
. It requires adherence to a few simple conventions
for locating templates:
templates.TemplatePath
("templates" by default).gadget/templates
additionally provides a registry of helper functions. Any function you want to have available to all templates can be added by passing it to templates.AddHelper
. The package defines two helpers that are automatically available:
request
gives you access to the current *gadget.Request
;render
allows you to inject subtemplates (i.e. partials, includes, etc.) into the current template. It takes the name of a template, minus the ".html" extension, and the context that you want to pass to the subtemplate. It will first look in the "templates/{{controller}}" subdirectory for the template specified, and fall back to "templates".Gadget doesn't have an authentication framework, but it does have hooks for plugging one in. It defines:
gadget.User
that has a single method, Authenticated() bool
Authenticated()
always returns false) UserIdentifier
with the signature func(*Request) User
You define a type that implements gadget.User
(presumably for which
Authenticated()
always returns true and then write a UserIdentifier
that
will return either your type or gadget.AnonymousUser
. You can then register
it with the framework by passing it to gadget.IdentifyUsersWith
. In your
controller methods you'll be able to do something like this:
if !r.User.Authenticated() {
return 403, "Unauthorized"
}
user := r.User.(*models.User)
The "serve" command registered by package github.com/redneckbeard/gadget/env
defines a debug flag. You can access that flag as env.Debug
. You write a lot
of code inside controller methods, so as a convenience, you can check the value
of env.Debug
by calling Request.Debug
.
If you need per-request debug state -- for example, seeing debugging variables
in the browser for an administrative user -- you can hook into
Request.Debug()
by setting gadget.SetDebugWith
to a function you define
with the signature func(*Request) bool
.
The env
subpackage also provides some rudimentary logging facilities. env.Log(interface{}...)
will write to stdout by default, but a file location can be specified with the -log
flag when running the serve
command. Basic data about the request/response cycle is automatically logged, and the output looks like this:
[04 Dec 13 20:44 EST] "GET / HTTP/1.1" 200 2825
(That's time, obvious HTTP stuff, response status code, bytes written.) Similarly to gadget.SetDebugWith
, you can override the request logging behavior by assigning a func(r *Request, status, contentLength int) string
to gadget.RequestLogger
.
Gadget will recover from panics; in debug mode, in returns the stack trace to the client. If debug mode is off, however, it will simply return an empty 500 and log the stack trace via env.Log
.
Gadget uses Quimby to support multiple commands from the same binary. You can just as easily register your own. Follow the example in the Quimby README.
Gadget is used by farmr.org, New England's 4th most popular website about database-driven urban farm management.