Skip to content

Commit

Permalink
feat(preferences): 🔊 improve messages shown when preferences are not …
Browse files Browse the repository at this point in the history
…valid
  • Loading branch information
joshuar committed Jul 31, 2024
1 parent 1cc6168 commit cf4dd0a
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 50 deletions.
4 changes: 1 addition & 3 deletions internal/preferences/prefs.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,7 @@ func File() string {
func (p *Preferences) Validate() error {
err := validate.Struct(p)
if err != nil {
showValidationErrors(err)

return fmt.Errorf("validation failed: %w", err)
return fmt.Errorf("%w: %s", ErrValidationFailed, parseValidationErrors(err))
}

return nil
Expand Down
74 changes: 72 additions & 2 deletions internal/preferences/prefs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,59 @@ import (
"github.com/stretchr/testify/require"
)

func deviceEqual(t *testing.T, got, want *Device) bool {
t.Helper()
switch {
case !reflect.DeepEqual(got.AppName, want.AppName):
t.Error("appName does not match")
return false
case !reflect.DeepEqual(got.AppVersion, want.AppVersion):
t.Error("appVersion does not match")
return false
case !reflect.DeepEqual(got.OsName, want.OsName):
t.Error("distro does not match")
return false
case !reflect.DeepEqual(got.OsVersion, want.OsVersion):
t.Errorf("distroVersion does not match: got %s want %s", got.OsVersion, want.OsVersion)
return false
case !reflect.DeepEqual(got.Name, want.Name):
t.Error("hostname does not match")
return false
case !reflect.DeepEqual(got.Model, want.Model):
t.Error("hwModel does not match")
return false
case !reflect.DeepEqual(got.Manufacturer, want.Manufacturer):
t.Error("hwVendor does not match")
return false
}
return true
}

func preferencesEqual(t *testing.T, got, want *Preferences) bool {
t.Helper()
switch {
case !deviceEqual(t, got.Device, want.Device):
t.Error("device does not match")
return false
case !reflect.DeepEqual(got.Hass, want.Hass):
t.Error("hass preferences do not match")
return false
case !reflect.DeepEqual(got.Registration, want.Registration):
t.Error("registration preferences do not match")
return false
case !reflect.DeepEqual(got.MQTT, want.MQTT):
t.Errorf("mqtt preferences do not match")
return false
case !reflect.DeepEqual(got.Version, want.Version):
t.Error("version does not match")
return false
case !reflect.DeepEqual(got.Registered, want.Registered):
t.Error("registered does not match")
return false
}
return true
}

func TestSetPath(t *testing.T) {
testPath := t.TempDir()

Expand Down Expand Up @@ -164,7 +217,7 @@ func TestLoad(t *testing.T) {
return
}
require.ErrorIs(t, err, tt.wantErrType)
if !reflect.DeepEqual(got, tt.want) {
if !preferencesEqual(t, got, tt.want) {
t.Errorf("Load() = %v, want %v", got, tt.want)
}
SetPath(origPath)
Expand Down Expand Up @@ -200,7 +253,24 @@ func TestPreferences_Validate(t *testing.T) {
},
},
{
name: "invalid",
name: "required field missing",
fields: fields{
MQTT: validPrefs.MQTT,
Registration: validPrefs.Registration,
Hass: validPrefs.Hass,
Device: validPrefs.Device,
},
wantErr: true,
},
{
name: "invalid field value",
fields: fields{
MQTT: validPrefs.MQTT,
Registration: &Registration{Server: "notaurl", Token: "somestring"},
Hass: validPrefs.Hass,
Device: validPrefs.Device,
Version: AppVersion,
},
wantErr: true,
},
}
Expand Down
4 changes: 1 addition & 3 deletions internal/preferences/registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ type Registration struct {
func (p *Registration) Validate() error {
err := validate.Struct(p)
if err != nil {
showValidationErrors(err)

return fmt.Errorf("validation failed: %w", err)
return fmt.Errorf("%w: %s", ErrValidationFailed, parseValidationErrors(err))
}

return nil
Expand Down
57 changes: 15 additions & 42 deletions internal/preferences/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,67 +6,40 @@
package preferences

import (
"encoding/json"
"errors"
"fmt"
"log/slog"
"strings"

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

var validate *validator.Validate

var ErrInternalValidationFailed = errors.New("internal validation error")

//nolint:tagliatelle
type validationError struct {
Namespace string `json:"namespace"` // can differ when a custom TagNameFunc is registered or
Field string `json:"field"` // by passing alt name to ReportError like below
StructNamespace string `json:"structNamespace"`
StructField string `json:"structField"`
Tag string `json:"tag"`
ActualTag string `json:"actualTag"`
Kind string `json:"kind"`
Type string `json:"type"`
Value string `json:"value"`
Param string `json:"param"`
Message string `json:"message"`
}
var ErrValidationFailed = errors.New("validation failed")

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

//nolint:errorlint
func showValidationErrors(validation error) {
//revive:disable:unhandled-error
func parseValidationErrors(validation error) string {
validationErrs, ok := validation.(validator.ValidationErrors)
if !ok {
slog.Error("Validation error.", "error", ErrInternalValidationFailed)

return
return "internal validation error"
}

for _, err := range validationErrs {
errDetails := validationError{
Namespace: err.Namespace(),
Field: err.Field(),
StructNamespace: err.StructNamespace(),
StructField: err.StructField(),
Tag: err.Tag(),
ActualTag: err.ActualTag(),
Kind: fmt.Sprintf("%v", err.Kind()),
Type: fmt.Sprintf("%v", err.Type()),
Value: fmt.Sprintf("%v", err.Value()),
Param: err.Param(),
Message: err.Error(),
}
var message strings.Builder

indent, err := json.MarshalIndent(errDetails, "", " ")
if err != nil {
slog.Error("Validation error.", "error", err.Error())
panic(err)
for _, err := range validationErrs {
switch err.Tag() {
case "required":
message.WriteString(err.Field() + " is required")
default:
message.WriteString(err.Field() + " should match " + err.Tag())
}

slog.Error("Validation", "error", string(indent))
message.WriteRune(' ')
}

return message.String()
}

0 comments on commit cf4dd0a

Please sign in to comment.