76 lines
2.1 KiB
Python
76 lines
2.1 KiB
Python
"""Validate listing `attributes` against a category's `field_schema`.
|
|
|
|
Returns (cleaned_attributes, errors). Caller rejects on any errors.
|
|
Supported field types: text, number, select, bool.
|
|
"""
|
|
|
|
_HOT_COLUMNS = {"condition", "job_type", "salary_min", "salary_max"}
|
|
|
|
|
|
class SchemaError(ValueError):
|
|
pass
|
|
|
|
|
|
def validate_attributes(category, raw):
|
|
raw = raw or {}
|
|
cleaned, errors = {}, {}
|
|
|
|
for field in category.fields():
|
|
name = field["name"]
|
|
ftype = field.get("type", "text")
|
|
required = field.get("required", False)
|
|
val = raw.get(name)
|
|
|
|
if val in (None, "", []):
|
|
if required:
|
|
errors[name] = "required"
|
|
continue
|
|
|
|
try:
|
|
cleaned[name] = _coerce(field, ftype, val)
|
|
except SchemaError as e:
|
|
errors[name] = str(e)
|
|
|
|
return cleaned, errors
|
|
|
|
|
|
def _coerce(field, ftype, val):
|
|
if ftype == "text":
|
|
s = str(val).strip()
|
|
mx = field.get("max", 255)
|
|
if len(s) > mx:
|
|
raise SchemaError(f"max length {mx}")
|
|
return s
|
|
|
|
if ftype == "number":
|
|
try:
|
|
n = int(val) if str(val).lstrip("-").isdigit() else float(val)
|
|
except (TypeError, ValueError):
|
|
raise SchemaError("must be a number")
|
|
if "min" in field and n < field["min"]:
|
|
raise SchemaError(f"min {field['min']}")
|
|
if "max" in field and n > field["max"]:
|
|
raise SchemaError(f"max {field['max']}")
|
|
return n
|
|
|
|
if ftype == "select":
|
|
opts = field.get("options", [])
|
|
if val not in opts:
|
|
raise SchemaError("invalid option")
|
|
return val
|
|
|
|
if ftype == "bool":
|
|
return bool(val) if isinstance(val, bool) else str(val).lower() in ("1", "true", "on", "yes")
|
|
|
|
raise SchemaError("unknown field type")
|
|
|
|
|
|
def hot_values(cleaned):
|
|
"""Map cleaned attributes -> denormalized Listing hot columns."""
|
|
return {
|
|
"attr_condition": cleaned.get("condition"),
|
|
"attr_job_type": cleaned.get("job_type"),
|
|
"attr_salary_min": cleaned.get("salary_min"),
|
|
"attr_salary_max": cleaned.get("salary_max"),
|
|
}
|