From 899819fb2f61d5a35657542ee4ffa881be7de092 Mon Sep 17 00:00:00 2001 From: Anders Kaseorg Date: Wed, 31 Jan 2024 21:26:58 -0800 Subject: [PATCH] openapi: Use Parameter class for generating curl examples. Signed-off-by: Anders Kaseorg (cherry picked from commit a67d1b57b91cf4eacf242a09d569225f9c50179f) --- zerver/openapi/curl_param_value_generators.py | 19 ++--- zerver/openapi/markdown_extension.py | 75 ++++++++++--------- 2 files changed, 46 insertions(+), 48 deletions(-) diff --git a/zerver/openapi/curl_param_value_generators.py b/zerver/openapi/curl_param_value_generators.py index 3b7efb859b..c39c6ee544 100644 --- a/zerver/openapi/curl_param_value_generators.py +++ b/zerver/openapi/curl_param_value_generators.py @@ -23,6 +23,7 @@ from zerver.lib.users import get_api_key from zerver.models import Client, Message, UserGroup, UserPresence from zerver.models.realms import get_realm from zerver.models.users import get_user +from zerver.openapi.openapi import Parameter GENERATOR_FUNCTIONS: Dict[str, Callable[[], Dict[str, object]]] = {} REGISTERED_GENERATOR_FUNCTIONS: Set[str] = set() @@ -71,28 +72,24 @@ def assert_all_helper_functions_called() -> None: def patch_openapi_example_values( entry: str, - params: List[Dict[str, Any]], + parameters: List[Parameter], request_body: Optional[Dict[str, Any]] = None, -) -> Tuple[List[Dict[str, object]], Optional[Dict[str, object]]]: +) -> Tuple[List[Parameter], Optional[Dict[str, object]]]: if entry not in GENERATOR_FUNCTIONS: - return params, request_body + return parameters, request_body func = GENERATOR_FUNCTIONS[entry] realm_example_values: Dict[str, object] = func() - for param in params: - param_name = param["name"] - if param_name in realm_example_values: - if "content" in param: - param["content"]["application/json"]["example"] = realm_example_values[param_name] - else: - param["example"] = realm_example_values[param_name] + for parameter in parameters: + if parameter.name in realm_example_values: + parameter.example = realm_example_values[parameter.name] if request_body is not None: properties = request_body["content"]["multipart/form-data"]["schema"]["properties"] for key, property in properties.items(): if key in realm_example_values: property["example"] = realm_example_values[key] - return params, request_body + return parameters, request_body @openapi_param_value_generator(["/fetch_api_key:post"]) diff --git a/zerver/openapi/markdown_extension.py b/zerver/openapi/markdown_extension.py index 48885b425b..63130b8311 100644 --- a/zerver/openapi/markdown_extension.py +++ b/zerver/openapi/markdown_extension.py @@ -21,11 +21,14 @@ from typing_extensions import override import zerver.openapi.python_examples from zerver.lib.markdown.priorities import PREPROCESSOR_PRIORITES from zerver.openapi.openapi import ( + NO_EXAMPLE, + Parameter, check_additional_imports, check_requires_administrator, generate_openapi_fixture, get_curl_include_exclude, get_openapi_description, + get_openapi_parameters, get_openapi_summary, get_parameters_description, get_responses_description, @@ -217,46 +220,45 @@ def curl_method_arguments(endpoint: str, method: str, api_url: str) -> List[str] def get_openapi_param_example_value_as_string( - endpoint: str, method: str, param: Dict[str, Any], curl_argument: bool = False + endpoint: str, method: str, parameter: Parameter, curl_argument: bool = False ) -> str: - jsonify = False - param_name = param["name"] - if "content" in param: - param = param["content"]["application/json"] - jsonify = True - if "type" in param["schema"]: - param_type = param["schema"]["type"] + if "type" in parameter.value_schema: + param_type = parameter.value_schema["type"] else: # Hack: Ideally, we'd extract a common function for handling # oneOf values in types and do something with the resulting # union type. But for this logic's purpose, it's good enough # to just check the first parameter. - param_type = param["schema"]["oneOf"][0]["type"] + param_type = parameter.value_schema["oneOf"][0]["type"] if param_type in ["object", "array"]: - example_value = param.get("example", None) - if not example_value: + if parameter.example is NO_EXAMPLE: msg = f"""All array and object type request parameters must have concrete examples. The openAPI documentation for {endpoint}/{method} is missing an example -value for the {param_name} parameter. Without this we cannot automatically generate a +value for the {parameter.name} parameter. Without this we cannot automatically generate a cURL example.""" raise ValueError(msg) - ordered_ex_val_str = json.dumps(example_value, sort_keys=True) + ordered_ex_val_str = json.dumps(parameter.example, sort_keys=True) # We currently don't have any non-JSON encoded arrays. - assert jsonify + assert parameter.json_encoded if curl_argument: - return " --data-urlencode " + shlex.quote(f"{param_name}={ordered_ex_val_str}") + return " --data-urlencode " + shlex.quote(f"{parameter.name}={ordered_ex_val_str}") return ordered_ex_val_str # nocoverage else: - example_value = param.get("example", DEFAULT_EXAMPLE[param_type]) - if isinstance(example_value, bool): - # Booleans are effectively JSON-encoded, in that we pass - # true/false, not the Python str(True) = "True" - jsonify = True - if jsonify: + if parameter.example is NO_EXAMPLE: + example_value = DEFAULT_EXAMPLE[param_type] + else: + example_value = parameter.example + + # Booleans are effectively JSON-encoded, in that we pass + # true/false, not the Python str(True) = "True" + if parameter.json_encoded or isinstance(example_value, (bool, float, int)): example_value = json.dumps(example_value) + else: + assert isinstance(example_value, str) + if curl_argument: - return " --data-urlencode " + shlex.quote(f"{param_name}={example_value}") + return " --data-urlencode " + shlex.quote(f"{parameter.name}={example_value}") return example_value @@ -274,23 +276,23 @@ def generate_curl_example( operation_entry = openapi_spec.openapi()["paths"][endpoint][method.lower()] global_security = openapi_spec.openapi()["security"] - operation_params = operation_entry.get("parameters", []) + parameters = get_openapi_parameters(endpoint, method) operation_request_body = operation_entry.get("requestBody", None) operation_security = operation_entry.get("security", None) if settings.RUNNING_OPENAPI_CURL_TEST: # nocoverage from zerver.openapi.curl_param_value_generators import patch_openapi_example_values - operation_params, operation_request_body = patch_openapi_example_values( - operation, operation_params, operation_request_body + parameters, operation_request_body = patch_openapi_example_values( + operation, parameters, operation_request_body ) format_dict = {} - for param in operation_params: - if param["in"] != "path": + for parameter in parameters: + if parameter.kind != "path": continue - example_value = get_openapi_param_example_value_as_string(endpoint, method, param) - format_dict[param["name"]] = example_value + example_value = get_openapi_param_example_value_as_string(endpoint, method, parameter) + format_dict[parameter.name] = example_value example_endpoint = endpoint.format_map(format_dict) curl_first_line_parts = ["curl", *curl_method_arguments(example_endpoint, method, api_url)] @@ -319,19 +321,18 @@ def generate_curl_example( if authentication_required: lines.append(" -u " + shlex.quote(f"{auth_email}:{auth_api_key}")) - for param in operation_params: - if param["in"] == "path": - continue - param_name = param["name"] - - if include is not None and param_name not in include: + for parameter in parameters: + if parameter.kind == "path": continue - if exclude is not None and param_name in exclude: + if include is not None and parameter.name not in include: + continue + + if exclude is not None and parameter.name in exclude: continue example_value = get_openapi_param_example_value_as_string( - endpoint, method, param, curl_argument=True + endpoint, method, parameter, curl_argument=True ) lines.append(example_value)