Skip to content

Commit 144bdff

Browse files
committed
Detect duplicate operationId fix
1 parent a274f59 commit 144bdff

File tree

3 files changed

+66
-9
lines changed

3 files changed

+66
-9
lines changed

openapi_spec_validator/exceptions.py

+4
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,7 @@ class ParameterDuplicateError(OpenAPIValidationError):
1515

1616
class UnresolvableParameterError(OpenAPIValidationError):
1717
pass
18+
19+
20+
class DuplicateOperationIDError(OpenAPIValidationError):
21+
pass

openapi_spec_validator/validators.py

+29-9
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from openapi_spec_validator.exceptions import (
88
ParameterDuplicateError, ExtraParametersError, UnresolvableParameterError,
9-
OpenAPIValidationError
9+
OpenAPIValidationError, DuplicateOperationIDError,
1010
)
1111
from openapi_spec_validator.decorators import ValidationErrorWrapper
1212
from openapi_spec_validator.factories import Draft4ExtendedValidatorFactory
@@ -157,8 +157,10 @@ def _iter_value_errors(self, schema, value):
157157

158158
class PathsValidator(object):
159159

160-
def __init__(self, dereferencer):
160+
def __init__(self, dereferencer, operation_ids_registry=None):
161161
self.dereferencer = dereferencer
162+
self.operation_ids_registry = [] if operation_ids_registry is None \
163+
else operation_ids_registry
162164

163165
@wraps_errors
164166
def iter_errors(self, paths):
@@ -168,13 +170,17 @@ def iter_errors(self, paths):
168170
yield err
169171

170172
def _iter_path_errors(self, url, path_item):
171-
return PathValidator(self.dereferencer).iter_errors(url, path_item)
173+
return PathValidator(
174+
self.dereferencer, self.operation_ids_registry).iter_errors(
175+
url, path_item)
172176

173177

174178
class PathValidator(object):
175179

176-
def __init__(self, dereferencer):
180+
def __init__(self, dereferencer, operation_ids_registry=None):
177181
self.dereferencer = dereferencer
182+
self.operation_ids_registry = [] if operation_ids_registry is None \
183+
else operation_ids_registry
178184

179185
@wraps_errors
180186
def iter_errors(self, url, path_item):
@@ -184,7 +190,9 @@ def iter_errors(self, url, path_item):
184190
yield err
185191

186192
def _iter_path_item_errors(self, url, path_item):
187-
return PathItemValidator(self.dereferencer).iter_errors(url, path_item)
193+
return PathItemValidator(
194+
self.dereferencer, self.operation_ids_registry).iter_errors(
195+
url, path_item)
188196

189197

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

196-
def __init__(self, dereferencer):
204+
def __init__(self, dereferencer, operation_ids_registry=None):
197205
self.dereferencer = dereferencer
206+
self.operation_ids_registry = [] if operation_ids_registry is None \
207+
else operation_ids_registry
198208

199209
@wraps_errors
200210
def iter_errors(self, url, path_item):
@@ -213,23 +223,33 @@ def iter_errors(self, url, path_item):
213223
yield err
214224

215225
def _iter_operation_errors(self, url, name, operation, path_parameters):
216-
return OperationValidator(self.dereferencer).iter_errors(
217-
url, name, operation, path_parameters)
226+
return OperationValidator(
227+
self.dereferencer, self.operation_ids_registry).iter_errors(
228+
url, name, operation, path_parameters)
218229

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

222233

223234
class OperationValidator(object):
224235

225-
def __init__(self, dereferencer):
236+
def __init__(self, dereferencer, seen_ids=None):
226237
self.dereferencer = dereferencer
238+
self.seen_ids = [] if seen_ids is None else seen_ids
227239

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

245+
operation_id = operation_deref.get('operationId')
246+
if operation_id is not None and operation_id in self.seen_ids:
247+
yield DuplicateOperationIDError(
248+
"Operation ID '{0}' for '{1}' in '{2}' is not unique".format(
249+
operation_id, name, url)
250+
)
251+
self.seen_ids.append(operation_id)
252+
233253
parameters = operation_deref.get('parameters', [])
234254
for err in self._iter_parameters_errors(parameters):
235255
yield err

tests/integration/test_validators.py

+33
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from openapi_spec_validator.exceptions import (
22
ExtraParametersError, UnresolvableParameterError, OpenAPIValidationError,
3+
DuplicateOperationIDError,
34
)
45

56

@@ -80,6 +81,38 @@ def test_same_parameters_names(self, validator):
8081
errors_list = list(errors)
8182
assert errors_list == []
8283

84+
def test_same_operation_ids(self, validator):
85+
spec = {
86+
'openapi': '3.0.0',
87+
'info': {
88+
'title': 'Test Api',
89+
'version': '0.0.1',
90+
},
91+
'paths': {
92+
'/test': {
93+
'post': {
94+
'operationId': 'operation1',
95+
'responses': {},
96+
},
97+
},
98+
'/test2': {
99+
'get': {
100+
'operationId': 'operation1',
101+
'responses': {},
102+
},
103+
},
104+
},
105+
}
106+
107+
errors = validator.iter_errors(spec)
108+
109+
errors_list = list(errors)
110+
assert len(errors_list) == 1
111+
assert errors_list[0].__class__ == DuplicateOperationIDError
112+
assert errors_list[0].message == (
113+
"Operation ID 'operation1' for 'get' in '/test2' is not unique"
114+
)
115+
83116
def test_allow_allof_required_no_properties(self, validator):
84117
spec = {
85118
'openapi': '3.0.0',

0 commit comments

Comments
 (0)