api docs: Read parameters and response fixtures from OpenAPI files.

This commit is contained in:
Yago González
2018-05-15 19:28:42 +02:00
parent 30682241c7
commit f84c9b919b
10 changed files with 100 additions and 35 deletions

View File

@@ -254,6 +254,9 @@ ignore_missing_imports = True
[mypy-two_factor,two_factor.*] [mypy-two_factor,two_factor.*]
ignore_missing_imports = True ignore_missing_imports = True
[mypy-yamole]
ignore_missing_imports = True

View File

@@ -360,10 +360,6 @@
"msg": "", "msg": "",
"result": "success" "result": "success"
}, },
"update-message": {
"msg": "",
"result": "success"
},
"update-message-edit-permission-error": { "update-message-edit-permission-error": {
"code": "BAD_REQUEST", "code": "BAD_REQUEST",
"msg": "You don't have permission to edit this message", "msg": "You don't have permission to edit this message",

View File

@@ -27,7 +27,7 @@ curl -X "PATCH" {{ api_url }}/v1/messages/<msg_id> \
<div data-language="python" markdown="1"> <div data-language="python" markdown="1">
{generate_code_example(python)|update-message|example} {generate_code_example(python)|/messages/{message_id}:patch|example}
</div> </div>
@@ -67,7 +67,7 @@ You only have permission to edit a message if:
## Arguments ## Arguments
{generate_api_arguments_table|arguments.json|update-message.md} {generate_api_arguments_table|zulip.yaml|/messages/{message_id}:patch}
## Response ## Response
@@ -75,9 +75,9 @@ You only have permission to edit a message if:
A typical successful JSON response may look like: A typical successful JSON response may look like:
{generate_code_example|update-message|fixture} {generate_code_example|/messages/{message_id}:patch|fixture(200)}
A typical JSON response for when one doesn't have the permission to A typical JSON response for when one doesn't have the permission to
edit a particular message: edit a particular message:
{generate_code_example|update-message-edit-permission-error|fixture} {generate_code_example|/messages/{message_id}:patch|fixture(400)}

View File

@@ -8,4 +8,4 @@ ZULIP_VERSION = "1.8.1+git"
# Typically, adding a dependency only requires a minor version bump, and # Typically, adding a dependency only requires a minor version bump, and
# removing a dependency requires a major version bump. # removing a dependency requires a major version bump.
PROVISION_VERSION = '20.3' PROVISION_VERSION = '20.4'

View File

@@ -1,10 +1,12 @@
from typing import Dict, Any, Optional, Iterable from typing import Dict, Any, Optional, Iterable
from io import StringIO from io import StringIO
from yamole import YamoleParser
import json import json
import os import os
from zerver.lib import mdiff from zerver.lib import mdiff
from zerver.lib.openapi import get_openapi_fixture
if False: if False:
from zulip import Client from zulip import Client
@@ -383,7 +385,8 @@ def update_message(client, message_id):
result = client.update_message(request) result = client.update_message(request)
# {code_example|end} # {code_example|end}
fixture = FIXTURES['update-message'] fixture = get_openapi_fixture('/messages/{message_id}', 'patch', '200')
test_against_fixture(result, fixture) test_against_fixture(result, fixture)
# test it was actually updated # test it was actually updated
@@ -491,7 +494,7 @@ TEST_FUNCTIONS = {
'render-message': render_message, 'render-message': render_message,
'stream-message': stream_message, 'stream-message': stream_message,
'private-message': private_message, 'private-message': private_message,
'update-message': update_message, '/messages/{message_id}:patch': update_message,
'get-stream-id': get_stream_id, 'get-stream-id': get_stream_id,
'get-subscribed-streams': list_subscriptions, 'get-subscribed-streams': list_subscriptions,
'get-all-streams': get_streams, 'get-all-streams': get_streams,

View File

@@ -4,10 +4,11 @@ import ujson
from markdown.extensions import Extension from markdown.extensions import Extension
from markdown.preprocessors import Preprocessor from markdown.preprocessors import Preprocessor
from zerver.lib.openapi import get_openapi_parameters
from typing import Any, Dict, Optional, List from typing import Any, Dict, Optional, List
import markdown import markdown
REGEXP = re.compile(r'\{generate_api_arguments_table\|\s*(.+?)\s*\|\s*(.+?)\s*\}') REGEXP = re.compile(r'\{generate_api_arguments_table\|\s*(.+?)\s*\|\s*(.+)\s*\}')
class MarkdownArgumentsTableGenerator(Extension): class MarkdownArgumentsTableGenerator(Extension):
@@ -16,8 +17,6 @@ class MarkdownArgumentsTableGenerator(Extension):
configs = {} configs = {}
self.config = { self.config = {
'base_path': ['.', 'Default location from which to evaluate relative paths for the JSON files.'], 'base_path': ['.', 'Default location from which to evaluate relative paths for the JSON files.'],
'openapi_path': ['../../../zerver/openapi',
'Default location from which to evaluate relative paths for the YAML files.']
} }
for key, value in configs.items(): for key, value in configs.items():
self.setConfig(key, value) self.setConfig(key, value)
@@ -41,19 +40,29 @@ class APIArgumentsTablePreprocessor(Preprocessor):
match = REGEXP.search(line) match = REGEXP.search(line)
if match: if match:
json_filename = match.group(1) filename = match.group(1)
doc_filename = match.group(2) doc_name = match.group(2)
json_filename = os.path.expanduser(json_filename) filename = os.path.expanduser(filename)
if not os.path.isabs(json_filename):
json_filename = os.path.normpath(os.path.join(self.base_path, json_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))
try: try:
with open(json_filename, 'r') as fp: if is_openapi_format:
json_obj = ujson.load(fp) endpoint, method = doc_name.rsplit(':', 1)
arguments = json_obj[doc_filename] arguments = get_openapi_parameters(endpoint, method)
text = self.render_table(arguments) else:
with open(filename, 'r') as fp:
json_obj = ujson.load(fp)
arguments = json_obj[doc_name]
text = self.render_table(arguments)
except Exception as e: except Exception as e:
print('Warning: could not find file {}. Ignoring ' print('Warning: could not find file {}. Ignoring '
'statement. Error: {}'.format(json_filename, e)) 'statement. Error: {}'.format(filename, e))
# If the file cannot be opened, just substitute an empty line # If the file cannot be opened, just substitute an empty line
# in place of the macro include line # in place of the macro include line
lines[loc] = REGEXP.sub('', line) lines[loc] = REGEXP.sub('', line)
@@ -101,11 +110,19 @@ class APIArgumentsTablePreprocessor(Preprocessor):
md_engine = markdown.Markdown(extensions=[]) md_engine = markdown.Markdown(extensions=[])
for argument in arguments: for argument in arguments:
oneof = ['`' + item + '`'
for item in argument.get('schema', {}).get('enum', [])]
description = argument['description']
if oneof:
description += '\nMust be one of: {}.'.format(', '.join(oneof))
# TODO: Swagger allows indicating where the argument goes
# (path, querystring, form data...). A column in the table should
# be added for this.
table.append(tr.format( table.append(tr.format(
argument=argument['argument'], argument=argument.get('argument') or argument.get('name'),
example=argument['example'], example=argument['example'],
required='Yes' if argument.get('required') else 'No', required='Yes' if argument.get('required') else 'No',
description=md_engine.convert(argument['description']), description=md_engine.convert(description),
)) ))
table.append("</tbody>") table.append("</tbody>")

View File

@@ -7,11 +7,13 @@ import inspect
from markdown.extensions import Extension from markdown.extensions import Extension
from markdown.preprocessors import Preprocessor from markdown.preprocessors import Preprocessor
from typing import Any, Dict, Optional, List from typing import Any, Dict, Optional, List
from yamole import YamoleParser
import markdown import markdown
import zerver.lib.api_test_helpers import zerver.lib.api_test_helpers
from zerver.lib.openapi import get_openapi_fixture
MACRO_REGEXP = re.compile(r'\{generate_code_example(\(\s*(.+?)\s*\))*\|\s*(.+?)\s*\|\s*(.+?)\s*(\(\s*(.+?)\s*\))?\}') MACRO_REGEXP = re.compile(r'\{generate_code_example(\(\s*(.+?)\s*\))*\|\s*(.+?)\s*\|\s*(.+?)\s*(\(\s*(.+)\s*\))?\}')
CODE_EXAMPLE_REGEX = re.compile(r'\# \{code_example\|\s*(.+?)\s*\}') CODE_EXAMPLE_REGEX = re.compile(r'\# \{code_example\|\s*(.+?)\s*\}')
PYTHON_CLIENT_CONFIG = """ PYTHON_CLIENT_CONFIG = """
@@ -138,8 +140,12 @@ class APICodeExamplesPreprocessor(Preprocessor):
def render_fixture(self, function: str, name: Optional[str]=None) -> List[str]: def render_fixture(self, function: str, name: Optional[str]=None) -> List[str]:
fixture = [] fixture = []
if name: # We assume that if the function we're rendering starts with a slash
fixture_dict = zerver.lib.api_test_helpers.FIXTURES[function][name] # it's a path in the endpoint and therefore it uses the new OpenAPI
# format.
if function.startswith('/'):
path, method = function.rsplit(':', 1)
fixture_dict = get_openapi_fixture(path, method, name)
else: else:
fixture_dict = zerver.lib.api_test_helpers.FIXTURES[function] fixture_dict = zerver.lib.api_test_helpers.FIXTURES[function]

26
zerver/lib/openapi.py Normal file
View File

@@ -0,0 +1,26 @@
# Set of helper functions to manipulate the OpenAPI files that define our REST
# API's specification.
import os
from typing import Any, Dict, List, Optional
from yamole import YamoleParser
OPENAPI_SPEC_PATH = os.path.abspath(os.path.join(
os.path.dirname(__file__),
'../openapi/zulip.yaml'))
with open(OPENAPI_SPEC_PATH) as file:
yaml_parser = YamoleParser(file)
OPENAPI_SPEC = yaml_parser.data
def get_openapi_fixture(endpoint: str, method: str,
response: Optional[str]='200') -> Dict[str, Any]:
return (OPENAPI_SPEC['paths'][endpoint][method.lower()]['responses']
[response]['content']['application/json']['schema']
['example'])
def get_openapi_parameters(endpoint: str,
method: str) -> List[Dict[str, Any]]:
return (OPENAPI_SPEC['paths'][endpoint][method]['parameters'])

View File

@@ -60,7 +60,8 @@ paths:
example: change_all example: change_all
- name: content - name: content
in: query in: query
description: Message's new body. description: |
The content of the message. Maximum message size of 10000 bytes.
schema: schema:
type: string type: string
example: Hello example: Hello
@@ -68,7 +69,11 @@ paths:
- basicAuth: [] - basicAuth: []
responses: responses:
'200': '200':
$ref: '#/components/responses/SimpleSuccess' description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/JsonSuccess'
'400': '400':
description: Bad request. description: Bad request.
content: content:
@@ -84,6 +89,12 @@ paths:
- The time limit for editing this message has past - The time limit for editing this message has past
- Nothing to change - Nothing to change
- Topic can't be empty - Topic can't be empty
- example:
{
"code": "BAD_REQUEST",
"msg": "You don't have permission to edit this message",
"result": "error"
}
components: components:
####################### #######################
# Security definitions # Security definitions
@@ -96,13 +107,13 @@ components:
Basic authentication, with the user's email as the username, and the Basic authentication, with the user's email as the username, and the
API key as the password. The API key can be fetched using the API key as the password. The API key can be fetched using the
`/fetch_api_key` or `/dev_fetch_api_key` endpoints. `/fetch_api_key` or `/dev_fetch_api_key` endpoints.
schemas: schemas:
JsonResponse: JsonResponse:
type: object type: object
properties: properties:
result: result:
type: string type: string
JsonSuccess: JsonSuccess:
allOf: allOf:
- $ref: '#/components/schemas/JsonResponse' - $ref: '#/components/schemas/JsonResponse'
@@ -115,7 +126,11 @@ components:
- success - success
msg: msg:
type: string type: string
- example:
{
"msg": "",
"result": "success"
}
JsonError: JsonError:
allOf: allOf:
- $ref: '#/components/schemas/JsonResponse' - $ref: '#/components/schemas/JsonResponse'

View File

@@ -96,8 +96,7 @@ def render_markdown_path(markdown_file_path: str, context: Optional[Dict[Any, An
), ),
zerver.lib.bugdown.fenced_code.makeExtension(), zerver.lib.bugdown.fenced_code.makeExtension(),
zerver.lib.bugdown.api_arguments_table_generator.makeExtension( zerver.lib.bugdown.api_arguments_table_generator.makeExtension(
base_path='templates/zerver/api/', base_path='templates/zerver/api/'),
openapi_path='zerver/openapi'),
zerver.lib.bugdown.api_code_examples.makeExtension(), zerver.lib.bugdown.api_code_examples.makeExtension(),
zerver.lib.bugdown.help_settings_links.makeExtension(), zerver.lib.bugdown.help_settings_links.makeExtension(),
] ]