zulip/zerver/lib/markdown/api_arguments_table_generat...

155 lines
6.3 KiB
Python

import json
import os
import re
from typing import Any, Dict, List, Mapping, Sequence
import markdown
from django.utils.html import escape as escape_html
from markdown.extensions import Extension
from markdown.preprocessors import Preprocessor
from zerver.openapi.openapi import get_openapi_parameters, likely_deprecated_parameter
REGEXP = re.compile(r'\{generate_api_arguments_table\|\s*(.+?)\s*\|\s*(.+)\s*\}')
class MarkdownArgumentsTableGenerator(Extension):
def __init__(self, configs: Mapping[str, Any] = {}) -> None:
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().__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)
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: 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) as fp:
json_obj = json.load(fp)
arguments = json_obj[doc_name]
if arguments:
text = self.render_table(arguments)
else:
text = ['This endpoint does not accept any parameters.']
# 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
else:
done = True
return lines
def render_table(self, arguments: Sequence[Mapping[str, Any]]) -> List[str]:
# TODO: Fix naming now that this no longer renders a table.
table = []
argument_template = """
<div class="api-argument" id="parameter-{argument}">
<p class="api-argument-name"><strong>{argument}</strong> {required} {deprecated} <a href="#parameter-{argument}" class="api-argument-hover-link"><i class="fa fa-chain"></i></a></p>
<div class="api-example">
<span class="api-argument-example-label">Example</span>: <code>{example}</code>
</div>
<div class="api-description">{description}</div>
<hr>
</div>"""
md_engine = markdown.Markdown(extensions=[])
arguments = sorted(arguments, key=lambda argument: 'deprecated' in argument)
for argument in arguments:
description = argument['description']
oneof = ['`' + str(item) + '`'
for item in argument.get('schema', {}).get('enum', [])]
if oneof:
description += '\nMust be one of: {}.'.format(', '.join(oneof))
default = argument.get('schema', {}).get('default')
if default is not None:
description += f'\nDefaults to `{json.dumps(default)}`.'
# TODO: OpenAPI allows indicating where the argument goes
# (path, querystring, form data...). We should document this detail.
example = ""
if 'example' in argument:
example = argument['example']
else:
example = json.dumps(argument['content']['application/json']['example'])
required_string: str = "required"
if argument.get('in', '') == 'path':
# Any path variable is required
assert argument['required']
required_string = 'required in path'
if argument.get('required', False):
required_block = f'<span class="api-argument-required">{required_string}</span>'
else:
required_block = '<span class="api-argument-optional">optional</span>'
# Test to make sure deprecated parameters are marked so.
if likely_deprecated_parameter(description):
assert(argument['deprecated'])
if argument.get('deprecated', False):
deprecated_block = '<span class="api-argument-deprecated">Deprecated</span>'
else:
deprecated_block = ''
table.append(argument_template.format(
argument=argument.get('argument') or argument.get('name'),
example=escape_html(example),
required=required_block,
deprecated=deprecated_block,
description=md_engine.convert(description),
))
return table
def makeExtension(*args: Any, **kwargs: str) -> MarkdownArgumentsTableGenerator:
return MarkdownArgumentsTableGenerator(kwargs)