validation.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678
  1. # -*- coding: utf-8 -*-
  2. """
  3. Validierungs-Middleware für Events.
  4. Validiert Event-Daten vor der Verarbeitung.
  5. """
  6. from __future__ import annotations
  7. from dataclasses import dataclass, field
  8. from typing import TYPE_CHECKING, Any, Callable, Type
  9. from trixy_core.events.middleware.base import (
  10. EventMiddleware,
  11. MiddlewareAction,
  12. MiddlewareContext,
  13. MiddlewareResult,
  14. )
  15. if TYPE_CHECKING:
  16. from trixy_core.events.event_data.base import EventData
  17. class EventValidationError(Exception):
  18. """
  19. Fehler bei der Event-Validierung.
  20. """
  21. def __init__(
  22. self,
  23. message: str,
  24. event_name: str,
  25. field: str | None = None,
  26. value: Any = None,
  27. ) -> None:
  28. """
  29. Initialisiert den Fehler.
  30. Args:
  31. message: Fehlermeldung.
  32. event_name: Name des Events.
  33. field: Betroffenes Feld.
  34. value: Ungültiger Wert.
  35. """
  36. super().__init__(message)
  37. self.event_name = event_name
  38. self.field = field
  39. self.value = value
  40. @dataclass
  41. class ValidationRule:
  42. """
  43. Regel für Event-Validierung.
  44. """
  45. field: str
  46. """Feldname (oder "*" für Event-Level)."""
  47. validator: Callable[[Any], bool]
  48. """Validierungsfunktion."""
  49. message: str = "Validierung fehlgeschlagen"
  50. """Fehlermeldung."""
  51. required: bool = False
  52. """Ob das Feld vorhanden sein muss."""
  53. allow_none: bool = True
  54. """Ob None erlaubt ist."""
  55. @dataclass
  56. class EventSchema:
  57. """
  58. Schema für Event-Validierung.
  59. """
  60. event_name: str
  61. """Name des Events (oder "*" für alle)."""
  62. rules: list[ValidationRule] = field(default_factory=list)
  63. """Validierungsregeln."""
  64. event_class: Type["EventData"] | None = None
  65. """Optionale Event-Klasse für Typ-Prüfung."""
  66. custom_validator: Callable[["EventData"], bool] | None = None
  67. """Optionale benutzerdefinierte Validierungsfunktion."""
  68. def add_rule(
  69. self,
  70. field: str,
  71. validator: Callable[[Any], bool],
  72. message: str = "Validierung fehlgeschlagen",
  73. required: bool = False,
  74. allow_none: bool = True,
  75. ) -> "EventSchema":
  76. """
  77. Fügt eine Regel hinzu (Fluent-API).
  78. Args:
  79. field: Feldname.
  80. validator: Validierungsfunktion.
  81. message: Fehlermeldung.
  82. required: Ob erforderlich.
  83. allow_none: Ob None erlaubt.
  84. Returns:
  85. Self für Verkettung.
  86. """
  87. self.rules.append(ValidationRule(
  88. field=field,
  89. validator=validator,
  90. message=message,
  91. required=required,
  92. allow_none=allow_none,
  93. ))
  94. return self
  95. class ValidationMiddleware(EventMiddleware):
  96. """
  97. Middleware für Event-Validierung.
  98. Validiert Event-Daten gegen definierte Schemas und Regeln.
  99. Example:
  100. middleware = ValidationMiddleware()
  101. # Schema definieren
  102. schema = EventSchema("user_login")
  103. schema.add_rule("username", lambda x: len(x) >= 3, "Username zu kurz")
  104. schema.add_rule("password", lambda x: len(x) >= 8, "Passwort zu kurz")
  105. middleware.register_schema(schema)
  106. # Oder mit Fluent-API
  107. middleware.for_event("user_register") \\
  108. .require("email") \\
  109. .rule("email", lambda x: "@" in x, "Ungültige E-Mail") \\
  110. .rule("age", lambda x: x >= 18, "Zu jung")
  111. """
  112. def __init__(
  113. self,
  114. name: str = "ValidationMiddleware",
  115. enabled: bool = True,
  116. strict: bool = True,
  117. on_validation_error: Callable[[EventValidationError], None] | None = None,
  118. ) -> None:
  119. """
  120. Initialisiert die Middleware.
  121. Args:
  122. name: Name der Middleware.
  123. enabled: Ob aktiv.
  124. strict: Ob bei Fehlern abgebrochen wird.
  125. on_validation_error: Callback bei Validierungsfehlern.
  126. """
  127. super().__init__(name, enabled)
  128. self._strict = strict
  129. self._on_validation_error = on_validation_error
  130. self._schemas: dict[str, EventSchema] = {}
  131. self._global_rules: list[ValidationRule] = []
  132. def register_schema(self, schema: EventSchema) -> None:
  133. """
  134. Registriert ein Event-Schema.
  135. Args:
  136. schema: Das Schema.
  137. """
  138. self._schemas[schema.event_name] = schema
  139. def for_event(self, event_name: str) -> "SchemaBuilder":
  140. """
  141. Startet einen Schema-Builder für ein Event.
  142. Args:
  143. event_name: Name des Events.
  144. Returns:
  145. SchemaBuilder für Fluent-API.
  146. """
  147. schema = EventSchema(event_name=event_name)
  148. self._schemas[event_name] = schema
  149. return SchemaBuilder(schema, self)
  150. def add_global_rule(
  151. self,
  152. field: str,
  153. validator: Callable[[Any], bool],
  154. message: str = "Validierung fehlgeschlagen",
  155. ) -> None:
  156. """
  157. Fügt eine globale Regel hinzu.
  158. Args:
  159. field: Feldname.
  160. validator: Validierungsfunktion.
  161. message: Fehlermeldung.
  162. """
  163. self._global_rules.append(ValidationRule(
  164. field=field,
  165. validator=validator,
  166. message=message,
  167. ))
  168. def _get_field_value(
  169. self,
  170. event_data: "EventData",
  171. field: str,
  172. ) -> tuple[bool, Any]:
  173. """
  174. Holt einen Feldwert aus den Event-Daten.
  175. Args:
  176. event_data: Die Event-Daten.
  177. field: Feldname.
  178. Returns:
  179. Tupel (gefunden, Wert).
  180. """
  181. # Direktes Attribut
  182. if hasattr(event_data, field):
  183. return True, getattr(event_data, field)
  184. # In Metadata
  185. if field in event_data.metadata:
  186. return True, event_data.metadata[field]
  187. return False, None
  188. def _validate_rule(
  189. self,
  190. event_data: "EventData",
  191. event_name: str,
  192. rule: ValidationRule,
  193. ) -> EventValidationError | None:
  194. """
  195. Validiert eine einzelne Regel.
  196. Args:
  197. event_data: Die Event-Daten.
  198. event_name: Name des Events.
  199. rule: Die Regel.
  200. Returns:
  201. Fehler oder None.
  202. """
  203. if rule.field == "*":
  204. # Event-Level-Validierung
  205. try:
  206. if not rule.validator(event_data):
  207. return EventValidationError(
  208. rule.message,
  209. event_name,
  210. )
  211. except Exception as e:
  212. return EventValidationError(
  213. f"Validierungsfehler: {e}",
  214. event_name,
  215. )
  216. return None
  217. found, value = self._get_field_value(event_data, rule.field)
  218. # Required-Prüfung
  219. if rule.required and not found:
  220. return EventValidationError(
  221. f"Pflichtfeld fehlt: {rule.field}",
  222. event_name,
  223. field=rule.field,
  224. )
  225. if not found:
  226. return None
  227. # None-Prüfung
  228. if value is None:
  229. if not rule.allow_none:
  230. return EventValidationError(
  231. f"Feld darf nicht None sein: {rule.field}",
  232. event_name,
  233. field=rule.field,
  234. value=value,
  235. )
  236. return None
  237. # Validator ausführen
  238. try:
  239. if not rule.validator(value):
  240. return EventValidationError(
  241. rule.message,
  242. event_name,
  243. field=rule.field,
  244. value=value,
  245. )
  246. except Exception as e:
  247. return EventValidationError(
  248. f"Validierungsfehler ({rule.field}): {e}",
  249. event_name,
  250. field=rule.field,
  251. value=value,
  252. )
  253. return None
  254. def _validate_event(
  255. self,
  256. event_name: str,
  257. event_data: "EventData",
  258. ) -> list[EventValidationError]:
  259. """
  260. Validiert ein Event.
  261. Args:
  262. event_name: Name des Events.
  263. event_data: Die Event-Daten.
  264. Returns:
  265. Liste von Fehlern.
  266. """
  267. errors: list[EventValidationError] = []
  268. # Globale Regeln
  269. for rule in self._global_rules:
  270. error = self._validate_rule(event_data, event_name, rule)
  271. if error:
  272. errors.append(error)
  273. # Event-spezifisches Schema
  274. schema = self._schemas.get(event_name) or self._schemas.get("*")
  275. if schema:
  276. # Event-Klassen-Prüfung
  277. if schema.event_class and not isinstance(event_data, schema.event_class):
  278. errors.append(EventValidationError(
  279. f"Falscher Event-Typ: erwartet {schema.event_class.__name__}",
  280. event_name,
  281. ))
  282. # Regeln prüfen
  283. for rule in schema.rules:
  284. error = self._validate_rule(event_data, event_name, rule)
  285. if error:
  286. errors.append(error)
  287. # Custom Validator
  288. if schema.custom_validator:
  289. try:
  290. if not schema.custom_validator(event_data):
  291. errors.append(EventValidationError(
  292. "Benutzerdefinierte Validierung fehlgeschlagen",
  293. event_name,
  294. ))
  295. except Exception as e:
  296. errors.append(EventValidationError(
  297. f"Validierungsfehler: {e}",
  298. event_name,
  299. ))
  300. return errors
  301. async def before(
  302. self,
  303. context: MiddlewareContext,
  304. ) -> MiddlewareResult:
  305. """
  306. Validiert Event-Daten vor der Verarbeitung.
  307. Args:
  308. context: Der Middleware-Kontext.
  309. Returns:
  310. Ergebnis mit CONTINUE oder ABORT.
  311. """
  312. errors = self._validate_event(context.event_name, context.event_data)
  313. if errors:
  314. # Speichere Fehler im Kontext
  315. context.set_property("validation_errors", errors)
  316. # Callbacks aufrufen
  317. if self._on_validation_error:
  318. for error in errors:
  319. self._on_validation_error(error)
  320. if self._strict:
  321. # In striktem Modus: Abbrechen
  322. context.errors.extend(errors)
  323. return MiddlewareResult(
  324. action=MiddlewareAction.ABORT,
  325. error=errors[0],
  326. )
  327. return MiddlewareResult()
  328. class SchemaBuilder:
  329. """
  330. Builder für Event-Schemas (Fluent-API).
  331. """
  332. def __init__(
  333. self,
  334. schema: EventSchema,
  335. middleware: ValidationMiddleware,
  336. ) -> None:
  337. """
  338. Initialisiert den Builder.
  339. Args:
  340. schema: Das Schema.
  341. middleware: Die Middleware.
  342. """
  343. self._schema = schema
  344. self._middleware = middleware
  345. def rule(
  346. self,
  347. field: str,
  348. validator: Callable[[Any], bool],
  349. message: str = "Validierung fehlgeschlagen",
  350. ) -> "SchemaBuilder":
  351. """
  352. Fügt eine Regel hinzu.
  353. Args:
  354. field: Feldname.
  355. validator: Validierungsfunktion.
  356. message: Fehlermeldung.
  357. Returns:
  358. Self für Verkettung.
  359. """
  360. self._schema.add_rule(field, validator, message)
  361. return self
  362. def require(
  363. self,
  364. field: str,
  365. message: str | None = None,
  366. ) -> "SchemaBuilder":
  367. """
  368. Markiert ein Feld als erforderlich.
  369. Args:
  370. field: Feldname.
  371. message: Optionale Fehlermeldung.
  372. Returns:
  373. Self für Verkettung.
  374. """
  375. self._schema.add_rule(
  376. field=field,
  377. validator=lambda x: True, # Immer gültig wenn vorhanden
  378. message=message or f"Pflichtfeld fehlt: {field}",
  379. required=True,
  380. )
  381. return self
  382. def not_none(
  383. self,
  384. field: str,
  385. message: str | None = None,
  386. ) -> "SchemaBuilder":
  387. """
  388. Verbietet None für ein Feld.
  389. Args:
  390. field: Feldname.
  391. message: Optionale Fehlermeldung.
  392. Returns:
  393. Self für Verkettung.
  394. """
  395. self._schema.add_rule(
  396. field=field,
  397. validator=lambda x: True,
  398. message=message or f"Feld darf nicht None sein: {field}",
  399. allow_none=False,
  400. )
  401. return self
  402. def type_check(
  403. self,
  404. field: str,
  405. expected_type: type,
  406. message: str | None = None,
  407. ) -> "SchemaBuilder":
  408. """
  409. Prüft den Typ eines Feldes.
  410. Args:
  411. field: Feldname.
  412. expected_type: Erwarteter Typ.
  413. message: Optionale Fehlermeldung.
  414. Returns:
  415. Self für Verkettung.
  416. """
  417. self._schema.add_rule(
  418. field=field,
  419. validator=lambda x: isinstance(x, expected_type),
  420. message=message or f"Falscher Typ für {field}: erwartet {expected_type.__name__}",
  421. )
  422. return self
  423. def range(
  424. self,
  425. field: str,
  426. min_val: float | None = None,
  427. max_val: float | None = None,
  428. message: str | None = None,
  429. ) -> "SchemaBuilder":
  430. """
  431. Prüft einen Wertebereich.
  432. Args:
  433. field: Feldname.
  434. min_val: Minimalwert.
  435. max_val: Maximalwert.
  436. message: Optionale Fehlermeldung.
  437. Returns:
  438. Self für Verkettung.
  439. """
  440. def validator(x: Any) -> bool:
  441. if min_val is not None and x < min_val:
  442. return False
  443. if max_val is not None and x > max_val:
  444. return False
  445. return True
  446. range_str = ""
  447. if min_val is not None and max_val is not None:
  448. range_str = f"[{min_val}, {max_val}]"
  449. elif min_val is not None:
  450. range_str = f">= {min_val}"
  451. elif max_val is not None:
  452. range_str = f"<= {max_val}"
  453. self._schema.add_rule(
  454. field=field,
  455. validator=validator,
  456. message=message or f"Wert für {field} außerhalb des Bereichs {range_str}",
  457. )
  458. return self
  459. def length(
  460. self,
  461. field: str,
  462. min_len: int | None = None,
  463. max_len: int | None = None,
  464. message: str | None = None,
  465. ) -> "SchemaBuilder":
  466. """
  467. Prüft die Länge eines Feldes.
  468. Args:
  469. field: Feldname.
  470. min_len: Minimale Länge.
  471. max_len: Maximale Länge.
  472. message: Optionale Fehlermeldung.
  473. Returns:
  474. Self für Verkettung.
  475. """
  476. def validator(x: Any) -> bool:
  477. try:
  478. length = len(x)
  479. if min_len is not None and length < min_len:
  480. return False
  481. if max_len is not None and length > max_len:
  482. return False
  483. return True
  484. except TypeError:
  485. return False
  486. self._schema.add_rule(
  487. field=field,
  488. validator=validator,
  489. message=message or f"Ungültige Länge für {field}",
  490. )
  491. return self
  492. def pattern(
  493. self,
  494. field: str,
  495. regex: str,
  496. message: str | None = None,
  497. ) -> "SchemaBuilder":
  498. """
  499. Prüft ein Regex-Pattern.
  500. Args:
  501. field: Feldname.
  502. regex: Regex-Pattern.
  503. message: Optionale Fehlermeldung.
  504. Returns:
  505. Self für Verkettung.
  506. """
  507. import re
  508. pattern = re.compile(regex)
  509. self._schema.add_rule(
  510. field=field,
  511. validator=lambda x: bool(pattern.match(str(x))),
  512. message=message or f"Wert für {field} entspricht nicht dem Pattern",
  513. )
  514. return self
  515. def one_of(
  516. self,
  517. field: str,
  518. values: list[Any],
  519. message: str | None = None,
  520. ) -> "SchemaBuilder":
  521. """
  522. Prüft, ob der Wert in einer Liste ist.
  523. Args:
  524. field: Feldname.
  525. values: Erlaubte Werte.
  526. message: Optionale Fehlermeldung.
  527. Returns:
  528. Self für Verkettung.
  529. """
  530. self._schema.add_rule(
  531. field=field,
  532. validator=lambda x: x in values,
  533. message=message or f"Wert für {field} nicht erlaubt: {values}",
  534. )
  535. return self
  536. def custom(
  537. self,
  538. validator: Callable[["EventData"], bool],
  539. ) -> "SchemaBuilder":
  540. """
  541. Setzt einen benutzerdefinierten Validator.
  542. Args:
  543. validator: Validierungsfunktion.
  544. Returns:
  545. Self für Verkettung.
  546. """
  547. self._schema.custom_validator = validator
  548. return self
  549. def event_class(
  550. self,
  551. cls: Type["EventData"],
  552. ) -> "SchemaBuilder":
  553. """
  554. Setzt die erwartete Event-Klasse.
  555. Args:
  556. cls: Die Event-Klasse.
  557. Returns:
  558. Self für Verkettung.
  559. """
  560. self._schema.event_class = cls
  561. return self
  562. def build(self) -> EventSchema:
  563. """
  564. Gibt das fertige Schema zurück.
  565. Returns:
  566. Das Schema.
  567. """
  568. return self._schema