Add support for explicit empty schemas
There might be a decorator explicitly stating the schema is empty meaning there is no body (this is different from schema with type "null"). In order to differentiate the empty schema from not set add a similar Sentinet `UNSET` as in OpenStackSDK and generate dummy schemas only when the schema is really unknown. Change-Id: I72434afea54af9aa6df205a27a8d66765bb0ffd7
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user