Skip to content

Commit afc02fb

Browse files
committed
Create simple tool to create skeleton of new rules
1 parent 580b6c2 commit afc02fb

File tree

6 files changed

+314
-1
lines changed

6 files changed

+314
-1
lines changed

.coveragerc

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ omit =
77
venv/*
88
/usr/*
99
setup.py
10+
create_new_rule.py
1011
./integration_tests/*
1112

1213
[report]

README.rst

+2
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,8 @@ Use the following steps to define a new rule:
176176
with two files: ``old.yaml`` and ``new.yaml``. The two files represent two versions of the swagger specs that need to be checked for
177177
backward compatibility.
178178
179+
Pro tip: You can the skeleton generated via ``python -m create_new_rule``.
180+
179181
Contributing
180182
~~~~~~~~~~~~
181183

create_new_rule.py

+304
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import absolute_import
3+
from __future__ import print_function
4+
from __future__ import unicode_literals
5+
6+
import os
7+
import re
8+
import sys
9+
import typing
10+
from argparse import ArgumentParser
11+
12+
from swagger_spec_compatibility.rules.common import Level
13+
from swagger_spec_compatibility.rules.common import RuleRegistry
14+
from swagger_spec_compatibility.rules.common import RuleType
15+
16+
17+
_RULE_TYPE_TEMPLATE = {
18+
RuleType.REQUEST_CONTRACT: 'REQ-E{number:03d}',
19+
RuleType.RESPONSE_CONTRACT: 'RES-E{number:03d}',
20+
RuleType.MISCELLANEOUS: 'MIS-E{number:03d}',
21+
}
22+
_RULE_CLASS_FILE_TEMPLATE = """# -*- coding: utf-8 -*-
23+
from __future__ import absolute_import
24+
from __future__ import print_function
25+
from __future__ import unicode_literals
26+
27+
import typing
28+
29+
from bravado_core.spec import Spec
30+
31+
from swagger_spec_compatibility.rules.common import BaseRule
32+
from swagger_spec_compatibility.rules.common import Level
33+
from swagger_spec_compatibility.rules.common import RuleType
34+
from swagger_spec_compatibility.rules.common import ValidationMessage
35+
36+
37+
class {detection_class_name}(BaseRule):
38+
description = {description}
39+
error_code = {error_code}
40+
error_level = {error_level}
41+
rule_type = {rule_type}
42+
short_name = {short_name}
43+
documentation_link = {documentation_link}
44+
45+
@classmethod
46+
def validate(cls, left_spec, right_spec):
47+
# type: (Spec, Spec) -> typing.Iterable[ValidationMessage]
48+
raise NotImplementedError()
49+
"""
50+
_RULE_CLASS_TEST_FILE_TEMPLATE = """# -*- coding: utf-8 -*-
51+
from __future__ import absolute_import
52+
from __future__ import print_function
53+
from __future__ import unicode_literals
54+
55+
56+
def test_dummy():
57+
assert 1 == 2
58+
"""
59+
_RULE_CLASS_DOC_TEMPLATE = """.. automodule:: swagger_spec_compatibility.rules.{module_name}
60+
:members:
61+
:undoc-members:
62+
:show-inheritance:
63+
"""
64+
_MINIMAL_SPECS = """swagger: '2.0'
65+
info:
66+
title: Minimal Case of {error_code} Rule
67+
version: '1.0'
68+
definitions: {{}}
69+
paths: {{}}
70+
"""
71+
_TESTER_TEMPLATE = """# -*- coding: utf-8 -*-
72+
from __future__ import absolute_import
73+
from __future__ import print_function
74+
from __future__ import unicode_literals
75+
76+
from os.path import abspath
77+
78+
from bravado.client import SwaggerClient
79+
from six.moves.urllib.parse import urljoin
80+
from six.moves.urllib.request import pathname2url
81+
82+
old_client = SwaggerClient.from_url(
83+
spec_url=urljoin('file:', pathname2url(abspath('old.yaml'))),
84+
)
85+
new_client = SwaggerClient.from_url(
86+
spec_url=urljoin('file:', pathname2url(abspath('new.yaml'))),
87+
)
88+
89+
raise NotImplementedError()
90+
"""
91+
_RULE_LONG_DOC_TEMPLATE = """[{error_code}] - {short_name}
92+
=====================================================
93+
94+
Rationale
95+
---------
96+
TODO
97+
98+
Mitigation
99+
----------
100+
TODO
101+
102+
Example
103+
-------
104+
Old Swagger Specs
105+
~~~~~~~~~~~~~~~~~
106+
107+
.. literalinclude:: examples/{error_code}/old.yaml
108+
:name: Old Swagger Spec
109+
:language: yaml
110+
:linenos:
111+
112+
New Swagger Specs
113+
~~~~~~~~~~~~~~~~~
114+
115+
.. literalinclude:: examples/{error_code}/new.yaml
116+
:name: New Swagger Spec
117+
:language: yaml
118+
:linenos:
119+
120+
.. Please highlight the different lines by using `:emphasize-lines: #`
121+
122+
Backward Incompatibility
123+
~~~~~~~~~~~~~~~~~~~~~~~~
124+
The following snippet triggers the incompatibility error.
125+
126+
.. literalinclude:: examples/{error_code}/tester.py
127+
:language: py
128+
:linenos:
129+
130+
**NOTE**: The code is taking advantage of `bravado <https://github.com/Yelp/bravado>`_
131+
"""
132+
133+
134+
def parser():
135+
# type: () -> ArgumentParser
136+
argument_parser = ArgumentParser(description='Helper to create new swagger-spec-compatiblity detection rules')
137+
argument_parser.add_argument(
138+
'--description',
139+
help='Short description of the rationale of the rule. This will be visible on CLI only',
140+
)
141+
argument_parser.add_argument(
142+
'--detection-class-name',
143+
help='Name of the detection class',
144+
required=True,
145+
)
146+
argument_parser.add_argument(
147+
'--documentation-link',
148+
help='Documentation link',
149+
)
150+
argument_parser.add_argument(
151+
'--error-level',
152+
help='Error level associated to the rule',
153+
choices=[item.name for item in Level],
154+
required=True,
155+
)
156+
argument_parser.add_argument(
157+
'--rule-type',
158+
help='Type of the rule associated',
159+
choices=[item.name for item in RuleType],
160+
required=True,
161+
)
162+
argument_parser.add_argument(
163+
'--short-name',
164+
help='Short name of the rule. This will be visible on CLI in case the rule is triggered',
165+
)
166+
return argument_parser
167+
168+
169+
def _create_code_skeleton(
170+
description, # type: typing.Optional[typing.Text]
171+
detection_class_name, # type: typing.Text
172+
documentation_link, # type: typing.Optional[typing.Text]
173+
error_code, # type: typing.Text
174+
error_level, # type: Level
175+
python_file_name_no_ext, # type: typing.Text
176+
rule_type, # type: RuleType
177+
short_name, # type: typing.Optional[typing.Text]
178+
):
179+
python_file_name = '{}.py'.format(python_file_name_no_ext)
180+
with open(os.path.join('swagger_spec_compatibility', 'rules', python_file_name), 'w') as f:
181+
f.write(str(_RULE_CLASS_FILE_TEMPLATE.format(
182+
description=description or repr('TODO'),
183+
detection_class_name=detection_class_name,
184+
documentation_link=documentation_link,
185+
error_code=repr(error_code),
186+
error_level='Level.{}'.format(error_level.name),
187+
python_file_name=python_file_name,
188+
rule_type='RuleType.{}'.format(rule_type.name),
189+
short_name=short_name or repr('TODO'),
190+
)))
191+
192+
with open(os.path.join('tests', 'rules', '{}_test.py'.format(python_file_name_no_ext)), 'w') as f:
193+
f.write(str(_RULE_CLASS_TEST_FILE_TEMPLATE))
194+
195+
196+
def _create_documentation_skeleton(
197+
description, # type: typing.Optional[typing.Text]
198+
detection_class_name, # type: typing.Text
199+
documentation_link, # type: typing.Optional[typing.Text]
200+
error_code, # type: typing.Text
201+
error_level, # type: Level
202+
python_file_name_no_ext, # type: typing.Text
203+
rule_type, # type: RuleType
204+
short_name, # type: typing.Optional[typing.Text]
205+
):
206+
module_doc_file_path = os.path.join('docs', 'source', 'swagger_spec_compatibility.rst')
207+
with open(module_doc_file_path, 'r') as f:
208+
swagger_spec_compatibility_lines = f.readlines()
209+
210+
add_new_rules_anchor = '.. ADD NEW RULES HERE\n'
211+
add_new_rules_anchor_index = swagger_spec_compatibility_lines.index(str(add_new_rules_anchor))
212+
swagger_spec_compatibility_lines[add_new_rules_anchor_index] = str('{doc}\n{anchor}'.format(
213+
anchor=add_new_rules_anchor,
214+
doc=_RULE_CLASS_DOC_TEMPLATE.format(module_name=python_file_name_no_ext),
215+
))
216+
217+
with open(module_doc_file_path, 'w') as f:
218+
f.writelines(swagger_spec_compatibility_lines)
219+
220+
rules_directory = os.path.join('docs', 'source', 'rules')
221+
examples_directory = os.path.join('docs', 'source', 'rules', 'examples', error_code)
222+
os.makedirs(examples_directory, exist_ok=True) # type: ignore # for some reason mypy does not find exist_ok parameter
223+
with open(os.path.join(examples_directory, 'old.yaml'), 'w') as f:
224+
f.write(str(_MINIMAL_SPECS.format(error_code=error_code)))
225+
with open(os.path.join(examples_directory, 'new.yaml'), 'w') as f:
226+
f.write(str(_MINIMAL_SPECS.format(error_code=error_code)))
227+
with open(os.path.join(examples_directory, 'tester.py'), 'w') as f:
228+
f.write(str(_TESTER_TEMPLATE))
229+
with open(os.path.join(rules_directory, '{}.rst'.format(error_code)), 'w') as f:
230+
f.write(str(_RULE_LONG_DOC_TEMPLATE.format(
231+
error_code=error_code,
232+
short_name=short_name or repr('TODO'),
233+
)))
234+
235+
with open(os.path.join(rules_directory, 'index.rst'.format(error_code)), 'r') as f:
236+
index_lines = f.readlines()
237+
238+
add_new_rules_index_anchor = '.. ADD HERE NEW {} rules\n'.format(_RULE_TYPE_TEMPLATE[rule_type])
239+
add_new_rules_index_anchor_index = index_lines.index(str(add_new_rules_index_anchor))
240+
index_lines[add_new_rules_index_anchor_index] = str(' {error_code}\n{anchor}'.format(
241+
anchor=add_new_rules_index_anchor,
242+
error_code=error_code,
243+
))
244+
245+
with open(os.path.join(rules_directory, 'index.rst'.format(error_code)), 'w') as f:
246+
f.writelines(index_lines)
247+
248+
249+
def create_rule_skeleton(
250+
description, # type: typing.Optional[typing.Text]
251+
detection_class_name, # type: typing.Text
252+
documentation_link, # type: typing.Optional[typing.Text]
253+
error_level, # type: Level
254+
rule_type, # type: RuleType
255+
short_name, # type: typing.Optional[typing.Text]
256+
):
257+
# type: (...) -> None
258+
def _camel_to_snake_case(text):
259+
# type: (typing.Text) -> typing.Text
260+
# Thanks to https://stackoverflow.com/a/1176023
261+
return re.sub(r'(?<!^)(?=[A-Z])', '_', text).lower()
262+
263+
python_file_name_no_ext = _camel_to_snake_case(detection_class_name)
264+
265+
error_code = _RULE_TYPE_TEMPLATE[rule_type].format(
266+
number=1 + sum(
267+
1
268+
for rule in RuleRegistry.rules()
269+
if rule.rule_type == rule_type
270+
),
271+
)
272+
273+
for method in _create_code_skeleton, _create_documentation_skeleton:
274+
method(
275+
description=description,
276+
detection_class_name=detection_class_name,
277+
documentation_link=documentation_link,
278+
error_code=error_code,
279+
error_level=error_level,
280+
python_file_name_no_ext=python_file_name_no_ext,
281+
rule_type=rule_type,
282+
short_name=short_name,
283+
)
284+
285+
286+
def main(args=None):
287+
# type: (typing.Optional[typing.List[typing.Text]]) -> None
288+
parsed_arguments = parser().parse_args(args=args)
289+
print(
290+
'The tool is far from perfect. Please make sure that all the created skeleton make sense ;)',
291+
file=sys.stderr,
292+
)
293+
create_rule_skeleton(
294+
description=parsed_arguments.description,
295+
detection_class_name=parsed_arguments.detection_class_name,
296+
documentation_link=parsed_arguments.documentation_link,
297+
error_level=Level[parsed_arguments.error_level],
298+
rule_type=RuleType[parsed_arguments.rule_type],
299+
short_name=parsed_arguments.short_name,
300+
)
301+
302+
303+
if __name__ == '__main__':
304+
main()

docs/source/rules/index.rst

+3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Request Contract Changes
1010
REQ-E001
1111
REQ-E002
1212
REQ-E003
13+
.. ADD HERE NEW REQ-E{number:03d} rules
1314
1415
Response Contract Changes
1516
-------------------------
@@ -20,6 +21,7 @@ Response Contract Changes
2021
RES-E001
2122
RES-E002
2223
RES-E003
24+
.. ADD HERE NEW RES-E{number:03d} rules
2325
2426
Miscellaneous Changes
2527
---------------------
@@ -29,3 +31,4 @@ Miscellaneous Changes
2931

3032
MIS-E001
3133
MIS-E002
34+
.. ADD HERE NEW MIS-E{number:03d} rules

docs/source/swagger_spec_compatibility.rst

+3
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ swagger_spec_compatibility Package
102102
:undoc-members:
103103
:show-inheritance:
104104

105+
.. ADD NEW RULES HERE
106+
107+
105108
:mod:`spec_utils` Module
106109
------------------------
107110

swagger_spec_compatibility/rules/common.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ class BaseRule(with_metaclass(RuleRegistry)):
169169
error_code = None # type: typing_extensions.ClassVar[typing.Text]
170170
# Short name of the rule. This will be visible on CLI in case the rule is triggered
171171
short_name = None # type: typing_extensions.ClassVar[typing.Text]
172-
# Short description of the rationale of the rule. This will be visible on CLI only.
172+
# Short description of the rationale of the rule. This will be visible on CLI only
173173
description = None # type: typing_extensions.ClassVar[typing.Text]
174174
# Error level associated to the rule
175175
error_level = None # type: typing_extensions.ClassVar[Level]

0 commit comments

Comments
 (0)