package cms import ( "fmt" "regexp" "strings" "github.com/asaskevich/govalidator" "github.com/labstack/echo/v4" ) var ( objectIDPattern = regexp.MustCompile(`^[a-fA-F0-9]{24}$`) filePathPattern = regexp.MustCompile(`^/[^/]*/v\d+/files/[a-fA-F0-9]{24}(/download)?$`) ) // ValidationService registers CMS-specific govalidator rules. type ValidationService struct{} // NewValidationService registers custom validators used by CMS forms. func NewValidationService() ValidationService { govalidator.CustomTypeTagMap.Set("mediaRef", govalidator.CustomTypeValidator(func(i interface{}, _ interface{}) bool { value, ok := i.(string) if !ok { return false } return isMediaReference(value) })) return ValidationService{} } func isMediaReference(value string) bool { if value == "" { return true } if govalidator.IsURL(value) { return true } if objectIDPattern.MatchString(value) { return true } return filePathPattern.MatchString(value) } // ValidateCMSForm validates a CMS create/update payload. func ValidateCMSForm(form *CMSForm) error { _, err := govalidator.ValidateStruct(form) return err } // ParseForm binds and validates a CMS form from the request. func ParseForm(c echo.Context) (*CMSForm, error) { var form CMSForm if err := c.Bind(&form); err != nil { return nil, fmt.Errorf("invalid request format: %w", err) } if err := ValidateCMSForm(&form); err != nil { return nil, err } return &form, nil } // FormatValidationErrors converts govalidator errors into user-facing messages. func FormatValidationErrors(err error) string { if err == nil { return "" } errs, ok := err.(govalidator.Errors) if !ok { return humanizeValidationError(err.Error()) } messages := make([]string, 0, len(errs)) for _, item := range errs { messages = append(messages, humanizeValidationError(item.Error())) } return strings.Join(messages, "; ") } func humanizeValidationError(raw string) string { field, rule, ok := parseGovalidatorError(raw) if !ok { return raw } label := fieldLabel(field) lowerRule := strings.ToLower(rule) switch { case strings.Contains(lowerRule, "mediaref"): return label + " must be a valid URL or uploaded file" case strings.Contains(lowerRule, "url"): return label + " must be a valid URL" case strings.Contains(lowerRule, "email"): return label + " must be a valid email address" case strings.Contains(lowerRule, "length"): return label + " has an invalid length" case strings.Contains(lowerRule, "range"): return label + " is out of the allowed range" case strings.Contains(lowerRule, "in("): return label + " has an invalid value" default: return label + " is invalid" } } func parseGovalidatorError(raw string) (field, rule string, ok bool) { parts := strings.SplitN(raw, ": ", 2) if len(parts) != 2 { return "", "", false } return parts[0], parts[1], true } func fieldLabel(field string) string { field = strings.TrimPrefix(field, "CMSForm.") replacer := strings.NewReplacer( "Hero.", "Hero section: ", "Chart.", "Chart section: ", "Features.", "Features section: ", "Challenges.", "Challenges section: ", "Advantages.", "Advantages section: ", "Contact.", "Contact section: ", "Footer.", "Footer section: ", ".", " ", ) label := replacer.Replace(field) label = strings.ReplaceAll(label, "Cards 0", "card 1") label = strings.ReplaceAll(label, "Cards 1", "card 2") label = strings.ReplaceAll(label, "Cards 2", "card 3") label = strings.ReplaceAll(label, "Cards 3", "card 4") label = strings.ReplaceAll(label, "Cards 4", "card 5") return label }