ducky/devices
ducky/devices/devices.go
Add tests for ToSlice and ToMap. Add simple unit tests that verify that ToSlice and ToMap both work as we expect. These are probably overly-simplistic, but so are the functions we're testing. Not sure about this... feel a bit conflicted. But let's try 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 // Device represents a specific device that updates can be pushed to.
20 type Device struct {
21 ID uuid.ID
22 Name string
23 Owner uuid.ID
24 Type DeviceType
25 Created time.Time
26 LastSeen time.Time
27 PushToken string
28 }
30 // ApplyChange returns a Device that is a copy of the passed Device,
31 // but with the passed DeviceChange applied.
32 func ApplyChange(d Device, change DeviceChange) Device {
33 result := d
34 if change.Name != nil {
35 result.Name = *change.Name
36 }
37 if change.Owner != nil {
38 result.Owner = *change.Owner
39 } else {
40 // We don't want to accidentally leave a slice that
41 // is owned by both behind.
42 result.Owner = d.Owner.Copy()
43 }
44 if change.Type != nil {
45 result.Type = *change.Type
46 }
47 if change.Created != nil {
48 result.Created = *change.Created
49 }
50 if change.LastSeen != nil {
51 result.LastSeen = *change.LastSeen
52 }
53 if change.PushToken != nil {
54 result.PushToken = *change.PushToken
55 }
56 return result
57 }
59 // DeviceChange represents a set of changes to a Device that will be used
60 // to update a Device.
61 type DeviceChange struct {
62 DeviceID uuid.ID
63 Name *string
64 Owner *uuid.ID
65 Type *DeviceType
66 Created *time.Time
67 LastSeen *time.Time
68 PushToken *string
69 }
71 // Storer is an interface to control how data is stored in and retrieved from
72 // the datastore.
73 type Storer interface {
74 GetDevices(ids []uuid.ID, c context.Context) (map[string]Device, error)
75 UpdateDevice(change DeviceChange, c context.Context) error
76 DeleteDevices(ids []uuid.ID, c context.Context) error
77 CreateDevices(devices []Device, c context.Context) error
78 ListDevicesByOwner(user uuid.ID, c context.Context) ([]Device, error)
79 }
81 // ErrDeviceAlreadyExists is returned when a Device is added to a Storer, but a
82 // Device with the same ID already exists in the Storer. The ID underlying
83 // ErrDeviceAlreadyExists will hold the ID that already exists.
84 type ErrDeviceAlreadyExists uuid.ID
86 func (e ErrDeviceAlreadyExists) Error() string {
87 return fmt.Sprintf("device with ID %s already exists in datastore", uuid.ID(e).String())
88 }
90 // GetMany returns as many of the Devices specified by the passed IDs as possible.
91 // They are returned as a map, with the key being the string version of the ID.
92 // No error will be returned if a Device can't be found.
93 func GetMany(ids []uuid.ID, c context.Context) (map[string]Device, error) {
94 results := map[string]Device{}
95 storer, err := getStorer(c)
96 if err != nil {
97 log.Printf("Error retrieving Storer: %+v\n", err)
98 return results, err
99 }
100 results, err = storer.GetDevices(ids, c)
101 if err != nil {
102 log.Printf("Error retrieving Devices from %T: %+v\n", storer, err)
103 return results, err
104 }
105 return results, nil
106 }
108 // Get returns the Device specified by the passed ID. If the Device can't be found,
109 // an ErrDeviceNotFound error is returned.
110 func Get(id uuid.ID, c context.Context) (Device, error) {
111 results, err := GetMany([]uuid.ID{id}, c)
112 if err != nil {
113 return Device{}, err
114 }
115 result, ok := results[id.String()]
116 if !ok {
117 return Device{}, ErrDeviceNotFound
118 }
119 return result, nil
120 }
122 // Update applies the DeviceChange to the passed Device, and returns the result. If
123 // the Device can't be found, an ErrDeviceNotFound error was returned.
124 func Update(device Device, change DeviceChange, c context.Context) (Device, error) {
125 storer, err := getStorer(c)
126 if err != nil {
127 log.Printf("Error retrieving Storer: %+v\n", err)
128 return Device{}, err
129 }
130 change.DeviceID = device.ID
131 err = storer.UpdateDevice(change, c)
132 if err != nil {
133 return Device{}, err
134 }
135 return ApplyChange(device, change), nil
136 }
138 // DeleteMany removes the passed IDs from the datastore. No error is returned if the
139 // ID doesn't correspond to a Device in the datastore.
140 func DeleteMany(ids []uuid.ID, c context.Context) error {
141 storer, err := getStorer(c)
142 if err != nil {
143 log.Printf("Error retrieving Storer: %+v\n", err)
144 return err
145 }
146 return storer.DeleteDevices(ids, c)
147 }
149 // Delete removes the passed ID from the datastore. No error is returned if the ID doesn't
150 // correspond to a Device in the datastore.
151 func Delete(id uuid.ID, c context.Context) error {
152 return DeleteMany([]uuid.ID{id}, c)
153 }
155 // CreateMany stores the passed Devices in the datastore, assigning default values if
156 // necessary. The Devices that were ultimately stored (including any default values, if
157 // applicable) are returned.
158 func CreateMany(devices []Device, c context.Context) ([]Device, error) {
159 storer, err := getStorer(c)
160 if err != nil {
161 log.Printf("Error retrieving Storer: %+v\n", err)
162 return []Device{}, err
163 }
164 modified := make([]Device, 0, len(devices))
165 for _, device := range devices {
166 if device.ID.IsZero() {
167 device.ID = uuid.NewID()
168 }
169 if device.Created.IsZero() {
170 device.Created = time.Now()
171 }
172 if device.LastSeen.IsZero() {
173 device.LastSeen = time.Now()
174 }
175 modified = append(modified, device)
176 }
177 err = storer.CreateDevices(devices, c)
178 if err != nil {
179 return []Device{}, err
180 }
181 return modified, nil
182 }
184 // Create stores the passed Device in the datastore, assigning default values if
185 // necessary. The Devices that were ultimately stored (including any default values, if
186 // applicable) are returned.
187 func Create(device Device, c context.Context) (Device, error) {
188 devices, err := CreateMany([]Device{device}, c)
189 if err != nil {
190 return Device{}, err
191 }
192 // There should never be a case where we don't return a result.
193 // Ideally, we'd return an error here instead of letting the panic
194 // happen, but seeing as I can't come up with a reason the error would
195 // occur, I'm having trouble coming up with a reasonable error to return.
196 return devices[0], nil
197 }
199 // ListByOwner returns a slice of all the Devices with an Owner property that
200 // matches the passed ID. There's no guarantee on the order the Devices will be
201 // returned in.
202 func ListByOwner(user uuid.ID, c context.Context) ([]Device, error) {
203 // 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.
204 storer, err := getStorer(c)
205 if err != nil {
206 log.Printf("Error retrieving Storer: %+v\n", err)
207 return []Device{}, err
208 }
209 devices, err := storer.ListDevicesByOwner(user, c)
210 return devices, err
211 }
213 // ToMap transforms the passed slice into a map by using the String method of
214 // each Device's ID as the key, and the Device as the value.
215 func ToMap(devices []Device) map[string]Device {
216 results := make(map[string]Device, len(devices))
217 for _, device := range devices {
218 results[device.ID.String()] = device
219 }
220 return results
221 }
223 // ToSlice transforms the passed map of Devices into a slice of Devices.
224 func ToSlice(devices map[string]Device) []Device {
225 results := make([]Device, 0, len(devices))
226 for _, device := range devices {
227 results = append(results, device)
228 }
229 return results
230 }