Completed
Push — master ( cb8f01...434676 )
by Carlos Eduardo
14s
created

OpenAPI   A

Complexity

Total Complexity 20

Size/Duplication

Total Lines 142
Duplicated Lines 0 %

Importance

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

12 Methods

Rating   Name   Duplication   Size   Complexity  
A _add_methods() 0 5 2
B _parse_docstring() 0 34 3
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 render_template() 0 5 1
A _parse_decorated_functions() 0 15 2
A _rule2path() 0 5 1
A _read_napp_info() 0 3 1
A _save() 0 8 2
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 _read_napp_info(self):
141
        filename = self._napp_path / 'kytos.json'
142
        return json.load(filename.open())
143
144
    def _save(self, context):
145
        tpl_env = Environment(
146
            loader=FileSystemLoader(str(self._template.parent)),
147
            trim_blocks=True)
148
        content = tpl_env.get_template(
149
            'openapi.yml.template').render(context)
150
        with self._api_file.open('w') as openapi:
151
            openapi.write(content)
152