kytos.utils.openapi.OpenAPI._add_function_paths()   A
last analyzed

Complexity

Conditions 2

Size

Total Lines 6
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 6
nop 2
dl 0
loc 6
ccs 6
cts 6
cp 1
crap 2
rs 10
c 0
b 0
f 0
1
"""Deal with OpenAPI v3."""
2 1
import json
3 1
import re
4
5 1
from jinja2 import Environment, FileSystemLoader
6
7
8 1
class OpenAPI:  # pylint: disable=too-few-public-methods
9
    """Create OpenAPI skeleton."""
10
11 1
    def __init__(self, napp_path, tpl_path):
12
        """Instantiate an OpenAPI object.
13
14
        Args:
15
            napp_path (string): Napp directory
16
            tlp_path (string): File name from template
17
18
        """
19 1
        self._napp_path = napp_path
20 1
        self._template = tpl_path / 'openapi.yml.template'
21 1
        self._api_file = napp_path / 'openapi.yml'
22
23 1
        self._napp_dict = self._parse_napp_metadata()
24
25
        # Data for a path
26 1
        self._summary = None
27 1
        self._description = None
28
29
        # Part of template context
30 1
        self._paths = {}
31
32 1
    def render_template(self):
33
        """Render and save API doc in openapi.yml."""
34 1
        self._parse_paths()
35 1
        context = dict(napp=self._napp_dict, paths=self._paths)
36 1
        self._save(context)
37
38 1
    def _parse_napp_metadata(self):
39
        """Return a NApp metadata file."""
40 1
        filename = self._napp_path / 'kytos.json'
41 1
        with open(filename, encoding='utf-8') as data_file:
42 1
            data = json.loads(data_file.read())
43 1
        return data
44
45 1
    def _parse_paths(self):
46 1
        main_file = self._napp_path / 'main.py'
47 1
        code = main_file.open().read()
48 1
        return self._parse_decorated_functions(code)
49
50 1
    def _parse_decorated_functions(self, code):
51
        """Return URL rule, HTTP methods and docstring."""
52 1
        matches = re.finditer(r"""
53
                # @rest decorators
54
                (?P<decorators>
55
                    (?:@rest\(.+?\)\n)+  # one or more @rest decorators inside
56
                )
57
                # docstring delimited by 3 double quotes
58
                .+?"{3}(?P<docstring>.+?)"{3}
59
                """, code, re.VERBOSE | re.DOTALL)
60
61 1
        for function_match in matches:
62 1
            m_dict = function_match.groupdict()
63 1
            self._parse_docstring(m_dict['docstring'])
64 1
            self._add_function_paths(m_dict['decorators'])
65
66 1
    def _get_absolute_rule(self, rule):
67 1
        napp_prefix = "/api/{username}/{name}/"
68 1
        relative_rule = rule[1:] if rule.startswith('/') else rule
69 1
        return napp_prefix.format_map(self._napp_dict) + relative_rule
70
71 1
    def _add_function_paths(self, decorators_str):
72 1
        for rule, parsed_methods in self._parse_decorators(decorators_str):
73 1
            absolute_rule = self._get_absolute_rule(rule)
74 1
            path_url = self._rule2path(absolute_rule)
75 1
            path_methods = self._paths.setdefault(path_url, {})
76 1
            self._add_methods(parsed_methods, path_methods)
77
78 1
    def _parse_docstring(self, docstring):
79
        """Parse the method docstring."""
80 1
        match = re.match(r"""
81
            # Following PEP 257
82
            \s* (?P<summary>[^\n]+?) \s*   # First line
83
84
            (                              # Description and YAML are optional
85
              (\n \s*){2}                  # Blank line
86
87
              # Description (optional)
88
              (
89
                (?!-{3,})                     # Don't use YAML as description
90
                \s* (?P<description>.+?) \s*  # Third line and maybe others
91
                (?=-{3,})?                    # Stop if "---" is found
92
              )?
93
94
              # YAML spec (optional) **currently not used**
95
              (
96
                -{3,}\n                       # "---" begins yaml spec
97
                (?P<open_api>.+)
98
              )?
99
            )?
100
            $""", docstring, re.VERBOSE | re.DOTALL)
101
102 1
        summary = 'TODO write the summary.'
103 1
        description = 'TODO write/remove the description'
104 1
        if match:
105 1
            m_dict = match.groupdict()
106 1
            summary = m_dict['summary']
107 1
            if m_dict['description']:
108
                description = re.sub(r'(\s|\n){2,}', ' ',
109
                                     m_dict['description'])
110 1
        self._summary = summary
111 1
        self._description = description
112
113 1
    def _parse_decorators(self, decorators_str):
114 1
        matches = re.finditer(r"""
115
             @rest\(
116
117
              ## Endpoint rule
118
              (?P<quote>['"])  # inside single or double quotes
119
                  (?P<rule>.+?)
120
              (?P=quote)
121
122
              ## HTTP methods (optional)
123
              (\s*,\s*
124
                  methods=(?P<methods>\[.+?\])
125
              )?
126
127
             .*?\)\s*$
128
            """, decorators_str, re.VERBOSE)
129
130 1
        for match in matches:
131 1
            rule = match.group('rule')
132 1
            methods = self._parse_methods(match.group('methods'))
133 1
            yield rule, methods
134
135 1
    @classmethod
136
    def _parse_methods(cls, list_string):
137
        """Return HTTP method list. Use json for security reasons."""
138 1
        if list_string is None:
139
            return ('GET',)
140
        # json requires double quotes
141 1
        json_list = list_string.replace("'", '"')
142 1
        return json.loads(json_list)
143
144 1
    def _add_methods(self, methods, path_methods):
145 1
        for method in methods:
146 1
            path_method = dict(summary=self._summary,
147
                               description=self._description)
148 1
            path_methods[method.lower()] = path_method
149
150 1
    @classmethod
151
    def _rule2path(cls, rule):
152
        """Convert relative Flask rule to absolute OpenAPI path."""
153 1
        typeless = re.sub(r'<\w+?:', '<', rule)  # remove Flask types
154 1
        return typeless.replace('<', '{').replace('>', '}')  # <> -> {}
155
156 1
    def _read_napp_info(self):
157 1
        filename = self._napp_path / 'kytos.json'
158 1
        return json.load(filename.open())
159
160 1
    def _save(self, context):
161 1
        tpl_env = Environment(
162
            loader=FileSystemLoader(str(self._template.parent)),
163
            trim_blocks=True)
164 1
        content = tpl_env.get_template(
165
            'openapi.yml.template').render(context)
166 1
        with self._api_file.open('w') as openapi:
167
            openapi.write(content)
168