package validators import ( "fmt" "reflect" "regexp" "strings" "time" "github.com/go-playground/validator/v10" "wucher/internal/transport/http/jsonapi" ) type Validator struct { validate *validator.Validate } var hoursPattern = regexp.MustCompile(`^\d+(\.\d+)?$`) // validateHoursOrDate accepts an hours value (e.g. "1200" or "1200.5") or a calendar // date "YYYY-MM-DD". Anything else (e.g. "1200 FH") is rejected. func validateHoursOrDate(fl validator.FieldLevel) bool { s := strings.TrimSpace(fl.Field().String()) if s == "" { return true } if hoursPattern.MatchString(s) { return true } _, err := time.Parse("2006-01-02", s) return err == nil } func New() *Validator { v := validator.New() v.RegisterTagNameFunc(func(fld reflect.StructField) string { name := fld.Tag.Get("json") if name == "" { return fld.Name } if idx := strings.Index(name, ","); idx != -1 { return name[:idx] } return name }) _ = v.RegisterValidation("hours_or_date", validateHoursOrDate) return &Validator{validate: v} } func (v *Validator) ValidateStruct(s any) []jsonapi.ErrorObject { if err := v.validate.Struct(s); err != nil { if ve, ok := err.(validator.ValidationErrors); ok { return toJSONAPIErrors(ve) } return []jsonapi.ErrorObject{{ Status: "400", Title: "Invalid request", Detail: err.Error(), }} } return nil } func toJSONAPIErrors(ve validator.ValidationErrors) []jsonapi.ErrorObject { errs := make([]jsonapi.ErrorObject, 0, len(ve)) for _, fe := range ve { field := fe.Field() jsonPath := "/data/attributes/" + strings.ReplaceAll(field, ".", "/") errs = append(errs, jsonapi.ErrorObject{ Status: "422", Title: "Validation error", Detail: validationMessage(field, fe), Source: &jsonapi.ErrorSource{Pointer: jsonPath}, }) } return errs } func validationMessage(field string, fe validator.FieldError) string { switch fe.Tag() { case "required": return field + " is required" case "uuid": return field + " must be a valid UUID" case "email": return field + " must be a valid email address" case "hours_or_date": return field + " must be flight hours (e.g. 1200) or a date in YYYY-MM-DD format" case "oneof": return field + " must be one of: " + fe.Param() default: return fmt.Sprintf("%s is invalid (%s)", field, fe.Tag()) } }