Kairo-Jinkendo/backend/entity_fields/validation.py
Lars a6fc86025c
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 1m54s
Test Suite / lint-backend (push) Successful in 2s
Test Suite / compose-smoke (push) Has been skipped
Test Suite / k6 /api/health Baseline (push) Successful in 19s
Test Suite / playwright-smoke (push) Successful in 15s
AP1.10b/10c: Entity Field System mit dynamischen Vorhaben-Zusatzfeldern.
EFS-Tabellen, Registry-Sync und Fields-API ermöglichen archetyp-spezifische Felder; Frontend rendert sie im Profil-Modal per FieldRenderer.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 13:53:07 +02:00

154 lines
4.8 KiB
Python

"""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}")