Permalink
Find file
Fetching contributors…
Cannot retrieve contributors at this time
330 lines (293 sloc) 8.09 KB
package main
import (
"code.google.com/p/go-uuid/uuid"
"encoding/json"
"errors"
"fmt"
"github.com/stretchr/graceful"
"github.com/zenazn/goji/web"
"log"
"net/http"
"runtime/debug"
"time"
)
// Helpers.
// WebRead reads the JSON request body into the given dataRef. It
// returns an error if the read fails for any reason.
func WebRead(req *http.Request, dataRef interface{}) error {
err := json.NewDecoder(req.Body).Decode(dataRef)
if err != nil {
err := &PgpinError{
Id: "bad-request",
Message: "malformed JSON body",
HttpStatus: 400,
}
return err
}
return nil
}
// WebRespond writes an HTTP response to the given resp, either
// according to status and data if err is nil, or according to err
// if it's non-nil. It will attempt to coerce err into a PgpinError
// and respond with an appropriate error message, falling back to
// a generic 500 error if it can't. All web responses should go
// through this function.
func WebRespond(resp http.ResponseWriter, status int, data interface{}, err error) {
if err != nil {
pgerr, ok := err.(*PgpinError)
if ok {
status = pgerr.HttpStatus
data = pgerr
} else {
log.Printf("web.error %+s", err.Error())
status = 500
data = &map[string]string{
"id": "internal-error",
"message": "internal server error",
}
}
}
b, err := json.MarshalIndent(data, "", " ")
if err != nil {
panic(err)
}
resp.WriteHeader(status)
_, err = resp.Write(b)
if err != nil {
log.Printf("web.ioerror %s", err.Error())
}
_, err = resp.Write([]byte("\n"))
if err != nil {
log.Printf("web.ioerror %s", err.Error())
}
}
// Middleware.
func WebJsoner(inner http.Handler) http.Handler {
outer := func(resp http.ResponseWriter, req *http.Request) {
resp.Header().Set("Content-Type", "application/json; charset=utf-8")
inner.ServeHTTP(resp, req)
}
return http.HandlerFunc(outer)
}
func WebRequestIder(c *web.C, h http.Handler) http.Handler {
fn := func(resp http.ResponseWriter, req *http.Request) {
var requestId string
given := req.Header.Get("X-Request-Id")
if given != "" {
requestId = given
}
given = req.Header.Get("Request-Id")
if given != "" {
requestId = given
}
if requestId == "" {
requestId = uuid.New()
}
resp.Header().Set("Request-Id", requestId)
h.ServeHTTP(resp, req)
}
return http.HandlerFunc(fn)
}
func WebLogger(c *web.C, inner http.Handler) http.Handler {
outer := func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
method := r.Method
path := r.URL.Path
requestId := w.Header().Get("Request-Id")
log.Printf("web.request.start request_id=%s method=%s path=%s", requestId, method, path)
inner.ServeHTTP(w, r)
elapsed := float64(time.Since(start)) / 1000000.0
log.Printf("web.request.finish request_id=%s method=%s path=%s elapsed=%f", requestId, method, path, elapsed)
}
return http.HandlerFunc(outer)
}
func WebTimer(timeout time.Duration) func(http.Handler) http.Handler {
return func(inner http.Handler) http.Handler {
data := &map[string]string{
"id": "request-timeout",
"message": "request timed out",
}
body, err := json.MarshalIndent(data, "", " ")
Must(err)
return http.TimeoutHandler(inner, timeout, string(body)+"\n")
}
}
func WebRecoverer(h http.Handler) http.Handler {
fn := func(resp http.ResponseWriter, req *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("web.panic: %s", err)
log.Print(string(debug.Stack()))
WebRespond(resp, 0, nil, &PgpinError{
Id: "internal-error",
Message: "internal server error",
HttpStatus: 500,
})
}
}()
h.ServeHTTP(resp, req)
}
return http.HandlerFunc(fn)
}
// Db endpoints.
type DbSlim struct {
Id string `json:"id"`
Name string `json:"name"`
}
func WebDbList(resp http.ResponseWriter, req *http.Request) {
dbs, err := DbList("")
dbSlims := []*DbSlim{}
for _, db := range dbs {
dbSlims = append(dbSlims, &DbSlim{Id: db.Id, Name: db.Name})
}
WebRespond(resp, 200, dbSlims, err)
}
func WebDbCreate(resp http.ResponseWriter, req *http.Request) {
db := &Db{}
err := WebRead(req, db)
if err == nil {
db, err = DbCreate(db.Name, db.Url)
}
WebRespond(resp, 201, db, err)
}
func WebDbUpdate(c web.C, resp http.ResponseWriter, req *http.Request) {
dbUpdate := &Db{}
db := &Db{}
err := WebRead(req, dbUpdate)
if err == nil {
db, err = DbGet(c.URLParams["id"])
if err == nil {
if dbUpdate.Name != "" {
db.Name = dbUpdate.Name
}
if dbUpdate.Url != "" {
db.Url = dbUpdate.Url
}
err = DbUpdate(db)
}
}
WebRespond(resp, 200, db, err)
}
func WebDbGet(c web.C, resp http.ResponseWriter, req *http.Request) {
db, err := DbGet(c.URLParams["id"])
WebRespond(resp, 200, db, err)
}
func WebDbDelete(c web.C, resp http.ResponseWriter, req *http.Request) {
db, err := DbDelete(c.URLParams["id"])
WebRespond(resp, 200, db, err)
}
// Pin endpoints.
type PinSlim struct {
Id string `json:"id"`
Name string `json:"name"`
}
func WebPinList(resp http.ResponseWriter, req *http.Request) {
pins, err := PinList("")
pinSlims := []*PinSlim{}
for _, pin := range pins {
pinSlims = append(pinSlims, &PinSlim{Id: pin.Id, Name: pin.Name})
}
WebRespond(resp, 200, pinSlims, err)
}
func WebPinCreate(resp http.ResponseWriter, req *http.Request) {
pin := &Pin{}
err := WebRead(req, pin)
if err == nil {
pin, err = PinCreate(pin.DbId, pin.Name, pin.Query)
}
WebRespond(resp, 201, pin, err)
}
func WebPinUpdate(c web.C, resp http.ResponseWriter, req *http.Request) {
pinUpdate := &Pin{}
pin := &Pin{}
err := WebRead(req, pinUpdate)
if err == nil {
pin, err = PinGet(c.URLParams["id"])
if err == nil {
if pinUpdate.Name != "" {
pin.Name = pinUpdate.Name
}
if pinUpdate.Query != "" {
pin.Query = pinUpdate.Query
}
err = PinUpdate(pin)
}
}
WebRespond(resp, 200, pin, err)
}
func WebPinGet(c web.C, resp http.ResponseWriter, req *http.Request) {
pin, err := PinGet(c.URLParams["id"])
WebRespond(resp, 200, pin, err)
}
func WebPinDelete(c web.C, resp http.ResponseWriter, req *http.Request) {
pin, err := PinDelete(c.URLParams["id"])
WebRespond(resp, 200, pin, err)
}
// Misc endpoints.
type Status struct {
Message string `json:"message"`
}
func WebStatus(resp http.ResponseWriter, req *http.Request) {
err := PgConn.Ping()
status := &Status{Message: "ok"}
WebRespond(resp, 200, status, err)
}
func WebTriggerError(resp http.ResponseWriter, req *http.Request) {
err := errors.New("a problem occurred")
WebRespond(resp, 0, nil, err)
}
func WebTriggerPanic(resp http.ResponseWriter, req *http.Request) {
panic("panic")
}
func WebTriggerSleep(resp http.ResponseWriter, req *http.Request) {
time.Sleep(ConfigWebTimeout - (100 * time.Millisecond))
status := &Status{Message: "sleepy"}
WebRespond(resp, 200, status, nil)
}
func WebTriggerTimeout(resp http.ResponseWriter, req *http.Request) {
time.Sleep(ConfigWebTimeout + (100 * time.Millisecond))
status := &Status{Message: "late"}
WebRespond(resp, 200, status, nil)
}
func WebNotFound(resp http.ResponseWriter, req *http.Request) {
err := &PgpinError{
Id: "not-found",
Message: "not found",
HttpStatus: 404,
}
WebRespond(resp, 0, nil, err)
}
// Server builder.
var WebMux *web.Mux
func WebBuild() {
WebMux = web.New()
WebMux.Use(WebJsoner)
WebMux.Use(WebRequestIder)
WebMux.Use(WebLogger)
WebMux.Use(WebTimer(ConfigWebTimeout))
WebMux.Use(WebRecoverer)
WebMux.Get("/v1/dbs", WebDbList)
WebMux.Post("/v1/dbs", WebDbCreate)
WebMux.Put("/v1/dbs/:id", WebDbUpdate)
WebMux.Get("/v1/dbs/:id", WebDbGet)
WebMux.Delete("/v1/dbs/:id", WebDbDelete)
WebMux.Get("/v1/pins", WebPinList)
WebMux.Post("/v1/pins", WebPinCreate)
WebMux.Put("/v1/pins/:id", WebPinUpdate)
WebMux.Get("/v1/pins/:id", WebPinGet)
WebMux.Delete("/v1/pins/:id", WebPinDelete)
WebMux.Get("/status", WebStatus)
WebMux.Get("/error", WebTriggerError)
WebMux.Get("/panic", WebTriggerPanic)
WebMux.Get("/sleep", WebTriggerSleep)
WebMux.Get("/timeout", WebTriggerTimeout)
WebMux.NotFound(WebNotFound)
}
func WebStart() {
log.Print("web.start")
PgStart()
RedisStart()
WebBuild()
addr := fmt.Sprintf(":%d", ConfigWebPort)
graceful.Run(addr, ConfigWebDrainInterval, WebMux)
}