|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
import inspect
|
|
|
import json
|
|
|
from collections.abc import Awaitable, Callable, Iterable
|
|
|
+from copy import deepcopy
|
|
|
from dataclasses import dataclass, replace
|
|
|
from typing import Any
|
|
|
|
|
|
@@ -130,7 +131,7 @@ class EventKernel:
|
|
|
|
|
|
resolved_request = replace(
|
|
|
request,
|
|
|
- arguments=resolved.arguments,
|
|
|
+ arguments=deepcopy(resolved.arguments),
|
|
|
raw_arguments=resolved.raw_arguments,
|
|
|
)
|
|
|
try:
|
|
|
@@ -155,6 +156,17 @@ class EventKernel:
|
|
|
error="event handler returned non-object payload",
|
|
|
used_fallback=used_fallback,
|
|
|
)
|
|
|
+ try:
|
|
|
+ payload = self._strict_json_copy(payload)
|
|
|
+ except (TypeError, ValueError):
|
|
|
+ return self._result(
|
|
|
+ definition,
|
|
|
+ request,
|
|
|
+ EventStatus.HANDLER_ERROR,
|
|
|
+ resolved=resolved,
|
|
|
+ error="event handler returned non-JSON payload",
|
|
|
+ used_fallback=used_fallback,
|
|
|
+ )
|
|
|
return self._result(
|
|
|
definition,
|
|
|
request,
|
|
|
@@ -207,7 +219,7 @@ class EventKernel:
|
|
|
)
|
|
|
resolved_request = replace(
|
|
|
request,
|
|
|
- arguments=resolved.arguments,
|
|
|
+ arguments=deepcopy(resolved.arguments),
|
|
|
raw_arguments=resolved.raw_arguments,
|
|
|
)
|
|
|
try:
|
|
|
@@ -238,6 +250,16 @@ class EventKernel:
|
|
|
resolved=resolved,
|
|
|
error="event handler returned non-object payload",
|
|
|
)
|
|
|
+ try:
|
|
|
+ payload = self._strict_json_copy(payload)
|
|
|
+ except (TypeError, ValueError):
|
|
|
+ return self._result(
|
|
|
+ definition,
|
|
|
+ request,
|
|
|
+ EventStatus.HANDLER_ERROR,
|
|
|
+ resolved=resolved,
|
|
|
+ error="event handler returned non-JSON payload",
|
|
|
+ )
|
|
|
return self._result(
|
|
|
definition,
|
|
|
request,
|
|
|
@@ -258,7 +280,7 @@ class EventKernel:
|
|
|
event_name=request.name,
|
|
|
status=EventStatus.UNKNOWN,
|
|
|
source=request.source,
|
|
|
- arguments=dict(request.arguments),
|
|
|
+ arguments=deepcopy(request.arguments),
|
|
|
raw_arguments=request.raw_arguments,
|
|
|
error="unknown event",
|
|
|
)
|
|
|
@@ -269,7 +291,7 @@ class EventKernel:
|
|
|
EventStatus.DISABLED,
|
|
|
resolved=ResolvedEventArguments(
|
|
|
event_name=request.name,
|
|
|
- arguments=dict(request.arguments),
|
|
|
+ arguments=deepcopy(request.arguments),
|
|
|
raw_arguments=request.raw_arguments,
|
|
|
),
|
|
|
error="event disabled",
|
|
|
@@ -286,7 +308,7 @@ class EventKernel:
|
|
|
return (
|
|
|
ResolvedEventArguments(
|
|
|
event_name=request.name,
|
|
|
- arguments=dict(request.arguments),
|
|
|
+ arguments=deepcopy(request.arguments),
|
|
|
raw_arguments=request.raw_arguments,
|
|
|
),
|
|
|
True,
|
|
|
@@ -316,8 +338,9 @@ class EventKernel:
|
|
|
request,
|
|
|
"event argument resolver returned invalid payload",
|
|
|
)
|
|
|
+ copied_arguments = deepcopy(arguments)
|
|
|
try:
|
|
|
- raw_arguments = self._json_arguments(arguments)
|
|
|
+ raw_arguments = self._json_arguments(copied_arguments)
|
|
|
except (TypeError, ValueError) as exc:
|
|
|
return None, False, self._resolution_error(
|
|
|
definition,
|
|
|
@@ -327,7 +350,7 @@ class EventKernel:
|
|
|
return (
|
|
|
ResolvedEventArguments(
|
|
|
event_name=request.name,
|
|
|
- arguments=dict(arguments),
|
|
|
+ arguments=copied_arguments,
|
|
|
raw_arguments=raw_arguments,
|
|
|
),
|
|
|
complete,
|
|
|
@@ -393,17 +416,27 @@ class EventKernel:
|
|
|
resolved=value,
|
|
|
used_fallback=True,
|
|
|
)
|
|
|
- return value, None
|
|
|
+ return (
|
|
|
+ ResolvedEventArguments(
|
|
|
+ event_name=value.event_name,
|
|
|
+ arguments=deepcopy(value.arguments),
|
|
|
+ raw_arguments=value.raw_arguments,
|
|
|
+ ),
|
|
|
+ None,
|
|
|
+ )
|
|
|
|
|
|
def _validate(
|
|
|
self,
|
|
|
definition: EventDefinition,
|
|
|
arguments: dict[str, Any],
|
|
|
) -> _ValidationResult:
|
|
|
- validator = self.registry.validator(definition.name)
|
|
|
- assert validator is not None
|
|
|
try:
|
|
|
- errors = list(validator.iter_errors(arguments))
|
|
|
+ errors = list(
|
|
|
+ self.registry.iter_validation_errors(
|
|
|
+ definition.name,
|
|
|
+ arguments,
|
|
|
+ )
|
|
|
+ )
|
|
|
except Exception as exc:
|
|
|
return _ValidationResult(
|
|
|
status=EventStatus.DEFINITION_ERROR,
|
|
|
@@ -411,7 +444,11 @@ class EventKernel:
|
|
|
)
|
|
|
if not errors:
|
|
|
return _ValidationResult()
|
|
|
- missing_errors = [error for error in errors if error.validator == "required"]
|
|
|
+ missing_errors = [
|
|
|
+ required_error
|
|
|
+ for error in errors
|
|
|
+ for required_error in _required_errors(error)
|
|
|
+ ]
|
|
|
error = missing_errors[0] if missing_errors else errors[0]
|
|
|
return _ValidationResult(
|
|
|
status=EventStatus.INVALID_ARGUMENTS,
|
|
|
@@ -450,7 +487,7 @@ class EventKernel:
|
|
|
resolved=resolved
|
|
|
or ResolvedEventArguments(
|
|
|
event_name=request.name,
|
|
|
- arguments=dict(request.arguments),
|
|
|
+ arguments=deepcopy(request.arguments),
|
|
|
raw_arguments=request.raw_arguments,
|
|
|
),
|
|
|
error=error,
|
|
|
@@ -473,9 +510,9 @@ class EventKernel:
|
|
|
event_name=request.name,
|
|
|
status=status,
|
|
|
source=request.source,
|
|
|
- arguments=dict(resolved.arguments),
|
|
|
+ arguments=deepcopy(resolved.arguments),
|
|
|
raw_arguments=resolved.raw_arguments,
|
|
|
- payload=payload or {},
|
|
|
+ payload=deepcopy(payload or {}),
|
|
|
error=error,
|
|
|
used_fallback=used_fallback,
|
|
|
result_policy=definition.result_policy,
|
|
|
@@ -496,11 +533,27 @@ class EventKernel:
|
|
|
allow_nan=False,
|
|
|
)
|
|
|
|
|
|
+ def _strict_json_copy(self, value: dict[str, Any]) -> dict[str, Any]:
|
|
|
+ copied = json.loads(
|
|
|
+ self._json_arguments(value),
|
|
|
+ parse_constant=_reject_json_constant,
|
|
|
+ )
|
|
|
+ if not isinstance(copied, dict) or not _json_values_equal(copied, value):
|
|
|
+ raise ValueError("JSON round-trip changed payload")
|
|
|
+ return copied
|
|
|
+
|
|
|
|
|
|
def _reject_json_constant(value: str) -> None:
|
|
|
raise ValueError(f"non-standard JSON constant: {value}")
|
|
|
|
|
|
|
|
|
+def _required_errors(error: ValidationError) -> Iterable[ValidationError]:
|
|
|
+ if error.validator == "required":
|
|
|
+ yield error
|
|
|
+ for nested_error in error.context:
|
|
|
+ yield from _required_errors(nested_error)
|
|
|
+
|
|
|
+
|
|
|
def _json_values_equal(left: Any, right: Any) -> bool:
|
|
|
if type(left) is not type(right):
|
|
|
return False
|