|
| 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() |
0 commit comments