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) }