feature
2015-03-16
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.
1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/server/endpoints.go Mon Mar 16 22:45:58 2015 -0400 1.3 @@ -0,0 +1,187 @@ 1.4 +package server 1.5 + 1.6 +import ( 1.7 + "encoding/json" 1.8 + "fmt" 1.9 + "log" 1.10 + "net/http" 1.11 + "strings" 1.12 + 1.13 + "bitbucket.org/ww/goautoneg" 1.14 + "code.secondbit.org/feature.hg" 1.15 +) 1.16 + 1.17 +const ( 1.18 + ErrMissing = "missing" 1.19 + ErrNotFound = "not_found" 1.20 + ErrConflict = "conflict" 1.21 + ErrActOfGod = "act_of_god" 1.22 +) 1.23 + 1.24 +var ( 1.25 + encoders = []string{"application/json"} 1.26 +) 1.27 + 1.28 +type flagCheck struct { 1.29 + ID string `json:"id,omitempty"` 1.30 + Passes bool `json:"pass,omitempty"` 1.31 +} 1.32 + 1.33 +type response struct { 1.34 + status int 1.35 + contentType string 1.36 + Checks []flagCheck `json:"checks,omitempty"` 1.37 + Flags []feature.Flag `json:"flags,omitempty"` 1.38 + Errors []respError `json:"errors,omitempty"` 1.39 +} 1.40 + 1.41 +func (r response) addErrors(e ...respError) { 1.42 + if r.Errors == nil { 1.43 + r.Errors = []respError{} 1.44 + } 1.45 + r.Errors = append(r.Errors, e...) 1.46 +} 1.47 + 1.48 +func (r response) write(w http.ResponseWriter) { 1.49 + w.Header().Set("Content-Type", r.contentType) 1.50 + if r.status == 0 { 1.51 + r.status = 200 1.52 + } 1.53 + w.WriteHeader(r.status) 1.54 + var err error 1.55 + switch r.contentType { 1.56 + case "application/json": 1.57 + enc := json.NewEncoder(w) 1.58 + err = enc.Encode(r) 1.59 + default: 1.60 + _, err = w.Write([]byte(strings.Join(encoders, "\n") + "\n")) 1.61 + } 1.62 + if err != nil { 1.63 + log.Printf("Error writing response: %+v\n", err) 1.64 + } 1.65 +} 1.66 + 1.67 +func responseFromRequest(r *http.Request) response { 1.68 + resp := responseFromRequest(r) 1.69 + if r.Header.Get("Accept") != "" { 1.70 + resp.contentType = goautoneg.Negotiate(r.Header.Get("Accept"), encoders) 1.71 + if resp.contentType == "" { 1.72 + resp.status = http.StatusNotAcceptable 1.73 + } 1.74 + } else { 1.75 + resp.contentType = "application/json" 1.76 + } 1.77 + return resp 1.78 +} 1.79 + 1.80 +type respError struct { 1.81 + Code string `json:"code"` 1.82 + Param string `json:"param,omitempty"` 1.83 + Field string `json:"field,omitempty"` 1.84 +} 1.85 + 1.86 +func FlagSetHandler(w http.ResponseWriter, r *http.Request, store flagStore) { 1.87 + resp := responseFromRequest(r) 1.88 + flags, err := store.list() 1.89 + if err != nil { 1.90 + switch err.code { 1.91 + default: 1.92 + log.Printf("Error listing flags: %+v\n", err) 1.93 + resp.status = http.StatusInternalServerError 1.94 + resp.addErrors(respError{Code: ErrActOfGod}) 1.95 + } 1.96 + resp.write(w) 1.97 + return 1.98 + } 1.99 + shard := r.URL.Query().Get("shard") 1.100 + if shard == "" { 1.101 + resp.Flags = flags 1.102 + resp.write(w) 1.103 + return 1.104 + } 1.105 + checks := make([]flagCheck, len(flags)) 1.106 + for pos, flag := range flags { 1.107 + checks[pos] = flagCheck{ID: flag.ID, Passes: flag.Permit(shard)} 1.108 + } 1.109 + resp.Checks = checks 1.110 + resp.write(w) 1.111 +} 1.112 + 1.113 +func CreateFlagHandler(w http.ResponseWriter, r *http.Request, store flagStore) { 1.114 + resp := responseFromRequest(r) 1.115 + var f feature.Flag 1.116 + // BUG(paddY): Need to unmarshal the request body 1.117 + if f.ID == "" { 1.118 + resp.status = http.StatusBadRequest 1.119 + resp.addErrors(respError{Code: ErrMissing, Field: "/id"}) 1.120 + resp.write(w) 1.121 + return 1.122 + } 1.123 + err := store.create(f) 1.124 + if err != nil { 1.125 + switch err.code { 1.126 + case errAlreadyExists: 1.127 + resp.status = http.StatusBadRequest 1.128 + resp.addErrors(respError{Code: ErrConflict, Field: "/id"}) 1.129 + default: 1.130 + log.Printf("Error creating flag: %+v\n", err) 1.131 + resp.status = http.StatusInternalServerError 1.132 + resp.addErrors(respError{Code: ErrActOfGod}) 1.133 + } 1.134 + resp.write(w) 1.135 + return 1.136 + } 1.137 + resp.Flags = []feature.Flag{f} 1.138 + resp.write(w) 1.139 +} 1.140 + 1.141 +func UpdateFlagHandler(w http.ResponseWriter, r *http.Request, store flagStore) { 1.142 + resp := responseFromRequest(r) 1.143 + var flags []feature.Flag 1.144 + // BUG(paddy): Need to unmarshal the request body 1.145 + for pos, f := range flags { 1.146 + if f.ID == "" { 1.147 + resp.addErrors(respError{Code: ErrMissing, Field: fmt.Sprintf("/id/%d", pos)}) 1.148 + resp.status = http.StatusBadRequest 1.149 + } 1.150 + } 1.151 + errs := store.update(flags) 1.152 + if errs != nil { 1.153 + for _, err := range errs { 1.154 + switch err.code { 1.155 + case errNotFound: 1.156 + resp.status = http.StatusNotFound 1.157 + resp.addErrors(respError{Code: ErrNotFound, Field: fmt.Sprintf("/id/%d", err.pos)}) 1.158 + default: 1.159 + log.Printf("Error updating flag: %+v\n", err) 1.160 + resp.status = http.StatusInternalServerError 1.161 + resp.addErrors(respError{Code: ErrActOfGod}) 1.162 + } 1.163 + } 1.164 + resp.write(w) 1.165 + return 1.166 + } 1.167 + resp.Flags = flags 1.168 + resp.write(w) 1.169 +} 1.170 + 1.171 +func DeleteFlagHandler(w http.ResponseWriter, r *http.Request, store flagStore) { 1.172 + resp := responseFromRequest(r) 1.173 + var id string 1.174 + // BUG(paddY): Need to pick a routing library and get the ID 1.175 + err := store.destroy(id) 1.176 + if err != nil { 1.177 + switch err.code { 1.178 + case ErrNotFound: 1.179 + resp.status = http.StatusNotFound 1.180 + resp.addErrors(respError{Code: ErrNotFound, Param: "id"}) 1.181 + default: 1.182 + resp.status = http.StatusInternalServerError 1.183 + resp.addErrors(respError{Code: ErrActOfGod}) 1.184 + } 1.185 + resp.write(w) 1.186 + return 1.187 + } 1.188 + resp.Flags = []feature.Flag{{ID: id}} 1.189 + resp.write(w) 1.190 +}