Merge "Add support for explicit empty schemas"

This commit is contained in:
Zuul
2024-08-16 17:45:29 +00:00
committed by Gerrit Code Review
2 changed files with 89 additions and 59 deletions
codegenerator/openapi

@ -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"
):

@ -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)