ducky/devices
ducky/devices/devices.go
Validate device creation. Update our uuid package to the latest, which is now based on the GitHub fork instead of the Google Code. Also, update our api package to its latest version, which now needs the pqarrays package as a dependency. We fleshed out the validateDeviceCreation. We now pass in the scopes we have (for broad access control) and the user ID (for fine-grained access control). This helper returns the first error it encounters, though it should probably return a slice so we can return multiple errors all at once. Before we even decode the request to create a Device, let's check if the user is even logged in. If we can't ascertain that or they're not, there's no point in even consuming the memory necessary to read the request, because we know we're not going to use it anyways. Finally actually validate the devices we're creating, and return an appropriate error for each error we can get. Also, the api.CheckScopes helper function now takes the scopes passed in as a string slice, and we have an api.GetScopes helper function to retrieve the scopes associated with the request. Let's not keep parsing that. We need two new scopes to control access for device creation; ScopeImport lets users import devices in and is pretty much admin access. ScopeCreateOtherUserDevices allows a user to create Devices that are owned by another user.
| paddy@0 | 1 package devices |
| paddy@0 | 2 |
| paddy@0 | 3 import ( |
| paddy@0 | 4 "errors" |
| paddy@5 | 5 "fmt" |
| paddy@0 | 6 "log" |
| paddy@0 | 7 "time" |
| paddy@0 | 8 |
| paddy@0 | 9 "golang.org/x/net/context" |
| paddy@0 | 10 |
| paddy@0 | 11 "code.secondbit.org/uuid.hg" |
| paddy@0 | 12 ) |
| paddy@0 | 13 |
| paddy@0 | 14 var ( |
| paddy@0 | 15 // ErrDeviceNotFound is returned when the specified device couldn't be found. |
| paddy@0 | 16 ErrDeviceNotFound = errors.New("device not found") |
| paddy@0 | 17 ) |
| paddy@0 | 18 |
| paddy@16 | 19 const ( |
| paddy@16 | 20 // MinDeviceNameLength is the minimum length a Device name can be. |
| paddy@16 | 21 MinDeviceNameLength = 1 |
| paddy@16 | 22 // MaxDeviceNameLength is the maximum length a Device name can be. |
| paddy@16 | 23 MaxDeviceNameLength = 64 |
| paddy@16 | 24 ) |
| paddy@16 | 25 |
| paddy@0 | 26 // Device represents a specific device that updates can be pushed to. |
| paddy@0 | 27 type Device struct { |
| paddy@0 | 28 ID uuid.ID |
| paddy@0 | 29 Name string |
| paddy@0 | 30 Owner uuid.ID |
| paddy@0 | 31 Type DeviceType |
| paddy@0 | 32 Created time.Time |
| paddy@0 | 33 LastSeen time.Time |
| paddy@0 | 34 PushToken string |
| paddy@0 | 35 } |
| paddy@0 | 36 |
| paddy@0 | 37 // ApplyChange returns a Device that is a copy of the passed Device, |
| paddy@0 | 38 // but with the passed DeviceChange applied. |
| paddy@0 | 39 func ApplyChange(d Device, change DeviceChange) Device { |
| paddy@0 | 40 result := d |
| paddy@0 | 41 if change.Name != nil { |
| paddy@0 | 42 result.Name = *change.Name |
| paddy@0 | 43 } |
| paddy@0 | 44 if change.Owner != nil { |
| paddy@0 | 45 result.Owner = *change.Owner |
| paddy@0 | 46 } else { |
| paddy@0 | 47 // We don't want to accidentally leave a slice that |
| paddy@0 | 48 // is owned by both behind. |
| paddy@0 | 49 result.Owner = d.Owner.Copy() |
| paddy@0 | 50 } |
| paddy@0 | 51 if change.Type != nil { |
| paddy@0 | 52 result.Type = *change.Type |
| paddy@0 | 53 } |
| paddy@0 | 54 if change.LastSeen != nil { |
| paddy@0 | 55 result.LastSeen = *change.LastSeen |
| paddy@0 | 56 } |
| paddy@0 | 57 if change.PushToken != nil { |
| paddy@0 | 58 result.PushToken = *change.PushToken |
| paddy@0 | 59 } |
| paddy@0 | 60 return result |
| paddy@0 | 61 } |
| paddy@0 | 62 |
| paddy@0 | 63 // DeviceChange represents a set of changes to a Device that will be used |
| paddy@0 | 64 // to update a Device. |
| paddy@0 | 65 type DeviceChange struct { |
| paddy@0 | 66 DeviceID uuid.ID |
| paddy@0 | 67 Name *string |
| paddy@0 | 68 Owner *uuid.ID |
| paddy@0 | 69 Type *DeviceType |
| paddy@0 | 70 LastSeen *time.Time |
| paddy@0 | 71 PushToken *string |
| paddy@0 | 72 } |
| paddy@0 | 73 |
| paddy@0 | 74 // Storer is an interface to control how data is stored in and retrieved from |
| paddy@0 | 75 // the datastore. |
| paddy@0 | 76 type Storer interface { |
| paddy@0 | 77 GetDevices(ids []uuid.ID, c context.Context) (map[string]Device, error) |
| paddy@0 | 78 UpdateDevice(change DeviceChange, c context.Context) error |
| paddy@0 | 79 DeleteDevices(ids []uuid.ID, c context.Context) error |
| paddy@0 | 80 CreateDevices(devices []Device, c context.Context) error |
| paddy@0 | 81 ListDevicesByOwner(user uuid.ID, c context.Context) ([]Device, error) |
| paddy@0 | 82 } |
| paddy@0 | 83 |
| paddy@12 | 84 // ErrDeviceAlreadyExists is returned when a Device is added to a Storer, but a |
| paddy@12 | 85 // Device with the same ID already exists in the Storer. The ID underlying |
| paddy@12 | 86 // ErrDeviceAlreadyExists will hold the ID that already exists. |
| paddy@5 | 87 type ErrDeviceAlreadyExists uuid.ID |
| paddy@5 | 88 |
| paddy@5 | 89 func (e ErrDeviceAlreadyExists) Error() string { |
| paddy@5 | 90 return fmt.Sprintf("device with ID %s already exists in datastore", uuid.ID(e).String()) |
| paddy@5 | 91 } |
| paddy@5 | 92 |
| paddy@0 | 93 // GetMany returns as many of the Devices specified by the passed IDs as possible. |
| paddy@0 | 94 // They are returned as a map, with the key being the string version of the ID. |
| paddy@0 | 95 // No error will be returned if a Device can't be found. |
| paddy@0 | 96 func GetMany(ids []uuid.ID, c context.Context) (map[string]Device, error) { |
| paddy@0 | 97 results := map[string]Device{} |
| paddy@15 | 98 storer, err := GetStorer(c) |
| paddy@0 | 99 if err != nil { |
| paddy@0 | 100 log.Printf("Error retrieving Storer: %+v\n", err) |
| paddy@0 | 101 return results, err |
| paddy@0 | 102 } |
| paddy@0 | 103 results, err = storer.GetDevices(ids, c) |
| paddy@0 | 104 if err != nil { |
| paddy@0 | 105 log.Printf("Error retrieving Devices from %T: %+v\n", storer, err) |
| paddy@0 | 106 return results, err |
| paddy@0 | 107 } |
| paddy@0 | 108 return results, nil |
| paddy@0 | 109 } |
| paddy@0 | 110 |
| paddy@0 | 111 // Get returns the Device specified by the passed ID. If the Device can't be found, |
| paddy@0 | 112 // an ErrDeviceNotFound error is returned. |
| paddy@0 | 113 func Get(id uuid.ID, c context.Context) (Device, error) { |
| paddy@0 | 114 results, err := GetMany([]uuid.ID{id}, c) |
| paddy@0 | 115 if err != nil { |
| paddy@0 | 116 return Device{}, err |
| paddy@0 | 117 } |
| paddy@0 | 118 result, ok := results[id.String()] |
| paddy@0 | 119 if !ok { |
| paddy@0 | 120 return Device{}, ErrDeviceNotFound |
| paddy@0 | 121 } |
| paddy@0 | 122 return result, nil |
| paddy@0 | 123 } |
| paddy@0 | 124 |
| paddy@0 | 125 // Update applies the DeviceChange to the passed Device, and returns the result. If |
| paddy@0 | 126 // the Device can't be found, an ErrDeviceNotFound error was returned. |
| paddy@0 | 127 func Update(device Device, change DeviceChange, c context.Context) (Device, error) { |
| paddy@15 | 128 storer, err := GetStorer(c) |
| paddy@0 | 129 if err != nil { |
| paddy@0 | 130 log.Printf("Error retrieving Storer: %+v\n", err) |
| paddy@0 | 131 return Device{}, err |
| paddy@0 | 132 } |
| paddy@0 | 133 change.DeviceID = device.ID |
| paddy@0 | 134 err = storer.UpdateDevice(change, c) |
| paddy@0 | 135 if err != nil { |
| paddy@0 | 136 return Device{}, err |
| paddy@0 | 137 } |
| paddy@0 | 138 return ApplyChange(device, change), nil |
| paddy@0 | 139 } |
| paddy@0 | 140 |
| paddy@0 | 141 // DeleteMany removes the passed IDs from the datastore. No error is returned if the |
| paddy@0 | 142 // ID doesn't correspond to a Device in the datastore. |
| paddy@0 | 143 func DeleteMany(ids []uuid.ID, c context.Context) error { |
| paddy@15 | 144 storer, err := GetStorer(c) |
| paddy@0 | 145 if err != nil { |
| paddy@0 | 146 log.Printf("Error retrieving Storer: %+v\n", err) |
| paddy@0 | 147 return err |
| paddy@0 | 148 } |
| paddy@0 | 149 return storer.DeleteDevices(ids, c) |
| paddy@0 | 150 } |
| paddy@0 | 151 |
| paddy@0 | 152 // Delete removes the passed ID from the datastore. No error is returned if the ID doesn't |
| paddy@0 | 153 // correspond to a Device in the datastore. |
| paddy@0 | 154 func Delete(id uuid.ID, c context.Context) error { |
| paddy@0 | 155 return DeleteMany([]uuid.ID{id}, c) |
| paddy@0 | 156 } |
| paddy@0 | 157 |
| paddy@0 | 158 // CreateMany stores the passed Devices in the datastore, assigning default values if |
| paddy@0 | 159 // necessary. The Devices that were ultimately stored (including any default values, if |
| paddy@0 | 160 // applicable) are returned. |
| paddy@0 | 161 func CreateMany(devices []Device, c context.Context) ([]Device, error) { |
| paddy@15 | 162 storer, err := GetStorer(c) |
| paddy@0 | 163 if err != nil { |
| paddy@0 | 164 log.Printf("Error retrieving Storer: %+v\n", err) |
| paddy@0 | 165 return []Device{}, err |
| paddy@0 | 166 } |
| paddy@0 | 167 modified := make([]Device, 0, len(devices)) |
| paddy@0 | 168 for _, device := range devices { |
| paddy@0 | 169 if device.ID.IsZero() { |
| paddy@0 | 170 device.ID = uuid.NewID() |
| paddy@0 | 171 } |
| paddy@0 | 172 if device.Created.IsZero() { |
| paddy@0 | 173 device.Created = time.Now() |
| paddy@0 | 174 } |
| paddy@0 | 175 if device.LastSeen.IsZero() { |
| paddy@0 | 176 device.LastSeen = time.Now() |
| paddy@0 | 177 } |
| paddy@0 | 178 modified = append(modified, device) |
| paddy@0 | 179 } |
| paddy@15 | 180 err = storer.CreateDevices(modified, c) |
| paddy@0 | 181 if err != nil { |
| paddy@0 | 182 return []Device{}, err |
| paddy@0 | 183 } |
| paddy@0 | 184 return modified, nil |
| paddy@0 | 185 } |
| paddy@0 | 186 |
| paddy@0 | 187 // Create stores the passed Device in the datastore, assigning default values if |
| paddy@0 | 188 // necessary. The Devices that were ultimately stored (including any default values, if |
| paddy@0 | 189 // applicable) are returned. |
| paddy@0 | 190 func Create(device Device, c context.Context) (Device, error) { |
| paddy@0 | 191 devices, err := CreateMany([]Device{device}, c) |
| paddy@0 | 192 if err != nil { |
| paddy@0 | 193 return Device{}, err |
| paddy@0 | 194 } |
| paddy@0 | 195 // There should never be a case where we don't return a result. |
| paddy@0 | 196 // Ideally, we'd return an error here instead of letting the panic |
| paddy@0 | 197 // happen, but seeing as I can't come up with a reason the error would |
| paddy@0 | 198 // occur, I'm having trouble coming up with a reasonable error to return. |
| paddy@0 | 199 return devices[0], nil |
| paddy@0 | 200 } |
| paddy@0 | 201 |
| paddy@0 | 202 // ListByOwner returns a slice of all the Devices with an Owner property that |
| paddy@0 | 203 // matches the passed ID. There's no guarantee on the order the Devices will be |
| paddy@0 | 204 // returned in. |
| paddy@0 | 205 func ListByOwner(user uuid.ID, c context.Context) ([]Device, error) { |
| paddy@0 | 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. |
| paddy@15 | 207 storer, err := GetStorer(c) |
| paddy@0 | 208 if err != nil { |
| paddy@0 | 209 log.Printf("Error retrieving Storer: %+v\n", err) |
| paddy@0 | 210 return []Device{}, err |
| paddy@0 | 211 } |
| paddy@0 | 212 devices, err := storer.ListDevicesByOwner(user, c) |
| paddy@0 | 213 return devices, err |
| paddy@0 | 214 } |
| paddy@7 | 215 |
| paddy@12 | 216 // ToMap transforms the passed slice into a map by using the String method of |
| paddy@12 | 217 // each Device's ID as the key, and the Device as the value. |
| paddy@7 | 218 func ToMap(devices []Device) map[string]Device { |
| paddy@7 | 219 results := make(map[string]Device, len(devices)) |
| paddy@7 | 220 for _, device := range devices { |
| paddy@7 | 221 results[device.ID.String()] = device |
| paddy@7 | 222 } |
| paddy@7 | 223 return results |
| paddy@7 | 224 } |
| paddy@7 | 225 |
| paddy@12 | 226 // ToSlice transforms the passed map of Devices into a slice of Devices. |
| paddy@7 | 227 func ToSlice(devices map[string]Device) []Device { |
| paddy@7 | 228 results := make([]Device, 0, len(devices)) |
| paddy@7 | 229 for _, device := range devices { |
| paddy@7 | 230 results = append(results, device) |
| paddy@7 | 231 } |
| paddy@7 | 232 return results |
| paddy@7 | 233 } |