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
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>
154 lines
4.8 KiB
Python
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}")
|