# -*- coding: utf-8 -*- from docutils import nodes import traceback from docutils.parsers.rst import Directive from past.builtins import basestring from sphinx.locale import _ from six.moves.urllib import parse as urlparse # Retain Py2 compatibility for urlparse import requests from requests_file import FileAdapter import json class swaggerv2doc(nodes.Admonition, nodes.Element): pass def visit_swaggerv2doc_node(self, node): self.visit_admonition(node) def depart_swaggerv2doc_node(self, node): self.depart_admonition(node) class SwaggerV2DocDirective(Directive): DEFAULT_GROUP = '' # this enables content in the directive has_content = True def processSwaggerURL(self, url): parsed_url = urlparse.urlparse(url) if not parsed_url.scheme: # Assume file relative to documentation env = self.state.document.settings.env relfn, absfn = env.relfn2path(url) env.note_dependency(relfn) with open(absfn) as fd: content = fd.read() return json.loads(content) else: s = requests.Session() s.mount('file://', FileAdapter()) r = s.get(url) return r.json() def create_item(self, key, value): para = nodes.paragraph() para += nodes.strong('', key) para += nodes.Text(value) item = nodes.list_item() item += para return item def expand_values(self, list): expanded_values = '' for value in list: expanded_values += value + ' ' return expanded_values def cell(self, contents): if isinstance(contents, basestring): contents = nodes.paragraph(text=contents) return nodes.entry('', contents) def row(self, cells): return nodes.row('', *[self.cell(c) for c in cells]) def create_table(self, head, body, colspec=None): table = nodes.table() tgroup = nodes.tgroup() table.append(tgroup) # Create a colspec for each column if colspec is None: colspec = [1 for n in range(len(head))] for width in colspec: tgroup.append(nodes.colspec(colwidth=width)) # Create the table headers thead = nodes.thead() thead.append(self.row(head)) tgroup.append(thead) # Create the table body tbody = nodes.tbody() tbody.extend([self.row(r) for r in body]) tgroup.append(tbody) return table def make_responses(self, responses): # Create an entry with swagger responses and a table of the response properties entries = [] paragraph = nodes.paragraph() paragraph += nodes.strong('', 'Responses') entries.append(paragraph) head = ['Name', 'Description', 'Type'] for response_name, response in responses.items(): paragraph = nodes.paragraph() paragraph += nodes.emphasis('', '%s - %s' % (response_name, response.get('description', ''))) entries.append(paragraph) body = [] # if the optional field properties is in the schema, display the properties if isinstance(response.get('schema'), dict) and 'properties' in response.get('schema'): for property_name, property in response.get('schema').get('properties', {}).items(): row = [] row.append(property_name) row.append(property.get('description', '')) row.append(property.get('type', '')) body.append(row) table = self.create_table(head, body) entries.append(table) return entries def make_parameters(self, parameters): entries = [] head = ['Name', 'Position', 'Description', 'Type'] body = [] for param in parameters: row = [] row.append(param.get('name', '')) row.append(param.get('in', '')) row.append(param.get('description', '')) row.append(param.get('type', '')) body.append(row) table = self.create_table(head, body) paragraph = nodes.paragraph() paragraph += nodes.strong('', 'Parameters') entries.append(paragraph) entries.append(table) return entries def make_method(self, path, method_type, method): swagger_node = swaggerv2doc(path) swagger_node += nodes.title(path, method_type.upper() + ' ' + path) paragraph = nodes.paragraph() paragraph += nodes.Text(method.get('summary', '')) bullet_list = nodes.bullet_list() method_sections = {'Description': 'description', 'Consumes': 'consumes', 'Produces': 'produces'} for title in method_sections: value_name = method_sections[title] value = method.get(value_name) if value is not None: bullet_list += self.create_item(title + ': \n', value) paragraph += bullet_list swagger_node += paragraph parameters = method.get('parameters') if parameters is not None: swagger_node += self.make_parameters(parameters) responses = method.get('responses') if responses is not None: swagger_node += self.make_responses(responses) return [swagger_node] def group_tags(self, api_desc): groups = {} if 'tags' in api_desc: for tag in api_desc['tags']: groups[tag['name']] = [] if len(groups) == 0: groups[SwaggerV2DocDirective.DEFAULT_GROUP] = [] for path, methods in api_desc['paths'].items(): for method_type, method in methods.items(): if SwaggerV2DocDirective.DEFAULT_GROUP in groups: groups[SwaggerV2DocDirective.DEFAULT_GROUP].append((path, method_type, method)) else: for tag in method['tags']: groups.setdefault(tag, []).append((path, method_type, method)) return groups def create_section(self, title): section = nodes.section(ids=[title]) section += nodes.title(title, title) return section def check_tags(self, selected_tags, tags, api_url): invalid_tags = list(set(selected_tags) - set(tags)) if len(invalid_tags) > 0: msg = self.reporter.error("Error. Tag '%s' not found in Swagger URL %s." % (invalid_tags[0], api_url)) return [msg] def run(self): self.reporter = self.state.document.reporter api_url = self.content[0] if len(self.content) > 1: selected_tags = self.content[1:] else: selected_tags = [] try: api_desc = self.processSwaggerURL(api_url) groups = self.group_tags(api_desc) self.check_tags(selected_tags, groups.keys(), api_url) entries = [] for tag_name, methods in groups.items(): if tag_name in selected_tags or len(selected_tags) == 0: section = self.create_section(tag_name) for path, method_type, method in methods: section += self.make_method(path, method_type, method) entries.append(section) return entries except Exception as e: error_message = 'Unable to process URL: %s' % api_url print(error_message) traceback.print_exc() error = nodes.error('') para_error = nodes.paragraph() para_error += nodes.Text(error_message + '. Please check that the URL is a valid Swagger api-docs URL and it is accesible') para_error_detailed = nodes.paragraph() para_error_detailed = nodes.strong('Processing error. See console output for a more detailed error') error += para_error error += para_error_detailed return [error]