diff --git a/README.md b/README.md index 9a1affb..3868411 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,10 @@ func main() { } ``` +Goji also includes a [sample application][sample] in the `example` folder which +was artificially constructed to show off all of Goji's features. Check it out! + +[sample]: https://github.com/zenazn/goji/tree/master/example Features -------- diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..c3b2da0 --- /dev/null +++ b/example/README.md @@ -0,0 +1,10 @@ +Gritter +======= + +Gritter is an example application built using Goji, where people who have +nothing better to do can post short 140-character "greets." + +A good place to start is with `main.go`, which contains a well-commented +walthrough of Goji's features. Gritter uses a couple custom middlwares, which +have been arbitrarily placed in `middleware.go`. Finally some uninteresting +"database models" live in `models.go`. diff --git a/example/main.go b/example/main.go new file mode 100644 index 0000000..a0dcc69 --- /dev/null +++ b/example/main.go @@ -0,0 +1,148 @@ +// Package example is a sample application built with Goji. Its goal is to give +// you a taste for what Goji looks like in the real world by artificially using +// all of its features. +// +// In particular, this is a complete working site for gritter.com, a site where +// users can post 140-character "greets". Any resemblance to real websites, +// alive or dead, is purely coincidental. +package main + +import ( + "fmt" + "io" + "net/http" + "regexp" + "strconv" + + "github.com/zenazn/goji" + "github.com/zenazn/goji/param" + "github.com/zenazn/goji/web" +) + +// Note: the code below cuts a lot of corners to make the example app simple. + +func main() { + // Add routes to the global handler + goji.Get("/", Root) + // Fully backwards compatible with net/http's Handlers + goji.Get("/greets", http.RedirectHandler("/", 301)) + // Use your favorite HTTP verbs + goji.Post("/greets", NewGreet) + // Use Sinatra-style patterns in your URLs + goji.Get("/users/:name", GetUser) + // Goji also supports regular expressions with named capture groups. + goji.Get(regexp.MustCompile(`^/greets/(?P\d+)$`), GetGreet) + + // Middleware can be used to inject behavior into your app. The + // middleware for this application are defined in middleware.go, but you + // can put them wherever you like. + goji.Use("PlainText", PlainText) + + // Sub-routes can be used to set custom middleware on sub-applications. + // Goji's interfaces are completely composable. + admin := web.New() + goji.Sub("/admin", admin) + + // Insert the super-secure middleware into the stack above the built-in + // AutomaticOptions middleware. + admin.Insert("SuperSecure", SuperSecure, "AutomaticOptions") + + // Set up admin routes. Note that sub-routes do *not* mutate the path in + // any way, so we need to supply full ("/admin" prefixed) paths. + admin.Get("/admin", AdminRoot) + admin.Get("/admin/finances", AdminFinances) + + // Use a custom 404 handler + goji.NotFound(NotFound) + + // Call Serve() at the bottom of your main() function, and it'll take + // care of everything else for you, including binding to a socket (with + // automatic support for systemd and Einhorn) and supporting graceful + // shutdown on SIGINT. Serve() is appropriate for both development and + // production. + goji.Serve() +} + +// Root route (GET "/"). Print a list of greets. +func Root(w http.ResponseWriter, r *http.Request) { + // In the real world you'd probably use a template or something. + io.WriteString(w, "Gritter\n======\n\n") + for i := len(Greets) - 1; i >= 0; i-- { + Greets[i].Write(w) + } +} + +// Create a new greet (POST "/greets"). Creates a greet and redirects you to the +// created greet. +// +// To post a new greet, try this at a shell: +// $ now=$(date +'%Y-%m-%mT%H:%M:%SZ') +// $ curl -i -d "user=carl&message=Hello+World&time=$now" localhost:8000/greets +func NewGreet(w http.ResponseWriter, r *http.Request) { + var greet Greet + + // Parse the POST body into the Greet struct. The format is the same as + // is emitted by (e.g.) jQuery.params. + r.ParseForm() + err := param.Parse(r.Form, &greet) + + if err != nil || len(greet.Message) > 140 { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // We make no effort to prevent races against other insertions. + Greets = append(Greets, greet) + url := fmt.Sprintf("/greets/%d", len(Greets)-1) + http.Redirect(w, r, url, http.StatusCreated) +} + +// Get a given user and her greets (GET "/user/:name") +func GetUser(c web.C, w http.ResponseWriter, r *http.Request) { + io.WriteString(w, "Gritter\n======\n\n") + handle := c.UrlParams["name"] + user, ok := Users[handle] + if !ok { + http.Error(w, http.StatusText(404), 404) + return + } + + user.Write(w, handle) + + io.WriteString(w, "\nGreets:\n") + for i := len(Greets) - 1; i >= 0; i-- { + if Greets[i].User == handle { + Greets[i].Write(w) + } + } +} + +// Get a particular greet by ID (GET "/greet/\d+"). Does no bounds checking, so +// will probably panic. +func GetGreet(c web.C, w http.ResponseWriter, r *http.Request) { + id, err := strconv.Atoi(c.UrlParams["id"]) + if err != nil { + http.Error(w, http.StatusText(404), 404) + return + } + // This will panic if id is too big. Try it out! + greet := Greets[id] + + io.WriteString(w, "Gritter\n======\n\n") + greet.Write(w) +} + +// Admin root (GET "/admin/root"). Much secret. Very administrate. Wow. +func AdminRoot(w http.ResponseWriter, r *http.Request) { + io.WriteString(w, "Gritter\n======\n\nSuper secret admin page!\n") +} + +// How are we doing? (GET "/admin/finances") +func AdminFinances(w http.ResponseWriter, r *http.Request) { + io.WriteString(w, "Gritter\n======\n\nWe're broke! :(\n") +} + +// 404 handler. +func NotFound(w http.ResponseWriter, r *http.Request) { + http.Error(w, "Umm... have you tried turning it off and on again?", 404) +} diff --git a/example/middleware.go b/example/middleware.go new file mode 100644 index 0000000..1b9a0ab --- /dev/null +++ b/example/middleware.go @@ -0,0 +1,47 @@ +package main + +import ( + "encoding/base64" + "net/http" + "strings" + + "github.com/zenazn/goji/web" +) + +// Middleware to render responses as plain text. +func PlainText(h http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + h.ServeHTTP(w, r) + } + return http.HandlerFunc(fn) +} + +// Nobody will ever guess this! +const Password = "admin:admin" + +// HTTP Basic Auth middleware for super-secret admin page. Shhhh! +func SuperSecure(c *web.C, h http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + auth := r.Header.Get("Authorization") + if !strings.HasPrefix(auth, "Basic ") { + pleaseAuth(w) + return + } + + password, err := base64.StdEncoding.DecodeString(auth[6:]) + if err != nil || string(password) != Password { + pleaseAuth(w) + return + } + + h.ServeHTTP(w, r) + } + return http.HandlerFunc(fn) +} + +func pleaseAuth(w http.ResponseWriter) { + w.Header().Set("WWW-Authenticate", `Basic realm="Gritter"`) + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte("Go away!\n")) +} diff --git a/example/models.go b/example/models.go new file mode 100644 index 0000000..ef9453c --- /dev/null +++ b/example/models.go @@ -0,0 +1,48 @@ +package main + +import ( + "fmt" + "io" + "time" +) + +// A greet is a 140-character micro-blogpost that has no resemblance whatsoever +// to the noise a bird makes. +type Greet struct { + User string `param:"user"` + Message string `param:"message"` + Time time.Time `param:"time"` +} + +// Store all our greets in a big list in memory, because, let's be honest, who's +// actually going to use a service that only allows you to post 140-character +// messages? +var Greets = []Greet{ + {"carl", "Welcome to Gritter!", time.Now()}, + {"alice", "Wanna know a secret?", time.Now()}, + {"bob", "Okay!", time.Now()}, + {"eve", "I'm listening...", time.Now()}, +} + +// Write out a representation of the greet +func (g Greet) Write(w io.Writer) { + fmt.Fprintf(w, "%s\n@%s at %s\n---\n", g.Message, g.User, + g.Time.Format(time.UnixDate)) +} + +// A user +type User struct { + Name, Bio string +} + +// All the users we know about! There aren't very many... +var Users = map[string]User{ + "alice": {"Alice in Wonderland", "Eating mushrooms"}, + "bob": {"Bob the Builder", "Making children dumber"}, + "carl": {"Carl Jackson", "Duct tape aficionado"}, +} + +// Write out the user +func (u User) Write(w io.Writer, handle string) { + fmt.Fprintf(w, "%s (@%s)\n%s\n", u.Name, handle, u.Bio) +}