Skip to content

Commit

Permalink
Merge pull request #140 from calvinmclean/feature/generic-storage
Browse files Browse the repository at this point in the history
Use generics in storage.Client
  • Loading branch information
calvinmclean committed Aug 21, 2023
2 parents 7dd7a33 + 0f11861 commit 1f015fe
Show file tree
Hide file tree
Showing 9 changed files with 196 additions and 176 deletions.
6 changes: 6 additions & 0 deletions garden-app/pkg/end_date.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package pkg

// EndDateable is a simple interface that requires a method to determine if something is end-dated
type EndDateable interface {
EndDated() bool
}
66 changes: 8 additions & 58 deletions garden-app/pkg/storage/gardens.go
Original file line number Diff line number Diff line change
@@ -1,82 +1,32 @@
package storage

import (
"errors"
"fmt"
"strings"

"github.com/calvinmclean/automated-garden/garden-app/pkg"
"github.com/madflojo/hord"
"github.com/rs/xid"
)

const gardenPrefix = "Garden_"

func gardenKey(id xid.ID) string {
return gardenPrefix + id.String()
}

// GetGarden ...
func (c *Client) GetGarden(id xid.ID) (*pkg.Garden, error) {
return c.getGarden(gardenPrefix + id.String())
return getOne[pkg.Garden](c, gardenKey(id))
}

// GetGardens ...
func (c *Client) GetGardens(getEndDated bool) ([]*pkg.Garden, error) {
keys, err := c.db.Keys()
if err != nil {
return nil, fmt.Errorf("error getting keys: %w", err)
}

results := []*pkg.Garden{}
for _, key := range keys {
if !strings.HasPrefix(key, gardenPrefix) {
continue
}

result, err := c.getGarden(key)
if err != nil {
return nil, fmt.Errorf("error getting Garden: %w", err)
}

if getEndDated || !result.EndDated() {
results = append(results, result)
}
}

return results, nil
return getMultiple[*pkg.Garden](c, getEndDated, gardenPrefix)
}

// SaveGarden ...
func (c *Client) SaveGarden(g *pkg.Garden) error {
asBytes, err := c.marshal(g)
if err != nil {
return fmt.Errorf("error marshalling Garden: %w", err)
}

err = c.db.Set(gardenPrefix+g.ID.String(), asBytes)
if err != nil {
return fmt.Errorf("error writing Garden to database: %w", err)
}

return nil
return save[*pkg.Garden](c, g, gardenKey(g.ID))
}

// DeleteGarden ...
func (c *Client) DeleteGarden(id xid.ID) error {
return c.db.Delete(gardenPrefix + id.String())
}

func (c *Client) getGarden(key string) (*pkg.Garden, error) {
dataBytes, err := c.db.Get(key)
if err != nil {
if errors.Is(hord.ErrNil, err) {
return nil, nil
}
return nil, fmt.Errorf("error getting Garden: %w", err)
}

var result pkg.Garden
err = c.unmarshal(dataBytes, &result)
if err != nil {
return nil, fmt.Errorf("error parsing Garden data: %w", err)
}

return &result, nil
return c.db.Delete(gardenKey(id))
}
76 changes: 76 additions & 0 deletions garden-app/pkg/storage/generic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package storage

import (
"errors"
"fmt"
"strings"

"github.com/calvinmclean/automated-garden/garden-app/pkg"
"github.com/madflojo/hord"
)

// getOne will use the provided key to read data from the data source. Then, it will Unmarshal
// into the generic type
func getOne[T any](c *Client, key string) (*T, error) {
dataBytes, err := c.db.Get(key)
if err != nil {
if errors.Is(hord.ErrNil, err) {
return nil, nil
}
return nil, fmt.Errorf("error getting data: %w", err)
}

var result T
err = c.unmarshal(dataBytes, &result)
if err != nil {
return nil, fmt.Errorf("error parsing data: %w", err)
}

return &result, nil
}

// getMultiple will use the provided prefix to read data from the data source. Then, it will use getOne
// to read each element into the correct type. These types must support `pkg.EndDateable` to allow
// excluding end-dated resources
func getMultiple[T pkg.EndDateable](c *Client, getEndDated bool, prefix string) ([]T, error) {
keys, err := c.db.Keys()
if err != nil {
return nil, fmt.Errorf("error getting keys: %w", err)
}

results := []T{}
for _, key := range keys {
if !strings.HasPrefix(key, prefix) {
continue
}

result, err := getOne[T](c, key)
if err != nil {
return nil, fmt.Errorf("error getting data: %w", err)
}
if result == nil {
continue
}

if getEndDated || !(*result).EndDated() {
results = append(results, *result)
}
}

return results, nil
}

// save marshals the provided item and writes it to the database
func save[T any](c *Client, item T, key string) error {
asBytes, err := c.marshal(item)
if err != nil {
return fmt.Errorf("error marshalling data: %w", err)
}

err = c.db.Set(key, asBytes)
if err != nil {
return fmt.Errorf("error writing data to database: %w", err)
}

return nil
}
77 changes: 77 additions & 0 deletions garden-app/pkg/storage/generic_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package storage

import (
"errors"
"testing"

"github.com/calvinmclean/automated-garden/garden-app/pkg"
"github.com/rs/xid"
"github.com/stretchr/testify/assert"
)

var id, _ = xid.FromString("c5cvhpcbcv45e8bp16dg")

func unmarshalError(_ []byte, _ interface{}) error {
return errors.New("unmarshal error")
}

func marshalError(_ interface{}) ([]byte, error) {
return nil, errors.New("marshal error")
}

func TestGetOneErrors(t *testing.T) {
c, err := NewClient(Config{Driver: "hashmap"})
assert.NoError(t, err)

t.Run("ErrorNilKey", func(t *testing.T) {
_, err := getOne[*pkg.Garden](c, "")
assert.Error(t, err)
assert.Equal(t, "error getting data: Key cannot be nil", err.Error())
})

t.Run("ErrorUnmarshal", func(t *testing.T) {
c.unmarshal = unmarshalError

err := save[*pkg.Garden](c, &pkg.Garden{ID: id}, gardenKey(id))
assert.NoError(t, err)

_, err = getOne[*pkg.Garden](c, gardenKey(id))
assert.Error(t, err)
assert.Equal(t, "error parsing data: unmarshal error", err.Error())
})
}

func TestGetMultipleErrors(t *testing.T) {
c, err := NewClient(Config{Driver: "hashmap"})
assert.NoError(t, err)

t.Run("ErrorUnmarshal", func(t *testing.T) {
c.unmarshal = unmarshalError

err := save[*pkg.Garden](c, &pkg.Garden{ID: id}, gardenKey(id))
assert.NoError(t, err)

_, err = getMultiple[*pkg.Garden](c, true, gardenPrefix)
assert.Error(t, err)
assert.Equal(t, "error getting data: error parsing data: unmarshal error", err.Error())
})
}

func TestSaveErrors(t *testing.T) {
c, err := NewClient(Config{Driver: "hashmap"})
assert.NoError(t, err)

t.Run("ErrorNilKey", func(t *testing.T) {
err := save[*pkg.Garden](c, &pkg.Garden{}, "")
assert.Error(t, err)
assert.Equal(t, "error writing data to database: Key cannot be nil", err.Error())
})

t.Run("ErrorMarshal", func(t *testing.T) {
c.marshal = marshalError

err := save[*pkg.Garden](c, &pkg.Garden{ID: id}, gardenKey(id))
assert.Error(t, err)
assert.Equal(t, "error marshalling data: marshal error", err.Error())
})
}
64 changes: 8 additions & 56 deletions garden-app/pkg/storage/water_schedules.go
Original file line number Diff line number Diff line change
@@ -1,84 +1,36 @@
package storage

import (
"errors"
"fmt"
"strings"

"github.com/calvinmclean/automated-garden/garden-app/pkg"
"github.com/madflojo/hord"
"github.com/rs/xid"
)

const waterSchedulePrefix = "WaterSchedule_"

func waterScheduleKey(id xid.ID) string {
return waterSchedulePrefix + id.String()
}

// GetWaterSchedule ...
func (c *Client) GetWaterSchedule(id xid.ID) (*pkg.WaterSchedule, error) {
return c.getWaterSchedule(waterSchedulePrefix + id.String())
return getOne[pkg.WaterSchedule](c, waterScheduleKey(id))
}

// GetWaterSchedules ...
func (c *Client) GetWaterSchedules(getEndDated bool) ([]*pkg.WaterSchedule, error) {
keys, err := c.db.Keys()
if err != nil {
return nil, fmt.Errorf("error getting keys: %w", err)
}

results := []*pkg.WaterSchedule{}
for _, key := range keys {
if !strings.HasPrefix(key, waterSchedulePrefix) {
continue
}

result, err := c.getWaterSchedule(key)
if err != nil {
return nil, fmt.Errorf("error getting keys: %w", err)
}

if getEndDated || !result.EndDated() {
results = append(results, result)
}
}

return results, nil
return getMultiple[*pkg.WaterSchedule](c, getEndDated, waterSchedulePrefix)
}

// SaveWaterSchedule ...
func (c *Client) SaveWaterSchedule(ws *pkg.WaterSchedule) error {
asBytes, err := c.marshal(ws)
if err != nil {
return fmt.Errorf("error marshalling WaterSchedule: %w", err)
}

err = c.db.Set(waterSchedulePrefix+ws.ID.String(), asBytes)
if err != nil {
return fmt.Errorf("error writing WaterSchedule to database: %w", err)
}

return nil
return save[*pkg.WaterSchedule](c, ws, waterScheduleKey(ws.ID))
}

// DeleteWaterSchedule ...
func (c *Client) DeleteWaterSchedule(id xid.ID) error {
return c.db.Delete(waterSchedulePrefix + id.String())
}

func (c *Client) getWaterSchedule(key string) (*pkg.WaterSchedule, error) {
dataBytes, err := c.db.Get(key)
if err != nil {
if errors.Is(hord.ErrNil, err) {
return nil, nil
}
return nil, fmt.Errorf("error getting WaterSchedule: %w", err)
}

var result pkg.WaterSchedule
err = c.unmarshal(dataBytes, &result)
if err != nil {
return nil, fmt.Errorf("error parsing WaterSchedule data: %w", err)
}

return &result, nil
return c.db.Delete(waterScheduleKey(id))
}

// GetMultipleWaterSchedules ...
Expand Down

0 comments on commit 1f015fe

Please sign in to comment.