api
2015-07-18
Child:57c9412e8000
api/api.go
First commit. Define common middleware, helpers, etc. that will be used across projects.
| paddy@0 | 1 package api |
| paddy@0 | 2 |
| paddy@0 | 3 import ( |
| paddy@0 | 4 "encoding/json" |
| paddy@0 | 5 "errors" |
| paddy@0 | 6 "log" |
| paddy@0 | 7 "net/http" |
| paddy@0 | 8 "strings" |
| paddy@0 | 9 |
| paddy@0 | 10 "bitbucket.org/ww/goautoneg" |
| paddy@0 | 11 |
| paddy@0 | 12 "code.secondbit.org/uuid.hg" |
| paddy@0 | 13 |
| paddy@0 | 14 "golang.org/x/net/context" |
| paddy@0 | 15 ) |
| paddy@0 | 16 |
| paddy@0 | 17 const ( |
| paddy@0 | 18 RequestErrAccessDenied = "access_denied" |
| paddy@0 | 19 RequestErrInsufficient = "insufficient" |
| paddy@0 | 20 RequestErrOverflow = "overflow" |
| paddy@0 | 21 RequestErrInvalidValue = "invalid_value" |
| paddy@0 | 22 RequestErrInvalidFormat = "invalid_format" |
| paddy@0 | 23 RequestErrMissing = "missing" |
| paddy@0 | 24 RequestErrNotFound = "not_found" |
| paddy@0 | 25 RequestErrConflict = "conflict" |
| paddy@0 | 26 RequestErrActOfGod = "act_of_god" |
| paddy@0 | 27 ) |
| paddy@0 | 28 |
| paddy@0 | 29 var ( |
| paddy@0 | 30 ActOfGodError = []RequestError{{Slug: RequestErrActOfGod}} |
| paddy@0 | 31 InvalidFormatError = []RequestError{{Slug: RequestErrInvalidFormat, Field: "/"}} |
| paddy@0 | 32 |
| paddy@0 | 33 Encoders = []string{"application/json"} |
| paddy@0 | 34 |
| paddy@0 | 35 ErrUserIDNotSet = errors.New("user ID not set") |
| paddy@0 | 36 ) |
| paddy@0 | 37 |
| paddy@0 | 38 type RequestError struct { |
| paddy@0 | 39 Slug string `json:"error,omitempty"` |
| paddy@0 | 40 Field string `json:"field,omitempty"` |
| paddy@0 | 41 Param string `json:"param,omitempty"` |
| paddy@0 | 42 Header string `json:"header,omitempty"` |
| paddy@0 | 43 } |
| paddy@0 | 44 |
| paddy@0 | 45 type ContextHandler func(http.ResponseWriter, *http.Request, context.Context) |
| paddy@0 | 46 |
| paddy@0 | 47 func NegotiateMiddleware(h http.Handler) http.Handler { |
| paddy@0 | 48 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| paddy@0 | 49 if r.Header.Get("Accept") != "" { |
| paddy@0 | 50 contentType := goautoneg.Negotiate(r.Header.Get("Accept"), Encoders) |
| paddy@0 | 51 if contentType == "" { |
| paddy@0 | 52 w.WriteHeader(http.StatusNotAcceptable) |
| paddy@0 | 53 w.Write([]byte("Unsupported content type requested: " + r.Header.Get("Accept"))) |
| paddy@0 | 54 return |
| paddy@0 | 55 } |
| paddy@0 | 56 } |
| paddy@0 | 57 h.ServeHTTP(w, r) |
| paddy@0 | 58 }) |
| paddy@0 | 59 } |
| paddy@0 | 60 |
| paddy@0 | 61 func Encode(w http.ResponseWriter, r *http.Request, status int, resp interface{}) { |
| paddy@0 | 62 contentType := goautoneg.Negotiate(r.Header.Get("Accept"), Encoders) |
| paddy@0 | 63 w.Header().Set("content-type", contentType) |
| paddy@0 | 64 w.WriteHeader(status) |
| paddy@0 | 65 var err error |
| paddy@0 | 66 switch contentType { |
| paddy@0 | 67 case "application/json": |
| paddy@0 | 68 enc := json.NewEncoder(w) |
| paddy@0 | 69 err = enc.Encode(resp) |
| paddy@0 | 70 default: |
| paddy@0 | 71 enc := json.NewEncoder(w) |
| paddy@0 | 72 err = enc.Encode(resp) |
| paddy@0 | 73 } |
| paddy@0 | 74 if err != nil { |
| paddy@0 | 75 log.Println(err) |
| paddy@0 | 76 } |
| paddy@0 | 77 } |
| paddy@0 | 78 |
| paddy@0 | 79 func Decode(r *http.Request, target interface{}) error { |
| paddy@0 | 80 defer r.Body.Close() |
| paddy@0 | 81 switch r.Header.Get("Content-Type") { |
| paddy@0 | 82 case "application/json": |
| paddy@0 | 83 dec := json.NewDecoder(r.Body) |
| paddy@0 | 84 return dec.Decode(target) |
| paddy@0 | 85 default: |
| paddy@0 | 86 dec := json.NewDecoder(r.Body) |
| paddy@0 | 87 return dec.Decode(target) |
| paddy@0 | 88 } |
| paddy@0 | 89 } |
| paddy@0 | 90 |
| paddy@0 | 91 func CORSMiddleware(h http.Handler) http.Handler { |
| paddy@0 | 92 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| paddy@0 | 93 w.Header().Set("Access-Control-Allow-Origin", "*") |
| paddy@0 | 94 w.Header().Set("Access-Control-Allow-Headers", r.Header.Get("Access-Control-Request-Headers")) |
| paddy@0 | 95 w.Header().Set("Access-Control-Allow-Credentials", "true") |
| paddy@0 | 96 if strings.ToLower(r.Method) == "options" { |
| paddy@0 | 97 methods := strings.Join(r.Header[http.CanonicalHeaderKey("Trout-Methods")], ", ") |
| paddy@0 | 98 w.Header().Set("Access-Control-Allow-Methods", methods) |
| paddy@0 | 99 w.Header().Set("Allow", methods) |
| paddy@0 | 100 w.WriteHeader(http.StatusOK) |
| paddy@0 | 101 return |
| paddy@0 | 102 } |
| paddy@0 | 103 h.ServeHTTP(w, r) |
| paddy@0 | 104 }) |
| paddy@0 | 105 } |
| paddy@0 | 106 |
| paddy@0 | 107 func ContextWrapper(c context.Context, handler ContextHandler) http.Handler { |
| paddy@0 | 108 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| paddy@0 | 109 handler(w, r, c) |
| paddy@0 | 110 }) |
| paddy@0 | 111 } |
| paddy@0 | 112 |
| paddy@0 | 113 func CheckScopes(r *http.Request, scopes ...string) bool { |
| paddy@0 | 114 passedStr := r.Header.Get("scopes") |
| paddy@0 | 115 passed := strings.Split(passedStr, " ") |
| paddy@0 | 116 for _, scope := range scopes { |
| paddy@0 | 117 var found bool |
| paddy@0 | 118 for _, p := range passed { |
| paddy@0 | 119 if scope == strings.TrimSpace(p) { |
| paddy@0 | 120 found = true |
| paddy@0 | 121 break |
| paddy@0 | 122 } |
| paddy@0 | 123 } |
| paddy@0 | 124 if !found { |
| paddy@0 | 125 return false |
| paddy@0 | 126 } |
| paddy@0 | 127 } |
| paddy@0 | 128 return true |
| paddy@0 | 129 } |
| paddy@0 | 130 |
| paddy@0 | 131 func AuthUser(r *http.Request) (uuid.ID, error) { |
| paddy@0 | 132 rawID := r.Header.Get("User-ID") |
| paddy@0 | 133 if rawID == "" { |
| paddy@0 | 134 return nil, ErrUserIDNotSet |
| paddy@0 | 135 } |
| paddy@0 | 136 return uuid.Parse(rawID) |
| paddy@0 | 137 } |