Skip to content
This repository has been archived by the owner on Apr 10, 2024. It is now read-only.

Further rust generator improvements #16

Merged
merged 6 commits into from
Feb 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 116 additions & 5 deletions codegenerator/common/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def get_plural_form(resource: str) -> str:

def find_resource_schema(
schema: dict, parent: str | None = None, resource_name: str | None = None
):
) -> tuple[dict | None, str | None]:
"""Find the actual resource schema in the body schema

Traverse through the body schema searching for an element that represent
Expand Down Expand Up @@ -128,7 +128,16 @@ def find_resource_schema(
and resource_name
and parent == get_plural_form(resource_name)
):
return (schema["items"], parent)
items = schema["items"]
if (
items.get("type") == "object"
and resource_name in items.get("properties", [])
and len(items.get("properties", []).keys()) == 1
):
# Most likely this is Keypair where we have keypairs.keypair.{}
return (items["properties"][resource_name], parent)
else:
return (items, parent)
elif (
not parent and schema.get("items", {}).get("type") == "object"
):
Expand All @@ -147,8 +156,8 @@ def find_resource_schema(
else schema.get("properties", {})
)
if not parent and resource_name in props:
# we are at the top level and there is property with the resource
# name - it is what we are searching for
# we are at the top level and there is property with the
# resource name - it is what we are searching for
el_type = props[resource_name]["type"]
if el_type == "array":
return (props[resource_name]["items"], resource_name)
Expand All @@ -165,7 +174,12 @@ def find_resource_schema(
keys = list(props.keys())
if len(keys) == 1:
# there is only one field in the object
return (props[keys[0]], keys[0])
if props[keys[0]].get("type") == "object":
# and it is itself an object
return (props[keys[0]], keys[0])
else:
# only field is not an object
return (schema, None)
else:
return (schema, None)
except Exception as ex:
Expand All @@ -176,6 +190,103 @@ def find_resource_schema(
return (None, None)


def find_response_schema(
responses: dict, response_key: str, action_name: str | None = None
):
"""Locate response schema

Some operations are having variety of possible responses (depending on
microversion, action, etc). Try to locate suitable response for the client.

The function iterates over all defined responses and for 2** appies the
following logic:

- if action_name is present AND oneOf is present AND action_name is in one
of the oneOf schemas -> return this schema

- if action_name is not present AND oneOf is present AND response_key is in
one of the OneOf candidates' properties (this is an object) -> return it

- action_name is not present AND oneOf is not present and (response_key or
plural of the response_key) in candidate -> return it

:param dict responses: Dictionary with responses as defined in OpenAPI spec
:param str response_key: Response key to be searching in responses (when
aciton_name is not given) :param str action_name: Action name to be
searching response for
"""
for code, rspec in responses.items():
if not code.startswith("2"):
continue
content = rspec.get("content", {})
if "application/json" in content:
response_spec = content["application/json"]
schema = response_spec["schema"]
oneof = schema.get("oneOf")
discriminator = schema.get("x-openstack", {}).get("discriminator")
if oneof:
if not discriminator:
# Server create returns server or reservation info. For the
# cli it is not very helpful and we look for response
# candidate with the resource_name in the response
for candidate in oneof:
if (
action_name
and candidate.get("x-openstack", {}).get(
"action-name"
)
== action_name
):
if response_key in candidate.get("properties", {}):
# If there is a object with resource_name in
# the props - this must be what we want to look
# at
return candidate["properties"][response_key]
else:
return candidate
elif (
not action_name
and response_key
and candidate.get("type") == "object"
and response_key in candidate.get("properties", {})
):
# Actually for the sake of the CLI it may make
# sense to merge all candidates
return candidate["properties"][response_key]
else:
raise NotImplementedError
elif (
not action_name
and schema
and (
response_key in schema
or (
schema.get("type") == "object"
and (
response_key in schema.get("properties", [])
or get_plural_form(response_key)
in schema.get("properties", [])
)
)
)
):
return schema
if not action_name:
# Could not find anything with the given response_key. If there is any
# 200/204 response - return it
for code in ["200", "204"]:
if code in responses:
schema = (
responses[code]
.get("content", {})
.get("application/json", {})
.get("schema")
)
if schema and "type" in schema:
return schema
return None


def get_resource_names_from_url(path: str):
"""Construct Resource name from the URL"""
path_elements = list(filter(None, path.split("/")))
Expand Down
32 changes: 26 additions & 6 deletions codegenerator/common/rust.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,9 +252,19 @@ def type_hint(self):

@property
def imports(self):
imports: set[str] = set(["serde::Deserialize"])
for field in self.fields.values():
imports.update(field.data_type.imports)
imports: set[str] = set([])
field_types = [x.data_type for x in self.fields.values()]
if len(field_types) > 1 or (
len(field_types) == 1
and not isinstance(field_types[0], Null)
and not isinstance(field_types[0], Dictionary)
and not isinstance(field_types[0], Array)
):
# We use structure only if it is not consisting from only Null
imports.add("serde::Deserialize")
imports.add("serde::Serialize")
for field_type in field_types:
imports.update(field_type.imports)
if self.additional_fields_type:
imports.add("std::collections::BTreeMap")
imports.update(self.additional_fields_type.imports)
Expand Down Expand Up @@ -305,6 +315,8 @@ def type_hint(self):
@property
def imports(self):
imports: set[str] = set()
imports.add("serde::Deserialize")
imports.add("serde::Serialize")
for kind in self.kinds.values():
imports.update(kind.data_type.imports)
return imports
Expand All @@ -325,7 +337,7 @@ def clap_macros(self) -> set[str]:
class StringEnum(BaseCompoundType):
base_type: str = "enum"
variants: dict[str, set[str]] = {}
imports: set[str] = set([])
imports: set[str] = set(["serde::Deserialize", "serde::Serialize"])
lifetimes: set[str] = set()
derive_container_macros: str = (
"#[derive(Debug, Deserialize, Clone, Serialize)]"
Expand Down Expand Up @@ -765,6 +777,9 @@ def _simplify_oneof_combinations(self, type_model, kinds):
elif string_klass in kinds_classes and dict_klass in kinds_classes:
# oneOf [string, dummy object] => JsonValue
# Simple string can be easily represented by JsonValue
for c in kinds:
# Discard dict
self.ignored_models.append(c["model"])
kinds.clear()
jsonval_klass = self.primitive_type_mapping[model.PrimitiveAny]
kinds.append({"local": jsonval_klass(), "class": jsonval_klass})
Expand Down Expand Up @@ -880,8 +895,11 @@ def get_root_data_type(self):
def get_imports(self):
"""Get complete set of additional imports required by all models in scope"""
imports: set[str] = set()
for item in self.refs.values():
imports.update(item.imports)
imports.update(self.get_root_data_type().imports)
for subt in self.get_subtypes():
imports.update(subt.imports)
# for item in self.refs.values():
# imports.update(item.imports)
for param in self.parameters.values():
imports.update(param.data_type.imports)
return imports
Expand All @@ -904,6 +922,8 @@ def subtype_requires_private_builders(self, subtype) -> bool:
for field in subtype.fields.values():
if "private" in field.builder_macros:
return True
if isinstance(subtype, Struct) and subtype.additional_fields_type:
return True
return False

def set_parameters(self, parameters: list[model.RequestParameter]) -> None:
Expand Down
63 changes: 63 additions & 0 deletions codegenerator/openapi/cinder.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ def _get_schema_ref(
action_name=None,
):
mime_type: str = "application/json"
# ### Volume
if name == "VolumesListResponse":
openapi_spec.components.schemas.setdefault(
name, TypeSchema(**cinder_schemas.VOLUMES_SCHEMA)
Expand All @@ -186,6 +187,68 @@ def _get_schema_ref(
name, TypeSchema(**cinder_schemas.VOLUME_CONTAINER_SCHEMA)
)
ref = f"#/components/schemas/{name}"
# ### Volume Metadata
elif name in [
"VolumesMetadataListResponse",
"VolumesMetadataUpdate_All",
"VolumesMetadataUpdate_AllResponse",
"VolumesMetadataCreateResponse",
"VolumesActionOs-Set_Image_MetadataResponse",
"VolumesActionOs-Show_Image_MetadataResponse",
]:
openapi_spec.components.schemas.setdefault(
name, TypeSchema(**cinder_schemas.METADATA_CONTAINER_SCHEMA)
)
ref = f"#/components/schemas/{name}"
elif name in [
"VolumesMetadataShowResponse",
"VolumesMetadataUpdate",
"VolumesMetadataUpdateResponse",
]:
openapi_spec.components.schemas.setdefault(
name, TypeSchema(**cinder_schemas.METADATA_ITEM_SCHEMA)
)
ref = f"#/components/schemas/{name}"
# Volume Actions
elif name == "VolumesActionRevertResponse":
return (None, None)
elif name == "VolumesActionOs-Reset_StatusRequest":
openapi_spec.components.schemas.setdefault(
name, TypeSchema(**cinder_schemas.VOLUME_RESET_STATUS_SCHEMA)
)
ref = f"#/components/schemas/{name}"
elif name in [
"VolumesActionOs-Reset_StatusResponse",
"VolumesActionOs-Force_DeleteResponse",
"VolumesActionOs-Force_DetachResponse",
"VolumesActionOs-Migrate_VolumeResponse",
"VolumesActionOs-Migrate_Volume_CompletionResponse",
"VolumesActionOs-AttachResponse",
"VolumesActionOs-DetachResponse",
"VolumesActionOs-ReserveResponse",
"VolumesActionOs-UnreserveResponse",
"VolumesActionOs-Begin_DetachingResponse",
"VolumesActionOs-Roll_DetachingResponse",
"VolumesActionOs-Initialize_ConnectionResponse",
"VolumesActionOs-Terminate_ConnectionResponse",
"VolumesActionOs-ExtendResponse",
"VolumesActionOs-Update_Readonly_FlagResponse",
"VolumesActionOs-RetypeResponse",
"VolumesActionOs-Set_BootableResponse",
"VolumesActionOs-ReimageResponse",
"VolumesActionOs-Unset_Image_MetadataResponse",
"VolumesActionOs-UnmanageResponse",
]:
return (None, None)
elif name == "VolumesActionOs-Volume_Upload_ImageResponse":
openapi_spec.components.schemas.setdefault(
name,
TypeSchema(
**cinder_schemas.VOLUME_UPLOAD_IMAGE_RESPONSE_SCHEMA
),
)
ref = f"#/components/schemas/{name}"
# Default
else:
(ref, mime_type) = super()._get_schema_ref(
openapi_spec, name, description, action_name=action_name
Expand Down
Loading