mirror of
https://github.com/zulip/zulip.git
synced 2025-11-13 02:17:19 +00:00
curl: Add code to auto generate cURL examples from OpenAPI docs.
This commit extends api_code_examples.py to support automatically generating cURL examples from the OpenAPI documentation. This way work won't have to be repeated and we can also drastically reduce the chance of introducing faulty cURL examples (via. an automated test which can now be easily created).
This commit is contained in:
committed by
Tim Abbott
parent
f280e9cf84
commit
b20cf095e7
@@ -8,7 +8,8 @@ from typing import Any, Dict, Optional, List
|
||||
import markdown
|
||||
|
||||
import zerver.openapi.python_examples
|
||||
from zerver.lib.openapi import get_openapi_fixture
|
||||
from zerver.lib.openapi import get_openapi_fixture, openapi_spec, \
|
||||
get_openapi_parameters
|
||||
|
||||
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*\}')
|
||||
@@ -33,6 +34,15 @@ client = zulip.Client(config_file="~/zuliprc-admin")
|
||||
|
||||
"""
|
||||
|
||||
DEFAULT_API_URL = "localhost:9991/api"
|
||||
DEFAULT_AUTH_EMAIL = "BOT_EMAIL_ADDRESS"
|
||||
DEFAULT_AUTH_API_KEY = "BOT_API_KEY"
|
||||
DEFAULT_EXAMPLE = {
|
||||
"integer": 1,
|
||||
"string": "demo",
|
||||
"boolean": False,
|
||||
}
|
||||
|
||||
def extract_python_code_example(source: List[str], snippet: List[str]) -> List[str]:
|
||||
start = -1
|
||||
end = -1
|
||||
@@ -77,11 +87,88 @@ def render_python_code_example(function: str, admin_config: Optional[bool]=False
|
||||
|
||||
return code_example
|
||||
|
||||
def curl_method_arguments(endpoint: str, method: str,
|
||||
api_url: str) -> List[str]:
|
||||
method = method.upper()
|
||||
url = "{}/v1{}".format(api_url, endpoint)
|
||||
valid_methods = ["GET", "POST", "DELETE", "PUT", "PATCH", "OPTIONS"]
|
||||
if method == valid_methods[0]:
|
||||
# Then we need to make sure that each -d option translates to becoming
|
||||
# a GET parameter (in the URL) and not a POST parameter (in the body).
|
||||
# TODO: remove the -X part by updating the linting rule. It's redundant.
|
||||
return ["-X", "GET", "-G", url]
|
||||
elif method in valid_methods:
|
||||
return ["-X", method, url]
|
||||
else:
|
||||
msg = "The request method {} is not one of {}".format(method,
|
||||
valid_methods)
|
||||
raise ValueError(msg)
|
||||
|
||||
def generate_curl_example(endpoint: str, method: str,
|
||||
auth_email: str=DEFAULT_AUTH_EMAIL,
|
||||
auth_api_key: str=DEFAULT_AUTH_API_KEY,
|
||||
api_url: str=DEFAULT_API_URL) -> List[str]:
|
||||
lines = ["```curl"]
|
||||
openapi_entry = openapi_spec.spec()['paths'][endpoint][method.lower()]
|
||||
|
||||
curl_first_line_parts = ["curl"] + curl_method_arguments(endpoint, method,
|
||||
api_url)
|
||||
lines.append(" ".join(curl_first_line_parts))
|
||||
|
||||
authentication_required = openapi_entry.get("security", False)
|
||||
if authentication_required:
|
||||
lines.append(" -u %s:%s" % (auth_email, auth_api_key))
|
||||
|
||||
openapi_example_params = get_openapi_parameters(endpoint, method)
|
||||
for packet in openapi_example_params:
|
||||
param_name = packet["name"]
|
||||
param_type = packet["schema"]["type"]
|
||||
if param_type in ["object", "array"]:
|
||||
example_value = packet.get("example", None)
|
||||
if not example_value:
|
||||
msg = """All array and object type request parameters must have
|
||||
concrete examples. The openAPI documentation for {}/{} is missing an example
|
||||
value for the {} parameter. Without this we cannot automatically generate a
|
||||
cURL example.""".format(endpoint, method, param_name)
|
||||
raise ValueError(msg)
|
||||
ordered_ex_val_str = json.dumps(example_value, sort_keys=True)
|
||||
line = " --data-urlencode {}='{}'".format(param_name, ordered_ex_val_str)
|
||||
else:
|
||||
example_value = packet.get("example", DEFAULT_EXAMPLE[param_type])
|
||||
if type(example_value) == bool:
|
||||
example_value = str(example_value).lower()
|
||||
line = " -d '{}={}'".format(param_name, example_value)
|
||||
lines.append(line)
|
||||
|
||||
for i in range(1, len(lines)-1):
|
||||
lines[i] = lines[i] + " \\"
|
||||
|
||||
lines.append("```")
|
||||
|
||||
return lines
|
||||
|
||||
def render_curl_example(function: str) -> List[str]:
|
||||
""" A simple wrapper around generate_curl_example. """
|
||||
parts = function.split(":")
|
||||
endpoint = parts[0]
|
||||
method = parts[1]
|
||||
kwargs = dict()
|
||||
if len(parts) > 2:
|
||||
kwargs["auth_email"] = parts[2]
|
||||
if len(parts) > 3:
|
||||
kwargs["auth_api_key"] = parts[3]
|
||||
if len(parts) > 4:
|
||||
kwargs["api_url"] = parts[4]
|
||||
return generate_curl_example(endpoint, method, **kwargs)
|
||||
|
||||
SUPPORTED_LANGUAGES = {
|
||||
'python': {
|
||||
'client_config': PYTHON_CLIENT_CONFIG,
|
||||
'admin_config': PYTHON_CLIENT_ADMIN_CONFIG,
|
||||
'render': render_python_code_example,
|
||||
},
|
||||
'curl': {
|
||||
'render': render_curl_example
|
||||
}
|
||||
} # type: Dict[str, Any]
|
||||
|
||||
@@ -91,7 +178,6 @@ class APICodeExamplesGenerator(Extension):
|
||||
'generate_code_example', APICodeExamplesPreprocessor(md, self.getConfigs()), '_begin'
|
||||
)
|
||||
|
||||
|
||||
class APICodeExamplesPreprocessor(Preprocessor):
|
||||
def __init__(self, md: markdown.Markdown, config: Dict[str, Any]) -> None:
|
||||
super(APICodeExamplesPreprocessor, self).__init__(md)
|
||||
|
||||
@@ -334,7 +334,7 @@ paths:
|
||||
items:
|
||||
type: object
|
||||
default: []
|
||||
example: [{"operator": "sender", "operand"}]
|
||||
example: [{"operand": "party", "operator": "stream"}]
|
||||
- name: client_gravatar
|
||||
in: query
|
||||
description: Whether the client supports computing gravatars URLs. If enabled,
|
||||
|
||||
@@ -6,11 +6,14 @@ import mock
|
||||
import inspect
|
||||
import typing
|
||||
from typing import Dict, Any, Set, Union, List, Callable, Tuple, Optional, Iterable, Mapping
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse
|
||||
|
||||
import zerver.lib.openapi as openapi
|
||||
from zerver.lib.bugdown.api_code_examples import generate_curl_example, \
|
||||
render_curl_example
|
||||
from zerver.lib.request import REQ
|
||||
from zerver.lib.test_classes import ZulipTestCase
|
||||
from zerver.lib.openapi import (
|
||||
@@ -541,3 +544,207 @@ so maybe we shouldn't include it in pending_endpoints.
|
||||
self.checked_endpoints.add(url_pattern)
|
||||
|
||||
self.check_for_non_existant_openapi_endpoints()
|
||||
|
||||
class TestCurlExampleGeneration(ZulipTestCase):
|
||||
|
||||
spec_mock_without_examples = {
|
||||
"paths": {
|
||||
"/mark_stream_as_read": {
|
||||
"post": {
|
||||
"description": "Mark all the unread messages in a stream as read.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "stream_id",
|
||||
"in": "query",
|
||||
"description": "The ID of the stream whose messages should be marked as read.",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
},
|
||||
"required": True
|
||||
},
|
||||
{
|
||||
"name": "bool_param",
|
||||
"in": "query",
|
||||
"description": "Just a boolean parameter.",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"required": True
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
spec_mock_with_invalid_method = {
|
||||
"paths": {
|
||||
"/endpoint": {
|
||||
"brew": {} # the data is irrelevant as is should be rejected.
|
||||
}
|
||||
}
|
||||
} # type: Dict[str, Any]
|
||||
|
||||
spec_mock_using_object = {
|
||||
"paths": {
|
||||
"/endpoint": {
|
||||
"get": {
|
||||
"description": "Get some info.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "param1",
|
||||
"in": "path",
|
||||
"description": "An object",
|
||||
"schema": {
|
||||
"type": "object"
|
||||
},
|
||||
"example": {
|
||||
"key": "value"
|
||||
},
|
||||
"required": True
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
spec_mock_using_object_without_example = {
|
||||
"paths": {
|
||||
"/endpoint": {
|
||||
"get": {
|
||||
"description": "Get some info.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "param1",
|
||||
"in": "path",
|
||||
"description": "An object",
|
||||
"schema": {
|
||||
"type": "object"
|
||||
},
|
||||
"required": True
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
spec_mock_using_array_without_example = {
|
||||
"paths": {
|
||||
"/endpoint": {
|
||||
"get": {
|
||||
"description": "Get some info.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "param1",
|
||||
"in": "path",
|
||||
"description": "An array",
|
||||
"schema": {
|
||||
"type": "array"
|
||||
},
|
||||
"required": True
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def test_generate_and_render_curl_example(self) -> None:
|
||||
generated_curl_example = generate_curl_example("/get_stream_id", "GET")
|
||||
expected_curl_example = [
|
||||
"```curl",
|
||||
"curl -X GET -G localhost:9991/api/v1/get_stream_id \\",
|
||||
" -u BOT_EMAIL_ADDRESS:BOT_API_KEY \\",
|
||||
" -d 'stream=Denmark'",
|
||||
"```"
|
||||
]
|
||||
self.assertEqual(generated_curl_example, expected_curl_example)
|
||||
|
||||
def test_generate_and_render_curl_example_with_nonexistant_endpoints(self) -> None:
|
||||
with self.assertRaises(KeyError):
|
||||
generate_curl_example("/mark_this_stream_as_read", "POST")
|
||||
with self.assertRaises(KeyError):
|
||||
generate_curl_example("/mark_stream_as_read", "GET")
|
||||
|
||||
def test_generate_and_render_curl_without_auth(self) -> None:
|
||||
generated_curl_example = generate_curl_example("/dev_fetch_api_key", "POST")
|
||||
expected_curl_example = [
|
||||
"```curl",
|
||||
"curl -X POST localhost:9991/api/v1/dev_fetch_api_key \\",
|
||||
" -d 'username=iago@zulip.com'",
|
||||
"```"
|
||||
]
|
||||
self.assertEqual(generated_curl_example, expected_curl_example)
|
||||
|
||||
@patch("zerver.lib.openapi.OpenAPISpec.spec")
|
||||
def test_generate_and_render_curl_with_default_examples(self, spec_mock: MagicMock) -> None:
|
||||
spec_mock.return_value = self.spec_mock_without_examples
|
||||
generated_curl_example = generate_curl_example("/mark_stream_as_read", "POST")
|
||||
expected_curl_example = [
|
||||
"```curl",
|
||||
"curl -X POST localhost:9991/api/v1/mark_stream_as_read \\",
|
||||
" -d 'stream_id=1' \\",
|
||||
" -d 'bool_param=false'",
|
||||
"```"
|
||||
]
|
||||
self.assertEqual(generated_curl_example, expected_curl_example)
|
||||
|
||||
@patch("zerver.lib.openapi.OpenAPISpec.spec")
|
||||
def test_generate_and_render_curl_with_invalid_method(self, spec_mock: MagicMock) -> None:
|
||||
spec_mock.return_value = self.spec_mock_with_invalid_method
|
||||
with self.assertRaises(ValueError):
|
||||
generate_curl_example("/endpoint", "BREW") # see: HTCPCP
|
||||
|
||||
def test_generate_and_render_curl_with_array_example(self) -> None:
|
||||
generated_curl_example = generate_curl_example("/messages", "GET")
|
||||
expected_curl_example = [
|
||||
'```curl',
|
||||
'curl -X GET -G localhost:9991/api/v1/messages \\',
|
||||
' -u BOT_EMAIL_ADDRESS:BOT_API_KEY \\',
|
||||
" -d 'anchor=42' \\",
|
||||
" -d 'use_first_unread_anchor=true' \\",
|
||||
" -d 'num_before=4' \\",
|
||||
" -d 'num_after=8' \\",
|
||||
' --data-urlencode narrow=\'[{"operand": "party", "operator": "stream"}]\' \\',
|
||||
" -d 'client_gravatar=true' \\",
|
||||
" -d 'apply_markdown=false'",
|
||||
'```'
|
||||
]
|
||||
self.assertEqual(generated_curl_example, expected_curl_example)
|
||||
|
||||
@patch("zerver.lib.openapi.OpenAPISpec.spec")
|
||||
def test_generate_and_render_curl_with_object(self, spec_mock: MagicMock) -> None:
|
||||
spec_mock.return_value = self.spec_mock_using_object
|
||||
generated_curl_example = generate_curl_example("/endpoint", "GET")
|
||||
expected_curl_example = [
|
||||
'```curl',
|
||||
'curl -X GET -G localhost:9991/api/v1/endpoint \\',
|
||||
' --data-urlencode param1=\'{"key": "value"}\'',
|
||||
'```'
|
||||
]
|
||||
self.assertEqual(generated_curl_example, expected_curl_example)
|
||||
|
||||
@patch("zerver.lib.openapi.OpenAPISpec.spec")
|
||||
def test_generate_and_render_curl_with_object_without_example(self, spec_mock: MagicMock) -> None:
|
||||
spec_mock.return_value = self.spec_mock_using_object_without_example
|
||||
with self.assertRaises(ValueError):
|
||||
generate_curl_example("/endpoint", "GET")
|
||||
|
||||
@patch("zerver.lib.openapi.OpenAPISpec.spec")
|
||||
def test_generate_and_render_curl_with_array_without_example(self, spec_mock: MagicMock) -> None:
|
||||
spec_mock.return_value = self.spec_mock_using_array_without_example
|
||||
with self.assertRaises(ValueError):
|
||||
generate_curl_example("/endpoint", "GET")
|
||||
|
||||
def test_generate_and_render_curl_wrapper(self) -> None:
|
||||
generated_curl_example = render_curl_example("/get_stream_id:GET:email:key:chat.zulip.org/api")
|
||||
expected_curl_example = [
|
||||
"```curl",
|
||||
"curl -X GET -G chat.zulip.org/api/v1/get_stream_id \\",
|
||||
" -u email:key \\",
|
||||
" -d 'stream=Denmark'",
|
||||
"```"
|
||||
]
|
||||
self.assertEqual(generated_curl_example, expected_curl_example)
|
||||
|
||||
Reference in New Issue
Block a user