fairy-slipper/fairy_slipper/cmd/wadl_to_swagger.py

1020 lines
37 KiB
Python

# Copyright (c) 2015 Russell Sim <russell.sim@gmail.com>
#
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import print_function
from __future__ import unicode_literals
from collections import defaultdict
from copy import copy
import json
import logging
import os
from os import path
import re
import textwrap
import xml.sax
from jinja2 import Environment
import prettytable
log = logging.getLogger(__name__)
TYPE_MAP = {
'string': 'string',
'xsd:string': 'string',
'csapi:string': 'string',
'xsd:int': 'integer',
'csapi:uuid': 'string',
'xsd:boolean': 'boolean',
'boolean': 'boolean',
'object': 'object',
'csapi:bool': 'boolean',
'xsd:bool': 'boolean',
'xsd:datetime': 'string',
'regexp': 'string',
'xsd:datetime': 'string',
'xsd:date': 'string',
'xsd:dict': 'object',
'dict': 'object',
'alarm': 'string',
'xsd:timestamp': 'string',
'xsd:char': 'string',
'list': 'array',
'csapi:flavorswithonlyidsnameslinks': 'string',
'csapi:imagestatus': 'string',
'csapi:imageswithonlyidsnameslinks': 'string',
'xsd:enum': 'string',
'xsd:anyuri': 'string',
'csapi:serverforupdate': 'string',
'capi:uuid': 'string',
'xsd:uuid': 'string',
'string': 'string',
'imageapi:string': 'string',
'imageapi:imagestatus': 'string',
'imageapi:uuid': 'string',
'csapi:uuid': 'string',
'csapi:serverforcreate': 'string',
'csapi:blockdevicemapping': 'string',
'csapi:serverswithonlyidsnameslinks': 'string',
'csapi:serverstatus': 'string',
'csapi:dict': 'object',
'imageforcreate': 'string',
'xsd:ip': 'string',
'xsd:base64binary': 'string',
'enum': 'array',
'xsd:float': 'number',
# TODO(arrsim) This array types also set the items
# "tags": {
# "type": "array",
# "items": {
# "type": "string"
'xsd:list': 'array',
'array': 'array',
}
FORMAT_MAP = {
'xsd:anyURI': 'uri',
'xsd:datetime': 'date-time',
'xsd:ip': 'ipv4',
'regexp': 'regexp',
'xsd:timestamp': 'timestamp',
}
STYLE_MAP = {
'template': 'path',
'plain': 'body',
'query': 'query',
'header': 'header',
}
MIME_MAP = {
'json': 'application/json',
'txt': 'text/plain',
'xml': 'application/xml',
}
VERSION_RE = re.compile('v[0-9\.]+')
WHITESPACE_RE = re.compile('[\s]+', re.MULTILINE)
URL_TEMPLATE_RE = re.compile('{[^{}]+}')
CAPTION_RE = re.compile('[*`]*')
MARKUP_RE = re.compile('[.,:;)]+')
environment = Environment()
HTTP_REQUEST = """{{ method }} {{ url }} HTTP/1.1
{% for key, value in headers.items() -%}
{{ key }}: {{ value }}
{% endfor %}
"""
HTTP_REQUEST_TMPL = environment.from_string(HTTP_REQUEST)
HTTP_RESPONSE = """HTTP/1.1 {{ status_code }}
{% for key, value in headers.items() -%}
{{ key }}: {{ value }}
{% endfor %}
{{ body }}
"""
HTTP_RESPONSE_TMPL = environment.from_string(HTTP_RESPONSE)
def create_parameter(name, _in, description='',
type='xsd:string', required=True):
return {
"name": name,
"in": STYLE_MAP[_in],
"description": description,
"required": True if required == 'true' else False,
"type": TYPE_MAP[type.lower()],
"format": FORMAT_MAP.get(type, ''),
}
def join_url(parts):
"""Return a joined url without any duplicate slashes"""
return '/'.join(parts).replace('//', '/')
class SubParser(xml.sax.ContentHandler):
def __init__(self, parent):
# general state
self.tag_stack = []
self.attr_stack = []
self.parent = parent
self.result = None
self.kwargs = {}
def startElement(self, name, _attrs):
attrs = dict(_attrs)
self.tag_stack.append(name)
self.attr_stack.append(attrs)
return attrs
def endElement(self, name):
self.tag_stack.pop()
self.attr_stack.pop()
if not self.tag_stack:
self.parent.detach_subparser(self.result, **self.kwargs)
def search_stack_for(self, tag_name):
for tag, attrs in zip(reversed(self.tag_stack),
reversed(self.attr_stack)):
if tag == tag_name:
return attrs
def on_top_tag_stack(self, *args):
return self.tag_stack[-len(args):] == list(args)
class TableMixin(object):
def visit_table(self, attrs):
self.__table = prettytable.PrettyTable(hrules=prettytable.ALL)
self.__table.header = False
def depart_table(self):
self.content.append('\n\n')
self.content.append(str(self.__table))
self.content.append('\n\n')
def visit_caption(self, attrs):
self.content_stack.append([])
def depart_caption(self):
content = ''.join(self.content_stack.pop()).strip()
content = CAPTION_RE.sub('', content)
content = WHITESPACE_RE.sub(' ', content)
content = '**' + content + '**'
self.content.append(content)
def visit_th(self, attrs):
self.__table.header = True
def depart_th(self):
heading = self.content.pop().strip()
self.__table.field_names.append(heading)
self.__table.align[heading] = 'l'
self.__table.valign[heading] = 't'
self.__table.max_width[heading] = 80
def visit_tr(self, attrs):
self.__row = []
def visit_td(self, attrs):
self.content_stack.append([])
def depart_td(self):
self.__row.append(''.join(self.content_stack.pop()).strip())
def depart_tr(self):
if self.__row:
columns = len(self.__table.field_names)
self.__row.extend(['' for n in range(columns - len(self.__row))])
self.__table.add_row(self.__row)
class ParaParser(SubParser, TableMixin):
EMPHASIS = {
'bold': '**',
'italic': '*'
}
def __init__(self, parent):
super(ParaParser, self).__init__(parent)
self.content_stack = [[]]
self.current_emphasis = None
self.nesting = 0
self.no_space = False
self.fill_width = 67
self.wrapper = textwrap.TextWrapper(width=self.fill_width)
self.shortdesc = False
self.inline_markup_stack = []
self.hyperlink_end = False
self.litblockstr = ''
self.base_indent = ' '
self.markup_end = False
@property
def content(self):
return self.content_stack[-1]
def startElement(self, name, _attrs):
super(ParaParser, self).startElement(name, _attrs)
fn = getattr(self, 'visit_%s' % name, None)
if fn:
fn(dict(_attrs))
def endElement(self, name):
content = ''.join(self.content)
self.result = content
super(ParaParser, self).endElement(name)
fn = getattr(self, 'depart_%s' % name, None)
if fn:
fn()
def characters(self, content):
if not content:
return
# Fold up any white space into a single char
if not self.on_top_tag_stack('programlisting'):
content = WHITESPACE_RE.sub(' ', content)
if content == ' ':
return
if content[0] == '\n':
return
if self.content:
if self.content[-1].endswith('\n'):
content = ' ' * self.nesting + content.strip()
elif self.content[-1].endswith(' '):
content = content.strip()
elif (self.on_top_tag_stack('programlisting')):
if self.content[-1].endswith('<'):
pass
else:
if self.search_stack_for('itemizedlist') is None:
content = '\n' + ' ' * self.nesting + content
else:
content = '\n' + self.base_indent * self.nesting + \
' ' + content
elif self.no_space:
content = content.strip()
elif self.hyperlink_end:
self.hyperlink_end = False
if content == '.' or content == ':':
pass
else:
content = ' ' + content.strip()
elif self.markup_end:
self.markup_end = False
if MARKUP_RE.match(content):
pass
else:
content = ' ' + content.strip()
else:
content = ' ' + content.strip()
if self.no_space is True:
self.inline_markup_stack.append(content)
else:
self.content.append(content)
def visit_listitem(self, attrs):
self.nesting = len([tag for tag in self.tag_stack
if tag == 'listitem']) - 1
if self.nesting > 0:
prev_nesting = self.nesting - 1
self.base_indent = ' ' * prev_nesting + ' '
else:
self.base_indent = ' '
self.content_stack.append([self.base_indent * self.nesting + '-'])
self.wrapper = textwrap.TextWrapper(
width=self.fill_width,
initial_indent=' ',
subsequent_indent=self.base_indent * self.nesting + ' ',)
def depart_listitem(self):
content = self.content_stack.pop()
self.content.append(''.join(content))
if self.content[-1].endswith('\n\n'):
pass
else:
self.content.append('\n')
self.nesting = len([tag for tag in self.tag_stack
if tag == 'listitem']) - 1
if self.nesting > 0:
prev_nesting = self.nesting - 1
self.base_indent = ' ' * prev_nesting + ' '
else:
self.base_indent = ' '
def depart_itemizedlist(self):
if self.search_stack_for('itemizedlist') is None:
self.wrapper = textwrap.TextWrapper(width=self.fill_width)
else:
self.wrapper = textwrap.TextWrapper(
width=self.fill_width,
initial_indent=self.base_indent * self.nesting + ' ',
subsequent_indent=self.base_indent * self.nesting + ' ',)
def depart_orderedlist(self):
if self.search_stack_for('itemizedlist') is None:
self.wrapper = textwrap.TextWrapper(width=self.fill_width)
def visit_para(self, attrs):
if attrs.get('role') == 'shortdesc':
self.shortdesc = True
self.content_stack.append([''])
if self.search_stack_for('itemizedlist') is not None:
return
if self.content:
if self.content[-1].endswith('\n\n'):
pass
elif self.content[-1].endswith('\n'):
self.content.append('\n')
def depart_para(self):
content = ''.join(self.content_stack.pop()).strip()
literal_block = True
parts = content.partition('::\n\n')
if parts[0] == content:
parts = content.partition('.. code-block::')
if parts[0] == content:
literal_block = False
else:
parts = content.partition(self.litblockstr)
if literal_block:
wrapped = self.wrapper.wrap(parts[0])
wrapped = '\n'.join(wrapped)
litcontent = parts[2].partition('\n\n')
if self.search_stack_for('itemizedlist') is None:
wrapped += '' + parts[1] + litcontent[0] + '\n'
else:
indent = self.base_indent * self.nesting + ' '
wrapped += indent + parts[1] + indent + litcontent[0] + '\n'
postwrap = self.wrapper.wrap(litcontent[2])
postwrap = '\n'.join(postwrap)
wrapped += postwrap
self.content.append(wrapped)
else:
wrapped = self.wrapper.wrap(content)
self.content.append('\n'.join(wrapped))
self.content.append('\n\n')
if self.search_stack_for('itemizedlist') is None:
pass
else:
self.wrapper = textwrap.TextWrapper(
width=self.fill_width,
initial_indent=self.base_indent * self.nesting + ' ',
subsequent_indent=self.base_indent * self.nesting + ' ',)
if self.shortdesc is True:
self.kwargs['shortdesc'] = self.result.strip()
# Reset state variables
self.content_stack = [[]]
self.shortdesc = False
def visit_title(self, attrs):
self.current_emphasis = attrs.get('role', 'bold')
self.no_space = True
def depart_title(self):
content = ' ' + self.EMPHASIS[self.current_emphasis]
content += ' '.join(self.inline_markup_stack[0:None])
content += self.EMPHASIS[self.current_emphasis]
self.content.append(content)
self.content.append('\n\n')
self.inline_markup_stack[:] = []
self.no_space = False
self.current_emphasis = None
def visit_code(self, attrs):
self.no_space = True
def depart_code(self):
content = ' ``'
if self.content:
if self.content[-1].endswith('(') or \
self.content[-1].endswith(' '):
content = '``'
content += ' '.join(self.inline_markup_stack[0:None])
content += '``'
self.content.append(content)
self.inline_markup_stack[:] = []
self.no_space = False
self.markup_end = True
def visit_emphasis(self, attrs):
# Bold is the default emphasis
self.current_emphasis = attrs.get('role', 'bold')
self.no_space = True
def depart_emphasis(self):
content = ' ' + self.EMPHASIS[self.current_emphasis]
if self.content:
if self.content[-1].endswith('(') or \
self.content[-1].endswith(' '):
content = '' + self.EMPHASIS[self.current_emphasis]
content += ' '.join(self.inline_markup_stack[0:None])
content += self.EMPHASIS[self.current_emphasis]
self.content.append(content)
self.inline_markup_stack[:] = []
self.no_space = False
self.current_emphasis = None
self.markup_end = True
def visit_programlisting(self, attrs):
self.nesting = 3
if not attrs:
if self.search_stack_for('itemizedlist') is None:
self.content.append('::\n\n')
else:
self.content.append(self.base_indent * self.nesting +
' ' + '::\n\n')
else:
if self.search_stack_for('itemizedlist') is None:
self.litblockstr = (
'.. code-block:: %s\n\n' % attrs['language']
)
self.content.append(
'.. code-block:: %s\n\n' % attrs['language']
)
else:
self.content.append(
self.base_indent * self.nesting +
' ' + '.. code-block:: %s\n\n' % attrs['language']
)
def depart_programlisting(self):
self.nesting = 0 # no indent for blank lines
self.content.append('\n\n')
def visit_link(self, attrs):
if attrs:
self.inline_markup_stack.append(attrs['xlink:href'])
self.no_space = True
def depart_link(self):
content = ' `'
# anonymous link
if len(self.inline_markup_stack) is 1:
content += ('<%s>`__' % self.inline_markup_stack[0])
else:
content += ' '.join(self.inline_markup_stack[1:None])
content += (' <%s>`_' % self.inline_markup_stack[0])
self.content.append(content)
self.inline_markup_stack[:] = []
self.no_space = False
self.hyperlink_end = True
class WADLHandler(xml.sax.ContentHandler):
def __init__(self, filename, api_ref):
self.filename = filename
self.api_ref = api_ref
self.method_tag_map = {method.split('#', 1)[1]: tag
for method, tag
in self.api_ref['method_tags'].items()
if method.split('#', 1)[0] == filename}
self.resource_tag_map = {resource.split('#', 1)[1]: tag
for resource, tag
in self.api_ref['resource_tags'].items()
if resource.split('#', 1)[0] == filename}
self.file_tag = self.api_ref['file_tags'].get(filename, None)
self.actual_tags = set(tag['name'] for tag in self.api_ref['tags'])
def startDocument(self):
# API state
self.apis = {}
self.current_api = None
self.schemas = {}
# Resource Mapping
self.resource_map = {}
self.resource_types = {}
self.resource_ids = defaultdict(list)
self.resource_id_stack = []
# URL paths
self.url_map = {}
self.url_params = {}
self.url = []
# general state
self.tag_stack = []
self.attr_stack = []
self.content = None
self.parser = None
def detach_subparser(self, result, **kwargs):
self.parser = None
self.result_fn(result, **kwargs)
self.result_fn = None
def attach_subparser(self, parser, result_fn):
self.parser = parser
self.result_fn = result_fn
def endDocument(self):
for api in self.apis.values():
for method in api:
method['consumes'] = list(method['consumes'])
method['produces'] = list(method['produces'])
def parameter_description(self, content, **kwargs):
name = self.search_stack_for('param')['name']
self.url_params[name] = content.strip()
def api_summary(self, content, **kwargs):
if kwargs.get('shortdesc'):
self.current_api['summary'] = kwargs['shortdesc']
self.current_api['description'] = content.strip()
def request_parameter_description(self, content, **kwargs):
param = self.search_stack_for('param')
style = STYLE_MAP[param['style']]
name = param['name']
if style == 'body':
parameters = self.current_api['parameters']
schema_name = parameters[0]['schema']['$ref'].rsplit('/', 1)[1]
schema = self.schemas[schema_name]
schema['properties'][name]['description'] = content.strip()
else:
self.current_api['parameters'][-1]['description'] = content.strip()
def response_schema_description(self, content, **kwargs):
status_code = self.search_stack_for('response')['status']
if ' ' in status_code:
status_codes = status_code.split(' ')
if '200' in status_codes:
status_code = '200'
elif '201' in status_codes:
status_code = '201'
# TODO(arrsim) need to do something with the other status
# codes
param = self.search_stack_for('param')
style = STYLE_MAP[param['style']]
name = param['name']
if style == 'header':
response = self.current_api['responses'][status_code]
response['headers'][name]['description'] = content.strip()
elif style == 'body':
parameters = self.current_api['parameters']
if len(parameters) > 0:
schema_name = parameters[0]['schema']['$ref'].rsplit('/', 1)[1]
schema_name = schema_name + '_' + status_code
schema = self.schemas[schema_name]
schema['properties'][name]['description'] = content.strip()
def search_stack_for(self, tag_name):
for tag, attrs in zip(reversed(self.tag_stack),
reversed(self.attr_stack)):
if tag == tag_name:
return attrs
def on_top_tag_stack(self, *args):
return self.tag_stack[-len(args):] == list(args)
def startElement(self, name, _attrs):
attrs = dict(_attrs)
if name == 'wadl:doc':
if self.on_top_tag_stack('resource', 'param'):
self.attach_subparser(ParaParser(self),
self.parameter_description)
if self.on_top_tag_stack('method'):
self.current_api['title'] = attrs.get('title')
self.attach_subparser(ParaParser(self), self.api_summary)
if self.on_top_tag_stack('request', 'representation',
'param'):
self.attach_subparser(ParaParser(self),
self.request_parameter_description)
if self.on_top_tag_stack('response', 'representation', 'param'):
self.attach_subparser(ParaParser(self),
self.response_schema_description)
if self.parser:
return self.parser.startElement(name, _attrs)
self.tag_stack.append(name)
self.attr_stack.append(attrs)
self.content = []
if name == 'method':
if 'id' in attrs and 'name' in attrs:
id = attrs['id']
if id in self.url_map:
url = self.url_map[id]
elif id in self.resource_map:
resource = self.resource_map[id]
url = self.resource_types[resource]
else:
log.warning("Can't find method %s", id)
# Create the minimal object to prevent creating
# exceptions for this case everywhere.
self.current_api = {
'produces': set(),
'consumes': set(),
'examples': {},
'responses': {},
'parameters': {},
}
return
tag = self.method_tag_map.get(id, '')
name = attrs['name'].lower()
if url in self.apis:
root_api = self.apis[url]
else:
self.apis[url] = root_api = []
self.current_api = {
'operationId': id,
'tags': set(),
'method': name,
'produces': set(),
'consumes': set(),
'examples': {},
'parameters': [{'in': "body",
'name': "body",
'description': "",
'required': False,
'schema': {
'$ref': "#/definitions/%s" % id
}}],
'responses': {},
}
if tag:
self.current_api['tags'].add(tag)
elif id in self.resource_ids:
for tag_id in reversed(self.resource_ids[id]):
r_tag_id = self.resource_tag_map.get(tag_id)
if r_tag_id not in self.actual_tags:
continue
self.current_api['tags'].add(r_tag_id)
break
if not self.current_api['tags']:
if self.file_tag:
self.current_api['tags'].add(self.file_tag)
self.current_api['tags'] = list(self.current_api['tags'])
# If there are no tags then we couldn't find the
# method in the chapters.
if self.current_api['tags']:
root_api.append(self.current_api)
else:
log.warning("No tags for method %s" % id)
for param, doc in self.url_params.items():
if ('{%s}' % param) in url:
self.current_api['parameters'].append(
create_parameter(param, 'template', doc))
# URL paths
if name == 'resource':
self.url.append(attrs.get('path', '').replace('//', '/'))
self.resource_id_stack.append(attrs.get('id', None))
if self.on_top_tag_stack('resource_type', 'method'):
self.resource_map[attrs.get('href').strip('#')] \
= self.attr_stack[-2]['id']
# Methods and Resource Types
if name == 'resource' and attrs.get('type'):
self.resource_types[attrs.get('type').strip('#')] \
= join_url(self.url)
if self.on_top_tag_stack('resource', 'method'):
href = attrs.get('href').strip('#')
self.url_map[href] = join_url(self.url)
self.resource_ids[href] = [r_id for r_id in self.resource_id_stack
if r_id]
if name == 'xsdxt:code':
if not attrs.get('href'):
return
if self.search_stack_for('response') is not None:
type = 'response'
status_code = self.search_stack_for('response')['status']
if ' ' in status_code:
status_codes = status_code.split(' ')
if '200' in status_codes:
status_code = '200'
# TODO(arrsim) need to do something with the other
# status codes
elif self.search_stack_for('request') is not None:
type = 'request'
else:
log.error("Can't find request or response tag. %s",
self.tag_stack)
raise Exception("Can't find request or response tag.")
media_type = MIME_MAP[attrs['href'].rsplit('.', 1)[-1]]
# XML is removed, skip all these
if media_type == 'application/xml':
return
pathname = path.join(path.dirname(self.filename), attrs['href'])
try:
sample = open(pathname).read()
if media_type == 'application/json':
sample = json.loads(sample)
except IOError:
log.warning("Can't find file %s" % pathname)
sample = None
if media_type != 'text/plain':
self.current_api['produces'].add(media_type)
self.current_api['consumes'].add(media_type)
if sample and type == 'response':
response = self.current_api['responses'][status_code]
response['examples'][media_type] = sample
# Set response sample filename for rst publishing
self.current_api['responses'][status_code]['response_sample'] \
= attrs['href']
elif sample and type == 'request':
# Add request examples (Not swagger supported)
self.current_api['examples'][media_type] = sample
# Set request sample filename for rst publishing
self.current_api['request_sample'] = attrs['href']
if name == 'response':
if 'status' not in attrs:
return
status_code = attrs['status']
response = {
'description': '',
'schema': {},
'headers': {},
'examples': {},
}
if ' ' in status_code:
status_codes = status_code.split(' ')
for status_code in status_codes:
# For each of the multiple status make copies of
# blank responses? The duplicates will be ignored
# by subsequent calls that update the response object.
self.current_api['responses'][status_code] = copy(response)
else:
self.current_api['responses'][status_code] = response
if self.on_top_tag_stack('request', 'representation', 'param'):
parameters = self.current_api['parameters']
name = attrs['name']
parameter = create_parameter(
name=name,
_in=attrs.get('style', 'plain'),
description='',
type=attrs.get('type', 'string'),
required=attrs.get('required'))
if parameter['in'] == 'body':
schema_name = parameters[0]['schema']['$ref'].rsplit('/', 1)[1]
if schema_name not in self.schemas:
self.schemas[schema_name] = {'type': 'object',
'properties': {}}
schema_properties = self.schemas[schema_name]['properties']
schema_properties[parameter['name']] = parameter
del parameter['name']
del parameter['in']
else:
parameters.append(parameter)
if self.on_top_tag_stack('response', 'representation', 'param'):
parameters = self.current_api['parameters']
status_code = self.attr_stack[-3]['status']
if ' ' in status_code:
status_codes = status_code.split(' ')
if '200' in status_codes:
status_code = '200'
elif '201' in status_codes:
status_code = '201'
# TODO(arrsim) need to do something with the other status codes
name = attrs['name']
parameter = create_parameter(
name=name,
_in=attrs.get('style', 'plain'),
description='',
type=attrs.get('type', 'string'),
required=attrs.get('required'))
if parameter['in'] == 'body':
if len(parameters) > 0:
s = parameters[0]['schema']['$ref']
schema_name = s.rsplit('/', 1)[1]
schema_name = schema_name + '_' + status_code
if schema_name not in self.schemas:
self.schemas[schema_name] = {'type': 'object',
'properties': {}}
schema_properties = self.schemas[schema_name]['properties']
schema_properties[parameter['name']] = parameter
response = self.current_api['responses'][status_code]
response['schema']['$ref'] = "#/definitions/%s" % \
schema_name
del parameter['name']
del parameter['in']
elif parameter['in'] == 'header':
headers = self.current_api['responses'][status_code]['headers']
headers[parameter['name']] = parameter
del parameter['name']
del parameter['in']
def endElement(self, name):
if self.parser:
return self.parser.endElement(name)
if self.current_api and name == 'method':
# Clean up the parameters of methods that have take no
# body content.
parameters = self.current_api['parameters']
if parameters and 'schema' in parameters[0]:
schema_name = parameters[0]['schema']['$ref'].rsplit('/', 1)[1]
if schema_name not in self.schemas:
self.current_api['parameters'] \
= self.current_api['parameters'][1:]
# URL paths
if name == 'resource':
self.url.pop()
self.resource_id_stack.pop()
self.tag_stack.pop()
self.attr_stack.pop()
def characters(self, content):
if self.parser:
return self.parser.characters(content)
content = content.strip()
if content:
self.content.append(content)
def main1(source_file, output_dir):
log.info('Reading API description from %s' % source_file)
api_ref = json.load(open(source_file))
files = set()
for filepath in api_ref['method_tags'].keys():
files.add(filepath.split('#', 1)[0])
for filepath in api_ref['resource_tags'].keys():
files.add(filepath.split('#', 1)[0])
for filepath in api_ref['file_tags'].keys():
files.add(filepath.split('#', 1)[0])
# Load supplementary examples file
example_name = (api_ref['service']
.replace('-admin', '')
.replace('-extensions', ''))
examples_file = path.join(path.dirname(source_file),
example_name + '-examples.json')
if path.exists(examples_file):
log.info('Reading examples from %s' % examples_file)
examples = json.load(open(examples_file))
else:
examples = []
output = {
u'info': {
'version': api_ref['version'],
'title': api_ref['title'],
'service': api_ref['service'],
'license': {
"name": "Apache 2.0",
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
}
},
u'paths': defaultdict(list),
u'tags': api_ref['tags'],
u'basePath': "/",
u'host': "developer.openstack.org",
u'definitions': {},
u'externalDocs': {
'description': 'OpenStack Docs',
'url': 'http://docs.openstack.org',
},
u"swagger": u"2.0",
}
for file in files:
log.info('Parsing %s' % file)
abs_filename = path.abspath(file)
ch = WADLHandler(abs_filename, api_ref)
xml.sax.parse(file, ch)
for urlpath, apis in ch.apis.items():
output['paths'][urlpath].extend(apis)
output['definitions'].update(ch.schemas)
for ex_request, ex_response in examples:
for urlpath in output['paths']:
url_matcher = "^" + URL_TEMPLATE_RE.sub('[^/]+', urlpath) + "$"
method = ex_request['method'].lower()
if re.match(url_matcher, ex_request['url']):
method_count = defaultdict(int)
for operation in output['paths'][urlpath]:
method_count[operation['method'].lower()] += 1
if any(i > 1 for i in method_count.values()):
# Skip any of the multi-payload endpoints. They
# are madness.
break
for operation in output['paths'][urlpath]:
if operation['method'].lower() == method:
break
else:
log.warning("Couldn't find any operations %s for %s",
method, urlpath)
break
request = HTTP_REQUEST_TMPL.render(
headers=ex_request['headers'],
method=ex_request['method'],
url=ex_request['url'])
operation['examples'] = {'text/plain': request}
# Override any responses
status_code = ex_response['status_code']
response = HTTP_RESPONSE_TMPL.render(
status_code=status_code,
headers=ex_response['headers'],
body=ex_response['body'] or '')
if status_code in operation['responses']:
operation['responses'][status_code]['examples'] = \
{'text/plain': response}
else:
operation['responses'][status_code] = \
{'examples': {'text/plain': response}}
else:
log.warning("Service %s %s doesn't have matching "
"URL for example %s %s",
output['info']['service'], output['info']['version'],
method, ex_request['url'])
os.chdir(output_dir)
pathname = '%s-%s-swagger.json' % (api_ref['service'],
api_ref['version'])
with open(pathname, 'w') as out_file:
json.dump(output, out_file, indent=2, sort_keys=True)
def main():
import argparse
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument(
'-v', '--verbose', action='count', default=0,
help="Increase verbosity (specify multiple times for more)")
parser.add_argument(
'-o', '--output-dir', action='store',
help="The directory to output the JSON files too.")
parser.add_argument(
'filename',
help="File to convert")
args = parser.parse_args()
log_level = logging.WARNING
if args.verbose == 1:
log_level = logging.INFO
elif args.verbose >= 2:
log_level = logging.DEBUG
logging.basicConfig(
level=log_level,
format='%(asctime)s %(name)s %(levelname)s %(message)s')
filename = path.abspath(args.filename)
main1(filename, output_dir=args.output_dir)