Add updating devices to apiv1.
We needed a way to be able to update devices after they were created. This is
supported in the devices package, we just needed to expose it using apiv1
endpoints.
In doing so, it became apparent that allowing users to change the Owner of their
Devices wasn't properly thought through, and pending a reason to use it, I'm
just removing it. The biggest issue came when trying to return usable error
messages; we couldn't distinguish between "you don't own the device you're
trying to update" and "you're not allowed to change the owner of the device". I
also couldn't figure out _who should be able to_ change the owner of the device,
which is generally an indication that I'm building a feature before I have a use
case for it.
To support this change, the apiv1.DeviceChange type needed its Owner property
removed.
I also needed to add deviceFromAPI and devicesFromAPI helpers to return
devices.Device types from apiv1.Device types.
There's now a new validateDeviceUpdate helper that checks to ensure that a
device update request is valid and the user has the appropriate permissions.
The createRequest type now accepts a slice of Devices, not a slice of
DeviceChanges, because we want to pass the Owner in.
A new updateRequest type is created, which accepts a DeviceChange to apply.
A new handleUpdateDevice handler is created, which is assigned to the endpoint
for PATCH requests against a device ID. It checks that the user is logged in,
the Device they're trying to update exists, and that it's a valid update. If all
of that is true, the device is updated and the updated device is returned.
Finally, we had to add two new scopes to support new functionality:
ScopeUpdateOtherUserDevices allows a user to update other user's devices, and
ScopeUpdateLastSeen allows a user to update the LastSeen property of a device.
Pending some better error messages, this should be a full implementation of
updating a device, which leaves only the deletion endpoint to deal with.
11 default404Handler = http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
12 w.WriteHeader(http.StatusNotFound)
13 w.Write([]byte("404 Not Found"))
16 default405Handler = http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
17 w.Header().Set("Allow", strings.Join(r.Header[http.CanonicalHeaderKey("Trout-Methods")], ", "))
18 w.WriteHeader(http.StatusMethodNotAllowed)
19 w.Write([]byte("405 Method Not Allowed"))
24 // RequestVars returns easy-to-access mappings of parameters to values for URL templates. Any {parameter} in
25 // your URL template will be available in the returned Header as a slice of strings, one for each instance of
26 // the {parameter}. In the case of a parameter name being used more than once in the same URL template, the
27 // values will be in the slice in the order they appeared in the template.
29 // Values can easily be accessed by using the .Get() method of the returned Header, though to access multiple
30 // values, they must be accessed through the map. All parameters use http.CanonicalHeaderKey for their formatting.
31 // When using .Get(), the parameter name will be transformed automatically. When utilising the Header as a map,
32 // the parameter name needs to have http.CanonicalHeaderKey applied manually.
33 func RequestVars(r *http.Request) http.Header {
35 for h, v := range r.Header {
36 stripped := strings.TrimPrefix(h, http.CanonicalHeaderKey("Trout-Param-"))
44 // Router defines a set of Endpoints that map requests to the http.Handlers. The http.Handler assigned to
45 // Handle404, if set, will be called when no Endpoint matches the current request. The http.Handler assigned
46 // to Handle405, if set, will be called when an Endpoint matches the current request, but has no http.Handler
47 // set for the HTTP method that the request used. Should either of these properties be unset, a default
48 // http.Handler will be used.
50 // The Router type is safe for use with empty values, but makes no attempt at concurrency-safety in adding
51 // Endpoints or in setting properties. It should also be noted that the adding Endpoints while simultaneously
52 // routing requests will lead to undefined and (almost certainly) undesirable behaviour. Routers are intended
53 // to be initialised with a set of Endpoints, and then start serving requests. Using them outside of this use
54 // case is unsupported.
57 Handle404 http.Handler
58 Handle405 http.Handler
61 func (router *Router) serve404(w http.ResponseWriter, r *http.Request, t time.Time) {
62 h := default404Handler
63 if router.Handle404 != nil {
66 r.Header.Set("Trout-Timer", strconv.FormatInt(time.Now().Sub(t).Nanoseconds(), 10))
70 func (router *Router) serve405(w http.ResponseWriter, r *http.Request, t time.Time) {
71 h := default405Handler
72 if router.Handle405 != nil {
75 r.Header.Set("Trout-Timer", strconv.FormatInt(time.Now().Sub(t).Nanoseconds(), 10))
79 func (router Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
82 router.serve404(w, r, start)
85 pieces := strings.Split(strings.ToLower(strings.Trim(r.URL.Path, "/")), "/")
87 defer router.t.RUnlock()
88 branches := make([]*branch, len(pieces))
89 path, ok := router.t.match(pieces)
91 router.serve404(w, r, start)
95 for i, pos := range path {
99 v := vars(branches, pieces)
100 for key, vals := range v {
101 r.Header[http.CanonicalHeaderKey("Trout-Param-"+key)] = vals
103 ms := make([]string, len(b.methods))
105 for m := range b.methods {
109 r.Header[http.CanonicalHeaderKey("Trout-Methods")] = ms
110 h := b.methods[r.Method]
112 router.serve405(w, r, start)
115 r.Header.Set("Trout-Timer", strconv.FormatInt(time.Now().Sub(start).Nanoseconds(), 10))
119 // Endpoint defines a new Endpoint on the Router. The Endpoint should be a URL template, using curly braces
120 // to denote parameters that should be filled at runtime. For example, `{id}` denotes a parameter named `id`
121 // that should be filled with whatever the request has in that space.
123 // Parameters are always `/`-separated strings. There is no support for regular expressions or other limitations
124 // on what may be in those strings. A parameter is simply defined as "whatever is between these two / characters".
125 func (router *Router) Endpoint(e string) *Endpoint {
126 e = strings.Trim(e, "/")
127 e = strings.ToLower(e)
128 pieces := strings.Split(e, "/")
133 defer router.t.Unlock()
134 if router.t.branch == nil {
135 router.t.branch = &branch{
137 children: []*branch{},
140 methods: map[string]http.Handler{},
143 closest := findClosestLeaf(pieces, router.t.branch)
145 for _, pos := range closest {
148 if len(closest) == len(pieces) {
149 return (*Endpoint)(b)
151 offset := len(closest)
152 for i := offset; i < len(pieces); i++ {
155 if len(piece) > 0 && piece[0:1] == "{" && piece[len(piece)-1:] == "}" {
157 piece = piece[1 : len(piece)-1]
159 b = b.addChild(piece, isParam)
161 return (*Endpoint)(b)
164 func vars(path []*branch, pieces []string) map[string][]string {
165 v := map[string][]string{}
166 for pos, p := range path {
172 v[p.key] = []string{pieces[pos]}
175 v[p.key] = append(v[p.key], pieces[pos])
180 func findClosestLeaf(pieces []string, b *branch) []int {
185 for i := 0; i < num; i++ {
188 if len(piece) > 0 && piece[0:1] == "{" && piece[len(piece)-1:] == "}" {
190 piece = piece[1 : len(piece)-1]
192 offset = pickNextRoute(b, offset, piece, isParam)
195 // exhausted our options, bail
198 // no match, maybe save this and backup
199 if len(path) > len(longest) {
200 longest = append([]int{}, path...) // copy them over so they don't get modified
202 path, offset = backup(path)
207 path = append(path, offset)
208 b = b.children[offset]
212 if len(longest) < len(path) {
213 longest = append([]int{}, path...)
218 func pickNextRoute(b *branch, offset int, input string, variable bool) int {
219 count := len(b.children)
220 for i := offset; i < count; i++ {
221 if b.children[i].key == input && b.children[i].isParam == variable {
228 // Endpoint defines a single URL template that requests can be matched against. It uses
229 // URL parameters to accept variables in the URL structure and make them available to
230 // the Handlers associated with the Endpoint.
233 // Handler associates the passed http.Handler with the Endpoint. This http.Handler will be
234 // used for all requests, regardless of the HTTP method they are using, unless overridden by
235 // the Methods method. Endpoints without a http.Handler associated with them will not be
236 // considered matches for requests, unless the request was made using an HTTP method that the
237 // Endpoint has an http.Handler mapped to.
238 func (e *Endpoint) Handler(h http.Handler) {
239 (*branch)(e).setHandler("", h)
242 // Methods simple returns a Methods object that will enable the mapping of the passed HTTP
243 // request methods to a Methods object. On its own, this function does not modify anything. It
244 // should, instead, be used as a friendly shorthand to get to the Methods.Handler method.
245 func (e *Endpoint) Methods(m ...string) Methods {
252 // Methods defines a pairing of an Endpoint to the HTTP request methods that should be mapped to
253 // specific http.Handlers. Its sole purpose is to enable the Methods.Handler method.
254 type Methods struct {
259 // Handler maps a Methods object to a specific http.Handler. This overrides the http.Handler
260 // associated with the Endpoint to only handle specific HTTP method(s).
261 func (m Methods) Handler(h http.Handler) {
263 for _, method := range m.m {
264 b.setHandler(method, h)