ducky/devices
2015-12-19
Parent:a700ede02f91
ducky/devices/devices.go
Fix JSON pointers in errors when creating devices. Our JSON pointers used to point at the root of the document, but the properties were actually in objects in an array keyed off of "devices", so we had to update the field property of our errors to match. While we were in there, we fixed the "insufficient" error for a Device's name to be "missing" when the name is an empty string. So far, the only way for a Device's name for it to be insufficient is _for it to be the empty string_, but if in the future we raise the minimum length of the Device name, there will be a distinction and I'd like the code to recognise it.
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 }