package api import ( "net/http" "reflect" "strings" "time" "github.com/go-playground/validator/v10" ) // validate is the package-level validator singleton. It is configured once to // report the json tag name (not the Go field name) so envelope field errors // match the wire contract, and to recognise the custom `yyyymmdd` date rule. var validate = newValidator() func newValidator() *validator.Validate { v := validator.New(validator.WithRequiredStructEnabled()) // Report fields by their json tag so writeFieldError names match the API. v.RegisterTagNameFunc(func(fld reflect.StructField) string { name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0] if name == "-" { return "" } return name }) // yyyymmdd validates a YYYY-MM-DD calendar date (mirrors validDate). _ = v.RegisterValidation("yyyymmdd", func(fl validator.FieldLevel) bool { s := fl.Field().String() _, err := time.Parse("2006-01-02", s) return err == nil }) return v } // validationMessage turns a single field error into a human-readable message // that preserves the intent of the old hand-written messages. func validationMessage(fe validator.FieldError) string { field := fe.Field() switch fe.Tag() { case "required": return field + " is required" case "oneof": return field + " must be one of: " + strings.ReplaceAll(fe.Param(), " ", ", ") case "gte": return field + " must be >= " + fe.Param() case "gt": return field + " must be > " + fe.Param() case "lte": return field + " must be <= " + fe.Param() case "min": return field + " must have at least " + fe.Param() + " item(s)" case "yyyymmdd": return field + " must be a YYYY-MM-DD date" default: return field + " is invalid" } } // validateStruct runs the struct validation rules. On the first violation it // writes a VALIDATION (400) field error naming the offending json field and // returns false; on success it returns true. dive/element errors fall back to // the struct-level field name so callers always get a usable field. func validateStruct(w http.ResponseWriter, v any) bool { err := validate.Struct(v) if err == nil { return true } if verrs, ok := err.(validator.ValidationErrors); ok && len(verrs) > 0 { fe := verrs[0] writeFieldError(w, http.StatusBadRequest, CodeValidation, validationMessage(fe), fe.Field()) return false } // Non-field validation error (e.g. invalid validation setup): generic 400. writeError(w, http.StatusBadRequest, "invalid request body") return false }