Files
juanatsap 6c7595b041 feat: add tag-based validation system with reflection caching
Implement a declarative struct tag validation system for Go:

- Add validator.go with sync.Map caching for reflection metadata
- Add rules.go with 11 built-in validation rules (required, email,
  pattern, honeypot, timing, etc.)
- Add errors.go with FieldError and ValidationErrors types
- Update ContactFormRequest with validate tags
- Add ValidateContactFormV2() using the new tag-based validator

Rules implemented:
- required/optional: field presence validation
- trim/sanitize: automatic value transformations
- min/max: UTF-8 aware length validation
- email: RFC 5322 email format validation
- pattern: predefined regex patterns (name, subject, company)
- no_injection: email header injection prevention
- honeypot: bot trap (must be empty)
- timing: timestamp validation for bot detection

Documentation:
- docs/go-validation-system.md: complete validation guide
- docs/go-template-system.md: template manager documentation
- docs/go-routes-api.md: routes and API reference
- docs/README.md: documentation index
2025-12-06 15:20:45 +00:00

225 lines
5.5 KiB
Go

package validation
import (
"reflect"
"strconv"
"strings"
"sync"
)
// Validator provides struct tag-based validation with caching
type Validator struct {
cache sync.Map // map[reflect.Type]*structMeta
}
// structMeta holds cached metadata about a struct
type structMeta struct {
fields []fieldMeta
}
// fieldMeta holds metadata about a single field
type fieldMeta struct {
index int // Field index in struct
name string // JSON field name (or struct field name if no json tag)
rules []ruleInfo // Validation rules to apply
}
// ruleInfo holds information about a single validation rule
type ruleInfo struct {
name string // Rule name (e.g., "required", "max")
param string // Optional parameter (e.g., "100" for "max=100")
}
// NewValidator creates a new validator instance
func NewValidator() *Validator {
return &Validator{}
}
// Validate validates a struct using its validate tags
// Returns ValidationErrors if validation fails, nil if successful
func (v *Validator) Validate(s interface{}) error {
val := reflect.ValueOf(s)
// Handle pointer to struct
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
if val.Kind() != reflect.Struct {
return ValidationErrors{{
Field: "validation",
Tag: "struct",
Message: "validate requires a struct or pointer to struct",
}}
}
// Get or create cached metadata
meta := v.getStructMeta(val.Type())
// Validate all fields
var errors ValidationErrors
for _, field := range meta.fields {
fieldVal := val.Field(field.index)
// Convert field value to string for validation
var strValue string
switch fieldVal.Kind() {
case reflect.String:
strValue = fieldVal.String()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
strValue = strconv.FormatInt(fieldVal.Int(), 10)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
strValue = strconv.FormatUint(fieldVal.Uint(), 10)
default:
continue // Skip unsupported types
}
// Apply transformations first (trim, sanitize)
transformedValue := v.applyFieldTransforms(strValue, field.rules)
// Update field value if it was transformed and is a string
if transformedValue != strValue && fieldVal.Kind() == reflect.String && fieldVal.CanSet() {
fieldVal.SetString(transformedValue)
strValue = transformedValue
}
// Validate field with all rules
for _, rule := range field.rules {
// Skip transform-only rules
if rule.name == "trim" || rule.name == "sanitize" {
continue
}
// Skip optional marker
if rule.name == "optional" {
continue
}
// Get validation function
validateFunc, exists := validationRules[rule.name]
if !exists {
errors = append(errors, FieldError{
Field: field.name,
Tag: rule.name,
Message: "unknown validation rule: " + rule.name,
})
continue
}
// Execute validation
if err := validateFunc(field.name, strValue, rule.param); err != nil {
errors = append(errors, *err)
}
}
}
if len(errors) > 0 {
return errors
}
return nil
}
// getStructMeta retrieves or creates cached metadata for a struct type
func (v *Validator) getStructMeta(t reflect.Type) *structMeta {
// Try to load from cache
if cached, ok := v.cache.Load(t); ok {
return cached.(*structMeta)
}
// Parse struct and cache metadata
meta := v.parseStruct(t)
v.cache.Store(t, meta)
return meta
}
// parseStruct parses a struct type and extracts validation metadata
func (v *Validator) parseStruct(t reflect.Type) *structMeta {
meta := &structMeta{
fields: make([]fieldMeta, 0, t.NumField()),
}
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
// Skip unexported fields
if !field.IsExported() {
continue
}
// Get field name from json tag, fallback to struct field name
fieldName := field.Name
if jsonTag := field.Tag.Get("json"); jsonTag != "" {
parts := strings.Split(jsonTag, ",")
if parts[0] != "" && parts[0] != "-" {
fieldName = parts[0]
}
}
// Parse validate tag
validateTag := field.Tag.Get("validate")
if validateTag == "" {
continue // No validation rules
}
rules := v.parseValidateTag(validateTag)
if len(rules) > 0 {
meta.fields = append(meta.fields, fieldMeta{
index: i,
name: fieldName,
rules: rules,
})
}
}
return meta
}
// parseValidateTag parses a validate tag string into rule infos
// Format: "rule1,rule2=param,rule3"
func (v *Validator) parseValidateTag(tag string) []ruleInfo {
parts := strings.Split(tag, ",")
rules := make([]ruleInfo, 0, len(parts))
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
// Split rule name and parameter (e.g., "max=100")
ruleParts := strings.SplitN(part, "=", 2)
rule := ruleInfo{
name: ruleParts[0],
}
if len(ruleParts) > 1 {
rule.param = ruleParts[1]
}
rules = append(rules, rule)
}
return rules
}
// applyFieldTransforms applies transformation rules to a field value
func (v *Validator) applyFieldTransforms(value string, rules []ruleInfo) string {
for _, rule := range rules {
switch rule.name {
case "trim":
value = strings.TrimSpace(value)
case "sanitize":
value = strings.TrimSpace(value)
// Remove newlines from header fields
value = strings.ReplaceAll(value, "\r", "")
value = strings.ReplaceAll(value, "\n", "")
}
}
return value
}
// ValidateStruct is a convenience function that creates a validator and validates
func ValidateStruct(s interface{}) error {
v := NewValidator()
return v.Validate(s)
}