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>
This commit is contained in:
102
internal/validator/validator.go
Normal file
102
internal/validator/validator.go
Normal file
@@ -0,0 +1,102 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user