Files
honeyDueAPI/internal/validator/validator.go
Trey t 6dac34e373 Migrate from Gin to Echo framework and add comprehensive integration tests
Major changes:
- Migrate all handlers from Gin to Echo framework
- Add new apperrors, echohelpers, and validator packages
- Update middleware for Echo compatibility
- Add ArchivedHandler to task categorization chain (archived tasks go to cancelled_tasks column)
- Add 6 new integration tests:
  - RecurringTaskLifecycle: NextDueDate advancement for weekly/monthly tasks
  - MultiUserSharing: Complex sharing with user removal
  - TaskStateTransitions: All state transitions and kanban column changes
  - DateBoundaryEdgeCases: Threshold boundary testing
  - CascadeOperations: Residence deletion cascade effects
  - MultiUserOperations: Shared residence collaboration
- Add single-purpose repository functions for kanban columns (GetOverdueTasks, GetDueSoonTasks, etc.)
- Fix RemoveUser route param mismatch (userId -> user_id)
- Fix determineExpectedColumn helper to correctly prioritize in_progress over overdue

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 13:52:08 -06:00

103 lines
2.6 KiB
Go

package validator
import (
"net/http"
"reflect"
"strings"
"github.com/go-playground/validator/v10"
"github.com/labstack/echo/v4"
)
// CustomValidator wraps go-playground/validator for Echo
type CustomValidator struct {
validator *validator.Validate
}
// NewCustomValidator creates a new validator instance
func NewCustomValidator() *CustomValidator {
v := validator.New()
// Use JSON tag names for field names in errors
v.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" {
return ""
}
return name
})
return &CustomValidator{validator: v}
}
// Validate implements echo.Validator interface
func (cv *CustomValidator) Validate(i interface{}) error {
if err := cv.validator.Struct(i); err != nil {
return err
}
return nil
}
// ValidationErrorResponse is the structured error response format
type ValidationErrorResponse struct {
Error string `json:"error"`
Fields map[string]FieldError `json:"fields,omitempty"`
}
// FieldError represents a single field validation error
type FieldError struct {
Message string `json:"message"`
Tag string `json:"tag"`
}
// FormatValidationErrors converts validator errors to structured response
func FormatValidationErrors(err error) *ValidationErrorResponse {
if validationErrors, ok := err.(validator.ValidationErrors); ok {
fields := make(map[string]FieldError)
for _, fe := range validationErrors {
fieldName := fe.Field()
fields[fieldName] = FieldError{
Message: formatMessage(fe),
Tag: fe.Tag(),
}
}
return &ValidationErrorResponse{
Error: "Validation failed",
Fields: fields,
}
}
return &ValidationErrorResponse{Error: err.Error()}
}
// HTTPError returns an echo.HTTPError with validation details
func HTTPError(c echo.Context, err error) error {
return c.JSON(http.StatusBadRequest, FormatValidationErrors(err))
}
func formatMessage(fe validator.FieldError) string {
switch fe.Tag() {
case "required":
return "This field is required"
case "required_without":
return "This field is required when " + fe.Param() + " is not provided"
case "required_with":
return "This field is required when " + fe.Param() + " is provided"
case "email":
return "Must be a valid email address"
case "min":
return "Must be at least " + fe.Param() + " characters"
case "max":
return "Must be at most " + fe.Param() + " characters"
case "len":
return "Must be exactly " + fe.Param() + " characters"
case "oneof":
return "Must be one of: " + fe.Param()
case "url":
return "Must be a valid URL"
case "uuid":
return "Must be a valid UUID"
default:
return "Invalid value"
}
}