Add the experience service with sites through analytics/AI hooks, migrations through 0011, and phase docs/manifests marking the track complete. Co-authored-by: Cursor <cursoragent@cursor.com>
56 lines
1.7 KiB
Python
56 lines
1.7 KiB
Python
"""Reusable validators for Experience foundation."""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import Any
|
|
|
|
from shared.exceptions import AppError
|
|
|
|
CODE_RE = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9_\-]{1,49}$")
|
|
|
|
|
|
class ValidationError(AppError):
|
|
def __init__(self, message: str, details: dict[str, Any] | None = None):
|
|
super().__init__(
|
|
message=message,
|
|
error_code="validation_error",
|
|
status_code=422,
|
|
details=details or {},
|
|
)
|
|
|
|
|
|
def validate_non_empty(value: str | None, field: str) -> str:
|
|
if value is None or not str(value).strip():
|
|
raise ValidationError(f"{field} الزامی است", {"field": field})
|
|
return str(value).strip()
|
|
|
|
|
|
def validate_code(code: str) -> str:
|
|
code = validate_non_empty(code, "code")
|
|
if not CODE_RE.match(code):
|
|
raise ValidationError(
|
|
"کد نامعتبر است",
|
|
{"field": "code", "pattern": CODE_RE.pattern},
|
|
)
|
|
return code
|
|
|
|
|
|
def validate_currency_code(code: str) -> str:
|
|
code = validate_non_empty(code, "currency_code").upper()
|
|
if len(code) != 3:
|
|
raise ValidationError("currency_code باید ۳ حرفی باشد", {"field": "currency_code"})
|
|
return code
|
|
|
|
|
|
def ensure_optimistic_version(entity: Any, expected: int) -> None:
|
|
current = getattr(entity, "version", None)
|
|
if current is None:
|
|
return
|
|
if current != expected:
|
|
raise AppError(
|
|
"نسخه موجودیت همخوان نیست",
|
|
error_code="version_conflict",
|
|
status_code=409,
|
|
details={"expected": expected, "actual": current},
|
|
)
|