diff --git a/codegenerator/openapi/base.py b/codegenerator/openapi/base.py index 7357aab..f1cd292 100644 --- a/codegenerator/openapi/base.py +++ b/codegenerator/openapi/base.py @@ -647,88 +647,20 @@ class OpenStackServerSourceBase: if action_name: operation_name = action_name - # Unwrap operation decorators to access all properties - f = func - while hasattr(f, "__wrapped__"): - closure = inspect.getclosurevars(f) - closure_locals = closure.nonlocals - min_ver = closure_locals.get("min_version", start_version) - if min_ver and not isinstance(min_ver, str): - min_ver = min_ver.get_string() - max_ver = closure_locals.get("max_version", end_version) - if max_ver and not isinstance(max_ver, str): - max_ver = max_ver.get_string() - - if "errors" in closure_locals: - expected_errors = closure_locals["errors"] - if isinstance(expected_errors, list): - expected_errors = [ - str(x) - for x in filter( - lambda x: isinstance(x, int), expected_errors - ) - ] - elif isinstance(expected_errors, int): - expected_errors = [str(expected_errors)] - if "request_body_schema" in closure_locals or hasattr( - f, "_request_body_schema" - ): - # Body type is known through method decorator - obj = closure_locals.get( - "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 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) - if "response_body_schema" in closure_locals or hasattr( - f, "_response_body_schema" - ): - # Response type is known through method decorator - PERFECT - obj = closure_locals.get( - "response_body_schema", - getattr(f, "_response_body_schema", {}), - ) - ser_schema = obj - if "query_params_schema" in closure_locals or hasattr( - f, "_request_query_schema" - ): - obj = closure_locals.get( - "query_params_schema", - getattr(f, "_request_query_schema", {}), - ) - query_params_versions.append((obj, min_ver, max_ver)) - - f = f.__wrapped__ + ( + query_params_versions, + body_schemas, + response_body_schema, + expected_errors, + ) = self._process_decorators( + func, + path_resource_names, + openapi_spec, + mode, + start_version, + end_version, + action_name, + ) if hasattr(func, "_wsme_definition"): fdef = getattr(func, "_wsme_definition") @@ -786,6 +718,8 @@ class OpenStackServerSourceBase: operation_name, ) + if ser_schema and not response_body_schema: + response_body_schema = ser_schema responses_spec = operation_spec.responses for error in expected_errors: responses_spec.setdefault(str(error), dict(description="Error")) @@ -830,7 +764,7 @@ class OpenStackServerSourceBase: if not action_name else f"Response of the {operation_spec.operationId}:{action_name} action" ), # noqa - schema_def=ser_schema, + schema_def=response_body_schema, action_name=action_name, ) @@ -910,7 +844,8 @@ class OpenStackServerSourceBase: **copy.deepcopy(spec["items"]) ) else: - raise RuntimeError("Error") + param_attrs["schema"] = TypeSchema(**copy.deepcopy(spec)) + param_attrs["description"] = spec.get("description") if min_ver: os_ext = param_attrs.setdefault("x-openstack", {}) os_ext["min-ver"] = min_ver @@ -1218,6 +1153,113 @@ class OpenStackServerSourceBase: response_code = "200" return [response_code] + def _process_decorators( + self, + func, + path_resource_names: list[str], + openapi_spec, + mode: str, + start_version, + end_version, + action_name: str | None, + ): + """Extract schemas from the decorated method.""" + # Unwrap operation decorators to access all properties + expected_errors: list[str] = [] + body_schemas: list[str] = [] + query_params_versions: list[tuple] = [] + response_body_schema: dict | None = None + + f = func + while hasattr(f, "__wrapped__"): + closure = inspect.getclosurevars(f) + closure_locals = closure.nonlocals + min_ver = closure_locals.get("min_version", start_version) + if min_ver and not isinstance(min_ver, str): + min_ver = min_ver.get_string() + max_ver = closure_locals.get("max_version", end_version) + if max_ver and not isinstance(max_ver, str): + max_ver = max_ver.get_string() + + if "errors" in closure_locals: + expected_errors = closure_locals["errors"] + if isinstance(expected_errors, list): + expected_errors = [ + str(x) + for x in filter( + lambda x: isinstance(x, int), expected_errors + ) + ] + elif isinstance(expected_errors, int): + expected_errors = [str(expected_errors)] + if "request_body_schema" in closure_locals or hasattr( + f, "_request_body_schema" + ): + # Body type is known through method decorator + obj = closure_locals.get( + "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 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) + + if "response_body_schema" in closure_locals or hasattr( + f, "_response_body_schema" + ): + # Response type is known through method decorator - PERFECT + obj = closure_locals.get( + "response_body_schema", + getattr(f, "_response_body_schema", {}), + ) + response_body_schema = obj + if "query_params_schema" in closure_locals or hasattr( + f, "_request_query_schema" + ): + obj = closure_locals.get( + "query_params_schema", + getattr(f, "_request_query_schema", {}), + ) + query_params_versions.append((obj, min_ver, max_ver)) + + f = f.__wrapped__ + + return ( + query_params_versions, + body_schemas, + response_body_schema, + expected_errors, + ) + def _convert_wsme_to_jsonschema(body_spec): """Convert WSME type description to JsonSchema""" diff --git a/codegenerator/openapi/keystone.py b/codegenerator/openapi/keystone.py index 5cfd47f..6c7d36a 100644 --- a/codegenerator/openapi/keystone.py +++ b/codegenerator/openapi/keystone.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. # +import copy import inspect from multiprocessing import Process import logging @@ -35,6 +36,7 @@ from codegenerator.openapi.keystone_schemas import role from codegenerator.openapi.keystone_schemas import service from codegenerator.openapi.keystone_schemas import user from codegenerator.openapi.utils import merge_api_ref_doc +from codegenerator.openapi.utils import rst_to_md class KeystoneGenerator(OpenStackServerSourceBase): @@ -151,10 +153,6 @@ class KeystoneGenerator(OpenStackServerSourceBase): for route in self.router.iter_rules(): if route.rule.startswith("/static"): continue - # if not route.rule.startswith("/v3/domains"): - # continue - if "/credentials/OS-EC2" in route.rule: - continue self._process_route(route, openapi_spec) @@ -358,26 +356,62 @@ class KeystoneGenerator(OpenStackServerSourceBase): path, method, ) + doc = inspect.getdoc(func) + if doc and not operation_spec.description: + doc = rst_to_md(doc) + operation_spec.description = LiteralScalarString(doc) + + query_params_versions = [] + body_schemas = [] + expected_errors = ["404"] + response_code = None + start_version = None + end_version = None + deser_schema: dict = {} + ser_schema: dict = {} + + ( + query_params_versions, + body_schemas, + ser_schema, + expected_errors, + ) = self._process_decorators( + func, + path_resource_names, + openapi_spec, + method, + start_version, + end_version, + None, + ) + + if query_params_versions: + so = sorted( + query_params_versions, + key=lambda d: ( + tuple(map(int, d[1].split("."))) if d[1] else (0, 0) + ), + ) + for data, min_ver, max_ver in so: + self.process_query_parameters( + openapi_spec, + operation_spec, + path_resource_names, + data, + min_ver, + max_ver, + ) + if method in ["PUT", "POST", "PATCH"]: - # This is clearly a modification operation but we know nothing about request - schema_name = ( - "".join([x.title() for x in path_resource_names]) - + method.title() - + "Request" - ) - - (schema_ref, mime_type) = self._get_schema_ref( + self.process_body_parameters( openapi_spec, - schema_name, - description=f"Request of the {operation_spec.operationId} operation", + operation_spec, + path_resource_names, + body_schemas, + None, + method, ) - if schema_ref: - content = operation_spec.requestBody = {"content": {}} - content["content"][mime_type] = { - "schema": {"$ref": schema_ref} - } - responses_spec = operation_spec.responses # Errors for error in ["403", "404"]: @@ -420,6 +454,7 @@ class KeystoneGenerator(OpenStackServerSourceBase): openapi_spec, schema_name, description=f"Response of the {operation_spec.operationId} operation", + schema_def=ser_schema, ) if schema_ref: @@ -486,7 +521,11 @@ class KeystoneGenerator(OpenStackServerSourceBase): # Default (ref, mime_type) = super()._get_schema_ref( - openapi_spec, name, description, action_name=action_name + openapi_spec, + name, + description, + schema_def=schema_def, + action_name=action_name, ) return (ref, mime_type) diff --git a/codegenerator/openapi/keystone_schemas/application_credential.py b/codegenerator/openapi/keystone_schemas/application_credential.py index 2383ce3..f771ea4 100644 --- a/codegenerator/openapi/keystone_schemas/application_credential.py +++ b/codegenerator/openapi/keystone_schemas/application_credential.py @@ -170,7 +170,7 @@ def _get_schema_ref( TypeSchema(**APPLICATION_CREDENTIAL_CREATE_SCHEMA), ) ref = f"#/components/schemas/{name}" - elif name in "UsersApplication_CredentialsPostResponse": + elif name == "UsersApplication_CredentialsPostResponse": openapi_spec.components.schemas.setdefault( name, TypeSchema(**APPLICATION_CREDENTIAL_CREATE_RESPONSE_SCHEMA),