Files
classifieds/app/services/field_schema.py
T
2026-06-15 11:23:05 -04:00

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