diff --git a/src/mock_vws/_services_validators/context.py b/src/mock_vws/_services_validators/context.py index dfaa90a90..a8d4d0e24 100644 --- a/src/mock_vws/_services_validators/context.py +++ b/src/mock_vws/_services_validators/context.py @@ -4,20 +4,28 @@ from collections.abc import Mapping from dataclasses import dataclass from functools import cached_property -from typing import Any, TypeIs +from typing import TypeIs from beartype import beartype +from pydantic import TypeAdapter from mock_vws._base64_decoding import decode_base64 from mock_vws._database_matchers import AnyDatabase +type JSONValue = ( + bool | int | float | str | list[JSONValue] | dict[str, JSONValue] | None +) +_OPTIONAL_STRING_ADAPTER: TypeAdapter[str | None] = TypeAdapter( + type=str | None, +) + @beartype -def _is_json_object(value: object, /) -> TypeIs[dict[str, Any]]: # pyrefly: ignore [explicit-any] +def _is_json_object(value: object, /) -> TypeIs[dict[str, JSONValue]]: """Return whether a decoded JSON value is an object. JSON object keys are always strings, so a ``dict`` from ``json.loads`` - is a ``dict[str, Any]``. + is a ``dict[str, JSONValue]``. """ return isinstance(value, dict) @@ -66,7 +74,7 @@ class ValidatorContext: allowed_for_inactive_cloud_project: bool @cached_property - def request_json(self) -> dict[str, Any]: # pyrefly: ignore [explicit-any] + def request_json(self) -> dict[str, JSONValue]: """The request body parsed as a JSON object. A route's JSON validator runs before any validator which reads this, @@ -99,7 +107,10 @@ def decoded_image(self) -> bytes | None: Raises: binascii.Error: The image cannot be base64 decoded. """ - image = self.request_json.get("image") + image = _OPTIONAL_STRING_ADAPTER.validate_python( + self.request_json.get("image"), + strict=True, + ) if image is None: return None return decode_base64(encoded_data=image) diff --git a/src/mock_vws/_services_validators/instance_id_validators.py b/src/mock_vws/_services_validators/instance_id_validators.py index 7c70b8a65..2243b0e48 100644 --- a/src/mock_vws/_services_validators/instance_id_validators.py +++ b/src/mock_vws/_services_validators/instance_id_validators.py @@ -50,7 +50,7 @@ def validate_instance_id_not_empty(*, context: ValidatorContext) -> None: """ instance_id = context.request_json["instance_id"] - if instance_id: + if instance_id != "": return _LOGGER.warning(msg='The value of "instance_id" is empty.') diff --git a/src/mock_vws/_services_validators/metadata_validators.py b/src/mock_vws/_services_validators/metadata_validators.py index 1568efaef..be0242615 100644 --- a/src/mock_vws/_services_validators/metadata_validators.py +++ b/src/mock_vws/_services_validators/metadata_validators.py @@ -5,6 +5,7 @@ from http import HTTPStatus from beartype import beartype +from pydantic import TypeAdapter from mock_vws._base64_decoding import decode_base64 from mock_vws._services_validators.context import ValidatorContext @@ -14,6 +15,9 @@ ) _LOGGER = logging.getLogger(name=__name__) +_OPTIONAL_METADATA_ADAPTER: TypeAdapter[str | None] = TypeAdapter( + type=str | None, +) @beartype @@ -30,7 +34,10 @@ def validate_metadata_size(*, context: ValidatorContext) -> None: large. """ request_json = context.request_json - application_metadata = request_json.get("application_metadata") + application_metadata = _OPTIONAL_METADATA_ADAPTER.validate_python( + request_json.get("application_metadata"), + strict=True, + ) if application_metadata is None: return decoded = decode_base64(encoded_data=application_metadata) @@ -55,7 +62,10 @@ def validate_metadata_encoding(*, context: ValidatorContext) -> None: decoded. """ request_json = context.request_json - application_metadata = request_json.get("application_metadata") + application_metadata = _OPTIONAL_METADATA_ADAPTER.validate_python( + request_json.get("application_metadata"), + strict=True, + ) if application_metadata is None: return diff --git a/src/mock_vws/_services_validators/name_validators.py b/src/mock_vws/_services_validators/name_validators.py index 80e029b42..591c4e5e9 100644 --- a/src/mock_vws/_services_validators/name_validators.py +++ b/src/mock_vws/_services_validators/name_validators.py @@ -4,6 +4,7 @@ from http import HTTPStatus from beartype import beartype +from pydantic import TypeAdapter from mock_vws._services_validators.context import ValidatorContext from mock_vws._services_validators.exceptions import ( @@ -18,6 +19,8 @@ _LOGGER = logging.getLogger(name=__name__) _MAX_CHARACTER_ORD = 65535 +_OPTIONAL_NAME_ADAPTER: TypeAdapter[str | None] = TypeAdapter(type=str | None) +_NAME_ADAPTER: TypeAdapter[str] = TypeAdapter(type=str) @beartype @@ -32,9 +35,10 @@ def _given_name(*, context: ValidatorContext) -> str | None: The value has already been checked to be a string by :py:func:`validate_name_type`. """ - request_json = context.request_json - name: str | None = request_json.get("name") # ty: ignore[unsound-assignment] - return name + return _OPTIONAL_NAME_ADAPTER.validate_python( + context.request_json.get("name"), + strict=True, + ) @beartype @@ -63,8 +67,10 @@ def _new_target_name(*, context: ValidatorContext) -> str: a request which does not give one, and :py:func:`validate_name_type` has already rejected one which is not a string. """ - name: str = context.request_json["name"] # ty: ignore[unsound-assignment] - return name + return _NAME_ADAPTER.validate_python( + context.request_json["name"], + strict=True, + ) @beartype diff --git a/src/mock_vws/_services_validators/width_validators.py b/src/mock_vws/_services_validators/width_validators.py index fa5b7bbdc..46a914a65 100644 --- a/src/mock_vws/_services_validators/width_validators.py +++ b/src/mock_vws/_services_validators/width_validators.py @@ -27,9 +27,6 @@ def validate_width(*, context: ValidatorContext) -> None: width = request_json["width"] - width_is_number = isinstance(width, int | float) - width_positive = width_is_number and width > 0 - - if not width_positive: + if not isinstance(width, int | float) or width <= 0: _LOGGER.warning(msg="Width is not a positive number.") raise FailError(status_code=HTTPStatus.BAD_REQUEST)