Coverage for functions \ flipdare \ firestore \ context \ _model_context.py: 50%
50 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-05-08 12:22 +1000
« prev ^ index » next coverage.py v7.13.0, created at 2026-05-08 12:22 +1000
1#!/usr/bin/env python
2# Copyright (c) 2026 Flipdare Pty Ltd. All rights reserved.
3#
4# This file is part of Flipdare's proprietary software and contains
5# confidential and copyrighted material. Unauthorised copying,
6# modification, distribution, or use of this file is strictly
7# prohibited without prior written permission from Flipdare Pty Ltd.
8#
9# This software includes third-party components licensed under MIT,
10# BSD, and Apache 2.0 licences. See THIRD_PARTY_NOTICES for details.
11#
13"""
14Base classes for model contexts and their factories.
16Provides common validation, error handling, and factory patterns for
17context objects that wrap multiple related models (e.g., Friend + Users,
18Dare + Users, Group + Members).
20All models are wrapped in PersistedWrapper to guarantee doc_id exists,
21eliminating the need for doc_id validation checks.
22"""
24from __future__ import annotations
26from abc import ABC, abstractmethod
27from typing import Any
29from flipdare.wrapper import PersistedWrapper
31__all__ = ["ModelContext"]
34class ModelContext(ABC):
35 """
36 Abstract base class for model contexts.
38 A context wraps one or more related models and provides:
39 - Validation that all required models exist and have doc_ids
40 - Safe property access that raises meaningful errors on invalid state
41 - Consistent error messaging
43 Subclasses should:
44 1. Store model references in __init__
45 2. Implement validate() to check all models are valid
46 3. Implement _error_messages property to return list of validation errors
47 4. Use _require_valid() before accessing model properties
48 """
50 def __init__(self) -> None:
51 """
52 Initialize context and cache validation result.
54 Note: Subclasses should call super().__init__() AFTER setting
55 all model attributes, since validate() is called during init.
56 """
57 self._cached_valid = self.validate()
59 @property
60 @abstractmethod
61 def doc_id(self) -> str: ...
63 @property
64 @abstractmethod
65 def _error_messages(self) -> list[str]:
66 """
67 Return list of validation error messages.
69 Should check all required models and return descriptive errors.
70 Return empty list if no errors.
72 Example:
73 @property
74 def _error_messages(self) -> list[str]:
75 errors = []
76 if err := self._validate_model(self._from_user, "from_user"):
77 errors.append(err)
78 if err := self._validate_model(self._to_user, "to_user"):
79 errors.append(err)
80 if err := self._validate_model(self._friend_model, "friend_model"):
81 errors.append(err)
82 return errors
84 """
85 ...
87 @abstractmethod
88 def validate(self) -> bool:
89 """
90 Validate that all required models are present and valid.
92 Should check all models using _is_model_valid() helper.
94 Example:
95 def validate(self) -> bool:
96 return (self._is_model_valid(self._from_user) and
97 self._is_model_valid(self._to_user) and
98 self._is_model_valid(self._friend_model))
100 """
101 ...
103 @property
104 def valid(self) -> bool:
105 """Whether this context passed validation."""
106 return self._cached_valid
108 @property
109 def error_str(self) -> str | None:
110 """
111 Get formatted validation error message, or None if valid.
113 Consistent naming with existing context classes (validation_error vs error_str).
114 """
115 if self.valid:
116 return None
118 messages = self._error_messages
119 if not messages:
120 return f"{self.__class__.__name__} validation failed with no specific errors"
122 msg = f"{self.__class__.__name__} validation errors:\n"
123 for m in messages:
124 msg += f" - {m}\n"
125 return msg.rstrip()
127 @property
128 def validation_error(self) -> str | None:
129 """
130 Get formatted validation error message, or None if valid.
132 Consistent naming with existing context classes (validation_error vs error_str).
133 """
134 if self.valid:
135 return None
137 messages = self._error_messages
138 if not messages:
139 return f"{self.__class__.__name__} validation failed with no specific errors"
141 msg = f"{self.__class__.__name__} validation errors:\n"
142 for m in messages:
143 msg += f" - {m}\n"
144 return msg.rstrip()
146 def _require_valid(self, context: str = "") -> None:
147 """
148 Raise ValueError if context is invalid.
150 Args:
151 context: Description of what operation requires validity (e.g., "access friend_model")
153 Raises:
154 ValueError: If context is invalid
156 Usage:
157 @property
158 def friend_model(self) -> FriendModel:
159 self._require_valid("access friend_model")
160 return self._friend_model # type: ignore
162 """
163 if not self.valid:
164 error_msg = self.validation_error or "Context is invalid"
165 if context:
166 raise ValueError(
167 f"Invalid {self.__class__.__name__}: cannot {context}.\n{error_msg}",
168 )
169 raise ValueError(f"Invalid {self.__class__.__name__}.\n{error_msg}")
171 def _is_model_valid(self, model: PersistedWrapper[Any] | None) -> bool:
172 """
173 Check if a persisted model is valid (not None).
175 Since PersistedWrapper guarantees doc_id exists, we only need to check for None.
177 Args:
178 model: PersistedWrapper to validate
180 Returns:
181 True if model exists, False if None
183 """
184 return model is not None
186 def _validate_model(self, model: PersistedWrapper[Any] | None, name: str) -> str | None:
187 """
188 Validate a persisted model and return error message if invalid.
190 Since PersistedWrapper guarantees doc_id exists, we only check if None.
192 Args:
193 model: PersistedWrapper to validate
194 name: Name to use in error message (e.g., "from_user", "friend")
196 Returns:
197 Error message if None, None if valid
199 Example:
200 @property
201 def _error_messages(self) -> list[str]:
202 errors = []
203 if err := self._validate_model(self._from_user, "from_user"):
204 errors.append(err)
205 if err := self._validate_model(self._to_user, "to_user"):
206 errors.append(err)
207 return errors
209 """
210 if model is None:
211 return f"{name} is not set"
212 return None