diff --git a/garden-app/pkg/notifications/client.go b/garden-app/pkg/notifications/client.go index dccc78ff..151fa331 100644 --- a/garden-app/pkg/notifications/client.go +++ b/garden-app/pkg/notifications/client.go @@ -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": @@ -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) {} diff --git a/garden-app/pkg/notifications/client_test.go b/garden-app/pkg/notifications/client_test.go index 8af117b2..3002da75 100644 --- a/garden-app/pkg/notifications/client_test.go +++ b/garden-app/pkg/notifications/client_test.go @@ -10,15 +10,15 @@ 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", }}, }, @@ -26,7 +26,7 @@ func TestConfigPatch(t *testing.T) { 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) @@ -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()) } diff --git a/garden-app/pkg/notifications/config.go b/garden-app/pkg/notifications/config.go new file mode 100644 index 00000000..d185706b --- /dev/null +++ b/garden-app/pkg/notifications/config.go @@ -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) +} diff --git a/garden-app/pkg/storage/client.go b/garden-app/pkg/storage/client.go index 7e6c8d37..209f1889 100644 --- a/garden-app/pkg/storage/client.go +++ b/garden-app/pkg/storage/client.go @@ -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) { @@ -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 } diff --git a/garden-app/server/mqtt_handler.go b/garden-app/server/mqtt_handler.go index d46ea79d..246937a1 100644 --- a/garden-app/server/mqtt_handler.go +++ b/garden-app/server/mqtt_handler.go @@ -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" ) @@ -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 { diff --git a/garden-app/server/notification_clients.go b/garden-app/server/notification_clients.go index 4f5dcade..06919440 100644 --- a/garden-app/server/notification_clients.go +++ b/garden-app/server/notification_clients.go @@ -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 } @@ -30,12 +30,12 @@ func NewNotificationClientsAPI(storageClient *storage.Client) (*NotificationClie storageClient: storageClient, } - api.API = babyapi.NewAPI[*notifications.Config]("NotificationClients", notificationClientsBasePath, func() *notifications.Config { return ¬ifications.Config{} }) + api.API = babyapi.NewAPI[*notifications.Client]("NotificationClients", notificationClientsBasePath, func() *notifications.Client { return ¬ifications.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)) } @@ -43,8 +43,8 @@ func NewNotificationClientsAPI(storageClient *storage.Client) (*NotificationClie 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)) @@ -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) } @@ -99,7 +93,7 @@ func (resp *NotificationClientTestResponse) Render(_ http.ResponseWriter, _ *htt } type NotificationClientResponse struct { - *notifications.Config + *notifications.Client Links []Link `json:"links,omitempty"` }