springbootAI 1.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spring/__init__.py +66 -0
- spring/ai/__init__.py +78 -0
- spring/ai/advisors.py +139 -0
- spring/ai/annotations.py +74 -0
- spring/ai/autoconfig.py +481 -0
- spring/ai/core.py +391 -0
- spring/ai/etl.py +188 -0
- spring/ai/memory.py +109 -0
- spring/ai/observability.py +129 -0
- spring/ai/providers.py +789 -0
- spring/ai/resilience.py +258 -0
- spring/ai/tools.py +106 -0
- spring/ai/vectorstore.py +303 -0
- spring/annotations/__init__.py +188 -0
- spring/annotations/cache.py +126 -0
- spring/annotations/cloud.py +207 -0
- spring/annotations/conditional.py +272 -0
- spring/annotations/core.py +864 -0
- spring/annotations/messaging.py +107 -0
- spring/aop/__init__.py +4 -0
- spring/aop/cloud_aop.py +404 -0
- spring/aop/comprehensive_aop.py +1015 -0
- spring/aop/method_interceptor.py +19 -0
- spring/aop/proxy_factory.py +55 -0
- spring/cloud/__init__.py +76 -0
- spring/cloud/discovery.py +364 -0
- spring/cloud/feign.py +469 -0
- spring/cloud/gateway.py +452 -0
- spring/cloud/load_balancer.py +149 -0
- spring/cloud/seata.py +557 -0
- spring/cloud/sentinel.py +525 -0
- spring/cloud/tracer.py +337 -0
- spring/config/__init__.py +21 -0
- spring/config/binding.py +206 -0
- spring/config/config_loader.py +405 -0
- spring/context/__init__.py +13 -0
- spring/context/application_context.py +589 -0
- spring/context/bean_definition.py +70 -0
- spring/context/bean_factory.py +1052 -0
- spring/context/registry.py +58 -0
- spring/context/scanner.py +106 -0
- spring/core/__init__.py +3 -0
- spring/core/graceful_shutdown.py +196 -0
- spring/core/typing_utils.py +50 -0
- spring/csv/__init__.py +52 -0
- spring/csv/annotations.py +402 -0
- spring/csv/converters.py +69 -0
- spring/csv/easy_csv.py +95 -0
- spring/csv/exceptions.py +27 -0
- spring/csv/reader.py +195 -0
- spring/csv/writer.py +155 -0
- spring/data/__init__.py +54 -0
- spring/data/page.py +181 -0
- spring/data/repository.py +274 -0
- spring/data/specification.py +228 -0
- spring/datasource/__init__.py +66 -0
- spring/datasource/annotations.py +133 -0
- spring/datasource/context.py +69 -0
- spring/datasource/dynamic.py +148 -0
- spring/event/__init__.py +7 -0
- spring/event/publisher.py +69 -0
- spring/excel/__init__.py +51 -0
- spring/excel/annotations.py +405 -0
- spring/excel/converters.py +231 -0
- spring/excel/easy_excel.py +94 -0
- spring/excel/exceptions.py +31 -0
- spring/excel/reader.py +254 -0
- spring/excel/style.py +95 -0
- spring/excel/writer.py +197 -0
- spring/i18n/__init__.py +97 -0
- spring/i18n/accessor.py +94 -0
- spring/i18n/auto_config.py +177 -0
- spring/i18n/holder.py +106 -0
- spring/i18n/locale.py +152 -0
- spring/i18n/locale_resolver.py +367 -0
- spring/i18n/message_source.py +250 -0
- spring/i18n/middleware.py +79 -0
- spring/i18n/properties.py +168 -0
- spring/i18n/sources.py +255 -0
- spring/logging/__init__.py +1 -0
- spring/logging/loguru_logger.py +228 -0
- spring/main.py +378 -0
- spring/messaging/__init__.py +1 -0
- spring/messaging/rabbitmq.py +302 -0
- spring/monitoring/__init__.py +1 -0
- spring/monitoring/prometheus.py +199 -0
- spring/orm/__init__.py +258 -0
- spring/orm/database.py +222 -0
- spring/orm/ddl_auto.py +1217 -0
- spring/orm/migration.py +419 -0
- spring/orm/mybatis_integration.py +400 -0
- spring/orm/pymybatis/__init__.py +86 -0
- spring/orm/pymybatis/annotations/__init__.py +30 -0
- spring/orm/pymybatis/annotations/annotations.py +332 -0
- spring/orm/pymybatis/cache/__init__.py +47 -0
- spring/orm/pymybatis/cache/cache.py +371 -0
- spring/orm/pymybatis/cache/redis_cache.py +434 -0
- spring/orm/pymybatis/circuit_breaker/__init__.py +21 -0
- spring/orm/pymybatis/circuit_breaker/circuit_breaker.py +424 -0
- spring/orm/pymybatis/configuration.py +525 -0
- spring/orm/pymybatis/core/__init__.py +10 -0
- spring/orm/pymybatis/core/sql_session.py +1382 -0
- spring/orm/pymybatis/core/sql_session_factory.py +76 -0
- spring/orm/pymybatis/dialect/__init__.py +9 -0
- spring/orm/pymybatis/dialect/dialect.py +445 -0
- spring/orm/pymybatis/dynamic_sql/__init__.py +9 -0
- spring/orm/pymybatis/dynamic_sql/dynamic_sql.py +900 -0
- spring/orm/pymybatis/interceptor/__init__.py +31 -0
- spring/orm/pymybatis/interceptor/interceptor.py +427 -0
- spring/orm/pymybatis/mapper/__init__.py +9 -0
- spring/orm/pymybatis/mapper/mapper.py +540 -0
- spring/orm/pymybatis/metrics/__init__.py +41 -0
- spring/orm/pymybatis/metrics/metrics.py +595 -0
- spring/orm/pymybatis/pool/__init__.py +9 -0
- spring/orm/pymybatis/pool/connection_pool.py +711 -0
- spring/orm/pymybatis/security/__init__.py +19 -0
- spring/orm/pymybatis/security/access_control.py +415 -0
- spring/orm/pymybatis/security/password_encoder.py +293 -0
- spring/orm/pymybatis/security/sensitive_data_masker.py +326 -0
- spring/orm/pymybatis/security/sql_injection_detector.py +675 -0
- spring/orm/pymybatis/transaction/__init__.py +9 -0
- spring/orm/pymybatis/transaction/transaction.py +288 -0
- spring/orm/pymybatis/type_handler/__init__.py +37 -0
- spring/orm/pymybatis/type_handler/type_handler.py +473 -0
- spring/orm/pymybatis/version.py +9 -0
- spring/orm/pymybatis/xml_parser/__init__.py +9 -0
- spring/orm/pymybatis/xml_parser/xml_parser.py +761 -0
- spring/retry/__init__.py +12 -0
- spring/retry/retry_annotations.py +71 -0
- spring/retry/retry_decorator.py +155 -0
- spring/scheduling/__init__.py +3 -0
- spring/scheduling/scheduler.py +389 -0
- spring/security/__init__.py +39 -0
- spring/security/jwt_utils.py +281 -0
- spring/security/replay_protection.py +206 -0
- spring/security/secret_manager.py +226 -0
- spring/security/security_aop.py +248 -0
- spring/security/security_context.py +172 -0
- spring/test/__init__.py +45 -0
- spring/test/slicing.py +341 -0
- spring/tracing/__init__.py +11 -0
- spring/tracing/skywalking.py +229 -0
- spring/tx/__init__.py +52 -0
- spring/tx/events.py +172 -0
- spring/tx/synchronization.py +143 -0
- spring/utils/__init__.py +5 -0
- spring/utils/banner.py +32 -0
- spring/utils/logger.py +73 -0
- spring/utils/redis_client.py +526 -0
- spring/validation/__init__.py +55 -0
- spring/validation/aop.py +141 -0
- spring/validation/constraints.py +357 -0
- spring/validation/exceptions.py +55 -0
- spring/validation/validator.py +139 -0
- spring/web/__init__.py +12 -0
- spring/web/actuator.py +319 -0
- spring/web/exception_handler.py +61 -0
- spring/web/health.py +399 -0
- spring/web/interceptor.py +91 -0
- spring/web/result.py +44 -0
- spring/web/swagger.py +601 -0
- spring/web/web_context.py +755 -0
- spring/websocket/__init__.py +86 -0
- spring/websocket/annotations.py +169 -0
- spring/websocket/broker.py +238 -0
- spring/websocket/exceptions.py +26 -0
- spring/websocket/handler.py +243 -0
- spring/websocket/router.py +526 -0
- spring/websocket/session.py +216 -0
- springbootai-1.8.0.dist-info/METADATA +2796 -0
- springbootai-1.8.0.dist-info/RECORD +175 -0
- springbootai-1.8.0.dist-info/WHEEL +5 -0
- springbootai-1.8.0.dist-info/entry_points.txt +2 -0
- springbootai-1.8.0.dist-info/licenses/LICENSE +7 -0
- springbootai-1.8.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"""SpringBootAI Bean Validation 约束注解 —— 字段级描述符。
|
|
2
|
+
|
|
3
|
+
设计原则:**复用项目既有范式,不重复造轮子**。本模块的字段级约束完全镜像 ORM 层
|
|
4
|
+
``spring/orm/ddl_auto.py`` 的 ``Column``/``Id`` 与 Excel 层 ``spring/excel/annotations.py``
|
|
5
|
+
的 ``ExcelProperty`` 元数据描述符范式:
|
|
6
|
+
|
|
7
|
+
- 字段级约束以**类属性描述符**形式声明(推荐),Python 自动回填字段名;也支持**函数装饰器**
|
|
8
|
+
形式(镜像 ``column()`` / ``ExcelProperty.__call__``)。
|
|
9
|
+
- 元数据通过 ``cls.__mro__`` 反射读取(与 ``Column``/``__column__`` 一致),由
|
|
10
|
+
``BeanValidator`` 统一收集并校验。
|
|
11
|
+
|
|
12
|
+
对齐 Jakarta Bean Validation(``javax.validation.constraints``)的核心约束:
|
|
13
|
+
``@NotNull`` / ``@NotBlank`` / ``@NotEmpty`` / ``@Size`` / ``@Min`` / ``@Max`` /
|
|
14
|
+
``@Pattern`` / ``@Email`` / ``@Positive`` / ``@PositiveOrZero`` / ``@Negative`` /
|
|
15
|
+
``@NegativeOrZero`` / ``@AssertTrue`` / ``@AssertFalse``。
|
|
16
|
+
|
|
17
|
+
与 Java 的差异(已标注):
|
|
18
|
+
- Java Bean Validation 是 JSR-380 标准 + Hibernate Validator 实现,运行时通过反射读注解;
|
|
19
|
+
本模块同样反射读字段描述符,但不依赖 JPA/Pydantic,适用于任意 Python 对象。
|
|
20
|
+
- 约束只支持字段级(Java 还支持方法参数级/返回值级),方法级校验由 ``@BeanValidate`` AOP
|
|
21
|
+
切面对参数对象整体校验实现(见 ``validator.py``)。
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import re
|
|
26
|
+
from typing import Any, Callable, Optional
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Constraint:
|
|
30
|
+
"""所有字段约束的基类(镜像 ORM ``Column`` 描述符范式)。
|
|
31
|
+
|
|
32
|
+
子类需实现 ``_check(value) -> Optional[str]``,返回违规消息字符串;通过返回 ``None``
|
|
33
|
+
表示通过。``message`` 可由用户覆盖。
|
|
34
|
+
|
|
35
|
+
两种使用方式(与 ``Column``/``ExcelProperty`` 一致):
|
|
36
|
+
|
|
37
|
+
1. 类属性描述符(推荐)::
|
|
38
|
+
|
|
39
|
+
class User:
|
|
40
|
+
name = NotBlank(message="姓名不能为空")
|
|
41
|
+
age = Min(0, message="年龄不能为负")
|
|
42
|
+
def __init__(self, name=None, age=None): ...
|
|
43
|
+
|
|
44
|
+
2. 函数装饰器(镜像 ``@column``)::
|
|
45
|
+
|
|
46
|
+
@NotBlank()
|
|
47
|
+
def name(self): ...
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
# 约束名,子类覆盖(用于校验报告归类)
|
|
51
|
+
constraint_name: str = "Constraint"
|
|
52
|
+
|
|
53
|
+
def __init__(self, message: Optional[str] = None):
|
|
54
|
+
self.message = message
|
|
55
|
+
self.attr_name: str = ""
|
|
56
|
+
|
|
57
|
+
def __set_name__(self, owner: type, name: str) -> None:
|
|
58
|
+
"""类属性描述符形式时,Python 自动回填字段名(镜像 ``ExcelProperty``)。"""
|
|
59
|
+
self.attr_name = name
|
|
60
|
+
|
|
61
|
+
def __call__(self, target: Callable) -> Callable:
|
|
62
|
+
"""函数装饰器形式:``@NotBlank()``,把约束挂到 ``__bean_constraint__`` 列表。
|
|
63
|
+
|
|
64
|
+
镜像 ORM ``column()`` 的 ``setattr(f, '__column__', col)`` 与
|
|
65
|
+
``ExcelProperty.__call__`` 的 ``setattr(target, '__excel_property__', self)``。
|
|
66
|
+
一个方法上可叠加多个约束(用列表累积,而非覆盖)。
|
|
67
|
+
"""
|
|
68
|
+
existing = getattr(target, "__bean_constraint__", None)
|
|
69
|
+
if isinstance(existing, list):
|
|
70
|
+
existing.append(self)
|
|
71
|
+
else:
|
|
72
|
+
setattr(target, "__bean_constraint__", [self])
|
|
73
|
+
if not self.attr_name:
|
|
74
|
+
self.attr_name = getattr(target, "__name__", "")
|
|
75
|
+
return target
|
|
76
|
+
|
|
77
|
+
def _check(self, value: Any) -> Optional[str]:
|
|
78
|
+
"""子类实现:返回违规消息(None 表示通过)。"""
|
|
79
|
+
raise NotImplementedError
|
|
80
|
+
|
|
81
|
+
def validate(self, value: Any) -> Optional[str]:
|
|
82
|
+
"""对外校验入口:返回违规消息或 None。
|
|
83
|
+
|
|
84
|
+
默认消息优先取用户自定义 ``message``,否则取 ``_check`` 返回的默认消息。
|
|
85
|
+
"""
|
|
86
|
+
msg = self._check(value)
|
|
87
|
+
if msg is None:
|
|
88
|
+
return None
|
|
89
|
+
return self.message if self.message else msg
|
|
90
|
+
|
|
91
|
+
def __repr__(self) -> str:
|
|
92
|
+
return f"{type(self).__name__}(attr={self.attr_name!r})"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ==================== 非空类约束 ====================
|
|
96
|
+
|
|
97
|
+
class NotNull(Constraint):
|
|
98
|
+
"""``@NotNull``:值不能为 None(允许空字符串/空集合)。
|
|
99
|
+
|
|
100
|
+
对齐 ``javax.validation.constraints.NotNull``。
|
|
101
|
+
"""
|
|
102
|
+
constraint_name = "NotNull"
|
|
103
|
+
|
|
104
|
+
def _check(self, value: Any) -> Optional[str]:
|
|
105
|
+
if value is None:
|
|
106
|
+
return "不能为 null"
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class NotBlank(Constraint):
|
|
111
|
+
"""``@NotBlank``:字符串不能为 None 且去除首尾空白后长度 > 0。
|
|
112
|
+
|
|
113
|
+
对齐 ``javax.validation.constraints.NotBlank``(仅作用于字符串)。
|
|
114
|
+
"""
|
|
115
|
+
constraint_name = "NotBlank"
|
|
116
|
+
|
|
117
|
+
def _check(self, value: Any) -> Optional[str]:
|
|
118
|
+
if value is None:
|
|
119
|
+
return "不能为空"
|
|
120
|
+
if not isinstance(value, str):
|
|
121
|
+
return None # 非字符串交给其他约束(如 NotNull)
|
|
122
|
+
if value.strip() == "":
|
|
123
|
+
return "不能为空白"
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class NotEmpty(Constraint):
|
|
128
|
+
"""``@NotEmpty``:不能为 None 且 size > 0(字符串/集合/字典/数组)。
|
|
129
|
+
|
|
130
|
+
对齐 ``javax.validation.constraints.NotEmpty``。
|
|
131
|
+
"""
|
|
132
|
+
constraint_name = "NotEmpty"
|
|
133
|
+
|
|
134
|
+
def _check(self, value: Any) -> Optional[str]:
|
|
135
|
+
if value is None:
|
|
136
|
+
return "不能为空"
|
|
137
|
+
try:
|
|
138
|
+
if len(value) == 0:
|
|
139
|
+
return "长度/大小必须大于 0"
|
|
140
|
+
except TypeError:
|
|
141
|
+
return None # 无 len() 的对象,不强制
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ==================== 长度/大小约束 ====================
|
|
146
|
+
|
|
147
|
+
class Size(Constraint):
|
|
148
|
+
"""``@Size``:字符串/集合/数组长度在 ``[min, max]`` 区间。
|
|
149
|
+
|
|
150
|
+
对齐 ``javax.validation.constraints.Size``。``min`` 默认 0,``max`` 默认 2^31-1。
|
|
151
|
+
"""
|
|
152
|
+
constraint_name = "Size"
|
|
153
|
+
|
|
154
|
+
def __init__(self, min: int = 0, max: int = 2 ** 31 - 1, message: Optional[str] = None):
|
|
155
|
+
super().__init__(message=message)
|
|
156
|
+
if min < 0:
|
|
157
|
+
raise ValueError("@Size min 不能为负")
|
|
158
|
+
if max < min:
|
|
159
|
+
raise ValueError("@Size max 不能小于 min")
|
|
160
|
+
self.min = min
|
|
161
|
+
self.max = max
|
|
162
|
+
|
|
163
|
+
def _check(self, value: Any) -> Optional[str]:
|
|
164
|
+
if value is None:
|
|
165
|
+
return None # null 交给 @NotNull
|
|
166
|
+
try:
|
|
167
|
+
length = len(value)
|
|
168
|
+
except TypeError:
|
|
169
|
+
return None # 无 len() 的对象,不强制
|
|
170
|
+
if length < self.min or length > self.max:
|
|
171
|
+
return f"长度必须在 {self.min} 到 {self.max} 之间(实际 {length})"
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# ==================== 数值范围约束 ====================
|
|
176
|
+
|
|
177
|
+
class Min(Constraint):
|
|
178
|
+
"""``@Min``:数值 >= value。对齐 ``javax.validation.constraints.Min``。"""
|
|
179
|
+
constraint_name = "Min"
|
|
180
|
+
|
|
181
|
+
def __init__(self, value: Any, message: Optional[str] = None):
|
|
182
|
+
super().__init__(message=message)
|
|
183
|
+
self.value = value
|
|
184
|
+
|
|
185
|
+
def _check(self, value: Any) -> Optional[str]:
|
|
186
|
+
if value is None:
|
|
187
|
+
return None
|
|
188
|
+
try:
|
|
189
|
+
if float(value) < float(self.value):
|
|
190
|
+
return f"必须大于等于 {self.value}"
|
|
191
|
+
except (TypeError, ValueError):
|
|
192
|
+
return None # 非数值,不强制
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class Max(Constraint):
|
|
197
|
+
"""``@Max``:数值 <= value。对齐 ``javax.validation.constraints.Max``。"""
|
|
198
|
+
constraint_name = "Max"
|
|
199
|
+
|
|
200
|
+
def __init__(self, value: Any, message: Optional[str] = None):
|
|
201
|
+
super().__init__(message=message)
|
|
202
|
+
self.value = value
|
|
203
|
+
|
|
204
|
+
def _check(self, value: Any) -> Optional[str]:
|
|
205
|
+
if value is None:
|
|
206
|
+
return None
|
|
207
|
+
try:
|
|
208
|
+
if float(value) > float(self.value):
|
|
209
|
+
return f"必须小于等于 {self.value}"
|
|
210
|
+
except (TypeError, ValueError):
|
|
211
|
+
return None
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class Positive(Constraint):
|
|
216
|
+
"""``@Positive``:数值 > 0。对齐 ``javax.validation.constraints.Positive``。"""
|
|
217
|
+
constraint_name = "Positive"
|
|
218
|
+
|
|
219
|
+
def _check(self, value: Any) -> Optional[str]:
|
|
220
|
+
if value is None:
|
|
221
|
+
return None
|
|
222
|
+
try:
|
|
223
|
+
if float(value) <= 0:
|
|
224
|
+
return "必须为正数"
|
|
225
|
+
except (TypeError, ValueError):
|
|
226
|
+
return None
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class PositiveOrZero(Constraint):
|
|
231
|
+
"""``@PositiveOrZero``:数值 >= 0。"""
|
|
232
|
+
constraint_name = "PositiveOrZero"
|
|
233
|
+
|
|
234
|
+
def _check(self, value: Any) -> Optional[str]:
|
|
235
|
+
if value is None:
|
|
236
|
+
return None
|
|
237
|
+
try:
|
|
238
|
+
if float(value) < 0:
|
|
239
|
+
return "必须大于等于 0"
|
|
240
|
+
except (TypeError, ValueError):
|
|
241
|
+
return None
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class Negative(Constraint):
|
|
246
|
+
"""``@Negative``:数值 < 0。"""
|
|
247
|
+
constraint_name = "Negative"
|
|
248
|
+
|
|
249
|
+
def _check(self, value: Any) -> Optional[str]:
|
|
250
|
+
if value is None:
|
|
251
|
+
return None
|
|
252
|
+
try:
|
|
253
|
+
if float(value) >= 0:
|
|
254
|
+
return "必须为负数"
|
|
255
|
+
except (TypeError, ValueError):
|
|
256
|
+
return None
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class NegativeOrZero(Constraint):
|
|
261
|
+
"""``@NegativeOrZero``:数值 <= 0。"""
|
|
262
|
+
constraint_name = "NegativeOrZero"
|
|
263
|
+
|
|
264
|
+
def _check(self, value: Any) -> Optional[str]:
|
|
265
|
+
if value is None:
|
|
266
|
+
return None
|
|
267
|
+
try:
|
|
268
|
+
if float(value) > 0:
|
|
269
|
+
return "必须小于等于 0"
|
|
270
|
+
except (TypeError, ValueError):
|
|
271
|
+
return None
|
|
272
|
+
return None
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# ==================== 字符串格式约束 ====================
|
|
276
|
+
|
|
277
|
+
class Pattern(Constraint):
|
|
278
|
+
"""``@Pattern``:字符串匹配正则。对齐 ``javax.validation.constraints.Pattern``。"""
|
|
279
|
+
constraint_name = "Pattern"
|
|
280
|
+
|
|
281
|
+
def __init__(self, regex: str, message: Optional[str] = None):
|
|
282
|
+
super().__init__(message=message)
|
|
283
|
+
try:
|
|
284
|
+
self._compiled = re.compile(regex)
|
|
285
|
+
except re.error as e:
|
|
286
|
+
raise ValueError(f"@Pattern 正则非法: {e}") from e
|
|
287
|
+
self.regex = regex
|
|
288
|
+
|
|
289
|
+
def _check(self, value: Any) -> Optional[str]:
|
|
290
|
+
if value is None:
|
|
291
|
+
return None
|
|
292
|
+
if not isinstance(value, str):
|
|
293
|
+
value = str(value)
|
|
294
|
+
if not self._compiled.search(value):
|
|
295
|
+
return f"不匹配模式 {self.regex!r}"
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
# 常见邮箱正则(与 Hibernate Validator Email 推荐一致,宽松版本)
|
|
300
|
+
_EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class Email(Constraint):
|
|
304
|
+
"""``@Email``:字符串为合法邮箱格式。对齐 ``javax.validation.constraints.Email``。
|
|
305
|
+
|
|
306
|
+
采用宽松邮箱正则(``local@domain.tld``),不追求 RFC 5322 完整覆盖,
|
|
307
|
+
生产环境如需更严格校验请配合 ``@Pattern`` 自定义。
|
|
308
|
+
"""
|
|
309
|
+
constraint_name = "Email"
|
|
310
|
+
|
|
311
|
+
def _check(self, value: Any) -> Optional[str]:
|
|
312
|
+
if value is None:
|
|
313
|
+
return None
|
|
314
|
+
if not isinstance(value, str):
|
|
315
|
+
value = str(value)
|
|
316
|
+
if value == "":
|
|
317
|
+
return None # 空串交给 @NotBlank
|
|
318
|
+
if not _EMAIL_RE.match(value):
|
|
319
|
+
return "邮箱格式不合法"
|
|
320
|
+
return None
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
# ==================== 布尔断言约束 ====================
|
|
324
|
+
|
|
325
|
+
class AssertTrue(Constraint):
|
|
326
|
+
"""``@AssertTrue``:值必须为 True(或真值)。对齐 ``javax.validation.constraints.AssertTrue``。"""
|
|
327
|
+
constraint_name = "AssertTrue"
|
|
328
|
+
|
|
329
|
+
def _check(self, value: Any) -> Optional[str]:
|
|
330
|
+
if value is None:
|
|
331
|
+
return None
|
|
332
|
+
if not bool(value):
|
|
333
|
+
return "必须为 true"
|
|
334
|
+
return None
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
class AssertFalse(Constraint):
|
|
338
|
+
"""``@AssertFalse``:值必须为 False(或假值)。对齐 ``javax.validation.constraints.AssertFalse``。"""
|
|
339
|
+
constraint_name = "AssertFalse"
|
|
340
|
+
|
|
341
|
+
def _check(self, value: Any) -> Optional[str]:
|
|
342
|
+
if value is None:
|
|
343
|
+
return None
|
|
344
|
+
if bool(value):
|
|
345
|
+
return "必须为 false"
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
__all__ = [
|
|
350
|
+
"Constraint",
|
|
351
|
+
"NotNull", "NotBlank", "NotEmpty",
|
|
352
|
+
"Size",
|
|
353
|
+
"Min", "Max",
|
|
354
|
+
"Positive", "PositiveOrZero", "Negative", "NegativeOrZero",
|
|
355
|
+
"Pattern", "Email",
|
|
356
|
+
"AssertTrue", "AssertFalse",
|
|
357
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""SpringBootAI Bean Validation 异常定义。
|
|
2
|
+
|
|
3
|
+
设计对齐 Jakarta Bean Validation(Hibernate Validator)的错误语义:
|
|
4
|
+
- ``ValidationError``:校验失败时抛出的汇总异常,包含全部约束违反。
|
|
5
|
+
- ``ConstraintViolation``:单条约束违反信息(字段、值、消息、约束类型)。
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, List
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ConstraintViolation:
|
|
13
|
+
"""单条约束违反记录。
|
|
14
|
+
|
|
15
|
+
属性:
|
|
16
|
+
attr_name: 违反约束的字段名。
|
|
17
|
+
value: 被校验的实际值。
|
|
18
|
+
constraint: 触发违反的约束注解实例(如 ``NotNull``/``Size``)。
|
|
19
|
+
message: 人可读的违规描述。
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
__slots__ = ("attr_name", "value", "constraint", "message")
|
|
23
|
+
|
|
24
|
+
def __init__(self, attr_name: str, value: Any, constraint: Any, message: str):
|
|
25
|
+
self.attr_name = attr_name
|
|
26
|
+
self.value = value
|
|
27
|
+
self.constraint = constraint
|
|
28
|
+
self.message = message
|
|
29
|
+
|
|
30
|
+
def __repr__(self) -> str:
|
|
31
|
+
cname = type(self.constraint).__name__
|
|
32
|
+
return (f"ConstraintViolation(attr={self.attr_name!r}, "
|
|
33
|
+
f"constraint={cname}, value={self.value!r}, message={self.message!r})")
|
|
34
|
+
|
|
35
|
+
def __str__(self) -> str:
|
|
36
|
+
return f"{self.attr_name}: {self.message}"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ValidationError(Exception):
|
|
40
|
+
"""Bean Validation 校验失败异常。
|
|
41
|
+
|
|
42
|
+
汇总一次校验产生的所有 ``ConstraintViolation``,便于上层统一处理或批量回显。
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, violations: List[ConstraintViolation]):
|
|
46
|
+
self.violations: List[ConstraintViolation] = list(violations)
|
|
47
|
+
lines = "; ".join(str(v) for v in self.violations) if self.violations else "校验失败"
|
|
48
|
+
super().__init__(lines)
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def messages(self) -> List[str]:
|
|
52
|
+
return [v.message for v in self.violations]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
__all__ = ["ConstraintViolation", "ValidationError"]
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""SpringBootAI Bean Validation 验证器。
|
|
2
|
+
|
|
3
|
+
``BeanValidator`` 反射实体类的字段约束(``Constraint`` 描述符或 ``__bean_constraint__``
|
|
4
|
+
函数装饰器),对一个对象实例执行全部约束校验,收集 ``ConstraintViolation``。
|
|
5
|
+
|
|
6
|
+
镜像 ORM ``DdlAutoManager._parse_entity`` 与 Excel ``parse_excel_columns`` 的反射范式:
|
|
7
|
+
遍历 ``cls.__mro__`` 的 ``__dict__``,自底向上收集每个字段的约束列表,子类覆盖父类。
|
|
8
|
+
|
|
9
|
+
用法::
|
|
10
|
+
|
|
11
|
+
from spring.validation import BeanValidator, NotBlank, Min
|
|
12
|
+
|
|
13
|
+
class User:
|
|
14
|
+
name = NotBlank(message="姓名不能为空")
|
|
15
|
+
age = Min(0)
|
|
16
|
+
def __init__(self, name=None, age=None):
|
|
17
|
+
self.name = name; self.age = age
|
|
18
|
+
|
|
19
|
+
violations = BeanValidator.validate(User(name="", age=-1))
|
|
20
|
+
if violations:
|
|
21
|
+
... # 处理违规
|
|
22
|
+
BeanValidator.validate_or_raise(User(name="Tom", age=18)) # 通过则不抛错
|
|
23
|
+
|
|
24
|
+
方法级校验由 ``@BeanValidate`` AOP 切面驱动(注册到 ``comprehensive_aop``),见模块 ``__init__``。
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import inspect
|
|
29
|
+
from typing import Any, Dict, List, Optional, Type
|
|
30
|
+
|
|
31
|
+
from .constraints import Constraint
|
|
32
|
+
from .exceptions import ConstraintViolation, ValidationError
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _collect_constraints(cls: Type) -> Dict[str, List[Constraint]]:
|
|
36
|
+
"""反射收集类(含 MRO 父类)每个字段的约束列表。
|
|
37
|
+
|
|
38
|
+
镜像 ORM ``_parse_entity`` 与 Excel ``parse_excel_columns`` 的 MRO 遍历:
|
|
39
|
+
自底向上(``reversed(cls.__mro__)``),子类约束覆盖父类同名字段。
|
|
40
|
+
支持两种声明形式:
|
|
41
|
+
1. 类属性 ``Constraint`` 描述符(如 ``name = NotBlank()``)。
|
|
42
|
+
2. 函数上的 ``__bean_constraint__`` 列表(``@NotBlank() def name(self): ...``)。
|
|
43
|
+
"""
|
|
44
|
+
collected: Dict[str, List[Constraint]] = {}
|
|
45
|
+
|
|
46
|
+
for base in reversed(cls.__mro__):
|
|
47
|
+
if base is object:
|
|
48
|
+
continue
|
|
49
|
+
for attr_name, value in vars(base).items():
|
|
50
|
+
if attr_name.startswith("__"):
|
|
51
|
+
continue
|
|
52
|
+
constraints: List[Constraint] = []
|
|
53
|
+
# 形式1:类属性 Constraint 描述符实例
|
|
54
|
+
if isinstance(value, Constraint):
|
|
55
|
+
if not value.attr_name:
|
|
56
|
+
value.attr_name = attr_name
|
|
57
|
+
constraints.append(value)
|
|
58
|
+
# 形式2:函数装饰器上的 __bean_constraint__ 列表
|
|
59
|
+
elif hasattr(value, "__bean_constraint__"):
|
|
60
|
+
clist = getattr(value, "__bean_constraint__")
|
|
61
|
+
if isinstance(clist, list):
|
|
62
|
+
for c in clist:
|
|
63
|
+
if isinstance(c, Constraint) and not c.attr_name:
|
|
64
|
+
c.attr_name = attr_name
|
|
65
|
+
constraints.append(c)
|
|
66
|
+
if constraints:
|
|
67
|
+
# 子类覆盖父类同名字段(与 ORM/Excel 解析一致)
|
|
68
|
+
collected[attr_name] = constraints
|
|
69
|
+
return collected
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _get_field_value(obj: Any, attr_name: str) -> Any:
|
|
73
|
+
"""从对象实例取字段值:优先 ``getattr``,失败则 None。"""
|
|
74
|
+
try:
|
|
75
|
+
return getattr(obj, attr_name)
|
|
76
|
+
except AttributeError:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class BeanValidator:
|
|
81
|
+
"""Bean Validation 校验器(静态方法风格,无状态,可直接调用)。
|
|
82
|
+
|
|
83
|
+
设计为无状态工具类,对齐 Jakarta ``Validator`` 接口的 ``validate`` 语义,
|
|
84
|
+
但简化为静态方法,避免在未接入 IoC 容器的场景下还需手动构造实例。
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def get_constraints(cls: Type) -> Dict[str, List[Constraint]]:
|
|
89
|
+
"""返回类上声明的字段约束映射(公开 API,便于调试/报告)。"""
|
|
90
|
+
return _collect_constraints(cls)
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def validate(obj: Any, groups: Optional[List[type]] = None) -> List[ConstraintViolation]:
|
|
94
|
+
"""校验对象实例,返回全部约束违反列表(通过则返回空列表)。
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
obj: 待校验对象。若是类,则按无实例处理(仅字段无值,仅 NotNull 类会触发)。
|
|
98
|
+
groups: 校验分组(对齐 Jakarta Validation groups)。当前实现:约束未声明 groups
|
|
99
|
+
时始终执行;声明了 groups 时仅当传入 groups 命中才执行。
|
|
100
|
+
(分组功能为兼容预留,约束默认不限定分组。)
|
|
101
|
+
"""
|
|
102
|
+
if obj is None:
|
|
103
|
+
return []
|
|
104
|
+
cls = obj if isinstance(obj, type) else type(obj)
|
|
105
|
+
constraints_map = _collect_constraints(cls)
|
|
106
|
+
|
|
107
|
+
violations: List[ConstraintViolation] = []
|
|
108
|
+
for attr_name, constraints in constraints_map.items():
|
|
109
|
+
value = None if isinstance(obj, type) else _get_field_value(obj, attr_name)
|
|
110
|
+
for constraint in constraints:
|
|
111
|
+
# 分组过滤:约束可选声明 groups
|
|
112
|
+
cgroups = getattr(constraint, "groups", None) or []
|
|
113
|
+
if cgroups:
|
|
114
|
+
if not groups or not any(g in cgroups for g in groups):
|
|
115
|
+
continue
|
|
116
|
+
msg = constraint.validate(value)
|
|
117
|
+
if msg is not None:
|
|
118
|
+
violations.append(ConstraintViolation(
|
|
119
|
+
attr_name=attr_name,
|
|
120
|
+
value=value,
|
|
121
|
+
constraint=constraint,
|
|
122
|
+
message=msg,
|
|
123
|
+
))
|
|
124
|
+
return violations
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
def validate_or_raise(obj: Any, groups: Optional[List[type]] = None) -> None:
|
|
128
|
+
"""校验对象实例,存在违反则抛出 ``ValidationError``;通过则无返回。"""
|
|
129
|
+
violations = BeanValidator.validate(obj, groups=groups)
|
|
130
|
+
if violations:
|
|
131
|
+
raise ValidationError(violations)
|
|
132
|
+
|
|
133
|
+
@staticmethod
|
|
134
|
+
def is_valid(obj: Any, groups: Optional[List[type]] = None) -> bool:
|
|
135
|
+
"""便捷判断:是否通过全部约束。"""
|
|
136
|
+
return not BeanValidator.validate(obj, groups=groups)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
__all__ = ["BeanValidator", "_collect_constraints"]
|
spring/web/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .web_context import WebApplicationContext
|
|
2
|
+
from .result import Result
|
|
3
|
+
from .interceptor import HandlerInterceptor, InterceptorRegistry
|
|
4
|
+
from .exception_handler import GlobalExceptionHandler
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"WebApplicationContext",
|
|
8
|
+
"Result",
|
|
9
|
+
"HandlerInterceptor",
|
|
10
|
+
"InterceptorRegistry",
|
|
11
|
+
"GlobalExceptionHandler",
|
|
12
|
+
]
|