Skip to content

Commit

Permalink
Merge pull request #57 from ystreibel/master
Browse files Browse the repository at this point in the history
Add handlers to validate methode and introduce format_checkers
  • Loading branch information
horejsek committed Sep 24, 2019
2 parents 807d3c4 + 78dd60e commit aa4ea89
Show file tree
Hide file tree
Showing 7 changed files with 65 additions and 15 deletions.
12 changes: 6 additions & 6 deletions fastjsonschema/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
__all__ = ('VERSION', 'JsonSchemaException', 'JsonSchemaDefinitionException', 'validate', 'compile', 'compile_to_code')


def validate(definition, data):
def validate(definition, data, handlers={}, formats={}):
"""
Validation function for lazy programmers or for use cases, when you need
to call validation only once, so you do not have to compile it first.
Expand All @@ -100,11 +100,11 @@ def validate(definition, data):
Preffered is to use :any:`compile` function.
"""
return compile(definition)(data)
return compile(definition, handlers, formats)(data)


# pylint: disable=redefined-builtin,dangerous-default-value,exec-used
def compile(definition, handlers={}):
def compile(definition, handlers={}, formats={}):
"""
Generates validation function for validating JSON schema passed in ``definition``.
Example:
Expand Down Expand Up @@ -150,7 +150,7 @@ def compile(definition, handlers={}):
Exception :any:`JsonSchemaException` is raised from generated funtion when
validation fails (data do not follow the definition).
"""
resolver, code_generator = _factory(definition, handlers)
resolver, code_generator = _factory(definition, handlers, formats)
global_state = code_generator.global_state
# Do not pass local state so it can recursively call itself.
exec(code_generator.func_code, global_state)
Expand Down Expand Up @@ -189,9 +189,9 @@ def compile_to_code(definition, handlers={}):
)


def _factory(definition, handlers):
def _factory(definition, handlers, formats={}):
resolver = RefResolver.from_schema(definition, handlers=handlers)
code_generator = _get_code_generator_class(definition)(definition, resolver=resolver)
code_generator = _get_code_generator_class(definition)(definition, resolver=resolver, formats=formats)
return resolver, code_generator


Expand Down
11 changes: 9 additions & 2 deletions fastjsonschema/draft04.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ class CodeGeneratorDraft04(CodeGenerator):
'uri': r'^\w+:(\/?\/?)[^\s]+\Z',
}

def __init__(self, definition, resolver=None):
super().__init__(definition, resolver)
def __init__(self, definition, resolver=None, formats={}):
super().__init__(definition, resolver, formats)
self._json_keywords_to_function.update((
('type', self.generate_type),
('enum', self.generate_enum),
Expand Down Expand Up @@ -248,6 +248,13 @@ def generate_format(self):
self.l('re.compile({variable})')
with self.l('except Exception:'):
self.l('raise JsonSchemaException("{name} must be a valid regex")')

# format checking from format callable
if format_ in self._formats:
with self.l('try:'):
self.l('formats[{}]({variable})', repr(format_))
with self.l('except Exception as e:'):
self.l('raise JsonSchemaException("{name} is not a valid {variable}") from e')
else:
self.l('pass')

Expand Down
4 changes: 2 additions & 2 deletions fastjsonschema/draft06.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ class CodeGeneratorDraft06(CodeGeneratorDraft04):
),
})

def __init__(self, definition, resolver=None):
super().__init__(definition, resolver)
def __init__(self, definition, resolver=None, formats={}):
super().__init__(definition, resolver, formats)
self._json_keywords_to_function.update((
('exclusiveMinimum', self.generate_exclusive_minimum),
('exclusiveMaximum', self.generate_exclusive_maximum),
Expand Down
4 changes: 2 additions & 2 deletions fastjsonschema/draft07.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ class CodeGeneratorDraft07(CodeGeneratorDraft06):
),
})

def __init__(self, definition, resolver=None):
super().__init__(definition, resolver)
def __init__(self, definition, resolver=None, formats={}):
super().__init__(definition, resolver, formats)
# pylint: disable=duplicate-code
self._json_keywords_to_function.update((
('if', self.generate_if_then_else),
Expand Down
12 changes: 11 additions & 1 deletion fastjsonschema/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class CodeGenerator:

INDENT = 4 # spaces

def __init__(self, definition, resolver=None):
def __init__(self, definition, resolver=None, formats={}):
self._code = []
self._compile_regexps = {}

Expand All @@ -48,6 +48,15 @@ def __init__(self, definition, resolver=None):
if resolver is None:
resolver = RefResolver.from_schema(definition)
self._resolver = resolver

# Initialize formats values as callable
for key, value in formats.items():
if isinstance(value, str):
formats[key] = lambda data: re.match(value, data)
elif isinstance(value, re.Pattern):
formats[key] = lambda data: value.match(data)
self._formats = formats

# add main function to `self._needed_validation_functions`
self._needed_validation_functions[self._resolver.get_uri()] = self._resolver.get_scope_name()

Expand Down Expand Up @@ -75,6 +84,7 @@ def global_state(self):
REGEX_PATTERNS=self._compile_regexps,
re=re,
JsonSchemaException=JsonSchemaException,
formats=self._formats
)

@property
Expand Down
4 changes: 2 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

@pytest.fixture
def asserter():
def f(definition, value, expected):
def f(definition, value, expected, formats={}):
# When test fails, it will show up code.
code_generator = CodeGeneratorDraft07(definition)
print(code_generator.func_code)
Expand All @@ -24,7 +24,7 @@ def f(definition, value, expected):
# By default old tests are written for draft-04.
definition.setdefault('$schema', 'http://json-schema.org/draft-04/schema')

validator = compile(definition)
validator = compile(definition, formats=formats)
if isinstance(expected, JsonSchemaException):
with pytest.raises(JsonSchemaException) as exc:
validator(value)
Expand Down
33 changes: 33 additions & 0 deletions tests/test_format.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
from builtins import ValueError

import datetime

import pytest

import re

from fastjsonschema import JsonSchemaException


Expand Down Expand Up @@ -31,3 +37,30 @@ def test_datetime(asserter, value, expected):
])
def test_hostname(asserter, value, expected):
asserter({'type': 'string', 'format': 'hostname'}, value, expected)


def __special_timestamp_format_checker(date_string: str) -> bool:
dt = datetime.datetime.fromisoformat(date_string).replace(tzinfo=datetime.timezone.utc)
dt_now = datetime.datetime.now(datetime.timezone.utc)
if dt > dt_now:
raise ValueError(f"{date_string} is in the future")
return True


pattern = "^(19|20)[0-9][0-9]-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01]) (0[0-9]|1[0-9]|2[0-3]):([0-5][0-9]):([" \
"0-5][0-9])\\.[0-9]{6}$ "


exc = JsonSchemaException('data is not a valid data')
@pytest.mark.parametrize('value, expected, formats', [
('', exc, {"special-timestamp": __special_timestamp_format_checker}),
('bla', exc, {"special-timestamp": __special_timestamp_format_checker}),
('2018-02-05T14:17:10.00', exc, {"special-timestamp": __special_timestamp_format_checker}),
('2019-03-12 13:08:03.001000\n', exc, {"special-timestamp": __special_timestamp_format_checker}),
('2999-03-12 13:08:03.001000', '2999-03-12 13:08:03.001000', exc, {"special-timestamp": __special_timestamp_format_checker}),
('2019-03-12 13:08:03.001000', '2019-03-12 13:08:03.001000', {"special-timestamp": __special_timestamp_format_checker}),
('2019-03-12 13:08:03.001000', '2019-03-12 13:08:03.001000', {"special-timestamp": pattern}),
('2019-03-12 13:08:03.001000', '2019-03-12 13:08:03.001000', {"special-timestamp": re.compile(pattern)}),
])
def test_special_datetime(asserter, value, expected, formats):
asserter({'type': 'string', 'format': 'special-timestamp'}, value, expected, formats=formats)

0 comments on commit aa4ea89

Please sign in to comment.