diff --git a/codegenerator/openapi/base.py b/codegenerator/openapi/base.py index 5cab9aa..6920ea4 100644 --- a/codegenerator/openapi/base.py +++ b/codegenerator/openapi/base.py @@ -18,7 +18,7 @@ import importlib import inspect import logging from pathlib import Path -from typing import Any, Callable +from typing import Any, Callable, Literal import re from codegenerator.common.schema import ParameterSchema @@ -35,6 +35,16 @@ from wsme import types as wtypes VERSION_RE = re.compile(r"[Vv][0-9\.]*") +# Workaround Python's lack of an undefined sentinel +# https://python-patterns.guide/python/sentinel-object/ +class Unset: + def __bool__(self) -> Literal[False]: + return False + + +UNSET: Unset = Unset() + + def get_referred_type_data(func, name: str): """Get python type object referred by the function @@ -893,9 +903,9 @@ class OpenStackServerSourceBase: mode, action_name, ): - op_body = operation_spec.requestBody.setdefault("content", {}) mime_type: str = "application/json" schema_name = None + schema_ref: str | Unset | None = None # We should not modify path_resource_names of the caller path_resource_names = path_resource_names.copy() # Create container schema with version discriminator @@ -909,19 +919,24 @@ class OpenStackServerSourceBase: if len(body_schemas) == 1: # There is only one body known at the moment - if cont_schema_name in openapi_spec.components.schemas: - # if we have already oneOf - add there - cont_schema = openapi_spec.components.schemas[cont_schema_name] - if cont_schema.oneOf and body_schemas[0] not in [ - x["$ref"] for x in cont_schema.oneOf - ]: - cont_schema.oneOf.append({"$ref": body_schemas[0]}) - schema_ref = f"#/components/schemas/{cont_schema_name}" - else: - # otherwise just use schema as body - schema_ref = body_schemas[0] + # None is a special case with explicitly no body supported + if body_schemas[0] is not UNSET: + if cont_schema_name in openapi_spec.components.schemas: + # if we have already oneOf - add there + cont_schema = openapi_spec.components.schemas[ + cont_schema_name + ] + if cont_schema.oneOf and body_schemas[0] not in [ + x["$ref"] for x in cont_schema.oneOf + ]: + cont_schema.oneOf.append({"$ref": body_schemas[0]}) + schema_ref = f"#/components/schemas/{cont_schema_name}" + else: + # otherwise just use schema as body + schema_ref = body_schemas[0] elif len(body_schemas) > 1: # We may end up here multiple times if we have versioned operation. In this case merge to what we have already + op_body = operation_spec.requestBody.setdefault("content", {}) old_schema = op_body.get(mime_type, {}).get("schema", {}) old_ref = ( old_schema.ref @@ -984,16 +999,20 @@ class OpenStackServerSourceBase: ) if mode == "action": + op_body = operation_spec.requestBody.setdefault("content", {}) js_content = op_body.setdefault(mime_type, {}) body_schema = js_content.setdefault("schema", {}) one_of = body_schema.setdefault("oneOf", []) - if schema_ref not in [x.get("$ref") for x in one_of]: + if schema_ref and schema_ref not in [ + x.get("$ref") for x in one_of + ]: one_of.append({"$ref": schema_ref}) os_ext = body_schema.setdefault("x-openstack", {}) os_ext["discriminator"] = "action" if cont_schema and action_name: cont_schema.openstack["action-name"] = action_name - elif schema_ref: + elif schema_ref is not None and schema_ref is not UNSET: + op_body = operation_spec.requestBody.setdefault("content", {}) js_content = op_body.setdefault(mime_type, {}) body_schema = js_content.setdefault("schema", {}) operation_spec.requestBody["content"][mime_type]["schema"] = ( @@ -1127,12 +1146,13 @@ class OpenStackServerSourceBase: "type": "object", "description": LiteralScalarString(description), } - schema = openapi_spec.components.schemas.setdefault( - name, - TypeSchema( - **schema_def, - ), - ) + if schema_def is not UNSET: + schema = openapi_spec.components.schemas.setdefault( + name, + TypeSchema( + **schema_def, + ), + ) if action_name: if not schema.openstack: @@ -1180,9 +1200,9 @@ class OpenStackServerSourceBase: """Extract schemas from the decorated method.""" # Unwrap operation decorators to access all properties expected_errors: list[str] = [] - body_schemas: list[str] = [] + body_schemas: list[str | Unset] = [] query_params_versions: list[tuple] = [] - response_body_schema: dict | None = None + response_body_schema: dict | Unset | None = UNSET f = func while hasattr(f, "__wrapped__"): @@ -1214,38 +1234,47 @@ class OpenStackServerSourceBase: "request_body_schema", getattr(f, "_request_body_schema", {}), ) - if obj.get("type") in ["object", "array"]: - # We only allow object and array bodies - # To prevent type name collision keep module name part of the name - typ_name = ( - "".join([x.title() for x in path_resource_names]) - + func.__name__.title() - + (f"_{min_ver.replace('.', '')}" if min_ver else "") - ) - comp_schema = openapi_spec.components.schemas.setdefault( - typ_name, - self._sanitize_schema( - copy.deepcopy(obj), - start_version=start_version, - end_version=end_version, - ), - ) + if obj is not None: + if obj.get("type") in ["object", "array"]: + # We only allow object and array bodies + # To prevent type name collision keep module name part of the name + typ_name = ( + "".join([x.title() for x in path_resource_names]) + + func.__name__.title() + + ( + f"_{min_ver.replace('.', '')}" + if min_ver + else "" + ) + ) + comp_schema = ( + openapi_spec.components.schemas.setdefault( + typ_name, + self._sanitize_schema( + copy.deepcopy(obj), + start_version=start_version, + end_version=end_version, + ), + ) + ) - if min_ver: - if not comp_schema.openstack: - comp_schema.openstack = {} - comp_schema.openstack["min-ver"] = min_ver - if max_ver: - if not comp_schema.openstack: - comp_schema.openstack = {} - comp_schema.openstack["max-ver"] = max_ver - if mode == "action": - if not comp_schema.openstack: - comp_schema.openstack = {} - comp_schema.openstack["action-name"] = action_name + if min_ver: + if not comp_schema.openstack: + comp_schema.openstack = {} + comp_schema.openstack["min-ver"] = min_ver + if max_ver: + if not comp_schema.openstack: + comp_schema.openstack = {} + comp_schema.openstack["max-ver"] = max_ver + if mode == "action": + if not comp_schema.openstack: + comp_schema.openstack = {} + comp_schema.openstack["action-name"] = action_name - ref_name = f"#/components/schemas/{typ_name}" - body_schemas.append(ref_name) + ref_name = f"#/components/schemas/{typ_name}" + body_schemas.append(ref_name) + else: + body_schemas.append(UNSET) if "response_body_schema" in closure_locals or hasattr( f, "_response_body_schema" @@ -1255,7 +1284,10 @@ class OpenStackServerSourceBase: "response_body_schema", getattr(f, "_response_body_schema", {}), ) - response_body_schema = obj + if obj is not None: + response_body_schema = obj + else: + response_body_schema = UNSET if "query_params_schema" in closure_locals or hasattr( f, "_request_query_schema" ): diff --git a/codegenerator/openapi/keystone.py b/codegenerator/openapi/keystone.py index 6c7d36a..2378df3 100644 --- a/codegenerator/openapi/keystone.py +++ b/codegenerator/openapi/keystone.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. # -import copy import inspect from multiprocessing import Process import logging @@ -22,7 +21,7 @@ from codegenerator.common.schema import ParameterSchema from codegenerator.common.schema import PathSchema from codegenerator.common.schema import SpecSchema from codegenerator.common.schema import TypeSchema -from codegenerator.openapi.base import OpenStackServerSourceBase +from codegenerator.openapi.base import OpenStackServerSourceBase, UNSET from codegenerator.openapi.keystone_schemas import application_credential from codegenerator.openapi.keystone_schemas import auth from codegenerator.openapi.keystone_schemas import common @@ -368,7 +367,7 @@ class KeystoneGenerator(OpenStackServerSourceBase): start_version = None end_version = None deser_schema: dict = {} - ser_schema: dict = {} + ser_schema: dict | None = {} ( query_params_versions, @@ -512,7 +511,7 @@ class KeystoneGenerator(OpenStackServerSourceBase): # Invoke modularized schema _get_schema_ref for resource_mod in self.RESOURCE_MODULES: hook = getattr(resource_mod, "_get_schema_ref", None) - if hook: + if hook and schema_def is not UNSET: (ref, mime_type, matched) = hook( openapi_spec, name, description, schema_def, action_name ) @@ -527,5 +526,4 @@ class KeystoneGenerator(OpenStackServerSourceBase): schema_def=schema_def, action_name=action_name, ) - return (ref, mime_type)