mirror of
https://github.com/zulip/zulip.git
synced 2025-11-09 16:37:23 +00:00
api docs: Read parameters and response fixtures from OpenAPI files.
This commit is contained in:
3
mypy.ini
3
mypy.ini
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>")
|
||||||
|
|||||||
@@ -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
26
zerver/lib/openapi.py
Normal 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'])
|
||||||
@@ -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'
|
||||||
|
|||||||
@@ -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(),
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user