Cyril Roelandt 9218f010a9 Properly document the enumeration type
Change-Id: I5ace904a43fb8d429c8ea212a80ef81e3b13ee84
2014-02-10 14:46:36 +01:00

401 lines
15 KiB
Python

#!/usr/bin/env python
# Copyright (C) 2013 eNovance SAS <licensing@enovance.com>
#
# Author: Cyril Roelandt <cyril.roelandt@enovance.com>
#
# 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 datetime import date
from docutils.nodes import SparseNodeVisitor, StopTraversal
import json
import os
from sphinx.builders import Builder
import tidylib
import xml.etree.ElementTree as ET
output_file = None
def generate_id(path, method):
path = path.replace('(', '')
path = path.replace(')', '')
elems = path.split('/')
elems = list(filter(lambda x: x, elems)) # Remove empty strings
elems = elems[1:] # Remove "vx" (v1, v2...)
n_elems = len(elems)
name = ""
if method == 'delete':
if elems[-1].endswith('_id'):
name += "delete" + elems[-1][0:-3].capitalize()
elif method == 'get':
if elems[-1].endswith('_id'):
name += "show" + elems[-1][0:-3].capitalize()
elif elems[-1].endswith('_name'):
name += "show" + elems[-1][0:-5].capitalize()
elif n_elems > 2:
if elems[-3][:-1] + '_id' == elems[-2]:
name += 'show' + elems[-3][:-1].capitalize()
name += elems[-1].capitalize()
elif elems[-3][:-1] + '_name' == elems[-2]:
name += 'show' + elems[-3][:-1].capitalize()
name += elems[-1].capitalize()
else:
name += "list" + elems[-1].capitalize()
elif method == 'post':
if elems[-1].endswith('_id'):
name += "create" + elems[-1][0:-3].capitalize()
elif elems[-1].endswith('_name'):
name += "create" + elems[-1][0:-5].capitalize()
else:
name += "create" + elems[-1][:-1].capitalize()
elif method == 'put':
if elems[-1].endswith('_id'):
name += "update" + elems[-1][0:-3].capitalize()
elif elems[-2].endswith('_id'):
# The name is probably something like ".../foos/foo_id/bar". This
# is the case in Ceilometer for "/v2/alarms/alarm_id/state"
name += "update" + elems[-2][0:-3].capitalize() + \
elems[-1].capitalize()
if not name:
name = raw_input('id for %s (%s)' % (path, method))
return name
def generate_title_from_id(id_):
words = []
current_start = 0
for i, char in enumerate(id_):
if not char.islower():
words.append(id_[current_start:i].lower())
current_start = i
if i > current_start:
words.append(id_[current_start:])
return ' '.join(words).capitalize()
def clean_up_xml(xml_str):
# When using UTF-8, tidy does not add an encoding attribute. Do it
# ourselves. See
# http://tidy.cvs.sourceforge.net/viewvc/tidy/tidy/src/lexer.c?
# revision=1.194&view=markup
xml_str = xml_str.replace('?>', ' encoding="UTF-8"?>', 1)
# tidy automatically inserts a whitespace at the end of a self-closing tag.
# See line 1347 at:
# http://tidy.cvs.sourceforge.net/viewvc/tidy/tidy/src/pprint.c?
# revision=1.119&view=markup
xml_str = xml_str.replace(' />', '/>')
# Add this comment right after the <?...?> line. Not sure how to do this
# using ElementTree.Comment(), since there is no parent element here.
# XXX(cyril): The starting year may not be the right one for every project.
xml_str = xml_str.replace('?>\n', '''?>
<!-- (C) 2012-%d OpenStack Foundation, All Rights Reserved -->
<!--*******************************************************-->
<!-- Import Common XML Entities -->
<!-- -->
<!-- You can resolve the entites with xmllint -->
<!-- -->
<!-- xmllint -noent os-compute-2.wadl -->
<!--*******************************************************-->
''' % date.today().year, 1)
return xml_str
class MyNodeVisitor(SparseNodeVisitor):
def __init__(self, document):
SparseNodeVisitor.__init__(self, document)
self.must_parse = False
self.methods = []
self.paths = {}
self.current_method = None
self.root = None
self.in_method_definition = False
self.needs_method_description = False
self.in_bullet_list = False
self.in_request = False
self.in_response = False
self.current_request_example = []
self.current_response_example = []
def depart_document(self, node):
if not self.must_parse:
# We've probably raised StopTraversal, but walkabout() will keep
# running the depart_* functions. Here, this is only
# depart_document(), so we have to leave early.
return
# We are done parsing this document, let's print everything we need.
resources = ET.SubElement(self.root, 'resources', {
'base': 'http://www.example.com'
})
# Create 'd', a dict of the form:
# {
# 'foo': {
# 'bar': {},
# 'baz': {}
# }
# }
# If the paths are 'foo/bar' and 'foo/baz'.
d = {}
for path, methods in self.paths.iteritems():
cd = d
l = path[1:-1].replace('(', '{').replace(')', '}').split('/')
for e in l:
if e not in cd:
cd[e] = {}
cd = cd[e]
def build_resources(root, d, path=''):
for k, v in d.iteritems():
tmp = ET.SubElement(root, 'resource', {
# NOTE(cyril): sometimes, id and path might differ. This
# should be good enough, though.
'id': k.replace('{', '').replace('}', ''),
'path': k,
})
if path + '/' + k + '/' in self.paths:
for method in self.paths[path + '/' + k + '/']:
ET.SubElement(tmp, 'method', {'href': '#' + method})
build_resources(tmp, v, path + '/' + k)
build_resources(resources, d)
# Now, add all the methods we've gathered.
for method in self.methods:
self.root.append(method)
# Finally, write the output.
with open(output_file, 'w+') as f:
options = {
'add-xml-decl': True,
'indent': True,
'indent-spaces': 4,
'input-xml': True,
'output-encoding': 'utf8',
'output-xml': True,
'wrap': 70
}
xml_str = tidylib.tidy_document(ET.tostring(self.root),
options=options)[0]
f.write(clean_up_xml(xml_str))
# If we're inside a bullet list, all the "paragraph" elements will be
# parameters description, so we need to know whether we currently are in a
# bullet list.
def depart_bullet_list(self, node):
self.in_bullet_list = False
def visit_bullet_list(self, node):
self.in_bullet_list = True
def visit_comment(self, node):
# If this .rst file is meant to be parsed by
# sphinxcontrib-docbookrestapi, then it must have a comment, before the
# first section, that just reads 'docbookrestapi'.
if node.astext() == 'docbookrestapi':
self.must_parse = True
def visit_document(self, node):
# This is where we should start. Let's build the root.
attrs = {
'xmlns': 'http://wadl.dev.java.net/2009/02',
'xmlns:xsdxt': 'http://docs.rackspacecloud.com/xsd-ext/v1.0',
'xmlns:wadl': 'http://wadl.dev.java.net/2009/02',
'xmlns:xsd': 'http://docs.rackspacecloud.com/xsd/v1.0'
}
self.root = ET.Element('application', attrs)
# If we are in a 'desc' node with the 'domain' attribute equal to 'http',
# we're in a method definition. The next 'paragraph' element will be the
# description of this method.
def depart_desc(self, node):
self.in_method_definition = False
def visit_desc(self, node):
attrs = dict(node.attlist())
if attrs['domain'] == 'http':
self.in_method_definition = True
def visit_desc_signature(self, node):
attrs = dict(node.attlist())
if 'method' in attrs and 'path' in attrs:
method_id = generate_id(attrs['path'], attrs['method'])
self.current_method = ET.Element('method', {
'id': method_id,
'name': attrs['method'].upper()
})
self.methods.append(self.current_method)
path = attrs['path'].replace('(', '{').replace(')', '}')
self.paths.setdefault(path, []).append(method_id)
self.current_wadl_doc = ET.SubElement(
self.current_method, 'wadl:doc', {
'xmlns': 'http://docbook.org/ns/docbook',
'xml:lang': 'EN',
'title': generate_title_from_id(method_id)
}
)
self.current_request = ET.SubElement(self.current_method,
'request')
self.current_response = ET.SubElement(self.current_method,
'response',
{'status': '200'})
self.needs_method_description = True
def visit_paragraph(self, node):
if self.in_method_definition and self.needs_method_description:
text = node.astext()
# Remove ":type data: foo" and ":return type: bar".
type_index = text.find(':type data:')
return_index = text.find(':return type:')
min_index = min(type_index, return_index)
if min_index > -1:
text = text[0:min_index]
# Create the doc node in the method.
ET.SubElement(self.current_wadl_doc, 'para', {
'role': 'shortdesc'
}).text = text
self.needs_method_description = False
elif self.in_bullet_list:
# We're describing a parameter here.
# They look like "param_name (type) -- description in one or more
# words".
text = node.astext()
dashes_index = text.find('--')
param_name = text[0:text.find(' ')]
param_type = text[text.find(' ') + 1: dashes_index - 2]
param_descr = text[dashes_index + 3:]
param_type = param_type[1:] # Remove '('
# Sometimes (especially when using enumerations), only some values
# may be allowed. If so, they should be listed in the
# documentation; store them here, and insert them in the code when
# creating the required elements.
valid_values = None
# There are probably more types.
if param_type.startswith("Enum"):
valid_values = param_type[5:-1].split(', ')
param_type = 'xsd:dict'
elif param_type.startswith("int"):
param_type = 'xsd:int'
elif param_type.startswith("list"):
param_type = 'xsd:list'
elif param_type .startswith("unicode"):
param_type = 'xsd:string'
else:
param_type = param_type[:-1] # Remove ')'
tmp = ET.SubElement(self.current_request, 'param', {
'name': param_name,
'type': param_type,
'required': 'false', # XXX Can we get the right value ?
'style': 'query' # XXX Can we get the right value ?
})
tmp = ET.SubElement(tmp, 'wadl:doc', {
'xml:lang': 'EN',
'xmlns': 'http://docbook.org/ns/docbook'
})
tmp = ET.SubElement(tmp, 'para')
tmp.text = param_descr
if valid_values:
tmp.text += ' Valid values are '
for i, value in enumerate(valid_values):
code = ET.SubElement(tmp, 'code')
code.text = value
if i + 1 != len(valid_values):
code.tail = ', '
if i + 2 == len(valid_values):
code.tail += 'or '
else:
code.tail = '.'
elif self.in_request or self.in_response:
self.visit_term(node)
def visit_section(self, node):
# If, by the time we visit the first section, we have not determined
# that this .rst file defines a REST API, then we probably should not
# be parsing it.
if not self.must_parse:
raise StopTraversal
def visit_term(self, node):
if self.in_request:
self.current_request_example.append(node.astext())
elif self.in_response:
self.current_response_example.append(node.astext())
def _finalize_json_example(self, parent, body):
tmp = ET.SubElement(parent, 'representation', {
'mediaType': 'application/json'
})
tmp = ET.SubElement(tmp, 'wadl:doc', {'xml:lang': 'EN'})
json_text = json.loads(''.join(body))
json_text = json.dumps(json_text, indent=4, sort_keys=True)
ET.SubElement(tmp, 'xsdxt:code').text = json_text
def visit_field_name(self, node):
text = node.astext()
if text == "Request json":
self.in_request = True
self.in_responses = False
elif text == "Response json":
self.in_request = False
self.in_response = True
else:
self.in_request = False
if self.current_request_example:
self._finalize_json_example(self.current_request,
self.current_request_example)
self.current_request_example = []
self.in_response = False
if self.current_response_example:
self._finalize_json_example(self.current_response,
self.current_response_example)
self.current_response_example = []
class DocBookBuilder(Builder):
name = 'docbook'
format = 'docbook'
out_suffix = '.wadl'
def get_outdated_docs(self):
return 'all documents' # XXX for now
def prepare_writing(self, docnames):
pass
def write_doc(self, docname, doctree):
global output_file
output_file = os.path.join(self.outdir, os.path.basename(docname) +
self.out_suffix)
visitor = MyNodeVisitor(doctree)
doctree.walkabout(visitor)
def get_target_uri(self, docname, typ=None):
return ''