"""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( "کد نامعتبر است — فقط لاتین/عدد/_/- و حداقل ۲ کاراکتر " "(مثال: main-shop یا ws_01). فارسی و فاصله مجاز نیست.", {"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}, )