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>
103 lines
2.6 KiB
Go
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"
|
|
}
|
|
}
|