Coverage for src / lexigram / contracts / exceptions / domain.py: 35%
69 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-19 05:41 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-19 05:41 +0800
1"""Domain and business logic exception classes."""
3from __future__ import annotations
5from typing import Any
7from lexigram.contracts.exceptions.base import LexigramError
10class DomainError(LexigramError):
11 """Business/domain-level errors (validation failures, not found, auth)."""
13 _code = "LEX_ERR_DOM_001"
15 def __init__(self, message: str = "Domain error", **kwargs: Any) -> None:
16 super().__init__(message, **kwargs)
19class NotFoundError(DomainError):
20 """Resource not found error (domain-level)."""
22 _code = "LEX_ERR_DOM_002"
24 def __init__(self, message: str = "Not found", **kwargs: Any) -> None:
25 super().__init__(message, **kwargs)
28class PermissionDeniedError(DomainError):
29 """Permission denied error (domain-level)."""
31 _code = "LEX_ERR_DOM_003"
33 def __init__(self, message: str = "Permission denied", **kwargs: Any) -> None:
34 super().__init__(message, **kwargs)
37class AuthenticationError(DomainError):
38 """Authentication error (domain-level)."""
40 _code = "LEX_ERR_DOM_004"
42 def __init__(self, message: str = "Authentication failed", **kwargs: Any) -> None:
43 super().__init__(message, **kwargs)
46class AuthorizationError(DomainError):
47 """Authorization/permission error (domain-level)."""
49 _code = "LEX_ERR_DOM_005"
51 def __init__(self, message: str = "Authorization failed", **kwargs: Any) -> None:
52 super().__init__(message, **kwargs)
55class RateLimitError(DomainError):
56 """Rate limiting error."""
58 _code = "LEX_ERR_DOM_006"
60 def __init__(self, message: str = "Rate limit exceeded", **kwargs: Any) -> None:
61 super().__init__(message, **kwargs)
64class ConflictError(DomainError):
65 """Resource conflict error."""
67 _code = "LEX_ERR_DOM_007"
69 def __init__(self, message: str = "Conflict", **kwargs: Any) -> None:
70 super().__init__(message, **kwargs)
73class SerializationError(LexigramError):
74 """Serialization/deserialization error."""
76 _code = "LEX_ERR_SERIAL_001"
78 def __init__(self, message: str = "Serialization error", **kwargs: Any) -> None:
79 super().__init__(message, **kwargs)
82class MappingError(LexigramError):
83 """Object mapping error."""
85 _code = "LEX_ERR_MAP_001"
87 def __init__(self, message: str = "Mapping error", **kwargs: Any) -> None:
88 super().__init__(message, **kwargs)
91class WebError(DomainError):
92 """Web layer error."""
94 _code = "LEX_ERR_WEB_001"
96 def __init__(self, message: str = "Web error", **kwargs: Any) -> None:
97 super().__init__(message, **kwargs)
100class FieldError(DomainError):
101 """A single field-level validation error.
103 Used to represent validation failures for individual fields in a domain
104 object. Can be returned in a Result or collected in a ValidationError.
106 Attributes:
107 field: Name of the field that failed validation.
108 message: Human-readable description of the failure.
109 code: Machine-readable error code for programmatic handling.
110 """
112 _code = "LEX_ERR_VAL_001"
114 def __init__(
115 self, field: str, message: str, code: str = "INVALID", **kwargs: Any
116 ) -> None:
117 # Call parent with the field error message - but we'll override message
118 super().__init__(f"{field}: {message}", **kwargs)
119 # Store field-specific attributes
120 self.field = field
121 # Override message to be just the field message (for backward compat with dataclass)
122 # The parent class message includes "field: message", but we want just "message"
123 self._field_message = message
124 self.message = message
125 # Override code
126 self.code = code
128 def __repr__(self) -> str:
129 return (
130 f"FieldError(field={self.field!r}, message={self.message!r}, "
131 f"code={self.code!r})"
132 )
135class ValidationError(DomainError):
136 """Data validation error."""
138 _code = "LEX_ERR_VAL_002"
140 def __init__(
141 self,
142 message: str = "Validation failed",
143 errors: list[FieldError] | None = None,
144 **kwargs: Any,
145 ) -> None:
146 if errors:
147 kwargs.setdefault("details", {})["errors"] = errors
148 super().__init__(message, **kwargs)
150 @property
151 def errors(self) -> list[FieldError]:
152 """Return the list of field errors."""
153 from typing import cast
155 return cast("list[FieldError]", self.details.get("errors", []))
157 def add_error(
158 self,
159 field: str,
160 message: str,
161 code: str = "invalid",
162 ) -> ValidationError:
163 """Add a field error and return self for chaining."""
164 if "errors" not in self.details:
165 self.details["errors"] = []
166 self.details["errors"].append(
167 FieldError(field=field, message=message, code=code)
168 )
169 return self
172__all__ = [
173 "AuthenticationError",
174 "AuthorizationError",
175 "ConflictError",
176 "DomainError",
177 "FieldError",
178 "MappingError",
179 "NotFoundError",
180 "PermissionDeniedError",
181 "RateLimitError",
182 "SerializationError",
183 "ValidationError",
184 "WebError",
185]