Skip to content

Commit f91603e

Browse files
committed
spec validators added
1 parent 54db537 commit f91603e

File tree

13 files changed

+498
-94
lines changed

13 files changed

+498
-94
lines changed

.travis.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@ language: python
22
sudo: false
33
matrix:
44
include:
5-
- python: 2.7
65
- python: 3.2
76
- python: 3.3
87
- python: 3.4
98
- python: 3.5
109
- python: 3.6
1110
- python: nightly
12-
- python: pypy
1311
- python: pypy3
1412
allow_failures:
1513
- python: 3.2

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,9 @@ If you want to iterate through validation errors:
3434

3535
```python
3636

37-
from openapi_spec_validator import openapi_v3_validator_factory
37+
from openapi_spec_validator import openapi_v3_spec_validator
3838

39-
validator = openapi_v3_validator_factory.create(spec)
40-
errors_iterator = validator.iter_errors(spec)
39+
errors_iterator = openapi_v3_spec_validator.iter_errors(spec)
4140
```
4241

4342
## License

openapi_spec_validator/__init__.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from openapi_spec_validator.handlers import UrlHandler
66
from openapi_spec_validator.schemas import get_openapi_schema
77
from openapi_spec_validator.factories import JSONSpecValidatorFactory
8+
from openapi_spec_validator.validators import SpecValidator
89

910
__author__ = 'Artur Maciąg'
1011
__email__ = '[email protected]'
@@ -25,8 +26,11 @@
2526
schema_v3, schema_v3_url,
2627
resolver_handlers=default_handlers,
2728
)
29+
openapi_v3_spec_validator = SpecValidator(
30+
openapi_v3_validator_factory,
31+
resolver_handlers=default_handlers,
32+
)
2833
# shortcuts
29-
validate_spec = validate_spec_factory(
30-
openapi_v3_validator_factory.create)
34+
validate_spec = validate_spec_factory(openapi_v3_spec_validator.validate)
3135
validate_spec_url = validate_spec_url_factory(
32-
openapi_v3_validator_factory.create, default_handlers)
36+
openapi_v3_spec_validator.validate, default_handlers)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from jsonschema.exceptions import ValidationError
2+
3+
4+
class OpenAPIValidationError(ValidationError):
5+
pass
6+
7+
8+
class ExtraParametersError(OpenAPIValidationError):
9+
pass
10+
11+
12+
class ParameterDuplicateError(OpenAPIValidationError):
13+
pass
14+
15+
16+
class UnresolvableParameterError(OpenAPIValidationError):
17+
pass

openapi_spec_validator/factories.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,18 +50,14 @@ def __init__(self, schema, schema_url='', resolver_handlers=None):
5050
def schema_resolver(self):
5151
return self._get_resolver(self.schema_url, self.schema)
5252

53-
def create(self, spec, spec_url=''):
54-
"""Creates json documents validator from spec.
55-
:param spec: json document in the form of a list or dict.
56-
:param spec_url: base uri to use when creating a
57-
RefResolver for the passed in spec_dict.
53+
def create(self, spec_resolver):
54+
"""Creates json documents validator from spec resolver.
55+
:param spec_resolver: reference resolver.
5856
5957
:return: RefResolver for spec with cached remote $refs used during
6058
validation.
6159
:rtype: :class:`jsonschema.RefResolver`
6260
"""
63-
spec_resolver = self._get_resolver(spec_url, spec)
64-
6561
validator_cls = self.spec_validator_factory.from_resolver(
6662
spec_resolver)
6763

openapi_spec_validator/managers.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,20 @@ def visit(self, key):
1717
yield key
1818
finally:
1919
del self[key]
20+
21+
22+
class ResolverManager(object):
23+
def __init__(self, resolver):
24+
self.resolver = resolver
25+
26+
@contextmanager
27+
def in_scope(self, item, scope='x-scope'):
28+
if scope not in item:
29+
yield self.resolver
30+
else:
31+
saved_scope_stack = self.resolver._scopes_stack
32+
try:
33+
self.resolver._scopes_stack = item[scope]
34+
yield self.resolver
35+
finally:
36+
self.resolver._scopes_stack = saved_scope_stack

openapi_spec_validator/shortcuts.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,16 @@
22
from six.moves.urllib import parse
33

44

5-
def validate_spec_factory(validator_factory_callable):
5+
def validate_spec_factory(validator_callable):
66
def validate(spec):
7-
validator = validator_factory_callable(spec)
8-
return validator.validate(spec)
7+
return validator_callable(spec)
98
return validate
109

1110

12-
def validate_spec_url_factory(validator_factory_callable, handlers):
11+
def validate_spec_url_factory(validator_callable, handlers):
1312
def validate(url):
1413
result = parse.urlparse(url)
1514
handler = handlers[result.scheme]
1615
spec = handler(url)
17-
validator = validator_factory_callable(spec, spec_url=url)
18-
return validator.validate(spec)
16+
return validator_callable(spec, spec_url=url)
1917
return validate
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
import logging
2+
import string
3+
4+
from jsonschema.validators import RefResolver
5+
from six import iteritems
6+
7+
from openapi_spec_validator.exceptions import (
8+
ParameterDuplicateError, ExtraParametersError, UnresolvableParameterError,
9+
)
10+
from openapi_spec_validator.managers import ResolverManager
11+
12+
log = logging.getLogger(__name__)
13+
14+
15+
def is_ref(spec):
16+
return isinstance(spec, dict) and '$ref' in spec
17+
18+
19+
class Dereferencer(object):
20+
21+
def __init__(self, spec_resolver):
22+
self.resolver_manager = ResolverManager(spec_resolver)
23+
24+
def dereference(self, item):
25+
log.debug("Dereferencing %s", item)
26+
if item is None or not is_ref(item):
27+
return item
28+
29+
ref = item['$ref']
30+
with self.resolver_manager.in_scope(item) as resolver:
31+
with resolver.resolving(ref) as target:
32+
return target
33+
34+
35+
class SpecValidator(object):
36+
37+
def __init__(self, validator_factory, resolver_handlers):
38+
self.validator_factory = validator_factory
39+
self.resolver_handlers = resolver_handlers
40+
41+
def validate(self, spec, spec_url=''):
42+
for error in self.iter_errors(spec, spec_url=spec_url):
43+
raise error
44+
45+
def iter_errors(self, spec, spec_url=''):
46+
spec_resolver = self._get_resolver(spec_url, spec)
47+
dereferencer = self._get_dereferencer(spec_resolver)
48+
49+
validator = self._get_validator(spec_resolver)
50+
yield from validator.iter_errors(spec)
51+
52+
paths = spec.get('paths', {})
53+
yield from self._iter_paths_errors(paths, dereferencer)
54+
55+
components = spec.get('components', {})
56+
yield from self._iter_components_errors(components, dereferencer)
57+
58+
def _get_resolver(self, base_uri, referrer):
59+
return RefResolver(
60+
base_uri, referrer, handlers=self.resolver_handlers)
61+
62+
def _get_dereferencer(self, spec_resolver):
63+
return Dereferencer(spec_resolver)
64+
65+
def _get_validator(self, spec_resolver):
66+
return self.validator_factory.create(spec_resolver)
67+
68+
def _iter_paths_errors(self, paths, dereferencer):
69+
return PathsValidator(dereferencer).iter_errors(paths)
70+
71+
def _iter_components_errors(self, components, dereferencer):
72+
return ComponentsValidator(dereferencer).iter_errors(components)
73+
74+
75+
class ComponentsValidator(object):
76+
77+
def __init__(self, dereferencer):
78+
self.dereferencer = dereferencer
79+
80+
def iter_errors(self, components):
81+
components_deref = self.dereferencer.dereference(components)
82+
83+
schemas = components_deref.get('schemas', {})
84+
yield from self._iter_schemas_errors(schemas)
85+
86+
def _iter_schemas_errors(self, schemas):
87+
return SchemasValidator(self.dereferencer).iter_errors(schemas)
88+
89+
90+
class SchemasValidator(object):
91+
92+
def __init__(self, dereferencer):
93+
self.dereferencer = dereferencer
94+
95+
def iter_errors(self, schemas):
96+
schemas_deref = self.dereferencer.dereference(schemas)
97+
for name, schema in iteritems(schemas_deref):
98+
yield from self._iter_schem_errors(schema)
99+
100+
def _iter_schem_errors(self, schema):
101+
return SchemaValidator(self.dereferencer).iter_errors(schema)
102+
103+
104+
class SchemaValidator(object):
105+
106+
def __init__(self, dereferencer):
107+
self.dereferencer = dereferencer
108+
109+
def iter_errors(self, schema):
110+
schema_deref = self.dereferencer.dereference(schema)
111+
112+
if 'allOf' in schema_deref:
113+
for inner_schema in schema_deref['allOf']:
114+
yield from self.iter_errors(inner_schema)
115+
116+
required = schema_deref.get('required', [])
117+
properties = schema_deref.get('properties', {}).keys()
118+
extra_properties = list(set(required) - set(properties))
119+
if extra_properties:
120+
yield ExtraParametersError(
121+
"Required list has not defined properties: {0}".format(
122+
extra_properties
123+
)
124+
)
125+
126+
127+
class PathsValidator(object):
128+
129+
def __init__(self, dereferencer):
130+
self.dereferencer = dereferencer
131+
132+
def iter_errors(self, paths):
133+
paths_deref = self.dereferencer.dereference(paths)
134+
for url, path_item in iteritems(paths_deref):
135+
yield from self._iter_path_errors(url, path_item)
136+
137+
def _iter_path_errors(self, url, path_item):
138+
return PathValidator(self.dereferencer).iter_errors(url, path_item)
139+
140+
141+
class PathValidator(object):
142+
143+
def __init__(self, dereferencer):
144+
self.dereferencer = dereferencer
145+
146+
def iter_errors(self, url, path_item):
147+
path_item_deref = self.dereferencer.dereference(path_item)
148+
149+
yield from self._iter_path_item_errors(url, path_item_deref)
150+
151+
def _iter_path_item_errors(self, url, path_item):
152+
return PathItemValidator(self.dereferencer).iter_errors(url, path_item)
153+
154+
155+
class PathItemValidator(object):
156+
157+
OPERATIONS = [
158+
'get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace',
159+
]
160+
161+
def __init__(self, dereferencer):
162+
self.dereferencer = dereferencer
163+
164+
def iter_errors(self, url, path_item):
165+
path_item_deref = self.dereferencer.dereference(path_item)
166+
167+
parameters = path_item_deref.get('parameters', [])
168+
yield from self._iter_parameters_errors(parameters)
169+
170+
for field_name, operation in iteritems(path_item):
171+
if field_name not in self.OPERATIONS:
172+
continue
173+
174+
yield from self._iter_operation_errors(
175+
url, field_name, operation, parameters)
176+
177+
def _iter_operation_errors(self, url, name, operation, path_parameters):
178+
return OperationValidator(self.dereferencer).iter_errors(
179+
url, name, operation, path_parameters)
180+
181+
def _iter_parameters_errors(self, parameters):
182+
return ParametersValidator(self.dereferencer).iter_errors(parameters)
183+
184+
185+
class OperationValidator(object):
186+
187+
def __init__(self, dereferencer):
188+
self.dereferencer = dereferencer
189+
190+
def iter_errors(self, url, name, operation, path_parameters=None):
191+
path_parameters = path_parameters or []
192+
operation_deref = self.dereferencer.dereference(operation)
193+
194+
parameters = operation_deref.get('parameters', [])
195+
yield from self._iter_parameters_errors(parameters)
196+
197+
all_params = list(set(
198+
list(self._get_path_param_names(path_parameters)) +
199+
list(self._get_path_param_names(parameters))
200+
))
201+
202+
for path in self._get_path_params_from_url(url):
203+
if path not in all_params:
204+
yield UnresolvableParameterError(
205+
"Path parameter '{0}' for '{1}' operation in '{2}' "
206+
"was not resolved".format(path, name, url)
207+
)
208+
209+
return []
210+
211+
def _get_path_param_names(self, params):
212+
for param in params:
213+
param_deref = self.dereferencer.dereference(param)
214+
if param_deref['in'] == 'path':
215+
yield param_deref['name']
216+
217+
def _get_path_params_from_url(self, url):
218+
formatter = string.Formatter()
219+
path_params = [item[1] for item in formatter.parse(url)]
220+
return filter(None, path_params)
221+
222+
def _iter_parameters_errors(self, parameters):
223+
return ParametersValidator(self.dereferencer).iter_errors(parameters)
224+
225+
226+
class ParametersValidator(object):
227+
228+
def __init__(self, dereferencer):
229+
self.dereferencer = dereferencer
230+
231+
def iter_errors(self, parameters):
232+
seen = set()
233+
for parameter in parameters:
234+
parameter_deref = self.dereferencer.dereference(parameter)
235+
key = (parameter_deref['name'], parameter_deref['in'])
236+
if key in seen:
237+
yield ParameterDuplicateError(
238+
"Duplicate parameter `{0}`".format(parameter_deref['name'])
239+
)
240+
seen.add(key)

setup.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,12 @@ def run_tests(self):
7070
tests_require=read_requirements('requirements_dev.txt'),
7171
cmdclass={'test': PyTest},
7272
classifiers=[
73-
"Development Status :: 5 - Production/Stable",
73+
"Development Status :: 4 - Beta",
7474
"Intended Audience :: Developers",
7575
"Topic :: Software Development :: Libraries :: Python Modules",
7676
"Operating System :: OS Independent",
77-
"Programming Language :: Python :: 2.7",
77+
"Programming Language :: Python :: 3.3",
78+
"Programming Language :: Python :: 3.4",
7879
"Programming Language :: Python :: 3.5",
7980
"Programming Language :: Python :: 3.6",
8081
],

0 commit comments

Comments
 (0)