2018-02-14 20:06:03 +00:00
|
|
|
"""Decorator for view methods to help with data validation."""
|
2021-04-20 15:40:41 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2022-07-21 11:07:42 +00:00
|
|
|
from collections.abc import Awaitable, Callable, Coroutine
|
2018-02-14 20:06:03 +00:00
|
|
|
from functools import wraps
|
2021-09-22 18:59:52 +00:00
|
|
|
from http import HTTPStatus
|
2018-02-14 20:06:03 +00:00
|
|
|
import logging
|
2022-07-21 11:07:42 +00:00
|
|
|
from typing import Any, TypeVar
|
2018-02-14 20:06:03 +00:00
|
|
|
|
2020-05-26 14:28:22 +00:00
|
|
|
from aiohttp import web
|
2022-07-21 11:07:42 +00:00
|
|
|
from typing_extensions import Concatenate, ParamSpec
|
2018-02-14 20:06:03 +00:00
|
|
|
import voluptuous as vol
|
|
|
|
|
2020-05-26 14:28:22 +00:00
|
|
|
from .view import HomeAssistantView
|
2019-08-12 03:38:18 +00:00
|
|
|
|
2022-07-21 11:07:42 +00:00
|
|
|
_HassViewT = TypeVar("_HassViewT", bound=HomeAssistantView)
|
|
|
|
_P = ParamSpec("_P")
|
|
|
|
|
2018-02-14 20:06:03 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
class RequestDataValidator:
|
|
|
|
"""Decorator that will validate the incoming data.
|
|
|
|
|
2021-01-26 09:27:20 +00:00
|
|
|
Takes in a voluptuous schema and adds 'data' as
|
2018-02-14 20:06:03 +00:00
|
|
|
keyword argument to the function call.
|
|
|
|
|
|
|
|
Will return a 400 if no JSON provided or doesn't match schema.
|
|
|
|
"""
|
|
|
|
|
2020-05-26 14:28:22 +00:00
|
|
|
def __init__(self, schema: vol.Schema, allow_empty: bool = False) -> None:
|
2018-02-14 20:06:03 +00:00
|
|
|
"""Initialize the decorator."""
|
2019-12-21 21:45:06 +00:00
|
|
|
if isinstance(schema, dict):
|
|
|
|
schema = vol.Schema(schema)
|
|
|
|
|
2018-02-14 20:06:03 +00:00
|
|
|
self._schema = schema
|
|
|
|
self._allow_empty = allow_empty
|
|
|
|
|
2020-05-26 14:28:22 +00:00
|
|
|
def __call__(
|
2022-07-21 11:07:42 +00:00
|
|
|
self,
|
|
|
|
method: Callable[
|
|
|
|
Concatenate[_HassViewT, web.Request, dict[str, Any], _P],
|
|
|
|
Awaitable[web.Response],
|
|
|
|
],
|
|
|
|
) -> Callable[
|
|
|
|
Concatenate[_HassViewT, web.Request, _P],
|
|
|
|
Coroutine[Any, Any, web.Response],
|
|
|
|
]:
|
2018-02-14 20:06:03 +00:00
|
|
|
"""Decorate a function."""
|
2019-07-31 19:25:30 +00:00
|
|
|
|
2018-02-14 20:06:03 +00:00
|
|
|
@wraps(method)
|
2020-05-26 14:28:22 +00:00
|
|
|
async def wrapper(
|
2022-07-21 11:07:42 +00:00
|
|
|
view: _HassViewT, request: web.Request, *args: _P.args, **kwargs: _P.kwargs
|
|
|
|
) -> web.Response:
|
2018-02-14 20:06:03 +00:00
|
|
|
"""Wrap a request handler with data validation."""
|
2022-07-21 11:07:42 +00:00
|
|
|
raw_data = None
|
2018-02-14 20:06:03 +00:00
|
|
|
try:
|
2022-07-21 11:07:42 +00:00
|
|
|
raw_data = await request.json()
|
2018-02-14 20:06:03 +00:00
|
|
|
except ValueError:
|
2019-07-31 19:25:30 +00:00
|
|
|
if not self._allow_empty or (await request.content.read()) != b"":
|
2020-07-05 21:04:19 +00:00
|
|
|
_LOGGER.error("Invalid JSON received")
|
2021-09-22 18:59:52 +00:00
|
|
|
return view.json_message("Invalid JSON.", HTTPStatus.BAD_REQUEST)
|
2022-07-21 11:07:42 +00:00
|
|
|
raw_data = {}
|
2018-02-14 20:06:03 +00:00
|
|
|
|
|
|
|
try:
|
2022-07-21 11:07:42 +00:00
|
|
|
data: dict[str, Any] = self._schema(raw_data)
|
2018-02-14 20:06:03 +00:00
|
|
|
except vol.Invalid as err:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.error("Data does not match schema: %s", err)
|
2020-04-09 19:43:42 +00:00
|
|
|
return view.json_message(
|
2021-09-22 18:59:52 +00:00
|
|
|
f"Message format incorrect: {err}", HTTPStatus.BAD_REQUEST
|
2020-04-09 19:43:42 +00:00
|
|
|
)
|
2018-02-14 20:06:03 +00:00
|
|
|
|
2022-07-21 11:07:42 +00:00
|
|
|
result = await method(view, request, data, *args, **kwargs)
|
2018-02-14 20:06:03 +00:00
|
|
|
return result
|
|
|
|
|
|
|
|
return wrapper
|