ducky/devices

Paddy 2015-11-27 Parent:f5a9d5f8f28d Child:03c49b4d3d9f

10:74dbc04879a7 Go to Latest

ducky/devices/devices.go

Implement and test updating devices, and reuse contexts. Update all our tests to use the same context.Context instance within each test case, so static analysis about how we're passing contexts around dosn't get tripped up. Also, write a test that will check to make sure that our Storer implementations all actually update the Device correctly. We create every possible permutation of a DeviceChange, just to make sure they all work.

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