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

1"""Domain and business logic exception classes.""" 

2 

3from __future__ import annotations 

4 

5from typing import Any 

6 

7from lexigram.contracts.exceptions.base import LexigramError 

8 

9 

10class DomainError(LexigramError): 

11 """Business/domain-level errors (validation failures, not found, auth).""" 

12 

13 _code = "LEX_ERR_DOM_001" 

14 

15 def __init__(self, message: str = "Domain error", **kwargs: Any) -> None: 

16 super().__init__(message, **kwargs) 

17 

18 

19class NotFoundError(DomainError): 

20 """Resource not found error (domain-level).""" 

21 

22 _code = "LEX_ERR_DOM_002" 

23 

24 def __init__(self, message: str = "Not found", **kwargs: Any) -> None: 

25 super().__init__(message, **kwargs) 

26 

27 

28class PermissionDeniedError(DomainError): 

29 """Permission denied error (domain-level).""" 

30 

31 _code = "LEX_ERR_DOM_003" 

32 

33 def __init__(self, message: str = "Permission denied", **kwargs: Any) -> None: 

34 super().__init__(message, **kwargs) 

35 

36 

37class AuthenticationError(DomainError): 

38 """Authentication error (domain-level).""" 

39 

40 _code = "LEX_ERR_DOM_004" 

41 

42 def __init__(self, message: str = "Authentication failed", **kwargs: Any) -> None: 

43 super().__init__(message, **kwargs) 

44 

45 

46class AuthorizationError(DomainError): 

47 """Authorization/permission error (domain-level).""" 

48 

49 _code = "LEX_ERR_DOM_005" 

50 

51 def __init__(self, message: str = "Authorization failed", **kwargs: Any) -> None: 

52 super().__init__(message, **kwargs) 

53 

54 

55class RateLimitError(DomainError): 

56 """Rate limiting error.""" 

57 

58 _code = "LEX_ERR_DOM_006" 

59 

60 def __init__(self, message: str = "Rate limit exceeded", **kwargs: Any) -> None: 

61 super().__init__(message, **kwargs) 

62 

63 

64class ConflictError(DomainError): 

65 """Resource conflict error.""" 

66 

67 _code = "LEX_ERR_DOM_007" 

68 

69 def __init__(self, message: str = "Conflict", **kwargs: Any) -> None: 

70 super().__init__(message, **kwargs) 

71 

72 

73class SerializationError(LexigramError): 

74 """Serialization/deserialization error.""" 

75 

76 _code = "LEX_ERR_SERIAL_001" 

77 

78 def __init__(self, message: str = "Serialization error", **kwargs: Any) -> None: 

79 super().__init__(message, **kwargs) 

80 

81 

82class MappingError(LexigramError): 

83 """Object mapping error.""" 

84 

85 _code = "LEX_ERR_MAP_001" 

86 

87 def __init__(self, message: str = "Mapping error", **kwargs: Any) -> None: 

88 super().__init__(message, **kwargs) 

89 

90 

91class WebError(DomainError): 

92 """Web layer error.""" 

93 

94 _code = "LEX_ERR_WEB_001" 

95 

96 def __init__(self, message: str = "Web error", **kwargs: Any) -> None: 

97 super().__init__(message, **kwargs) 

98 

99 

100class FieldError(DomainError): 

101 """A single field-level validation error. 

102 

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. 

105 

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 """ 

111 

112 _code = "LEX_ERR_VAL_001" 

113 

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 

127 

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 ) 

133 

134 

135class ValidationError(DomainError): 

136 """Data validation error.""" 

137 

138 _code = "LEX_ERR_VAL_002" 

139 

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) 

149 

150 @property 

151 def errors(self) -> list[FieldError]: 

152 """Return the list of field errors.""" 

153 from typing import cast 

154 

155 return cast("list[FieldError]", self.details.get("errors", [])) 

156 

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 

170 

171 

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]