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.
| 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 } |