Skip to content

Detect duplicate operationId fix #109

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions openapi_spec_validator/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ class ParameterDuplicateError(OpenAPIValidationError):

class UnresolvableParameterError(OpenAPIValidationError):
pass


class DuplicateOperationIDError(OpenAPIValidationError):
pass
38 changes: 29 additions & 9 deletions openapi_spec_validator/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from openapi_spec_validator.exceptions import (
ParameterDuplicateError, ExtraParametersError, UnresolvableParameterError,
OpenAPIValidationError
OpenAPIValidationError, DuplicateOperationIDError,
)
from openapi_spec_validator.decorators import ValidationErrorWrapper
from openapi_spec_validator.factories import Draft4ExtendedValidatorFactory
Expand Down Expand Up @@ -157,8 +157,10 @@ def _iter_value_errors(self, schema, value):

class PathsValidator(object):

def __init__(self, dereferencer):
def __init__(self, dereferencer, operation_ids_registry=None):
self.dereferencer = dereferencer
self.operation_ids_registry = [] if operation_ids_registry is None \
else operation_ids_registry

@wraps_errors
def iter_errors(self, paths):
Expand All @@ -168,13 +170,17 @@ def iter_errors(self, paths):
yield err

def _iter_path_errors(self, url, path_item):
return PathValidator(self.dereferencer).iter_errors(url, path_item)
return PathValidator(
self.dereferencer, self.operation_ids_registry).iter_errors(
url, path_item)


class PathValidator(object):

def __init__(self, dereferencer):
def __init__(self, dereferencer, operation_ids_registry=None):
self.dereferencer = dereferencer
self.operation_ids_registry = [] if operation_ids_registry is None \
else operation_ids_registry

@wraps_errors
def iter_errors(self, url, path_item):
Expand All @@ -184,7 +190,9 @@ def iter_errors(self, url, path_item):
yield err

def _iter_path_item_errors(self, url, path_item):
return PathItemValidator(self.dereferencer).iter_errors(url, path_item)
return PathItemValidator(
self.dereferencer, self.operation_ids_registry).iter_errors(
url, path_item)


class PathItemValidator(object):
Expand All @@ -193,8 +201,10 @@ class PathItemValidator(object):
'get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace',
]

def __init__(self, dereferencer):
def __init__(self, dereferencer, operation_ids_registry=None):
self.dereferencer = dereferencer
self.operation_ids_registry = [] if operation_ids_registry is None \
else operation_ids_registry

@wraps_errors
def iter_errors(self, url, path_item):
Expand All @@ -213,23 +223,33 @@ def iter_errors(self, url, path_item):
yield err

def _iter_operation_errors(self, url, name, operation, path_parameters):
return OperationValidator(self.dereferencer).iter_errors(
url, name, operation, path_parameters)
return OperationValidator(
self.dereferencer, self.operation_ids_registry).iter_errors(
url, name, operation, path_parameters)

def _iter_parameters_errors(self, parameters):
return ParametersValidator(self.dereferencer).iter_errors(parameters)


class OperationValidator(object):

def __init__(self, dereferencer):
def __init__(self, dereferencer, seen_ids=None):
self.dereferencer = dereferencer
self.seen_ids = [] if seen_ids is None else seen_ids

@wraps_errors
def iter_errors(self, url, name, operation, path_parameters=None):
path_parameters = path_parameters or []
operation_deref = self.dereferencer.dereference(operation)

operation_id = operation_deref.get('operationId')
if operation_id is not None and operation_id in self.seen_ids:
yield DuplicateOperationIDError(
"Operation ID '{0}' for '{1}' in '{2}' is not unique".format(
operation_id, name, url)
)
self.seen_ids.append(operation_id)

parameters = operation_deref.get('parameters', [])
for err in self._iter_parameters_errors(parameters):
yield err
Expand Down
35 changes: 35 additions & 0 deletions tests/integration/test_validators.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from openapi_spec_validator.exceptions import (
ExtraParametersError, UnresolvableParameterError, OpenAPIValidationError,
DuplicateOperationIDError,
)


Expand Down Expand Up @@ -80,6 +81,40 @@ def test_same_parameters_names(self, validator):
errors_list = list(errors)
assert errors_list == []

def test_same_operation_ids(self, validator):
spec = {
'openapi': '3.0.0',
'info': {
'title': 'Test Api',
'version': '0.0.1',
},
'paths': {
'/test': {
'get': {
'operationId': 'operation1',
'responses': {},
},
'post': {
'operationId': 'operation1',
'responses': {},
},
},
'/test2': {
'get': {
'operationId': 'operation1',
'responses': {},
},
},
},
}

errors = validator.iter_errors(spec)

errors_list = list(errors)
assert len(errors_list) == 2
assert errors_list[0].__class__ == DuplicateOperationIDError
assert errors_list[1].__class__ == DuplicateOperationIDError

def test_allow_allof_required_no_properties(self, validator):
spec = {
'openapi': '3.0.0',
Expand Down