"""Component library validators — Phase 11.2.""" from __future__ import annotations import re from typing import Any from app.models.types import ( ComponentKind, ComponentStatus, ComponentVersionStatus, ) from app.validators.foundation import ValidationError, validate_non_empty from shared.exceptions import AppError SLOT_KEY_RE = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9_\-.]{0,99}$") COMPONENT_STATUS_TRANSITIONS: dict[ComponentStatus, frozenset[ComponentStatus]] = { ComponentStatus.DRAFT: frozenset( {ComponentStatus.ACTIVE, ComponentStatus.ARCHIVED} ), ComponentStatus.ACTIVE: frozenset( {ComponentStatus.DEPRECATED, ComponentStatus.ARCHIVED} ), ComponentStatus.DEPRECATED: frozenset( {ComponentStatus.ACTIVE, ComponentStatus.ARCHIVED} ), ComponentStatus.ARCHIVED: frozenset(), } VERSION_STATUS_TRANSITIONS: dict[ ComponentVersionStatus, frozenset[ComponentVersionStatus] ] = { ComponentVersionStatus.DRAFT: frozenset({ComponentVersionStatus.PUBLISHED}), ComponentVersionStatus.PUBLISHED: frozenset({ComponentVersionStatus.RETIRED}), ComponentVersionStatus.RETIRED: frozenset(), } def validate_slot_key(slot_key: str) -> str: slot_key = validate_non_empty(slot_key, "slot_key") if not SLOT_KEY_RE.match(slot_key): raise ValidationError( "slot_key نامعتبر است", {"field": "slot_key", "pattern": SLOT_KEY_RE.pattern}, ) return slot_key def validate_component_kind(kind: ComponentKind | str) -> ComponentKind: if isinstance(kind, ComponentKind): return kind try: return ComponentKind(kind) except ValueError as exc: raise ValidationError( "kind نامعتبر است", {"field": "kind", "allowed": [k.value for k in ComponentKind]}, ) from exc def ensure_component_status_transition( current: ComponentStatus, target: ComponentStatus ) -> None: if current == target: return allowed = COMPONENT_STATUS_TRANSITIONS.get(current, frozenset()) if target not in allowed: raise AppError( "گذار وضعیت کامپوننت مجاز نیست", error_code="invalid_component_status_transition", status_code=409, details={"current": current.value, "target": target.value}, ) def ensure_version_status_transition( current: ComponentVersionStatus, target: ComponentVersionStatus ) -> None: if current == target: return allowed = VERSION_STATUS_TRANSITIONS.get(current, frozenset()) if target not in allowed: raise AppError( "گذار وضعیت نسخه کامپوننت مجاز نیست", error_code="invalid_component_version_status_transition", status_code=409, details={"current": current.value, "target": target.value}, ) def ensure_version_mutable(version: Any) -> None: if getattr(version, "is_immutable", False) or getattr( version, "status", None ) == ComponentVersionStatus.PUBLISHED: raise AppError( "نسخه منتشرشده قابل ویرایش نیست", error_code="component_version_immutable", status_code=409, ) if getattr(version, "status", None) == ComponentVersionStatus.RETIRED: raise AppError( "نسخه بازنشسته‌شده قابل ویرایش نیست", error_code="component_version_retired", status_code=409, ) def ensure_schema_payload(payload: dict | None, field: str) -> dict | None: if payload is None: return None if not isinstance(payload, dict): raise ValidationError(f"{field} باید شیء JSON باشد", {"field": field}) return payload