Skip to content

Commit

Permalink
feat(hass): ✨ add validation of sensor requests
Browse files Browse the repository at this point in the history
- support requests providing a validation function that is run before request is sent to HA
- add basic validation to sensor registration and update requests
  • Loading branch information
joshuar committed Sep 5, 2024
1 parent d088120 commit 3e2c560
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 7 deletions.
14 changes: 13 additions & 1 deletion internal/hass/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ var (
}
)

// Validate is a request that supports validation of its values.
type Validate interface {
Validate() error
}

// GetRequest is a HTTP GET request.
type GetRequest any

Expand Down Expand Up @@ -180,7 +185,7 @@ func (c *Client) handleSensorUpdate(ctx context.Context, details sensor.Details)

response, err := send[sensor.StateUpdateResponse](ctx, c, req)
if err != nil {
return fmt.Errorf("failed to send sensor update request: %w", err)
return fmt.Errorf("failed to send sensor update request for %s: %w", details.ID(), err)
}

if response == nil {
Expand Down Expand Up @@ -335,6 +340,13 @@ func send[T any](ctx context.Context, client *Client, requestDetails any) (T, er
return response, ErrInvalidClient
}

// If the request supports validation, make sure it is valid.
if a, ok := requestDetails.(Validate); ok {
if err := a.Validate(); err != nil {
return response, fmt.Errorf("validation failed: %w", err)
}
}

requestObj := client.endpoint.R().SetContext(ctx)
requestObj = requestObj.SetError(&responseErr)
requestObj = requestObj.SetResult(&response)
Expand Down
21 changes: 15 additions & 6 deletions internal/hass/sensor/sensor.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ type Details interface {

type stateUpdateRequest struct {
StateAttributes map[string]any `json:"attributes,omitempty"`
State any `json:"state"`
Icon string `json:"icon,omitempty"`
State any `json:"state" validate:"required"`
Icon string `json:"icon,omitempty" validate:"startswith=mdi"`
Type string `json:"type"`
UniqueID string `json:"unique_id"`
UniqueID string `json:"unique_id" validate:"required"`
}

func createStateUpdateRequest(sensor State) *stateUpdateRequest {
Expand All @@ -77,8 +77,8 @@ func createStateUpdateRequest(sensor State) *stateUpdateRequest {

type registrationRequest struct {
*stateUpdateRequest
Name string `json:"name,omitempty"`
UnitOfMeasurement string `json:"unit_of_measurement,omitempty"`
Name string `json:"name" validate:"required"`
UnitOfMeasurement string `json:"unit_of_measurement,omitempty" validate:"required_with:DeviceClass"`
StateClass string `json:"state_class,omitempty"`
EntityCategory string `json:"entity_category,omitempty"`
DeviceClass string `json:"device_class,omitempty"`
Expand Down Expand Up @@ -119,7 +119,16 @@ type LocationRequest struct {

type Request struct {
Data any `json:"data"`
RequestType string `json:"type"`
RequestType string `json:"type" validate:"oneof=register_sensor update_sensor_states update_location"`
}

func (r *Request) Validate() error {
err := validate.Struct(r.Data)
if err != nil {
return fmt.Errorf("%w: %s", ErrValidationFailed, parseValidationErrors(err))
}

return nil
}

func (r *Request) RequestBody() json.RawMessage {
Expand Down
45 changes: 45 additions & 0 deletions internal/hass/sensor/validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) 2024 Joshua Rich <joshua.rich@gmail.com>
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT

package sensor

import (
"errors"
"strings"

"github.com/go-playground/validator/v10"
)

var validate *validator.Validate

var ErrValidationFailed = errors.New("validation failed")

func init() {
validate = validator.New(validator.WithRequiredStructEnabled())
}

//nolint:errorlint
//revive:disable:unhandled-error
func parseValidationErrors(validation error) string {
validationErrs, ok := validation.(validator.ValidationErrors)
if !ok {
return "internal validation error"
}

var message strings.Builder

for _, err := range validationErrs {
switch err.Tag() {
case "required":
message.WriteString(err.Field() + " is required")
default:
message.WriteString(err.Field() + " should match " + err.Tag())
}

message.WriteRune(' ')
}

return message.String()
}

0 comments on commit 3e2c560

Please sign in to comment.