validator: Optionally record a type_structure attribute.

We plan to use these records to check and record the schema of Zulip's
events for the purposes of API documentation.

Based on an original messier commit by tabbott.

In theory, a nicer version of this would be able to work directly off
the mypy type system, but this will be good enough for our use case.
This commit is contained in:
Vishnu KS 2019-12-11 16:33:20 +05:30 committed by Tim Abbott
parent 9230213bde
commit 1c6435d4cc
3 changed files with 75 additions and 1 deletions

View File

@ -15,6 +15,9 @@ exclude_lines =
raise UnexpectedWebhookEventType
# Don't require coverage for blocks only run when type-checking
if TYPE_CHECKING:
# Don't require coverage for the settings.LOG_API_EVENT_TYPES code paths
# These are only run in a special testing mode, so will fail normal coverage.
if settings.LOG_API_EVENT_TYPES:
# PEP 484 overloading syntax
^\s*\.\.\.

View File

@ -28,19 +28,37 @@ for any particular type of object.
import re
import ujson
from django.utils.translation import ugettext as _
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import validate_email, URLValidator
from typing import Iterable, Optional, Tuple, cast, List
from typing import Any, Dict, Iterable, Optional, Tuple, cast, List, Callable, TypeVar
from datetime import datetime
from zerver.lib.request import JsonableError
from zerver.lib.types import Validator, ProfileFieldData
FuncT = Callable[..., Any]
TypeStructure = TypeVar("TypeStructure")
# The type_structure system is designed to support using the validators in
# test_events.py to create documentation for our event formats.
#
# Ultimately, it should be possible to do this with mypy rather than a
# parallel system.
def set_type_structure(type_structure: TypeStructure) -> Callable[[FuncT], Any]:
def _set_type_structure(func: FuncT) -> FuncT:
if settings.LOG_API_EVENT_TYPES:
func.type_structure = type_structure # type: ignore # monkey-patching
return func
return _set_type_structure
@set_type_structure("str")
def check_string(var_name: str, val: object) -> Optional[str]:
if not isinstance(val, str):
return _('%s is not a string') % (var_name,)
return None
@set_type_structure("str")
def check_required_string(var_name: str, val: object) -> Optional[str]:
error = check_string(var_name, val)
if error:
@ -52,10 +70,12 @@ def check_required_string(var_name: str, val: object) -> Optional[str]:
return None
@set_type_structure("str")
def check_short_string(var_name: str, val: object) -> Optional[str]:
return check_capped_string(50)(var_name, val)
def check_capped_string(max_length: int) -> Validator:
@set_type_structure("str")
def validator(var_name: str, val: object) -> Optional[str]:
if not isinstance(val, str):
return _('%s is not a string') % (var_name,)
@ -63,9 +83,11 @@ def check_capped_string(max_length: int) -> Validator:
return _("{var_name} is too long (limit: {max_length} characters)").format(
var_name=var_name, max_length=max_length)
return None
return validator
def check_string_fixed_length(length: int) -> Validator:
@set_type_structure("str")
def validator(var_name: str, val: object) -> Optional[str]:
if not isinstance(val, str):
return _('%s is not a string') % (var_name,)
@ -75,9 +97,11 @@ def check_string_fixed_length(length: int) -> Validator:
return None
return validator
@set_type_structure("str")
def check_long_string(var_name: str, val: object) -> Optional[str]:
return check_capped_string(500)(var_name, val)
@set_type_structure("date")
def check_date(var_name: str, val: object) -> Optional[str]:
if not isinstance(val, str):
return _('%s is not a string') % (var_name,)
@ -87,12 +111,14 @@ def check_date(var_name: str, val: object) -> Optional[str]:
return _('%s is not a date') % (var_name,)
return None
@set_type_structure("int")
def check_int(var_name: str, val: object) -> Optional[str]:
if not isinstance(val, int):
return _('%s is not an integer') % (var_name,)
return None
def check_int_in(possible_values: List[int]) -> Validator:
@set_type_structure("int")
def validator(var_name: str, val: object) -> Optional[str]:
not_int = check_int(var_name, val)
if not_int is not None:
@ -103,16 +129,19 @@ def check_int_in(possible_values: List[int]) -> Validator:
return validator
@set_type_structure("float")
def check_float(var_name: str, val: object) -> Optional[str]:
if not isinstance(val, float):
return _('%s is not a float') % (var_name,)
return None
@set_type_structure("bool")
def check_bool(var_name: str, val: object) -> Optional[str]:
if not isinstance(val, bool):
return _('%s is not a boolean') % (var_name,)
return None
@set_type_structure("str")
def check_color(var_name: str, val: object) -> Optional[str]:
if not isinstance(val, str):
return _('%s is not a string') % (var_name,)
@ -123,6 +152,12 @@ def check_color(var_name: str, val: object) -> Optional[str]:
return None
def check_none_or(sub_validator: Validator) -> Validator:
if settings.LOG_API_EVENT_TYPES:
type_structure = 'none_or_' + sub_validator.type_structure # type: ignore # monkey-patching
else:
type_structure = None
@set_type_structure(type_structure)
def f(var_name: str, val: object) -> Optional[str]:
if val is None:
return None
@ -131,6 +166,15 @@ def check_none_or(sub_validator: Validator) -> Validator:
return f
def check_list(sub_validator: Optional[Validator], length: Optional[int]=None) -> Validator:
if settings.LOG_API_EVENT_TYPES:
if sub_validator:
type_structure = [sub_validator.type_structure] # type: ignore # monkey-patching
else:
type_structure = 'list' # type: ignore # monkey-patching
else:
type_structure = None # type: ignore # monkey-patching
@set_type_structure(type_structure)
def f(var_name: str, val: object) -> Optional[str]:
if not isinstance(val, list):
return _('%s is not a list') % (var_name,)
@ -153,6 +197,9 @@ def check_dict(required_keys: Iterable[Tuple[str, Validator]]=[],
optional_keys: Iterable[Tuple[str, Validator]]=[],
value_validator: Optional[Validator]=None,
_allow_only_listed_keys: bool=False) -> Validator:
type_structure = {} # type: Dict[str, Any]
@set_type_structure(type_structure)
def f(var_name: str, val: object) -> Optional[str]:
if not isinstance(val, dict):
return _('%s is not a dict') % (var_name,)
@ -165,6 +212,8 @@ def check_dict(required_keys: Iterable[Tuple[str, Validator]]=[],
error = sub_validator(vname, val[k])
if error:
return error
if settings.LOG_API_EVENT_TYPES:
type_structure[k] = sub_validator.type_structure # type: ignore # monkey-patching
for k, sub_validator in optional_keys:
if k in val:
@ -172,6 +221,8 @@ def check_dict(required_keys: Iterable[Tuple[str, Validator]]=[],
error = sub_validator(vname, val[k])
if error:
return error
if settings.LOG_API_EVENT_TYPES:
type_structure[k] = sub_validator.type_structure # type: ignore # monkey-patching
if value_validator:
for key in val:
@ -179,6 +230,8 @@ def check_dict(required_keys: Iterable[Tuple[str, Validator]]=[],
error = value_validator(vname, val[key])
if error:
return error
if settings.LOG_API_EVENT_TYPES:
type_structure['any'] = value_validator.type_structure # type: ignore # monkey-patching
if _allow_only_listed_keys:
required_keys_set = set(x[0] for x in required_keys)
@ -203,6 +256,13 @@ def check_variable_type(allowed_type_funcs: Iterable[Validator]) -> Validator:
`allowed_type_funcs`: the check_* validator functions for the possible data
types for this variable.
"""
if settings.LOG_API_EVENT_TYPES:
type_structure = 'any("%s")' % ([x.type_structure for x in allowed_type_funcs],) # type: ignore # monkey-patching
else:
type_structure = None # type: ignore # monkey-patching
@set_type_structure(type_structure)
def enumerated_type_check(var_name: str, val: object) -> Optional[str]:
for func in allowed_type_funcs:
if not func(var_name, val):
@ -211,6 +271,7 @@ def check_variable_type(allowed_type_funcs: Iterable[Validator]) -> Validator:
return enumerated_type_check
def equals(expected_val: object) -> Validator:
@set_type_structure('equals("%s")' % (str(expected_val),))
def f(var_name: str, val: object) -> Optional[str]:
if val != expected_val:
return (_('%(variable)s != %(expected_value)s (%(value)s is wrong)') %
@ -220,12 +281,14 @@ def equals(expected_val: object) -> Validator:
return None
return f
@set_type_structure('str')
def validate_login_email(email: str) -> None:
try:
validate_email(email)
except ValidationError as err:
raise JsonableError(str(err.message))
@set_type_structure('str')
def check_url(var_name: str, val: object) -> Optional[str]:
# First, ensure val is a string
string_msg = check_string(var_name, val)
@ -239,6 +302,7 @@ def check_url(var_name: str, val: object) -> Optional[str]:
except ValidationError:
return _('%s is not a URL') % (var_name,)
@set_type_structure('str')
def check_external_account_url_pattern(var_name: str, val: object) -> Optional[str]:
error = check_string(var_name, val)
if error:
@ -332,6 +396,7 @@ def check_widget_content(widget_content: object) -> Optional[str]:
# Converter functions for use with has_request_variables
@set_type_structure('int')
def to_non_negative_int(s: str, max_int_size: int=2**32-1) -> int:
x = int(s)
if x < 0:
@ -340,6 +405,7 @@ def to_non_negative_int(s: str, max_int_size: int=2**32-1) -> int:
raise ValueError('%s is too large (max %s)' % (x, max_int_size))
return x
@set_type_structure('any(List[int], str)]')
def check_string_or_int_list(var_name: str, val: object) -> Optional[str]:
if isinstance(val, str):
return None
@ -349,6 +415,7 @@ def check_string_or_int_list(var_name: str, val: object) -> Optional[str]:
return check_list(check_int)(var_name, val)
@set_type_structure('any(int, str)')
def check_string_or_int(var_name: str, val: object) -> Optional[str]:
if isinstance(val, str) or isinstance(val, int):
return None

View File

@ -336,6 +336,10 @@ CUSTOM_LOGO_URL = None # type: Optional[str]
# development.
INITIAL_PASSWORD_SALT = None # type: Optional[str]
# Settings configuring the special instrumention of the send_event
# code path used in generating API documentation for /events.
LOG_API_EVENT_TYPES = False
# Used to control whether certain management commands are run on
# the server.
# TODO: Replace this with a smarter "run on only one server" system.