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