Skip to content

Commit

Permalink
Refactor notifications.Client to be easier to use
Browse files Browse the repository at this point in the history
  • Loading branch information
calvinmclean committed Apr 28, 2024
1 parent 658f803 commit 2bd4961
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 103 deletions.
76 changes: 5 additions & 71 deletions garden-app/pkg/notifications/client.go
Original file line number Diff line number Diff line change
@@ -1,63 +1,20 @@
package notifications

import (
"errors"
"fmt"
"net/http"
"time"

"github.com/calvinmclean/automated-garden/garden-app/pkg/notifications/fake"
"github.com/calvinmclean/automated-garden/garden-app/pkg/notifications/pushover"

"github.com/calvinmclean/babyapi"
)

// Client is an interface defining the possible methods used to interact with the notification APIs
type Client interface {
// client is an interface defining the possible methods used to interact with the notification APIs
type client interface {
SendMessage(title, message string) error
}

// Config is used to identify and configure a client type
type Config struct {
ID babyapi.ID `json:"id" yaml:"id"`
Type string `json:"type" yaml:"type"`
Options map[string]interface{} `json:"options" yaml:"options"`
}

func (nc *Config) GetID() string {
return nc.ID.String()
}

func (nc *Config) Render(_ http.ResponseWriter, _ *http.Request) error {
return nil
}

func (nc *Config) Bind(r *http.Request) error {
if nc == nil {
return errors.New("missing required NotificationClient fields")
}

err := nc.ID.Bind(r)
if err != nil {
return err
}

switch r.Method {
case http.MethodPut, http.MethodPost:
if nc.Type == "" {
return errors.New("missing required type field")
}
if nc.Options == nil {
return errors.New("missing required options field")
}
}

return nil
}

// NewClient will use the config to create and return the correct type of notification client
func NewClient(c *Config) (Client, error) {
var client Client
// newClient will use the config to create and return the correct type of notification client
func newClient(c *Client) (client, error) {
var client client
var err error
switch c.Type {
case "pushover":
Expand All @@ -70,26 +27,3 @@ func NewClient(c *Config) (Client, error) {

return client, err
}

// Patch allows modifying an existing Config with fields from a new one
func (nc *Config) Patch(newConfig *Config) *babyapi.ErrResponse {
if newConfig.Type != "" {
nc.Type = newConfig.Type
}

if nc.Options == nil && newConfig.Options != nil {
nc.Options = map[string]interface{}{}
}
for k, v := range newConfig.Options {
nc.Options[k] = v
}

return nil
}

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

func (*Config) SetEndDate(_ time.Time) {}
12 changes: 6 additions & 6 deletions garden-app/pkg/notifications/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,23 @@ import (
func TestConfigPatch(t *testing.T) {
tests := []struct {
name string
newConfig *Config
newConfig *Client
}{
{
"PatchType",
&Config{Type: "other_type"},
&Client{Type: "other_type"},
},
{
"PatchOptions",
&Config{Options: map[string]interface{}{
&Client{Options: map[string]interface{}{
"key": "value",
}},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Config{}
c := &Client{}
err := c.Patch(tt.newConfig)
require.Nil(t, err)
assert.Equal(t, tt.newConfig, c)
Expand All @@ -35,11 +35,11 @@ func TestConfigPatch(t *testing.T) {
}

func TestNewClientInvalidType(t *testing.T) {
_, err := NewClient(&Config{Type: "DNE"})
_, err := newClient(&Client{Type: "DNE"})
assert.Error(t, err)
assert.Equal(t, "invalid type 'DNE'", err.Error())
}

func TestEndDated(t *testing.T) {
assert.False(t, (&Config{}).EndDated())
assert.False(t, (&Client{}).EndDated())
}
87 changes: 87 additions & 0 deletions garden-app/pkg/notifications/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package notifications

import (
"errors"
"fmt"
"net/http"
"time"

"github.com/calvinmclean/babyapi"
)

// Client is used to interact with an external notification API. It has generic options to allow multiple Client implementations
type Client struct {
ID babyapi.ID `json:"id" yaml:"id"`
Type string `json:"type" yaml:"type"`
Options map[string]any `json:"options" yaml:"options"`
}

// TestCreate will call the Client implementation's initialization function to make sure it is valid and able to connect
func (nc *Client) TestCreate() error {
_, err := newClient(nc)
return err
}

func (nc *Client) GetID() string {
return nc.ID.String()
}

func (nc *Client) Render(_ http.ResponseWriter, _ *http.Request) error {
return nil
}

func (nc *Client) Bind(r *http.Request) error {
if nc == nil {
return errors.New("missing required NotificationClient fields")
}

err := nc.ID.Bind(r)
if err != nil {
return err
}

switch r.Method {
case http.MethodPut, http.MethodPost:
if nc.Type == "" {
return errors.New("missing required type field")
}
if nc.Options == nil {
return errors.New("missing required options field")
}
}

return nil
}

// Patch allows modifying an existing Config with fields from a new one
func (nc *Client) Patch(newConfig *Client) *babyapi.ErrResponse {
if newConfig.Type != "" {
nc.Type = newConfig.Type
}

if nc.Options == nil && newConfig.Options != nil {
nc.Options = map[string]any{}
}
for k, v := range newConfig.Options {
nc.Options[k] = v
}

return nil
}

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

func (*Client) SetEndDate(_ time.Time) {}

// SendMessage will send a notification using the client created from this config
func (nc *Client) SendMessage(title, message string) error {
client, err := newClient(nc)
if err != nil {
return fmt.Errorf("error initializing client: %w", err)
}

return client.SendMessage(title, message)
}
4 changes: 2 additions & 2 deletions garden-app/pkg/storage/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ type Client struct {
Zones babyapi.Storage[*pkg.Zone]
WaterSchedules babyapi.Storage[*pkg.WaterSchedule]
WeatherClientConfigs babyapi.Storage[*weather.Config]
NotificationClientConfigs babyapi.Storage[*notifications.Config]
NotificationClientConfigs babyapi.Storage[*notifications.Client]
}

func NewClient(config Config) (*Client, error) {
Expand All @@ -40,7 +40,7 @@ func NewClient(config Config) (*Client, error) {
Zones: kv.NewClient[*pkg.Zone](db, "Zone"),
WaterSchedules: kv.NewClient[*pkg.WaterSchedule](db, "WaterSchedule"),
WeatherClientConfigs: kv.NewClient[*weather.Config](db, "WeatherClient"),
NotificationClientConfigs: kv.NewClient[*notifications.Config](db, "NotificationClient"),
NotificationClientConfigs: kv.NewClient[*notifications.Client](db, "NotificationClient"),
}, nil
}

Expand Down
11 changes: 2 additions & 9 deletions garden-app/server/mqtt_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"time"

"github.com/calvinmclean/automated-garden/garden-app/pkg"
"github.com/calvinmclean/automated-garden/garden-app/pkg/notifications"
"github.com/calvinmclean/automated-garden/garden-app/pkg/storage"
mqtt "github.com/eclipse/paho.mqtt.golang"
)
Expand Down Expand Up @@ -104,14 +103,8 @@ func (h *MQTTHandler) Handle(_ mqtt.Client, msg mqtt.Message) {
title := fmt.Sprintf("%s finished watering", zone.Name)
message := fmt.Sprintf("watered for %s", waterDuration.String())

for _, ncConfig := range notificationClients {
ncLogger := logger.With(notificationClientIDLogField, ncConfig.GetID())

nc, err := notifications.NewClient(ncConfig)
if err != nil {
ncLogger.Error("error initializing notification client", "error", err)
continue
}
for _, nc := range notificationClients {
ncLogger := logger.With(notificationClientIDLogField, nc.GetID())

err = nc.SendMessage(title, message)
if err != nil {
Expand Down
24 changes: 9 additions & 15 deletions garden-app/server/notification_clients.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const (
// NotificationClientsAPI encapsulates the structs and dependencies necessary for the NotificationClients API
// to function, including storage and configuring
type NotificationClientsAPI struct {
*babyapi.API[*notifications.Config]
*babyapi.API[*notifications.Client]

storageClient *storage.Client
}
Expand All @@ -30,21 +30,21 @@ func NewNotificationClientsAPI(storageClient *storage.Client) (*NotificationClie
storageClient: storageClient,
}

api.API = babyapi.NewAPI[*notifications.Config]("NotificationClients", notificationClientsBasePath, func() *notifications.Config { return &notifications.Config{} })
api.API = babyapi.NewAPI[*notifications.Client]("NotificationClients", notificationClientsBasePath, func() *notifications.Client { return &notifications.Client{} })
api.SetStorage(api.storageClient.NotificationClientConfigs)

api.SetOnCreateOrUpdate(func(_ *http.Request, nc *notifications.Config) *babyapi.ErrResponse {
api.SetOnCreateOrUpdate(func(_ *http.Request, nc *notifications.Client) *babyapi.ErrResponse {
// make sure a valid NotificationClient can still be created
_, err := notifications.NewClient(nc)
err := nc.TestCreate()
if err != nil {
return babyapi.ErrInvalidRequest(fmt.Errorf("invalid request to update NotificationClient: %w", err))
}

return nil
})

api.SetResponseWrapper(func(nc *notifications.Config) render.Renderer {
return &NotificationClientResponse{Config: nc}
api.SetResponseWrapper(func(nc *notifications.Client) render.Renderer {
return &NotificationClientResponse{Client: nc}
})

api.AddCustomIDRoute(http.MethodPost, "/test", babyapi.Handler(api.testNotificationClient))
Expand All @@ -67,20 +67,14 @@ func (api *NotificationClientsAPI) testNotificationClient(_ http.ResponseWriter,
return httpErr
}

nc, err := notifications.NewClient(notificationClient)
if err != nil {
logger.Error("unable to get NotificationClient", "error", err)
return InternalServerError(err)
}

var req TestNotificationClientRequest
err = json.NewDecoder(r.Body).Decode(&req)
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
logger.Error("unable to parse TestNotificationClientRequest", "error", err)
return babyapi.ErrInvalidRequest(err)
}

err = nc.SendMessage(req.Title, req.Message)
err = notificationClient.SendMessage(req.Title, req.Message)
if err != nil {
return babyapi.ErrInvalidRequest(err)
}
Expand All @@ -99,7 +93,7 @@ func (resp *NotificationClientTestResponse) Render(_ http.ResponseWriter, _ *htt
}

type NotificationClientResponse struct {
*notifications.Config
*notifications.Client

Links []Link `json:"links,omitempty"`
}
Expand Down

0 comments on commit 2bd4961

Please sign in to comment.