2017-12-27 08:57:47 +01:00
|
|
|
import re
|
|
|
|
import os
|
|
|
|
import ujson
|
|
|
|
|
|
|
|
from markdown.extensions import Extension
|
|
|
|
from markdown.preprocessors import Preprocessor
|
2018-05-15 19:28:42 +02:00
|
|
|
from zerver.lib.openapi import get_openapi_parameters
|
2017-12-27 08:57:47 +01:00
|
|
|
from typing import Any, Dict, Optional, List
|
|
|
|
import markdown
|
|
|
|
|
2018-05-15 19:28:42 +02:00
|
|
|
REGEXP = re.compile(r'\{generate_api_arguments_table\|\s*(.+?)\s*\|\s*(.+)\s*\}')
|
2017-12-27 08:57:47 +01:00
|
|
|
|
|
|
|
|
|
|
|
class MarkdownArgumentsTableGenerator(Extension):
|
2018-05-17 17:32:40 +02:00
|
|
|
def __init__(self, configs: Optional[Dict[str, Any]]=None) -> None:
|
|
|
|
if configs is None:
|
|
|
|
configs = {}
|
2017-12-27 08:57:47 +01:00
|
|
|
self.config = {
|
|
|
|
'base_path': ['.', 'Default location from which to evaluate relative paths for the JSON files.'],
|
|
|
|
}
|
|
|
|
for key, value in configs.items():
|
|
|
|
self.setConfig(key, value)
|
|
|
|
|
|
|
|
def extendMarkdown(self, md: markdown.Markdown, md_globals: Dict[str, Any]) -> None:
|
|
|
|
md.preprocessors.add(
|
|
|
|
'generate_api_arguments', APIArgumentsTablePreprocessor(md, self.getConfigs()), '_begin'
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
class APIArgumentsTablePreprocessor(Preprocessor):
|
|
|
|
def __init__(self, md: markdown.Markdown, config: Dict[str, Any]) -> None:
|
|
|
|
super(APIArgumentsTablePreprocessor, self).__init__(md)
|
|
|
|
self.base_path = config['base_path']
|
|
|
|
|
|
|
|
def run(self, lines: List[str]) -> List[str]:
|
|
|
|
done = False
|
|
|
|
while not done:
|
|
|
|
for line in lines:
|
|
|
|
loc = lines.index(line)
|
|
|
|
match = REGEXP.search(line)
|
|
|
|
|
2018-07-13 14:09:03 +02:00
|
|
|
if not match:
|
|
|
|
continue
|
|
|
|
|
|
|
|
filename = match.group(1)
|
|
|
|
doc_name = match.group(2)
|
|
|
|
filename = os.path.expanduser(filename)
|
|
|
|
|
|
|
|
is_openapi_format = filename.endswith('.yaml')
|
|
|
|
|
|
|
|
if not os.path.isabs(filename):
|
|
|
|
parent_dir = self.base_path
|
|
|
|
filename = os.path.normpath(os.path.join(parent_dir, filename))
|
|
|
|
|
|
|
|
if is_openapi_format:
|
|
|
|
endpoint, method = doc_name.rsplit(':', 1)
|
|
|
|
arguments = [] # type: List[Dict[str, Any]]
|
|
|
|
|
|
|
|
try:
|
|
|
|
arguments = get_openapi_parameters(endpoint, method)
|
|
|
|
except KeyError as e:
|
|
|
|
# Don't raise an exception if the "parameters"
|
|
|
|
# field is missing; we assume that's because the
|
|
|
|
# endpoint doesn't accept any parameters
|
|
|
|
if e.args != ('parameters',):
|
|
|
|
raise e
|
|
|
|
else:
|
|
|
|
with open(filename, 'r') as fp:
|
|
|
|
json_obj = ujson.load(fp)
|
|
|
|
arguments = json_obj[doc_name]
|
|
|
|
|
|
|
|
if arguments:
|
|
|
|
text = self.render_table(arguments)
|
|
|
|
else:
|
|
|
|
text = ['This endpoint does not consume any arguments.']
|
|
|
|
# The line that contains the directive to include the macro
|
|
|
|
# may be preceded or followed by text or tags, in that case
|
|
|
|
# we need to make sure that any preceding or following text
|
|
|
|
# stays the same.
|
|
|
|
line_split = REGEXP.split(line, maxsplit=0)
|
|
|
|
preceding = line_split[0]
|
|
|
|
following = line_split[-1]
|
|
|
|
text = [preceding] + text + [following]
|
|
|
|
lines = lines[:loc] + text + lines[loc+1:]
|
|
|
|
break
|
2017-12-27 08:57:47 +01:00
|
|
|
else:
|
|
|
|
done = True
|
|
|
|
return lines
|
|
|
|
|
|
|
|
def render_table(self, arguments: List[Dict[str, Any]]) -> List[str]:
|
|
|
|
table = []
|
|
|
|
beginning = """
|
|
|
|
<table class="table">
|
|
|
|
<thead>
|
|
|
|
<tr>
|
|
|
|
<th>Argument</th>
|
|
|
|
<th>Example</th>
|
|
|
|
<th>Required</th>
|
|
|
|
<th>Description</th>
|
|
|
|
</tr>
|
|
|
|
</thead>
|
|
|
|
<tbody>
|
|
|
|
"""
|
|
|
|
tr = """
|
|
|
|
<tr>
|
|
|
|
<td><code>{argument}</code></td>
|
|
|
|
<td><code>{example}</code></td>
|
|
|
|
<td>{required}</td>
|
|
|
|
<td>{description}</td>
|
|
|
|
</tr>
|
|
|
|
"""
|
|
|
|
|
|
|
|
table.append(beginning)
|
|
|
|
|
|
|
|
md_engine = markdown.Markdown(extensions=[])
|
|
|
|
|
|
|
|
for argument in arguments:
|
2018-07-26 20:48:34 +02:00
|
|
|
description = argument['description']
|
|
|
|
|
2018-05-15 19:28:42 +02:00
|
|
|
oneof = ['`' + item + '`'
|
|
|
|
for item in argument.get('schema', {}).get('enum', [])]
|
|
|
|
if oneof:
|
|
|
|
description += '\nMust be one of: {}.'.format(', '.join(oneof))
|
2018-07-26 20:48:34 +02:00
|
|
|
|
|
|
|
default = argument.get('schema', {}).get('default')
|
|
|
|
if default is not None:
|
|
|
|
description += '\nDefaults to `{}`.'.format(ujson.dumps(default))
|
|
|
|
|
2018-05-15 19:28:42 +02:00
|
|
|
# TODO: Swagger allows indicating where the argument goes
|
|
|
|
# (path, querystring, form data...). A column in the table should
|
|
|
|
# be added for this.
|
2017-12-27 08:57:47 +01:00
|
|
|
table.append(tr.format(
|
2018-05-15 19:28:42 +02:00
|
|
|
argument=argument.get('argument') or argument.get('name'),
|
2018-07-13 06:59:48 +02:00
|
|
|
# Show this as JSON to avoid changing the quoting style, which
|
|
|
|
# may cause problems with JSON encoding.
|
|
|
|
example=ujson.dumps(argument['example']),
|
2018-05-15 19:23:49 +02:00
|
|
|
required='Yes' if argument.get('required') else 'No',
|
2018-05-15 19:28:42 +02:00
|
|
|
description=md_engine.convert(description),
|
2017-12-27 08:57:47 +01:00
|
|
|
))
|
|
|
|
|
|
|
|
table.append("</tbody>")
|
|
|
|
table.append("</table>")
|
|
|
|
|
|
|
|
return table
|
|
|
|
|
|
|
|
def makeExtension(*args: Any, **kwargs: str) -> MarkdownArgumentsTableGenerator:
|
|
|
|
return MarkdownArgumentsTableGenerator(kwargs)
|