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