Skip to content

Commit

Permalink
Merge pull request #49 from analogue/fix_udf_validation
Browse files Browse the repository at this point in the history
Fix validation for user-defined formats
  • Loading branch information
analogue committed Sep 30, 2015
2 parents 94550c2 + b190e47 commit 4df9e6c
Show file tree
Hide file tree
Showing 11 changed files with 262 additions and 52 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
2.4.1 (2015-XX-XX)
------------------
- Fixed validation of user-defined formats

2.4.0 (2015-08-13)
------------------
- Support relative '$ref' external references in swagger.json
Expand Down
75 changes: 72 additions & 3 deletions bravado_core/formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@
Support for the 'format' key in the swagger spec as outlined in
https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#dataTypeFormat
"""

import functools
import warnings
from collections import namedtuple
from jsonschema import FormatChecker

import six
import dateutil.parser

from bravado_core import schema
from bravado_core.exception import SwaggerValidationError

if six.PY3:
long = int
Expand Down Expand Up @@ -46,18 +48,38 @@ def to_python(spec, value):


def register_format(swagger_format):
"""Register a user defined format with bravado-core.
"""Register a user-defined format with bravado-core.
:type swagger_format: :class:`SwaggerFormat`
"""
global _formatters
_formatters[swagger_format.format] = swagger_format

# Need to maintain a separate list of UDFs for jsonschema validation
global _user_defined_formats
_user_defined_formats.append(swagger_format)


def unregister_format(swagger_format):
"""Unregister an existing user-defined format.
:type swagger_format: :class:`SwaggerFormat`
"""
global _formatters
del _formatters[swagger_format.format]

global _user_defined_formats
_user_defined_formats.remove(swagger_format)

# Invalidate so it is rebuilt
global _format_checker
_format_checker = None


def get_format(format):
"""Get registered formatter mapped to given format.
:param format: Format name like int, base64, etc.
:param format: Format name like int32, base64, etc.
:type format: str
:rtype: :class:`SwaggerFormat` or None
"""
Expand Down Expand Up @@ -88,6 +110,53 @@ class SwaggerFormat(namedtuple('SwaggerFormat',
"""


def return_true_wrapper(validate_func):
"""Decorator for the SwaggerFormat.validate function to always return True.
The contract for `SwaggerFormat.validate` is to raise an exception
when validation fails. However, the contract for jsonschema's
validate function is to raise an exception or return True. This wrapper
bolts-on the `return True` part.
:param validate_func: SwaggerFormat.validate function
:return: wrapped callable
"""
@functools.wraps(validate_func)
def wrapper(validatable_primitive):
validate_func(validatable_primitive)
return True

return wrapper


# jsonschema.FormatChecker
_format_checker = None


# List of newly registered user-defined SwaggerFormats
_user_defined_formats = []


def get_format_checker():
"""
Build and cache a :class:`jsonschema.FormatChecker` for validating
user-defined Swagger formats.
:rtype: :class:`jsonschema.FormatChecker`
"""
global _format_checker
if _format_checker is None:
_format_checker = FormatChecker()
for swagger_format in _user_defined_formats:
validate = return_true_wrapper(swagger_format.validate)
# `checks` is a function decorator, hence the unusual registration
# mechanism.
_format_checker.checks(
swagger_format.format,
raises=(SwaggerValidationError,))(validate)
return _format_checker


_formatters = {
'byte': SwaggerFormat(
format='byte',
Expand Down
23 changes: 8 additions & 15 deletions bravado_core/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@
customize the behavior.
"""

from bravado_core.exception import (wrap_exception,
SwaggerMappingError,
SwaggerValidationError)
from bravado_core.exception import SwaggerMappingError
from bravado_core.schema import SWAGGER_PRIMITIVES
from bravado_core.formatter import get_format
from bravado_core.formatter import get_format_checker
from bravado_core.swagger20_validator import Swagger20Validator


Expand Down Expand Up @@ -37,33 +35,28 @@ def validate_schema_object(spec, value):
obj_type, value))


@wrap_exception(SwaggerValidationError)
def validate_user_format(spec, value):
formatter = get_format(spec.get('format'))
if formatter:
formatter.validate(value)


def validate_primitive(spec, value):
"""
:param spec: spec for a swagger primitive type in dict form
:type value: int, string, float, long, etc
"""
Swagger20Validator(spec).validate(value)
validate_user_format(spec, value)
Swagger20Validator(
spec, format_checker=get_format_checker()).validate(value)


def validate_array(spec, value):
"""
:param spec: spec for an 'array' type in dict form
:type value: list
"""
Swagger20Validator(spec).validate(value)
Swagger20Validator(
spec, format_checker=get_format_checker()).validate(value)


def validate_object(spec, value):
"""
:param spec: spec for an 'object' type in dict form
:type value: dict
"""
Swagger20Validator(spec).validate(value)
Swagger20Validator(
spec, format_checker=get_format_checker()).validate(value)
8 changes: 0 additions & 8 deletions docs/source/changelog.rst

This file was deleted.

1 change: 1 addition & 0 deletions docs/source/changelog.rst
1 change: 0 additions & 1 deletion tests/formatter/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
__author__ = 'spatel'
28 changes: 28 additions & 0 deletions tests/validate/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from contextlib import contextmanager

from bravado_core.exception import SwaggerValidationError
from bravado_core.formatter import SwaggerFormat
from bravado_core.formatter import register_format
from bravado_core.formatter import unregister_format


def validate_email_address(email_address):
if '@' not in email_address:
raise SwaggerValidationError('dude, you need an @')


email_address_format = SwaggerFormat(
format='email_address',
to_wire=lambda x: x,
to_python=lambda x: x,
validate=validate_email_address,
description='blah')


@contextmanager
def registered_format(swagger_format):
register_format(swagger_format)
try:
yield
finally:
unregister_format(swagger_format)
44 changes: 44 additions & 0 deletions tests/validate/validate_array_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import pytest

from bravado_core.validate import validate_array
from tests.validate.conftest import registered_format, email_address_format


@pytest.fixture
Expand Down Expand Up @@ -54,3 +55,46 @@ def test_uniqueItems_false(int_array_spec):
int_array_spec['uniqueItems'] = False
validate_array(int_array_spec, [1, 2, 3])
validate_array(int_array_spec, [1, 2, 1, 4])


@pytest.fixture
def email_address_array_spec():
return {
'type': 'array',
'items': {
'type': 'string',
'format': 'email_address',
}
}


def test_user_defined_format_success(email_address_array_spec):
request_body = ['foo@bar.com']
with registered_format(email_address_format):
# No exception thrown == success
validate_array(email_address_array_spec, request_body)


def test_user_defined_format_failure(email_address_array_spec):
request_body = ['i_am_not_a_valid_email_address']

with registered_format(email_address_format):
with pytest.raises(ValidationError) as excinfo:
validate_array(email_address_array_spec, request_body)
assert "'i_am_not_a_valid_email_address' is not a 'email_address'" in \
str(excinfo.value)


def test_builtin_format_still_works_when_user_defined_format_used():
ipaddress_array_spec = {
'type': 'array',
'items': {
'type': 'string',
'format': 'ipv4',
}
}
request_body = ['not_an_ip_address']
with registered_format(email_address_format):
with pytest.raises(ValidationError) as excinfo:
validate_array(ipaddress_array_spec, request_body)
assert "'not_an_ip_address' is not a 'ipv4'" in str(excinfo.value)
55 changes: 55 additions & 0 deletions tests/validate/validate_object_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import pytest

from bravado_core.validate import validate_object
from tests.validate.conftest import registered_format, email_address_format


@pytest.fixture
Expand Down Expand Up @@ -62,3 +63,57 @@ def test_required_OK(address_spec):
with pytest.raises(ValidationError) as excinfo:
validate_object(address_spec, address)
assert 'is a required property' in str(excinfo.value)


@pytest.fixture
def email_address_object_spec():
return {
'type': 'object',
'required': ['email_address'],
'properties': {
'email_address': {
'type': 'string',
'format': 'email_address',
}
}
}


def test_user_defined_format_success(email_address_object_spec):
request_body = {
'email_address': 'foo@bar.com'
}
with registered_format(email_address_format):
# No exception thrown == success
validate_object(email_address_object_spec, request_body)


def test_user_defined_format_failure(email_address_object_spec):
request_body = {
'email_address': 'i_am_not_a_valid_email_address'
}
with registered_format(email_address_format):
with pytest.raises(ValidationError) as excinfo:
validate_object(email_address_object_spec, request_body)
assert "'i_am_not_a_valid_email_address' is not a 'email_address'" in \
str(excinfo.value)


def test_builtin_format_still_works_when_user_defined_format_used():
ipaddress_spec = {
'type': 'object',
'required': ['ipaddress'],
'properties': {
'ipaddress': {
'type': 'string',
'format': 'ipv4',
}
}
}
request_body = {
'ipaddress': 'not_an_ip_address'
}
with registered_format(email_address_format):
with pytest.raises(ValidationError) as excinfo:
validate_object(ipaddress_spec, request_body)
assert "'not_an_ip_address' is not a 'ipv4'" in str(excinfo.value)
40 changes: 40 additions & 0 deletions tests/validate/validate_primitive_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import pytest

from bravado_core.validate import validate_primitive
from tests.validate.conftest import registered_format, email_address_format


@pytest.fixture
Expand Down Expand Up @@ -198,3 +199,42 @@ def test_doesnt_blow_up_when_spec_has_a_require_key():
'require': True,
}
validate_primitive(string_spec, 'foo')


@pytest.fixture
def email_address_spec():
return {
'type': 'string',
'format': 'email_address',
}


def test_user_defined_format_success(email_address_spec):
request_body = 'foo@bar.com'

with registered_format(email_address_format):
# No exception thrown == success
validate_primitive(email_address_spec, request_body)


def test_user_defined_format_failure(email_address_spec):
request_body = 'i_am_not_a_valid_email_address'

with registered_format(email_address_format):
with pytest.raises(ValidationError) as excinfo:
validate_primitive(email_address_spec, request_body)
assert "'i_am_not_a_valid_email_address' is not a 'email_address'" in \
str(excinfo.value)


def test_builtin_format_still_works_when_user_defined_format_used():
ipaddress_spec = {
'type': 'string',
'format': 'ipv4',
}
request_body = 'not_an_ip_address'

with registered_format(email_address_format):
with pytest.raises(ValidationError) as excinfo:
validate_primitive(ipaddress_spec, request_body)
assert "'not_an_ip_address' is not a 'ipv4'" in str(excinfo.value)
Loading

0 comments on commit 4df9e6c

Please sign in to comment.