-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create simple tool to create skeleton of new rules
- Loading branch information
1 parent
580b6c2
commit 7d7c71b
Showing
6 changed files
with
314 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ omit = | |
venv/* | ||
/usr/* | ||
setup.py | ||
create_new_rule.py | ||
./integration_tests/* | ||
|
||
[report] | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,304 @@ | ||
# -*- coding: utf-8 -*- | ||
from __future__ import absolute_import | ||
from __future__ import print_function | ||
from __future__ import unicode_literals | ||
|
||
import os | ||
import re | ||
import sys | ||
import typing | ||
from argparse import ArgumentParser | ||
|
||
from swagger_spec_compatibility.rules.common import Level | ||
from swagger_spec_compatibility.rules.common import RuleRegistry | ||
from swagger_spec_compatibility.rules.common import RuleType | ||
|
||
|
||
_RULE_TYPE_TEMPLATE = { | ||
RuleType.REQUEST_CONTRACT: 'REQ-E{number:03d}', | ||
RuleType.RESPONSE_CONTRACT: 'RES-E{number:03d}', | ||
RuleType.MISCELLANEOUS: 'MIS-E{number:03d}', | ||
} | ||
_RULE_CLASS_FILE_TEMPLATE = """# -*- coding: utf-8 -*- | ||
from __future__ import absolute_import | ||
from __future__ import print_function | ||
from __future__ import unicode_literals | ||
import typing | ||
from bravado_core.spec import Spec | ||
from swagger_spec_compatibility.rules.common import BaseRule | ||
from swagger_spec_compatibility.rules.common import Level | ||
from swagger_spec_compatibility.rules.common import RuleType | ||
from swagger_spec_compatibility.rules.common import ValidationMessage | ||
class {detection_class_name}(BaseRule): | ||
description = {description} | ||
error_code = {error_code} | ||
error_level = {error_level} | ||
rule_type = {rule_type} | ||
short_name = {short_name} | ||
documentation_link = {documentation_link} | ||
@classmethod | ||
def validate(cls, left_spec, right_spec): | ||
# type: (Spec, Spec) -> typing.Iterable[ValidationMessage] | ||
raise NotImplementedError() | ||
""" | ||
_RULE_CLASS_TEST_FILE_TEMPLATE = """# -*- coding: utf-8 -*- | ||
from __future__ import absolute_import | ||
from __future__ import print_function | ||
from __future__ import unicode_literals | ||
def test_dummy(): | ||
assert 1 == 2 | ||
""" | ||
_RULE_CLASS_DOC_TEMPLATE = """.. automodule:: swagger_spec_compatibility.rules.{module_name} | ||
:members: | ||
:undoc-members: | ||
:show-inheritance: | ||
""" | ||
_MINIMAL_SPECS = """swagger: '2.0' | ||
info: | ||
title: Minimal Case of {error_code} Rule | ||
version: '1.0' | ||
definitions: {{}} | ||
paths: {{}} | ||
""" | ||
_TESTER_TEMPLATE = """# -*- coding: utf-8 -*- | ||
from __future__ import absolute_import | ||
from __future__ import print_function | ||
from __future__ import unicode_literals | ||
from os.path import abspath | ||
from bravado.client import SwaggerClient | ||
from six.moves.urllib.parse import urljoin | ||
from six.moves.urllib.request import pathname2url | ||
old_client = SwaggerClient.from_url( | ||
spec_url=urljoin('file:', pathname2url(abspath('old.yaml'))), | ||
) | ||
new_client = SwaggerClient.from_url( | ||
spec_url=urljoin('file:', pathname2url(abspath('new.yaml'))), | ||
) | ||
raise NotImplementedError() | ||
""" | ||
_RULE_LONG_DOC_TEMPLATE = """[{error_code}] - {short_name} | ||
===================================================== | ||
Rationale | ||
--------- | ||
TODO | ||
Mitigation | ||
---------- | ||
TODO | ||
Example | ||
------- | ||
Old Swagger Specs | ||
~~~~~~~~~~~~~~~~~ | ||
.. literalinclude:: examples/{error_code}/old.yaml | ||
:name: Old Swagger Spec | ||
:language: yaml | ||
:linenos: | ||
New Swagger Specs | ||
~~~~~~~~~~~~~~~~~ | ||
.. literalinclude:: examples/{error_code}/new.yaml | ||
:name: New Swagger Spec | ||
:language: yaml | ||
:linenos: | ||
.. Please highlight the different lines by using `:emphasize-lines: #` | ||
Backward Incompatibility | ||
~~~~~~~~~~~~~~~~~~~~~~~~ | ||
The following snippet triggers the incompatibility error. | ||
.. literalinclude:: examples/{error_code}/tester.py | ||
:language: py | ||
:linenos: | ||
**NOTE**: The code is taking advantage of `bravado <https://github.com/Yelp/bravado>`_ | ||
""" | ||
|
||
|
||
def parser(): | ||
# type: () -> ArgumentParser | ||
argument_parser = ArgumentParser(description='Helper to create new swagger-spec-compatiblity detection rules') | ||
argument_parser.add_argument( | ||
'--description', | ||
help='Short description of the rationale of the rule. This will be visible on CLI only', | ||
) | ||
argument_parser.add_argument( | ||
'--detection-class-name', | ||
help='Name of the detection class', | ||
required=True, | ||
) | ||
argument_parser.add_argument( | ||
'--documentation-link', | ||
help='Documentation link', | ||
) | ||
argument_parser.add_argument( | ||
'--error-level', | ||
help='Error level associated to the rule', | ||
choices=[item.name for item in Level], | ||
required=True, | ||
) | ||
argument_parser.add_argument( | ||
'--rule-type', | ||
help='Type of the rule associated', | ||
choices=[item.name for item in RuleType], | ||
required=True, | ||
) | ||
argument_parser.add_argument( | ||
'--short-name', | ||
help='Short name of the rule. This will be visible on CLI in case the rule is triggered', | ||
) | ||
return argument_parser | ||
|
||
|
||
def _create_code_skeleton( | ||
description, # type: typing.Optional[typing.Text] | ||
detection_class_name, # type: typing.Text | ||
documentation_link, # type: typing.Optional[typing.Text] | ||
error_code, # type: typing.Text | ||
error_level, # type: Level | ||
python_file_name_no_ext, # type: typing.Text | ||
rule_type, # type: RuleType | ||
short_name, # type: typing.Optional[typing.Text] | ||
): | ||
python_file_name = '{}.py'.format(python_file_name_no_ext) | ||
with open(os.path.join('swagger_spec_compatibility', 'rules', python_file_name), 'w') as f: | ||
f.write(str(_RULE_CLASS_FILE_TEMPLATE.format( | ||
description=description or repr('TODO'), | ||
detection_class_name=detection_class_name, | ||
documentation_link=documentation_link, | ||
error_code=repr(error_code), | ||
error_level='Level.{}'.format(error_level.name), | ||
python_file_name=python_file_name, | ||
rule_type='RuleType.{}'.format(rule_type.name), | ||
short_name=short_name or repr('TODO'), | ||
))) | ||
|
||
with open(os.path.join('tests', 'rules', '{}_test.py'.format(python_file_name_no_ext)), 'w') as f: | ||
f.write(str(_RULE_CLASS_TEST_FILE_TEMPLATE)) | ||
|
||
|
||
def _create_documentation_skeleton( | ||
description, # type: typing.Optional[typing.Text] | ||
detection_class_name, # type: typing.Text | ||
documentation_link, # type: typing.Optional[typing.Text] | ||
error_code, # type: typing.Text | ||
error_level, # type: Level | ||
python_file_name_no_ext, # type: typing.Text | ||
rule_type, # type: RuleType | ||
short_name, # type: typing.Optional[typing.Text] | ||
): | ||
module_doc_file_path = os.path.join('docs', 'source', 'swagger_spec_compatibility.rst') | ||
with open(module_doc_file_path, 'r') as f: | ||
swagger_spec_compatibility_lines = f.readlines() | ||
|
||
add_new_rules_anchor = '.. ADD NEW RULES HERE\n' | ||
add_new_rules_anchor_index = swagger_spec_compatibility_lines.index(str(add_new_rules_anchor)) | ||
swagger_spec_compatibility_lines[add_new_rules_anchor_index] = str('{doc}\n{anchor}'.format( | ||
anchor=add_new_rules_anchor, | ||
doc=_RULE_CLASS_DOC_TEMPLATE.format(module_name=python_file_name_no_ext), | ||
)) | ||
|
||
with open(module_doc_file_path, 'w') as f: | ||
f.writelines(swagger_spec_compatibility_lines) | ||
|
||
rules_directory = os.path.join('docs', 'source', 'rules') | ||
examples_directory = os.path.join('docs', 'source', 'rules', 'examples', error_code) | ||
os.makedirs(examples_directory, exist_ok=True) # type: ignore # for some reason mypy does not find exist_ok parameter | ||
with open(os.path.join(examples_directory, 'old.yaml'), 'w') as f: | ||
f.write(str(_MINIMAL_SPECS.format(error_code=error_code))) | ||
with open(os.path.join(examples_directory, 'new.yaml'), 'w') as f: | ||
f.write(str(_MINIMAL_SPECS.format(error_code=error_code))) | ||
with open(os.path.join(examples_directory, 'tester.py'), 'w') as f: | ||
f.write(str(_TESTER_TEMPLATE)) | ||
with open(os.path.join(rules_directory, '{}.rst'.format(error_code)), 'w') as f: | ||
f.write(str(_RULE_LONG_DOC_TEMPLATE.format( | ||
error_code=error_code, | ||
short_name=short_name or repr('TODO'), | ||
))) | ||
|
||
with open(os.path.join(rules_directory, 'index.rst'.format(error_code)), 'r') as f: | ||
index_lines = f.readlines() | ||
|
||
add_new_rules_index_anchor = '.. ADD HERE NEW {} rules\n'.format(_RULE_TYPE_TEMPLATE[rule_type]) | ||
add_new_rules_index_anchor_index = index_lines.index(str(add_new_rules_index_anchor)) | ||
index_lines[add_new_rules_index_anchor_index] = str(' {error_code}\n{anchor}'.format( | ||
anchor=add_new_rules_index_anchor, | ||
error_code=error_code, | ||
)) | ||
|
||
with open(os.path.join(rules_directory, 'index.rst'.format(error_code)), 'w') as f: | ||
f.writelines(index_lines) | ||
|
||
|
||
def create_rule_skeleton( | ||
description, # type: typing.Optional[typing.Text] | ||
detection_class_name, # type: typing.Text | ||
documentation_link, # type: typing.Optional[typing.Text] | ||
error_level, # type: Level | ||
rule_type, # type: RuleType | ||
short_name, # type: typing.Optional[typing.Text] | ||
): | ||
def _camel_to_snake_case(text): | ||
# type: (typing.Text) -> typing.Text | ||
# Thanks to https://stackoverflow.com/a/1176023 | ||
return re.sub(r'(?<!^)(?=[A-Z])', '_', text).lower() | ||
|
||
python_file_name_no_ext = _camel_to_snake_case(detection_class_name) | ||
|
||
error_code = _RULE_TYPE_TEMPLATE[rule_type].format( | ||
number=1 + sum( | ||
1 | ||
for rule in RuleRegistry.rules() | ||
if rule.rule_type == rule_type | ||
), | ||
) | ||
|
||
for method in _create_code_skeleton, _create_documentation_skeleton: | ||
method( | ||
description=description, | ||
detection_class_name=detection_class_name, | ||
documentation_link=documentation_link, | ||
error_code=error_code, | ||
error_level=error_level, | ||
python_file_name_no_ext=python_file_name_no_ext, | ||
rule_type=rule_type, | ||
short_name=short_name, | ||
) | ||
return 1 | ||
|
||
|
||
def main(args=None): | ||
# type: (typing.Optional[typing.List[typing.Text]]) -> None | ||
parsed_arguments = parser().parse_args(args=args) | ||
print( | ||
'The tool is far from perfect. Please make sure that all the created skeleton make sense ;)', | ||
file=sys.stderr, | ||
) | ||
create_rule_skeleton( | ||
description=parsed_arguments.description, | ||
detection_class_name=parsed_arguments.detection_class_name, | ||
documentation_link=parsed_arguments.documentation_link, | ||
error_level=Level[parsed_arguments.error_level], | ||
rule_type=RuleType[parsed_arguments.rule_type], | ||
short_name=parsed_arguments.short_name, | ||
) | ||
|
||
|
||
if __name__ == '__main__': | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters