Skip to content

Commit

Permalink
Use generics in storage.Client
Browse files Browse the repository at this point in the history
  • Loading branch information
calvinmclean committed Aug 21, 2023
1 parent 7dd7a33 commit fd24e5e
Show file tree
Hide file tree
Showing 7 changed files with 115 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 Garden: %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
}
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
66 changes: 10 additions & 56 deletions garden-app/pkg/storage/weather_clients.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
package storage

import (
"errors"
"fmt"
"strings"

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

const weatherclientPrefix = "WeatherClient_"
const weatherClientPrefix = "WeatherClient_"

func weatherClientKey(id xid.ID) string {
return weatherClientPrefix + id.String()
}

// GetWeatherClient ...
func (c *Client) GetWeatherClient(id xid.ID) (weather.Client, error) {
clientConfig, err := c.getWeatherClientConfig(weatherclientPrefix + id.String())
clientConfig, err := getOne[weather.Config](c, weatherClientKey(id))
if err != nil {
return nil, fmt.Errorf("error getting weather client config: %w", err)
}
Expand All @@ -32,69 +33,22 @@ func (c *Client) GetWeatherClient(id xid.ID) (weather.Client, error) {

// GetWeatherClientConfig ...
func (c *Client) GetWeatherClientConfig(id xid.ID) (*weather.Config, error) {
return c.getWeatherClientConfig(weatherclientPrefix + id.String())
return getOne[weather.Config](c, weatherClientKey(id))
}

// GetWeatherClientConfigs ...
func (c *Client) GetWeatherClientConfigs() ([]*weather.Config, error) {
keys, err := c.db.Keys()
if err != nil {
return nil, fmt.Errorf("error getting keys: %w", err)
}

results := []*weather.Config{}
for _, key := range keys {
if !strings.HasPrefix(key, weatherclientPrefix) {
continue
}

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

results = append(results, result)
}

return results, nil
return getMultiple[*weather.Config](c, true, weatherClientPrefix)
}

// SaveWeatherClientConfig ...
func (c *Client) SaveWeatherClientConfig(wc *weather.Config) error {
asBytes, err := c.marshal(wc)
if err != nil {
return fmt.Errorf("error marshalling WeatherClient: %w", err)
}

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

return nil
return save[*weather.Config](c, wc, weatherClientKey(wc.ID))
}

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

func (c *Client) getWeatherClientConfig(key string) (*weather.Config, 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 WeatherClient: %w", err)
}

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

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

// GetWaterSchedulesUsingWeatherClient will return all WaterSchedules that rely on this WeatherClient
Expand Down
5 changes: 5 additions & 0 deletions garden-app/pkg/weather/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ func (c *Config) Patch(newConfig *Config) {
}
}

// EndDated allows this to satisfy an interface even though the resources does not have end-dates
func (c *Config) EndDated() bool {
return false
}

// clientWrapper wraps any other implementation of the interface in order to add basic Prometheus summary metrics
// and caching
type clientWrapper struct {
Expand Down

0 comments on commit fd24e5e

Please sign in to comment.