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