Passed
Pull Request — master (#121)
by Carlos Eduardo
01:22
created

OpenAPI   A

Complexity

Total Complexity 20

Size/Duplication

Total Lines 160
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 160
rs 10
wmc 20

12 Methods

Rating   Name   Duplication   Size   Complexity  
A _add_methods() 0 5 2
B _parse_docstring() 0 34 3
A _read_napp_info() 0 3 1
A __init__() 0 14 1
A _add_function_paths() 0 6 2
A _parse_paths() 0 4 1
A _parse_methods() 0 8 2
A _parse_decorators() 0 21 2
A _save() 0 8 2
A render_template() 0 5 1
A _parse_decorated_functions() 0 15 2
A _rule2path() 0 5 1
1
"""Deal with OpenAPI v3."""
2
import json
3
import re
4
5
from jinja2 import Environment, FileSystemLoader
6
from kytos.core.api_server import APIServer
7
from kytos.core.napps.base import NApp
8
9
10
class OpenAPI:  # pylint: disable=too-few-public-methods
11
    """Create OpenAPI skeleton."""
12
13
    def __init__(self, napp_path, tpl_path):
14
        self._napp_path = napp_path
15
        self._template = tpl_path / 'openapi.yml.template'
16
        self._api_file = napp_path / 'openapi.yml'
17
18
        metadata = napp_path / 'kytos.json'
19
        self._napp = NApp.create_from_json(metadata)
20
21
        # Data for a path
22
        self._summary = None
23
        self._description = None
24
25
        # Part of template context
26
        self._paths = {}
27
28
    def render_template(self):
29
        """Render and save API doc in openapi.yml."""
30
        self._parse_paths()
31
        context = dict(napp=self._napp.__dict__, paths=self._paths)
32
        self._save(context)
33
34
    def _parse_paths(self):
35
        main_file = self._napp_path / 'main.py'
36
        code = main_file.open().read()
37
        return self._parse_decorated_functions(code)
38
39
    def _parse_decorated_functions(self, code):
40
        """Return URL rule, HTTP methods and docstring."""
41
        matches = re.finditer(r"""
42
                # @rest decorators
43
                (?P<decorators>
44
                    (?:@rest\(.+?\)\n)+  # one or more @rest decorators inside
45
                )
46
                # docstring delimited by 3 double quotes
47
                .+?"{3}(?P<docstring>.+?)"{3}
48
                """, code, re.VERBOSE | re.DOTALL)
49
50
        for function_match in matches:
51
            m_dict = function_match.groupdict()
52
            self._parse_docstring(m_dict['docstring'])
53
            self._add_function_paths(m_dict['decorators'])
54
55
    def _add_function_paths(self, decorators_str):
56
        for rule, parsed_methods in self._parse_decorators(decorators_str):
57
            absolute_rule = APIServer.get_absolute_rule(rule, self._napp)
58
            path_url = self._rule2path(absolute_rule)
59
            path_methods = self._paths.setdefault(path_url, {})
60
            self._add_methods(parsed_methods, path_methods)
61
62
    def _parse_docstring(self, docstring):
63
        """Parse the method docstring."""
64
        match = re.match(r"""
65
            # Following PEP 257
66
            \s* (?P<summary>[^\n]+?) \s*   # First line
67
68
            (                              # Description and YAML are optional
69
              (\n \s*){2}                  # Blank line
70
71
              # Description (optional)
72
              (
73
                (?!-{3,})                     # Don't use YAML as description
74
                \s* (?P<description>.+?) \s*  # Third line and maybe others
75
                (?=-{3,})?                    # Stop if "---" is found
76
              )?
77
78
              # YAML spec (optional) **currently not used**
79
              (
80
                -{3,}\n                       # "---" begins yaml spec
81
                (?P<open_api>.+)
82
              )?
83
            )?
84
            $""", docstring, re.VERBOSE | re.DOTALL)
85
86
        summary = 'TODO write the summary.'
87
        description = 'TODO write/remove the description'
88
        if match:
89
            m_dict = match.groupdict()
90
            summary = m_dict['summary']
91
            if m_dict['description']:
92
                description = re.sub(r'(\s|\n){2,}', ' ',
93
                                     m_dict['description'])
94
        self._summary = summary
95
        self._description = description
96
97
    def _parse_decorators(self, decorators_str):
98
        matches = re.finditer(r"""
99
             @rest\(
100
101
              ## Endpoint rule
102
              (?P<quote>['"])  # inside single or double quotes
103
                  (?P<rule>.+?)
104
              (?P=quote)
105
106
              ## HTTP methods (optional)
107
              (\s*,\s*
108
                  methods=(?P<methods>\[.+?\])
109
              )?
110
111
             .*?\)\s*$
112
            """, decorators_str, re.VERBOSE)
113
114
        for match in matches:
115
            rule = match.group('rule')
116
            methods = self._parse_methods(match.group('methods'))
117
            yield rule, methods
118
119
    @classmethod
120
    def _parse_methods(cls, list_string):
121
        """Return HTTP method list. Use json for security reasons."""
122
        if list_string is None:
123
            return APIServer.DEFAULT_METHODS
124
        # json requires double quotes
125
        json_list = list_string.replace("'", '"')
126
        return json.loads(json_list)
127
128
    def _add_methods(self, methods, path_methods):
129
        for method in methods:
130
            path_method = dict(summary=self._summary,
131
                               description=self._description)
132
            path_methods[method.lower()] = path_method
133
134
    @classmethod
135
    def _rule2path(cls, rule):
136
        """Convert relative Flask rule to absolute OpenAPI path."""
137
        typeless = re.sub(r'<\w+?:', '<', rule)  # remove Flask types
138
        return typeless.replace('<', '{').replace('>', '}')  # <> -> {}
139
140
#     def _parse_docstring(self, function):
141
#         summary = 'TODO write the summary.'
142
#         description = 'TODO write/remove the description'
143
#         if function.__doc__:
144
#             docstring = inspect.cleandoc(function.__doc__)
145
#             lines = docstring.split('\n')
146
#             summary = lines[0]
147
#             if len(lines) > 2:
148
#                 description = ' '.join(lines[2:])
149
#         self._summary = summary
150
#         self._description = description
151
152
#     @staticmethod
153
#     def _get_url_methods(function):
154
#         for url, options in function.route_params:
155
#             methods = options.get('methods', APIServer.DEFAULT_METHODS)
156
#             yield url, methods
157
158
    def _read_napp_info(self):
159
        filename = self._napp_path / 'kytos.json'
160
        return json.load(filename.open())
161
162
    def _save(self, context):
163
        tpl_env = Environment(
164
            loader=FileSystemLoader(str(self._template.parent)),
165
            trim_blocks=True)
166
        content = tpl_env.get_template(
167
            'openapi.yml.template').render(context)
168
        with self._api_file.open('w') as openapi:
169
            openapi.write(content)
170
171
#    def _get_paths(self):
172
#        napp_class = self._load_napp()
173
#        rest_functions = APIServer.get_decorated_functions(napp_class)
174
#        paths = []
175
#        for function in rest_functions:
176
#            self._parse_docstring(function)
177
#            fn_paths = [self._get_fn_paths(function)]
178
#            paths.extend(fn_paths)
179
#        return paths
180
#
181
#    def _get_fn_paths(self, function):
182
#        for rule, methods in function.route_params:
183
#            path = {}
184
#            absolute_rule = APIServer.get_absolute_rule(rule, self._napp)
185
#            path['path'] = self._rule2path(absolute_rule)
186
#            self._add_methods(methods, path)
187
#            yield path
188
189
190
# class DecoratorParser:  # pylint: disable=too-few-public-methods
191
#     """Parse OpenAPI data from methods decorated with @rest."""
192
#
193
#     @classmethod
194
#     def parse_api(cls, code, napp_id):
195
#         """Return data for rendering the openapi template."""
196
#         for function in cls._parse_decorator(code):
197
#             path = cls._rule2path(function['rule'], napp_id)
198
#             http_methods = cls._parse_methods(function['methods'])
199
#             docstring = cls._parse_docstring(function['docstring'])
200
#             if not docstring:
201
#                 continue
202
#
203
#             url = dict(path=path, methods=http_methods)
204
#
205
#             open_api = docstring.pop('open_api')
206
#             open_api.update(docstring)
207
#
208
#             open_api = {path: open_api}
209
#
210
#     @classmethod
211
#     def _rule2path(cls, rule, napp_id):
212
#         """Convert relative Flask rule to absolute OpenAPI path."""
213
#         typeless = re.sub(r'<\w+?:', '<', rule)  # remove Flask types
214
#         rel_path = typeless.replace('<', '{').replace('>', '}')  # <> -> {}
215
#         return '/api/' + napp_id + '/' + rel_path
216