Gadget

A smallish web framework for Go

View the Project on GitHub redneckbeard/gadget

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

Show me how it works!

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.

Project layout

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.

Running Gadget

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

Asset files

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.

Routing

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"),
    ),
)

Controllers

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:

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.

Action filters

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.

Request and explicit Response objects

Request objects

gadget.Request embeds http.Request and provides a few convenience facilities:

Response objects

In 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

Brokers

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:

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:

Users

Gadget doesn't have an authentication framework, but it does have hooks for plugging one in. It defines:

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)

Debugging and logging

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.

Custom command line tools

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.

Sites using Gadget

Gadget is used by farmr.org, New England's 4th most popular website about database-driven urban farm management.