ducky/devices

Paddy 2016-01-02 Parent:a700ede02f91

20:ed1b5ba69551 Go to Latest

ducky/devices/devices.go

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.

History
1 package devices
3 import (
4 "errors"
5 "fmt"
6 "log"
7 "time"
9 "golang.org/x/net/context"
11 "code.secondbit.org/uuid.hg"
12 )
14 var (
15 // ErrDeviceNotFound is returned when the specified device couldn't be found.
16 ErrDeviceNotFound = errors.New("device not found")
17 )
19 const (
20 // MinDeviceNameLength is the minimum length a Device name can be.
21 MinDeviceNameLength = 1
22 // MaxDeviceNameLength is the maximum length a Device name can be.
23 MaxDeviceNameLength = 64
24 )
26 // Device represents a specific device that updates can be pushed to.
27 type Device struct {
28 ID uuid.ID
29 Name string
30 Owner uuid.ID
31 Type DeviceType
32 Created time.Time
33 LastSeen time.Time
34 PushToken string
35 }
37 // ApplyChange returns a Device that is a copy of the passed Device,
38 // but with the passed DeviceChange applied.
39 func ApplyChange(d Device, change DeviceChange) Device {
40 result := d
41 if change.Name != nil {
42 result.Name = *change.Name
43 }
44 if change.Owner != nil {
45 result.Owner = *change.Owner
46 } else {
47 // We don't want to accidentally leave a slice that
48 // is owned by both behind.
49 result.Owner = d.Owner.Copy()
50 }
51 if change.Type != nil {
52 result.Type = *change.Type
53 }
54 if change.LastSeen != nil {
55 result.LastSeen = *change.LastSeen
56 }
57 if change.PushToken != nil {
58 result.PushToken = *change.PushToken
59 }
60 return result
61 }
63 // DeviceChange represents a set of changes to a Device that will be used
64 // to update a Device.
65 type DeviceChange struct {
66 DeviceID uuid.ID
67 Name *string
68 Owner *uuid.ID
69 Type *DeviceType
70 LastSeen *time.Time
71 PushToken *string
72 }
74 // Storer is an interface to control how data is stored in and retrieved from
75 // the datastore.
76 type Storer interface {
77 GetDevices(ids []uuid.ID, c context.Context) (map[string]Device, error)
78 UpdateDevice(change DeviceChange, c context.Context) error
79 DeleteDevices(ids []uuid.ID, c context.Context) error
80 CreateDevices(devices []Device, c context.Context) error
81 ListDevicesByOwner(user uuid.ID, c context.Context) ([]Device, error)
82 }
84 // ErrDeviceAlreadyExists is returned when a Device is added to a Storer, but a
85 // Device with the same ID already exists in the Storer. The ID underlying
86 // ErrDeviceAlreadyExists will hold the ID that already exists.
87 type ErrDeviceAlreadyExists uuid.ID
89 func (e ErrDeviceAlreadyExists) Error() string {
90 return fmt.Sprintf("device with ID %s already exists in datastore", uuid.ID(e).String())
91 }
93 // GetMany returns as many of the Devices specified by the passed IDs as possible.
94 // They are returned as a map, with the key being the string version of the ID.
95 // No error will be returned if a Device can't be found.
96 func GetMany(ids []uuid.ID, c context.Context) (map[string]Device, error) {
97 results := map[string]Device{}
98 storer, err := GetStorer(c)
99 if err != nil {
100 log.Printf("Error retrieving Storer: %+v\n", err)
101 return results, err
102 }
103 results, err = storer.GetDevices(ids, c)
104 if err != nil {
105 log.Printf("Error retrieving Devices from %T: %+v\n", storer, err)
106 return results, err
107 }
108 return results, nil
109 }
111 // Get returns the Device specified by the passed ID. If the Device can't be found,
112 // an ErrDeviceNotFound error is returned.
113 func Get(id uuid.ID, c context.Context) (Device, error) {
114 results, err := GetMany([]uuid.ID{id}, c)
115 if err != nil {
116 return Device{}, err
117 }
118 result, ok := results[id.String()]
119 if !ok {
120 return Device{}, ErrDeviceNotFound
121 }
122 return result, nil
123 }
125 // Update applies the DeviceChange to the passed Device, and returns the result. If
126 // the Device can't be found, an ErrDeviceNotFound error was returned.
127 func Update(device Device, change DeviceChange, c context.Context) (Device, error) {
128 storer, err := GetStorer(c)
129 if err != nil {
130 log.Printf("Error retrieving Storer: %+v\n", err)
131 return Device{}, err
132 }
133 change.DeviceID = device.ID
134 err = storer.UpdateDevice(change, c)
135 if err != nil {
136 return Device{}, err
137 }
138 return ApplyChange(device, change), nil
139 }
141 // DeleteMany removes the passed IDs from the datastore. No error is returned if the
142 // ID doesn't correspond to a Device in the datastore.
143 func DeleteMany(ids []uuid.ID, c context.Context) error {
144 storer, err := GetStorer(c)
145 if err != nil {
146 log.Printf("Error retrieving Storer: %+v\n", err)
147 return err
148 }
149 return storer.DeleteDevices(ids, c)
150 }
152 // Delete removes the passed ID from the datastore. No error is returned if the ID doesn't
153 // correspond to a Device in the datastore.
154 func Delete(id uuid.ID, c context.Context) error {
155 return DeleteMany([]uuid.ID{id}, c)
156 }
158 // CreateMany stores the passed Devices in the datastore, assigning default values if
159 // necessary. The Devices that were ultimately stored (including any default values, if
160 // applicable) are returned.
161 func CreateMany(devices []Device, c context.Context) ([]Device, error) {
162 storer, err := GetStorer(c)
163 if err != nil {
164 log.Printf("Error retrieving Storer: %+v\n", err)
165 return []Device{}, err
166 }
167 modified := make([]Device, 0, len(devices))
168 for _, device := range devices {
169 if device.ID.IsZero() {
170 device.ID = uuid.NewID()
171 }
172 if device.Created.IsZero() {
173 device.Created = time.Now()
174 }
175 if device.LastSeen.IsZero() {
176 device.LastSeen = time.Now()
177 }
178 modified = append(modified, device)
179 }
180 err = storer.CreateDevices(modified, c)
181 if err != nil {
182 return []Device{}, err
183 }
184 return modified, nil
185 }
187 // Create stores the passed Device in the datastore, assigning default values if
188 // necessary. The Devices that were ultimately stored (including any default values, if
189 // applicable) are returned.
190 func Create(device Device, c context.Context) (Device, error) {
191 devices, err := CreateMany([]Device{device}, c)
192 if err != nil {
193 return Device{}, err
194 }
195 // There should never be a case where we don't return a result.
196 // Ideally, we'd return an error here instead of letting the panic
197 // happen, but seeing as I can't come up with a reason the error would
198 // occur, I'm having trouble coming up with a reasonable error to return.
199 return devices[0], nil
200 }
202 // ListByOwner returns a slice of all the Devices with an Owner property that
203 // matches the passed ID. There's no guarantee on the order the Devices will be
204 // returned in.
205 func ListByOwner(user uuid.ID, c context.Context) ([]Device, error) {
206 // BUG(paddy): Eventually, we'll need to support paging for devices. But right now, I don't foresee any user creating enough of them to make pagination worthwhile.
207 storer, err := GetStorer(c)
208 if err != nil {
209 log.Printf("Error retrieving Storer: %+v\n", err)
210 return []Device{}, err
211 }
212 devices, err := storer.ListDevicesByOwner(user, c)
213 return devices, err
214 }
216 // ToMap transforms the passed slice into a map by using the String method of
217 // each Device's ID as the key, and the Device as the value.
218 func ToMap(devices []Device) map[string]Device {
219 results := make(map[string]Device, len(devices))
220 for _, device := range devices {
221 results[device.ID.String()] = device
222 }
223 return results
224 }
226 // ToSlice transforms the passed map of Devices into a slice of Devices.
227 func ToSlice(devices map[string]Device) []Device {
228 results := make([]Device, 0, len(devices))
229 for _, device := range devices {
230 results = append(results, device)
231 }
232 return results
233 }