ducky/devices
ducky/devices/devices.go
Start testing our Storers. Set up a file to start testing our Storer implementations, and write a sample test for the happy path of GetDevices--e.g., we expect no errors. This also required us to setup a helper to compare two Device instances and see if they're equal or not, and if not, how they differ. In the future, we'll keep adding test methods to test more logical paths for the interface contract, and to test more of the methods for the interface.
| paddy@0 | 1 package devices |
| paddy@0 | 2 |
| paddy@0 | 3 import ( |
| paddy@0 | 4 "errors" |
| paddy@0 | 5 "log" |
| paddy@0 | 6 "time" |
| paddy@0 | 7 |
| paddy@0 | 8 "golang.org/x/net/context" |
| paddy@0 | 9 |
| paddy@0 | 10 "code.secondbit.org/uuid.hg" |
| paddy@0 | 11 ) |
| paddy@0 | 12 |
| paddy@0 | 13 var ( |
| paddy@0 | 14 // ErrDeviceNotFound is returned when the specified device couldn't be found. |
| paddy@0 | 15 ErrDeviceNotFound = errors.New("device not found") |
| paddy@0 | 16 ) |
| paddy@0 | 17 |
| paddy@0 | 18 // Device represents a specific device that updates can be pushed to. |
| paddy@0 | 19 type Device struct { |
| paddy@0 | 20 ID uuid.ID |
| paddy@0 | 21 Name string |
| paddy@0 | 22 Owner uuid.ID |
| paddy@0 | 23 Type DeviceType |
| paddy@0 | 24 Created time.Time |
| paddy@0 | 25 LastSeen time.Time |
| paddy@0 | 26 PushToken string |
| paddy@0 | 27 } |
| paddy@0 | 28 |
| paddy@0 | 29 // ApplyChange returns a Device that is a copy of the passed Device, |
| paddy@0 | 30 // but with the passed DeviceChange applied. |
| paddy@0 | 31 func ApplyChange(d Device, change DeviceChange) Device { |
| paddy@0 | 32 result := d |
| paddy@0 | 33 if change.Name != nil { |
| paddy@0 | 34 result.Name = *change.Name |
| paddy@0 | 35 } |
| paddy@0 | 36 if change.Owner != nil { |
| paddy@0 | 37 result.Owner = *change.Owner |
| paddy@0 | 38 } else { |
| paddy@0 | 39 // We don't want to accidentally leave a slice that |
| paddy@0 | 40 // is owned by both behind. |
| paddy@0 | 41 result.Owner = d.Owner.Copy() |
| paddy@0 | 42 } |
| paddy@0 | 43 if change.Type != nil { |
| paddy@0 | 44 result.Type = *change.Type |
| paddy@0 | 45 } |
| paddy@0 | 46 if change.Created != nil { |
| paddy@0 | 47 result.Created = *change.Created |
| paddy@0 | 48 } |
| paddy@0 | 49 if change.LastSeen != nil { |
| paddy@0 | 50 result.LastSeen = *change.LastSeen |
| paddy@0 | 51 } |
| paddy@0 | 52 if change.PushToken != nil { |
| paddy@0 | 53 result.PushToken = *change.PushToken |
| paddy@0 | 54 } |
| paddy@0 | 55 return result |
| paddy@0 | 56 } |
| paddy@0 | 57 |
| paddy@0 | 58 // DeviceChange represents a set of changes to a Device that will be used |
| paddy@0 | 59 // to update a Device. |
| paddy@0 | 60 type DeviceChange struct { |
| paddy@0 | 61 DeviceID uuid.ID |
| paddy@0 | 62 Name *string |
| paddy@0 | 63 Owner *uuid.ID |
| paddy@0 | 64 Type *DeviceType |
| paddy@0 | 65 Created *time.Time |
| paddy@0 | 66 LastSeen *time.Time |
| paddy@0 | 67 PushToken *string |
| paddy@0 | 68 } |
| paddy@0 | 69 |
| paddy@0 | 70 // Storer is an interface to control how data is stored in and retrieved from |
| paddy@0 | 71 // the datastore. |
| paddy@0 | 72 type Storer interface { |
| paddy@0 | 73 GetDevices(ids []uuid.ID, c context.Context) (map[string]Device, error) |
| paddy@0 | 74 UpdateDevice(change DeviceChange, c context.Context) error |
| paddy@0 | 75 DeleteDevices(ids []uuid.ID, c context.Context) error |
| paddy@0 | 76 CreateDevices(devices []Device, c context.Context) error |
| paddy@0 | 77 ListDevicesByOwner(user uuid.ID, c context.Context) ([]Device, error) |
| paddy@0 | 78 |
| paddy@0 | 79 // These are used for testing only. |
| paddy@0 | 80 Factory(c context.Context) (Storer, error) |
| paddy@0 | 81 Destroy(c context.Context) error |
| paddy@0 | 82 } |
| paddy@0 | 83 |
| paddy@0 | 84 // GetMany returns as many of the Devices specified by the passed IDs as possible. |
| paddy@0 | 85 // They are returned as a map, with the key being the string version of the ID. |
| paddy@0 | 86 // No error will be returned if a Device can't be found. |
| paddy@0 | 87 func GetMany(ids []uuid.ID, c context.Context) (map[string]Device, error) { |
| paddy@0 | 88 results := map[string]Device{} |
| paddy@0 | 89 storer, err := getStorer(c) |
| paddy@0 | 90 if err != nil { |
| paddy@0 | 91 log.Printf("Error retrieving Storer: %+v\n", err) |
| paddy@0 | 92 return results, err |
| paddy@0 | 93 } |
| paddy@0 | 94 results, err = storer.GetDevices(ids, c) |
| paddy@0 | 95 if err != nil { |
| paddy@0 | 96 log.Printf("Error retrieving Devices from %T: %+v\n", storer, err) |
| paddy@0 | 97 return results, err |
| paddy@0 | 98 } |
| paddy@0 | 99 return results, nil |
| paddy@0 | 100 } |
| paddy@0 | 101 |
| paddy@0 | 102 // Get returns the Device specified by the passed ID. If the Device can't be found, |
| paddy@0 | 103 // an ErrDeviceNotFound error is returned. |
| paddy@0 | 104 func Get(id uuid.ID, c context.Context) (Device, error) { |
| paddy@0 | 105 results, err := GetMany([]uuid.ID{id}, c) |
| paddy@0 | 106 if err != nil { |
| paddy@0 | 107 return Device{}, err |
| paddy@0 | 108 } |
| paddy@0 | 109 result, ok := results[id.String()] |
| paddy@0 | 110 if !ok { |
| paddy@0 | 111 return Device{}, ErrDeviceNotFound |
| paddy@0 | 112 } |
| paddy@0 | 113 return result, nil |
| paddy@0 | 114 } |
| paddy@0 | 115 |
| paddy@0 | 116 // Update applies the DeviceChange to the passed Device, and returns the result. If |
| paddy@0 | 117 // the Device can't be found, an ErrDeviceNotFound error was returned. |
| paddy@0 | 118 func Update(device Device, change DeviceChange, c context.Context) (Device, error) { |
| paddy@0 | 119 storer, err := getStorer(c) |
| paddy@0 | 120 if err != nil { |
| paddy@0 | 121 log.Printf("Error retrieving Storer: %+v\n", err) |
| paddy@0 | 122 return Device{}, err |
| paddy@0 | 123 } |
| paddy@0 | 124 change.DeviceID = device.ID |
| paddy@0 | 125 err = storer.UpdateDevice(change, c) |
| paddy@0 | 126 if err != nil { |
| paddy@0 | 127 return Device{}, err |
| paddy@0 | 128 } |
| paddy@0 | 129 return ApplyChange(device, change), nil |
| paddy@0 | 130 } |
| paddy@0 | 131 |
| paddy@0 | 132 // DeleteMany removes the passed IDs from the datastore. No error is returned if the |
| paddy@0 | 133 // ID doesn't correspond to a Device in the datastore. |
| paddy@0 | 134 func DeleteMany(ids []uuid.ID, c context.Context) error { |
| paddy@0 | 135 storer, err := getStorer(c) |
| paddy@0 | 136 if err != nil { |
| paddy@0 | 137 log.Printf("Error retrieving Storer: %+v\n", err) |
| paddy@0 | 138 return err |
| paddy@0 | 139 } |
| paddy@0 | 140 return storer.DeleteDevices(ids, c) |
| paddy@0 | 141 } |
| paddy@0 | 142 |
| paddy@0 | 143 // Delete removes the passed ID from the datastore. No error is returned if the ID doesn't |
| paddy@0 | 144 // correspond to a Device in the datastore. |
| paddy@0 | 145 func Delete(id uuid.ID, c context.Context) error { |
| paddy@0 | 146 return DeleteMany([]uuid.ID{id}, c) |
| paddy@0 | 147 } |
| paddy@0 | 148 |
| paddy@0 | 149 // CreateMany stores the passed Devices in the datastore, assigning default values if |
| paddy@0 | 150 // necessary. The Devices that were ultimately stored (including any default values, if |
| paddy@0 | 151 // applicable) are returned. |
| paddy@0 | 152 func CreateMany(devices []Device, c context.Context) ([]Device, error) { |
| paddy@0 | 153 storer, err := getStorer(c) |
| paddy@0 | 154 if err != nil { |
| paddy@0 | 155 log.Printf("Error retrieving Storer: %+v\n", err) |
| paddy@0 | 156 return []Device{}, err |
| paddy@0 | 157 } |
| paddy@0 | 158 modified := make([]Device, 0, len(devices)) |
| paddy@0 | 159 for _, device := range devices { |
| paddy@0 | 160 if device.ID.IsZero() { |
| paddy@0 | 161 device.ID = uuid.NewID() |
| paddy@0 | 162 } |
| paddy@0 | 163 if device.Created.IsZero() { |
| paddy@0 | 164 device.Created = time.Now() |
| paddy@0 | 165 } |
| paddy@0 | 166 if device.LastSeen.IsZero() { |
| paddy@0 | 167 device.LastSeen = time.Now() |
| paddy@0 | 168 } |
| paddy@0 | 169 modified = append(modified, device) |
| paddy@0 | 170 } |
| paddy@0 | 171 err = storer.CreateDevices(devices, c) |
| paddy@0 | 172 if err != nil { |
| paddy@0 | 173 return []Device{}, err |
| paddy@0 | 174 } |
| paddy@0 | 175 return modified, nil |
| paddy@0 | 176 } |
| paddy@0 | 177 |
| paddy@0 | 178 // Create stores the passed Device in the datastore, assigning default values if |
| paddy@0 | 179 // necessary. The Devices that were ultimately stored (including any default values, if |
| paddy@0 | 180 // applicable) are returned. |
| paddy@0 | 181 func Create(device Device, c context.Context) (Device, error) { |
| paddy@0 | 182 devices, err := CreateMany([]Device{device}, c) |
| paddy@0 | 183 if err != nil { |
| paddy@0 | 184 return Device{}, err |
| paddy@0 | 185 } |
| paddy@0 | 186 // There should never be a case where we don't return a result. |
| paddy@0 | 187 // Ideally, we'd return an error here instead of letting the panic |
| paddy@0 | 188 // happen, but seeing as I can't come up with a reason the error would |
| paddy@0 | 189 // occur, I'm having trouble coming up with a reasonable error to return. |
| paddy@0 | 190 return devices[0], nil |
| paddy@0 | 191 } |
| paddy@0 | 192 |
| paddy@0 | 193 // ListByOwner returns a slice of all the Devices with an Owner property that |
| paddy@0 | 194 // matches the passed ID. There's no guarantee on the order the Devices will be |
| paddy@0 | 195 // returned in. |
| paddy@0 | 196 func ListByOwner(user uuid.ID, c context.Context) ([]Device, error) { |
| paddy@0 | 197 // 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@0 | 198 storer, err := getStorer(c) |
| paddy@0 | 199 if err != nil { |
| paddy@0 | 200 log.Printf("Error retrieving Storer: %+v\n", err) |
| paddy@0 | 201 return []Device{}, err |
| paddy@0 | 202 } |
| paddy@0 | 203 devices, err := storer.ListDevicesByOwner(user, c) |
| paddy@0 | 204 return devices, err |
| paddy@0 | 205 } |