schemas.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637
  1. # -*- coding: utf-8 -*-
  2. """
  3. Schema-basierte Validierung für komplexe Datenstrukturen.
  4. Bietet deklarative Schema-Definitionen für die Validierung
  5. von Dictionaries und Dataclasses.
  6. """
  7. from __future__ import annotations
  8. import dataclasses
  9. from dataclasses import dataclass, field as dataclass_field
  10. from typing import Any, Callable, Generic, Sequence, Type, TypeVar, get_type_hints
  11. from trixy_core.utils.validation.errors import (
  12. FieldError,
  13. SchemaError,
  14. ValidationError,
  15. ValidationErrors,
  16. )
  17. from trixy_core.utils.validation.validators import (
  18. ListValidator,
  19. RequiredValidator,
  20. TypeValidator,
  21. Validator,
  22. )
  23. T = TypeVar("T")
  24. @dataclass
  25. class Field:
  26. """
  27. Definition eines Schema-Feldes.
  28. Beschreibt die Validierungsregeln für ein einzelnes Feld
  29. in einem Schema.
  30. """
  31. name: str
  32. """Name des Feldes."""
  33. validators: list[Validator] = dataclass_field(default_factory=list)
  34. """Liste von Validatoren für das Feld."""
  35. required: bool = True
  36. """Ob das Feld erforderlich ist."""
  37. default: Any = None
  38. """Standardwert falls nicht vorhanden."""
  39. default_factory: Callable[[], Any] | None = None
  40. """Factory-Funktion für den Standardwert."""
  41. description: str = ""
  42. """Beschreibung des Feldes."""
  43. alias: str | None = None
  44. """Alternativer Name im Input."""
  45. coerce: Callable[[Any], Any] | None = None
  46. """Funktion zur Typkonvertierung vor Validierung."""
  47. def get_default(self) -> Any:
  48. """
  49. Gibt den Standardwert zurück.
  50. Returns:
  51. Standardwert oder Ergebnis der Factory.
  52. """
  53. if self.default_factory:
  54. return self.default_factory()
  55. return self.default
  56. def validate(self, value: Any) -> None:
  57. """
  58. Validiert einen Wert mit allen Validatoren.
  59. Args:
  60. value: Der zu validierende Wert.
  61. Raises:
  62. ValidationError: Wenn Validierung fehlschlägt.
  63. """
  64. # Typkonvertierung falls konfiguriert
  65. if self.coerce and value is not None:
  66. try:
  67. value = self.coerce(value)
  68. except Exception as e:
  69. raise ValidationError(
  70. message=f"Konvertierung fehlgeschlagen: {e}",
  71. field=self.name,
  72. value=value,
  73. code="coercion_error",
  74. )
  75. # Alle Validatoren anwenden
  76. for validator in self.validators:
  77. validator.validate(value, self.name)
  78. @dataclass
  79. class NestedField(Field):
  80. """
  81. Feld für verschachtelte Schemas.
  82. Ermöglicht rekursive Validierung von komplexen Strukturen.
  83. """
  84. schema: "Schema | None" = None
  85. """Das verschachtelte Schema."""
  86. def validate(self, value: Any) -> None:
  87. """Validiert den Wert mit dem verschachtelten Schema."""
  88. super().validate(value)
  89. if self.schema and value is not None:
  90. try:
  91. self.schema.validate(value)
  92. except ValidationError as e:
  93. raise ValidationError(
  94. message=f"Verschachtelter Fehler: {e.message}",
  95. field=self.name,
  96. value=value,
  97. code="nested_error",
  98. errors=e.errors if hasattr(e, "errors") else [],
  99. )
  100. @dataclass
  101. class ListField(Field):
  102. """
  103. Feld für Listen mit Element-Validierung.
  104. Validiert Listen und deren einzelne Elemente.
  105. """
  106. item_validators: list[Validator] = dataclass_field(default_factory=list)
  107. """Validatoren für jedes Element."""
  108. item_schema: "Schema | None" = None
  109. """Schema für komplexe Element-Typen."""
  110. min_items: int | None = None
  111. """Minimale Anzahl Elemente."""
  112. max_items: int | None = None
  113. """Maximale Anzahl Elemente."""
  114. def validate(self, value: Any) -> None:
  115. """Validiert die Liste und deren Elemente."""
  116. super().validate(value)
  117. if value is None:
  118. return
  119. if not isinstance(value, (list, tuple)):
  120. raise ValidationError(
  121. message="Muss eine Liste sein",
  122. field=self.name,
  123. value=value,
  124. code="type_error",
  125. )
  126. # Längen-Prüfung
  127. if self.min_items is not None and len(value) < self.min_items:
  128. raise ValidationError(
  129. message=f"Mindestens {self.min_items} Elemente erforderlich",
  130. field=self.name,
  131. value=value,
  132. code="min_items",
  133. )
  134. if self.max_items is not None and len(value) > self.max_items:
  135. raise ValidationError(
  136. message=f"Höchstens {self.max_items} Elemente erlaubt",
  137. field=self.name,
  138. value=value,
  139. code="max_items",
  140. )
  141. # Element-Validierung
  142. errors = []
  143. for i, item in enumerate(value):
  144. item_field = f"{self.name}[{i}]"
  145. # Validatoren für Elemente
  146. for validator in self.item_validators:
  147. try:
  148. validator.validate(item, item_field)
  149. except ValidationError as e:
  150. errors.append(FieldError(
  151. field=item_field,
  152. message=e.message,
  153. value=item,
  154. code=e.code,
  155. ))
  156. # Schema für komplexe Elemente
  157. if self.item_schema:
  158. try:
  159. self.item_schema.validate(item)
  160. except ValidationErrors as e:
  161. for err in e.errors:
  162. errors.append(FieldError(
  163. field=f"{item_field}.{err.field}",
  164. message=err.message,
  165. value=err.value,
  166. code=err.code,
  167. ))
  168. except ValidationError as e:
  169. errors.append(FieldError(
  170. field=item_field,
  171. message=e.message,
  172. value=item,
  173. code=e.code,
  174. ))
  175. if errors:
  176. raise ValidationErrors(errors)
  177. class Schema:
  178. """
  179. Schema für strukturierte Datenvalidierung.
  180. Definiert die erwartete Struktur und Validierungsregeln
  181. für ein Dictionary oder Objekt.
  182. """
  183. def __init__(
  184. self,
  185. name: str = "",
  186. fields: Sequence[Field] | None = None,
  187. strict: bool = False,
  188. allow_extra: bool = True,
  189. ) -> None:
  190. """
  191. Initialisiert das Schema.
  192. Args:
  193. name: Name des Schemas.
  194. fields: Liste von Feld-Definitionen.
  195. strict: Ob alle Felder erforderlich sind (überschreibt Field.required).
  196. allow_extra: Ob zusätzliche Felder erlaubt sind.
  197. """
  198. self.name = name
  199. self._fields: dict[str, Field] = {}
  200. self._aliases: dict[str, str] = {}
  201. self.strict = strict
  202. self.allow_extra = allow_extra
  203. if fields:
  204. for f in fields:
  205. self.add_field(f)
  206. def add_field(self, field: Field) -> "Schema":
  207. """
  208. Fügt ein Feld zum Schema hinzu.
  209. Args:
  210. field: Die Feld-Definition.
  211. Returns:
  212. Self für Method-Chaining.
  213. """
  214. self._fields[field.name] = field
  215. if field.alias:
  216. self._aliases[field.alias] = field.name
  217. return self
  218. def field(
  219. self,
  220. name: str,
  221. *validators: Validator,
  222. required: bool = True,
  223. default: Any = None,
  224. description: str = "",
  225. ) -> "Schema":
  226. """
  227. Fügt ein einfaches Feld hinzu (Builder-Pattern).
  228. Args:
  229. name: Feldname.
  230. *validators: Validatoren für das Feld.
  231. required: Ob das Feld erforderlich ist.
  232. default: Standardwert.
  233. description: Beschreibung.
  234. Returns:
  235. Self für Method-Chaining.
  236. """
  237. self.add_field(Field(
  238. name=name,
  239. validators=list(validators),
  240. required=required,
  241. default=default,
  242. description=description,
  243. ))
  244. return self
  245. def nested(
  246. self,
  247. name: str,
  248. schema: "Schema",
  249. required: bool = True,
  250. description: str = "",
  251. ) -> "Schema":
  252. """
  253. Fügt ein verschachteltes Feld hinzu.
  254. Args:
  255. name: Feldname.
  256. schema: Das verschachtelte Schema.
  257. required: Ob das Feld erforderlich ist.
  258. description: Beschreibung.
  259. Returns:
  260. Self für Method-Chaining.
  261. """
  262. self.add_field(NestedField(
  263. name=name,
  264. schema=schema,
  265. required=required,
  266. description=description,
  267. ))
  268. return self
  269. def list_field(
  270. self,
  271. name: str,
  272. *item_validators: Validator,
  273. item_schema: "Schema | None" = None,
  274. min_items: int | None = None,
  275. max_items: int | None = None,
  276. required: bool = True,
  277. ) -> "Schema":
  278. """
  279. Fügt ein Listen-Feld hinzu.
  280. Args:
  281. name: Feldname.
  282. *item_validators: Validatoren für Elemente.
  283. item_schema: Schema für komplexe Elemente.
  284. min_items: Minimale Anzahl.
  285. max_items: Maximale Anzahl.
  286. required: Ob das Feld erforderlich ist.
  287. Returns:
  288. Self für Method-Chaining.
  289. """
  290. self.add_field(ListField(
  291. name=name,
  292. item_validators=list(item_validators),
  293. item_schema=item_schema,
  294. min_items=min_items,
  295. max_items=max_items,
  296. required=required,
  297. ))
  298. return self
  299. def _resolve_field_name(self, key: str) -> str | None:
  300. """Löst Aliase auf."""
  301. if key in self._fields:
  302. return key
  303. return self._aliases.get(key)
  304. def validate(self, data: dict[str, Any]) -> dict[str, Any]:
  305. """
  306. Validiert ein Dictionary gegen das Schema.
  307. Args:
  308. data: Die zu validierenden Daten.
  309. Returns:
  310. Validierte und normalisierte Daten.
  311. Raises:
  312. ValidationErrors: Wenn Validierung fehlschlägt.
  313. """
  314. if not isinstance(data, dict):
  315. raise ValidationError(
  316. message="Daten müssen ein Dictionary sein",
  317. field="",
  318. value=data,
  319. code="type_error",
  320. )
  321. errors: list[FieldError] = []
  322. result: dict[str, Any] = {}
  323. # Bekannte Felder verarbeiten
  324. for field_name, field_def in self._fields.items():
  325. # Wert aus data holen (mit Alias-Unterstützung)
  326. value = None
  327. found = False
  328. if field_name in data:
  329. value = data[field_name]
  330. found = True
  331. elif field_def.alias and field_def.alias in data:
  332. value = data[field_def.alias]
  333. found = True
  334. # Erforderlichkeit prüfen
  335. is_required = self.strict or field_def.required
  336. if not found:
  337. if is_required:
  338. errors.append(FieldError(
  339. field=field_name,
  340. message="Dieses Feld ist erforderlich",
  341. code="required",
  342. ))
  343. else:
  344. result[field_name] = field_def.get_default()
  345. continue
  346. # Typkonvertierung
  347. if field_def.coerce and value is not None:
  348. try:
  349. value = field_def.coerce(value)
  350. except Exception as e:
  351. errors.append(FieldError(
  352. field=field_name,
  353. message=f"Konvertierung fehlgeschlagen: {e}",
  354. value=value,
  355. code="coercion_error",
  356. ))
  357. continue
  358. # Validierung
  359. try:
  360. field_def.validate(value)
  361. result[field_name] = value
  362. except ValidationErrors as e:
  363. errors.extend(e.errors)
  364. except ValidationError as e:
  365. errors.append(FieldError(
  366. field=field_name,
  367. message=e.message,
  368. value=value,
  369. code=e.code,
  370. ))
  371. # Extra-Felder prüfen
  372. if not self.allow_extra:
  373. known_keys = set(self._fields.keys()) | set(self._aliases.keys())
  374. extra_keys = set(data.keys()) - known_keys
  375. for key in extra_keys:
  376. errors.append(FieldError(
  377. field=key,
  378. message="Unbekanntes Feld",
  379. value=data[key],
  380. code="extra_field",
  381. ))
  382. else:
  383. # Extra-Felder übernehmen
  384. known_keys = set(self._fields.keys()) | set(self._aliases.keys())
  385. for key in data:
  386. if key not in known_keys:
  387. result[key] = data[key]
  388. if errors:
  389. raise ValidationErrors(errors)
  390. return result
  391. def is_valid(self, data: dict[str, Any]) -> bool:
  392. """
  393. Prüft, ob Daten valide sind.
  394. Args:
  395. data: Die zu prüfenden Daten.
  396. Returns:
  397. True wenn valide.
  398. """
  399. try:
  400. self.validate(data)
  401. return True
  402. except (ValidationError, ValidationErrors):
  403. return False
  404. def get_errors(self, data: dict[str, Any]) -> list[FieldError]:
  405. """
  406. Gibt alle Validierungsfehler zurück.
  407. Args:
  408. data: Die zu prüfenden Daten.
  409. Returns:
  410. Liste von FieldErrors.
  411. """
  412. try:
  413. self.validate(data)
  414. return []
  415. except ValidationErrors as e:
  416. return e.errors
  417. except ValidationError as e:
  418. return e.errors
  419. def validate_dict(
  420. data: dict[str, Any],
  421. schema: Schema,
  422. ) -> dict[str, Any]:
  423. """
  424. Validiert ein Dictionary mit einem Schema.
  425. Args:
  426. data: Die zu validierenden Daten.
  427. schema: Das Schema für die Validierung.
  428. Returns:
  429. Validierte Daten.
  430. Raises:
  431. ValidationErrors: Wenn Validierung fehlschlägt.
  432. """
  433. return schema.validate(data)
  434. def validate_dataclass(
  435. data: dict[str, Any],
  436. dataclass_type: Type[T],
  437. strict: bool = False,
  438. ) -> T:
  439. """
  440. Validiert Daten und konvertiert sie in eine Dataclass.
  441. Verwendet die Type-Hints der Dataclass für automatische
  442. Validierung.
  443. Args:
  444. data: Die zu validierenden Daten.
  445. dataclass_type: Die Ziel-Dataclass.
  446. strict: Ob alle Felder erforderlich sind.
  447. Returns:
  448. Instanz der Dataclass.
  449. Raises:
  450. ValidationErrors: Wenn Validierung fehlschlägt.
  451. """
  452. if not dataclasses.is_dataclass(dataclass_type):
  453. raise SchemaError(
  454. message="Typ muss eine Dataclass sein",
  455. schema_name=dataclass_type.__name__,
  456. )
  457. # Schema aus Dataclass generieren
  458. schema = Schema(name=dataclass_type.__name__, strict=strict)
  459. type_hints = get_type_hints(dataclass_type)
  460. for dc_field in dataclasses.fields(dataclass_type):
  461. field_type = type_hints.get(dc_field.name, Any)
  462. # Erforderlichkeit aus Default ableiten
  463. has_default = (
  464. dc_field.default is not dataclasses.MISSING
  465. or dc_field.default_factory is not dataclasses.MISSING
  466. )
  467. validators: list[Validator] = []
  468. # Typ-Validator hinzufügen
  469. if field_type is not Any:
  470. try:
  471. validators.append(TypeValidator(field_type, allow_none=True))
  472. except Exception:
  473. pass # Komplexe Typen ignorieren
  474. schema.add_field(Field(
  475. name=dc_field.name,
  476. validators=validators,
  477. required=not has_default,
  478. default=(
  479. dc_field.default
  480. if dc_field.default is not dataclasses.MISSING
  481. else None
  482. ),
  483. default_factory=(
  484. dc_field.default_factory
  485. if dc_field.default_factory is not dataclasses.MISSING
  486. else None
  487. ),
  488. ))
  489. # Validieren
  490. validated = schema.validate(data)
  491. # Dataclass erstellen
  492. return dataclass_type(**validated)
  493. # Vordefinierte Schemas für häufige Anwendungsfälle
  494. def email_field(name: str = "email", required: bool = True) -> Field:
  495. """Erstellt ein E-Mail-Feld mit Pattern-Validierung."""
  496. from trixy_core.utils.validation.validators import PatternValidator
  497. return Field(
  498. name=name,
  499. validators=[
  500. PatternValidator(r"^[^@\s]+@[^@\s]+\.[^@\s]+$"),
  501. ],
  502. required=required,
  503. description="E-Mail-Adresse",
  504. )
  505. def url_field(name: str = "url", required: bool = True) -> Field:
  506. """Erstellt ein URL-Feld mit Pattern-Validierung."""
  507. from trixy_core.utils.validation.validators import PatternValidator
  508. return Field(
  509. name=name,
  510. validators=[
  511. PatternValidator(r"^https?://[^\s/$.?#].[^\s]*$"),
  512. ],
  513. required=required,
  514. description="URL",
  515. )
  516. def phone_field(name: str = "phone", required: bool = True) -> Field:
  517. """Erstellt ein Telefon-Feld mit Pattern-Validierung."""
  518. from trixy_core.utils.validation.validators import PatternValidator
  519. return Field(
  520. name=name,
  521. validators=[
  522. PatternValidator(r"^\+?[\d\s\-()]+$"),
  523. ],
  524. required=required,
  525. description="Telefonnummer",
  526. )