"""Validierung dynamischer Feldwerte gegen Definitionen — AP1.10b/10c.""" from __future__ import annotations import re from datetime import date, datetime from typing import Any, Optional from urllib.parse import urlparse VALID_FIELD_TYPES = frozenset( {"text", "longtext", "number", "boolean", "date", "enum", "actor_ref", "url"} ) _URL_SCHEME_RE = re.compile(r"^[a-zA-Z][a-zA-Z0-9+.-]*:") def _parse_date(value: str) -> date: try: return date.fromisoformat(value) except ValueError as exc: raise ValueError(f"Ungültiges Datum: {value}") from exc def _validate_url(value: str) -> None: if not value.strip(): return parsed = urlparse(value.strip()) if parsed.scheme and parsed.netloc: return if _URL_SCHEME_RE.match(value.strip()): return raise ValueError(f"Ungültige URL: {value}") def normalize_field_value( *, field_type: str, raw_value: Any, validation_json: Optional[dict[str, Any]] = None, ) -> dict[str, Any]: """Wert normalisieren und in DB-Spalten aufteilen.""" validation_json = validation_json or {} if field_type in {"text", "longtext", "url", "actor_ref", "enum"}: if raw_value is None: text = "" elif not isinstance(raw_value, str): raise ValueError(f"Feld erwartet Text, erhalten: {type(raw_value).__name__}") else: text = raw_value.strip() if field_type == "url" and text: _validate_url(text) if field_type == "enum" and text: options = validation_json.get("options") or [] if options and text not in options: raise ValueError(f"Ungültiger Enum-Wert: {text}") return { "value_json": None, "value_text": text, "value_number": None, "value_boolean": None, "value_date": None, "api_value": text, } if field_type == "number": if raw_value is None or raw_value == "": return { "value_json": None, "value_text": "", "value_number": None, "value_boolean": None, "value_date": None, "api_value": None, } try: number = float(raw_value) except (TypeError, ValueError) as exc: raise ValueError(f"Ungültige Zahl: {raw_value}") from exc return { "value_json": None, "value_text": str(number), "value_number": number, "value_boolean": None, "value_date": None, "api_value": number, } if field_type == "boolean": if raw_value is None: bool_val = False elif isinstance(raw_value, bool): bool_val = raw_value elif isinstance(raw_value, str): lowered = raw_value.strip().lower() if lowered in {"true", "1", "yes", "on"}: bool_val = True elif lowered in {"false", "0", "no", "off", ""}: bool_val = False else: raise ValueError(f"Ungültiger Boolean-Wert: {raw_value}") else: bool_val = bool(raw_value) return { "value_json": None, "value_text": "true" if bool_val else "false", "value_number": None, "value_boolean": bool_val, "value_date": None, "api_value": bool_val, } if field_type == "date": if raw_value is None or raw_value == "": return { "value_json": None, "value_text": "", "value_number": None, "value_boolean": None, "value_date": None, "api_value": None, } if isinstance(raw_value, date) and not isinstance(raw_value, datetime): parsed = raw_value elif isinstance(raw_value, str): parsed = _parse_date(raw_value) else: raise ValueError(f"Ungültiges Datum: {raw_value}") iso = parsed.isoformat() return { "value_json": None, "value_text": iso, "value_number": None, "value_boolean": None, "value_date": parsed, "api_value": iso, } raise ValueError(f"Unbekannter Feldtyp: {field_type}") def validate_required_fields( *, definitions: list[dict[str, Any]], values: dict[str, Any], ) -> None: for definition in definitions: if not definition.get("required"): continue key = definition["field_key"] raw = values.get(key) if raw is None or (isinstance(raw, str) and not raw.strip()): raise ValueError(f"Pflichtfeld fehlt: {definition.get('label') or key}")