Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use generics in storage.Client #140

Merged
merged 1 commit into from
Aug 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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