feature

Paddy 2015-03-16

0:caad72abc05a Go to Latest

feature/server/endpoints.go

First pass at an implementation. Implement the calculation to determine whether a string is included in the partition or not, using crc32 to coerce the string to an evenly distributed uint32, and a modulo to turn it into a percentage. Implement a store to keep track of available flags and the level of the partition for each flag. Implement an API to retrieve, create, and modify these available flags.

History
paddy@0 1 package server
paddy@0 2
paddy@0 3 import (
paddy@0 4 "encoding/json"
paddy@0 5 "fmt"
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 "code.secondbit.org/feature.hg"
paddy@0 12 )
paddy@0 13
paddy@0 14 const (
paddy@0 15 ErrMissing = "missing"
paddy@0 16 ErrNotFound = "not_found"
paddy@0 17 ErrConflict = "conflict"
paddy@0 18 ErrActOfGod = "act_of_god"
paddy@0 19 )
paddy@0 20
paddy@0 21 var (
paddy@0 22 encoders = []string{"application/json"}
paddy@0 23 )
paddy@0 24
paddy@0 25 type flagCheck struct {
paddy@0 26 ID string `json:"id,omitempty"`
paddy@0 27 Passes bool `json:"pass,omitempty"`
paddy@0 28 }
paddy@0 29
paddy@0 30 type response struct {
paddy@0 31 status int
paddy@0 32 contentType string
paddy@0 33 Checks []flagCheck `json:"checks,omitempty"`
paddy@0 34 Flags []feature.Flag `json:"flags,omitempty"`
paddy@0 35 Errors []respError `json:"errors,omitempty"`
paddy@0 36 }
paddy@0 37
paddy@0 38 func (r response) addErrors(e ...respError) {
paddy@0 39 if r.Errors == nil {
paddy@0 40 r.Errors = []respError{}
paddy@0 41 }
paddy@0 42 r.Errors = append(r.Errors, e...)
paddy@0 43 }
paddy@0 44
paddy@0 45 func (r response) write(w http.ResponseWriter) {
paddy@0 46 w.Header().Set("Content-Type", r.contentType)
paddy@0 47 if r.status == 0 {
paddy@0 48 r.status = 200
paddy@0 49 }
paddy@0 50 w.WriteHeader(r.status)
paddy@0 51 var err error
paddy@0 52 switch r.contentType {
paddy@0 53 case "application/json":
paddy@0 54 enc := json.NewEncoder(w)
paddy@0 55 err = enc.Encode(r)
paddy@0 56 default:
paddy@0 57 _, err = w.Write([]byte(strings.Join(encoders, "\n") + "\n"))
paddy@0 58 }
paddy@0 59 if err != nil {
paddy@0 60 log.Printf("Error writing response: %+v\n", err)
paddy@0 61 }
paddy@0 62 }
paddy@0 63
paddy@0 64 func responseFromRequest(r *http.Request) response {
paddy@0 65 resp := responseFromRequest(r)
paddy@0 66 if r.Header.Get("Accept") != "" {
paddy@0 67 resp.contentType = goautoneg.Negotiate(r.Header.Get("Accept"), encoders)
paddy@0 68 if resp.contentType == "" {
paddy@0 69 resp.status = http.StatusNotAcceptable
paddy@0 70 }
paddy@0 71 } else {
paddy@0 72 resp.contentType = "application/json"
paddy@0 73 }
paddy@0 74 return resp
paddy@0 75 }
paddy@0 76
paddy@0 77 type respError struct {
paddy@0 78 Code string `json:"code"`
paddy@0 79 Param string `json:"param,omitempty"`
paddy@0 80 Field string `json:"field,omitempty"`
paddy@0 81 }
paddy@0 82
paddy@0 83 func FlagSetHandler(w http.ResponseWriter, r *http.Request, store flagStore) {
paddy@0 84 resp := responseFromRequest(r)
paddy@0 85 flags, err := store.list()
paddy@0 86 if err != nil {
paddy@0 87 switch err.code {
paddy@0 88 default:
paddy@0 89 log.Printf("Error listing flags: %+v\n", err)
paddy@0 90 resp.status = http.StatusInternalServerError
paddy@0 91 resp.addErrors(respError{Code: ErrActOfGod})
paddy@0 92 }
paddy@0 93 resp.write(w)
paddy@0 94 return
paddy@0 95 }
paddy@0 96 shard := r.URL.Query().Get("shard")
paddy@0 97 if shard == "" {
paddy@0 98 resp.Flags = flags
paddy@0 99 resp.write(w)
paddy@0 100 return
paddy@0 101 }
paddy@0 102 checks := make([]flagCheck, len(flags))
paddy@0 103 for pos, flag := range flags {
paddy@0 104 checks[pos] = flagCheck{ID: flag.ID, Passes: flag.Permit(shard)}
paddy@0 105 }
paddy@0 106 resp.Checks = checks
paddy@0 107 resp.write(w)
paddy@0 108 }
paddy@0 109
paddy@0 110 func CreateFlagHandler(w http.ResponseWriter, r *http.Request, store flagStore) {
paddy@0 111 resp := responseFromRequest(r)
paddy@0 112 var f feature.Flag
paddy@0 113 // BUG(paddY): Need to unmarshal the request body
paddy@0 114 if f.ID == "" {
paddy@0 115 resp.status = http.StatusBadRequest
paddy@0 116 resp.addErrors(respError{Code: ErrMissing, Field: "/id"})
paddy@0 117 resp.write(w)
paddy@0 118 return
paddy@0 119 }
paddy@0 120 err := store.create(f)
paddy@0 121 if err != nil {
paddy@0 122 switch err.code {
paddy@0 123 case errAlreadyExists:
paddy@0 124 resp.status = http.StatusBadRequest
paddy@0 125 resp.addErrors(respError{Code: ErrConflict, Field: "/id"})
paddy@0 126 default:
paddy@0 127 log.Printf("Error creating flag: %+v\n", err)
paddy@0 128 resp.status = http.StatusInternalServerError
paddy@0 129 resp.addErrors(respError{Code: ErrActOfGod})
paddy@0 130 }
paddy@0 131 resp.write(w)
paddy@0 132 return
paddy@0 133 }
paddy@0 134 resp.Flags = []feature.Flag{f}
paddy@0 135 resp.write(w)
paddy@0 136 }
paddy@0 137
paddy@0 138 func UpdateFlagHandler(w http.ResponseWriter, r *http.Request, store flagStore) {
paddy@0 139 resp := responseFromRequest(r)
paddy@0 140 var flags []feature.Flag
paddy@0 141 // BUG(paddy): Need to unmarshal the request body
paddy@0 142 for pos, f := range flags {
paddy@0 143 if f.ID == "" {
paddy@0 144 resp.addErrors(respError{Code: ErrMissing, Field: fmt.Sprintf("/id/%d", pos)})
paddy@0 145 resp.status = http.StatusBadRequest
paddy@0 146 }
paddy@0 147 }
paddy@0 148 errs := store.update(flags)
paddy@0 149 if errs != nil {
paddy@0 150 for _, err := range errs {
paddy@0 151 switch err.code {
paddy@0 152 case errNotFound:
paddy@0 153 resp.status = http.StatusNotFound
paddy@0 154 resp.addErrors(respError{Code: ErrNotFound, Field: fmt.Sprintf("/id/%d", err.pos)})
paddy@0 155 default:
paddy@0 156 log.Printf("Error updating flag: %+v\n", err)
paddy@0 157 resp.status = http.StatusInternalServerError
paddy@0 158 resp.addErrors(respError{Code: ErrActOfGod})
paddy@0 159 }
paddy@0 160 }
paddy@0 161 resp.write(w)
paddy@0 162 return
paddy@0 163 }
paddy@0 164 resp.Flags = flags
paddy@0 165 resp.write(w)
paddy@0 166 }
paddy@0 167
paddy@0 168 func DeleteFlagHandler(w http.ResponseWriter, r *http.Request, store flagStore) {
paddy@0 169 resp := responseFromRequest(r)
paddy@0 170 var id string
paddy@0 171 // BUG(paddY): Need to pick a routing library and get the ID
paddy@0 172 err := store.destroy(id)
paddy@0 173 if err != nil {
paddy@0 174 switch err.code {
paddy@0 175 case ErrNotFound:
paddy@0 176 resp.status = http.StatusNotFound
paddy@0 177 resp.addErrors(respError{Code: ErrNotFound, Param: "id"})
paddy@0 178 default:
paddy@0 179 resp.status = http.StatusInternalServerError
paddy@0 180 resp.addErrors(respError{Code: ErrActOfGod})
paddy@0 181 }
paddy@0 182 resp.write(w)
paddy@0 183 return
paddy@0 184 }
paddy@0 185 resp.Flags = []feature.Flag{{ID: id}}
paddy@0 186 resp.write(w)
paddy@0 187 }