Respect MV based responses
Many calls have different schemas in the response per MV. Previously the hope was that it is possible to somehow magically merge them providing a single response type, but this failed dramatically. "Hail" to microversions. Depends-On: https://review.opendev.org/c/openstack/nova/+/986660 Change-Id: I6a19a897b49efee6ce9c33985b2e84cebe7827bc Signed-off-by: Artem Goncharov <artem.goncharov@gmail.com>
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
import copy
|
||||
import logging
|
||||
import re
|
||||
from pathlib import Path
|
||||
@@ -237,6 +238,8 @@ def find_resource_schema(
|
||||
schema["type"] = "string"
|
||||
elif isinstance(schema["enum"][0], bool):
|
||||
schema["type"] = "boolean"
|
||||
elif "const" in schema:
|
||||
return (None, None)
|
||||
else:
|
||||
raise RuntimeError(f"No type in {schema}")
|
||||
schema_type = schema["type"]
|
||||
@@ -310,31 +313,58 @@ def find_resource_schema(
|
||||
return (None, None)
|
||||
|
||||
|
||||
def find_response_schema(
|
||||
responses: dict, response_key: str, action_name: str | None = None
|
||||
):
|
||||
"""Locate response schema
|
||||
def ensure_microversion_bounds(
|
||||
microversion: str | None, x_openstack: dict[str, Any]
|
||||
) -> bool:
|
||||
"""Check the microversion range match for the x-openstack data of the schema
|
||||
candidate
|
||||
|
||||
:returns bool: true when the requested microversion is not outside of the
|
||||
schema bounds.
|
||||
"""
|
||||
|
||||
if microversion and "min-ver" in x_openstack:
|
||||
# compare current min-ver >= requested
|
||||
requested_mv = tuple(int(x) for x in microversion.split("."))
|
||||
candidate_min_mv = tuple(
|
||||
int(x) for x in x_openstack["min-ver"].split(".")
|
||||
)
|
||||
if candidate_min_mv > requested_mv:
|
||||
return False
|
||||
# compare current max-ver <= requested
|
||||
if "max-ver" in x_openstack:
|
||||
candidate_max_mv = tuple(
|
||||
int(x) for x in x_openstack["max-ver"].split(".")
|
||||
)
|
||||
if candidate_max_mv < requested_mv:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def get_response_candidates(
|
||||
responses: dict,
|
||||
response_key: str,
|
||||
action_name: str | None = None,
|
||||
microversion: str | None = None,
|
||||
) -> list[tuple[dict[str, Any], dict[str, Any] | None]]:
|
||||
"""Find all operation response candidates
|
||||
|
||||
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** applies 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 dict responses: Response definition from the OpenAPI spec.
|
||||
:param str response_key: Response key to be searching in responses (when
|
||||
action_name is not given) :param str action_name: Action name to be
|
||||
searching response for
|
||||
action_name is not given)
|
||||
:param str action_name: Action name to be searching response for
|
||||
:param str microversion: Optional microversion to search potential responses
|
||||
for.
|
||||
|
||||
:returns list[tuple[dict, dict], dict[str, Any]|None responses: List of
|
||||
tuples representing the response body and the extension data
|
||||
associated with the response.
|
||||
"""
|
||||
response_schemas: list[tuple[dict[str, Any], dict[str, Any] | None]] = []
|
||||
|
||||
for code, rspec in responses.items():
|
||||
if not code.startswith("2"):
|
||||
continue
|
||||
@@ -349,37 +379,152 @@ def find_response_schema(
|
||||
# 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
|
||||
kind_suffix = ord("a")
|
||||
for candidate in oneof:
|
||||
if (
|
||||
action_name
|
||||
and candidate.get("x-openstack", {}).get(
|
||||
"action-name"
|
||||
ext = candidate.get("x-openstack", {})
|
||||
if "action-name" in ext and (
|
||||
(
|
||||
action_name
|
||||
and action_name != ext.get("action-name")
|
||||
)
|
||||
== action_name
|
||||
or (not 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
|
||||
# This is the action response and not the one we are
|
||||
# interested in
|
||||
continue
|
||||
elif (
|
||||
not action_name
|
||||
and response_key
|
||||
and candidate.get("type") == "object"
|
||||
and response_key in candidate.get("properties", {})
|
||||
ext
|
||||
and action_name
|
||||
and action_name == ext.get("action-name")
|
||||
):
|
||||
# Actually for the sake of the CLI it may make
|
||||
# sense to merge all candidates
|
||||
return candidate["properties"][response_key]
|
||||
# action-name set inside the candidate - final
|
||||
# response if the name matches
|
||||
|
||||
# When a microversion is requested check the
|
||||
# range
|
||||
if not ensure_microversion_bounds(
|
||||
microversion, ext
|
||||
):
|
||||
continue
|
||||
response_schemas.append((candidate, ext))
|
||||
elif not action_name and response_key:
|
||||
# response key present in the candidate - final
|
||||
# response (but maybe a tricky one)
|
||||
if (
|
||||
candidate.get("type") == "object"
|
||||
and not response_key in candidate["properties"]
|
||||
and get_plural_form(response_key)
|
||||
in candidate["properties"]
|
||||
):
|
||||
# Actually this looks like a list result
|
||||
# compute.server.list_detailed is a oneOf of MVs
|
||||
# with certain MVs {"properties": {"server":
|
||||
# {"items": {"oneOf"....
|
||||
if not ensure_microversion_bounds(
|
||||
microversion, ext
|
||||
):
|
||||
continue
|
||||
res_def = candidate["properties"][
|
||||
get_plural_form(response_key)
|
||||
]
|
||||
if res_def.get("type") != "array":
|
||||
raise RuntimeError(
|
||||
f"List operation has been identified, but the response object is not array"
|
||||
)
|
||||
if "oneOf" in res_def.get("items"):
|
||||
subkind_suffix = ord("a")
|
||||
kind_ext = ext or {}
|
||||
base = copy.deepcopy(candidate)
|
||||
base["properties"][
|
||||
get_plural_form(response_key)
|
||||
]["items"].pop("oneOf", None)
|
||||
for subkind in res_def["items"]["oneOf"]:
|
||||
kind_ext = copy.deepcopy(kind_ext)
|
||||
kind_ext["kind-suffix"] = chr(
|
||||
subkind_suffix
|
||||
)
|
||||
kind_schema = copy.deepcopy(base)
|
||||
kind_schema["properties"][
|
||||
get_plural_form(response_key)
|
||||
]["items"] = subkind
|
||||
response_schemas.append(
|
||||
(kind_schema, kind_ext)
|
||||
)
|
||||
subkind_suffix += 1
|
||||
else:
|
||||
response_schemas.append((candidate, ext))
|
||||
|
||||
elif (
|
||||
candidate.get("type") == "object"
|
||||
and (
|
||||
response_key
|
||||
in candidate.get("properties", {})
|
||||
)
|
||||
and "oneOf"
|
||||
in candidate["properties"][response_key]
|
||||
):
|
||||
# This is a very dirty thing e.g., nova does
|
||||
# with the response where the response schema
|
||||
# is a "oneOf" from few incompatible kinds
|
||||
# (server show). It can only be treated by
|
||||
# splitting them into individual variants.
|
||||
subkind_suffix = ord("a")
|
||||
kind_ext = ext or {}
|
||||
base = copy.deepcopy(candidate)
|
||||
base["properties"][response_key].pop(
|
||||
"oneOf", None
|
||||
)
|
||||
for kind in candidate["properties"][
|
||||
response_key
|
||||
]["oneOf"]:
|
||||
# When a microversion is requested check the
|
||||
# range
|
||||
if not ensure_microversion_bounds(
|
||||
microversion, kind_ext
|
||||
):
|
||||
continue
|
||||
kind_ext = copy.deepcopy(kind_ext)
|
||||
kind_ext["kind-suffix"] = chr(
|
||||
subkind_suffix
|
||||
)
|
||||
kind_schema = copy.deepcopy(base)
|
||||
kind_schema["properties"][
|
||||
response_key
|
||||
].update(kind)
|
||||
response_schemas.append(
|
||||
(kind_schema, kind_ext)
|
||||
)
|
||||
subkind_suffix += 1
|
||||
# a candidate without any reasonable identity and
|
||||
# we are not searching for an action - most likely
|
||||
# a response
|
||||
elif ext and "min-ver" in ext:
|
||||
# When a microversion is requested check the
|
||||
# range
|
||||
if not ensure_microversion_bounds(
|
||||
microversion, ext
|
||||
):
|
||||
continue
|
||||
|
||||
response_schemas.append((candidate, ext))
|
||||
else:
|
||||
# e.g., compute.server.create has a oneOf response. We need
|
||||
# to treat them as individual possibilities since there is
|
||||
# no other practical way.
|
||||
|
||||
kind_ext = ext or {}
|
||||
kind_ext = copy.deepcopy(kind_ext)
|
||||
kind_ext["kind-suffix"] = chr(kind_suffix)
|
||||
response_schemas.append((candidate, kind_ext))
|
||||
kind_suffix += 1
|
||||
else:
|
||||
raise NotImplementedError
|
||||
elif (
|
||||
not action_name
|
||||
and schema
|
||||
schema
|
||||
and not action_name
|
||||
and (
|
||||
response_key in schema
|
||||
or get_plural_form(response_key) in schema
|
||||
or (
|
||||
schema.get("type") == "object"
|
||||
and (
|
||||
@@ -391,10 +536,10 @@ def find_response_schema(
|
||||
or schema.get("type") == "string"
|
||||
)
|
||||
):
|
||||
return schema
|
||||
if not action_name:
|
||||
response_schemas.append((schema, schema.get("x-openstack")))
|
||||
if not action_name and not microversion and len(response_schemas) == 0:
|
||||
# Could not find anything with the given response_key. If there is any
|
||||
# 200/204 response - return it
|
||||
# 200/204 response and it is not an action/MV lookup - return it
|
||||
for code in ["200", "201", "202", "204"]:
|
||||
if code in responses:
|
||||
schema = (
|
||||
@@ -403,12 +548,38 @@ def find_response_schema(
|
||||
.get("application/json", {})
|
||||
.get("schema")
|
||||
)
|
||||
if schema and "type" in schema:
|
||||
return schema
|
||||
return None
|
||||
if schema:
|
||||
ext = schema.get("x-openstack", {})
|
||||
if "oneOf" in schema:
|
||||
# e.g., compute.server.create has a oneOf response. We need
|
||||
# to treat them as individual possibilities since there is
|
||||
# no other practical way.
|
||||
|
||||
# Build up the base (if any) that need to be merged with
|
||||
# every candidate `{"type": "object", "oneOf": ...}`
|
||||
base = copy.deepcopy(schema)
|
||||
base.pop("oneOf")
|
||||
kind_suffix = ord("a")
|
||||
for kind in schema["oneOf"]:
|
||||
# merge the base into the kind
|
||||
kind.update(**base)
|
||||
|
||||
kind_ext = ext or {}
|
||||
kind_ext = copy.deepcopy(kind_ext)
|
||||
kind_ext["kind-suffix"] = chr(kind_suffix)
|
||||
response_schemas.append((kind, kind_ext))
|
||||
kind_suffix += 1
|
||||
elif "type" in schema:
|
||||
# just a single plain schema - last resort
|
||||
response_schemas.append(
|
||||
(schema, schema.get("x-openstack"))
|
||||
)
|
||||
return response_schemas
|
||||
|
||||
|
||||
def get_operation_variants(spec: dict, action_name: str | None = None):
|
||||
def get_operation_variants(
|
||||
spec: dict, action_name: str | None = None
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Find operation body suitable for the generator"""
|
||||
request_body = spec.get("requestBody")
|
||||
# List of operation variants (based on the body)
|
||||
@@ -462,6 +633,9 @@ def get_operation_variants(spec: dict, action_name: str | None = None):
|
||||
"min-ver": subvariant_spec.get(
|
||||
"min-ver"
|
||||
),
|
||||
"max-ver": subvariant_spec.get(
|
||||
"max-ver"
|
||||
),
|
||||
"mime_type": mime_type,
|
||||
}
|
||||
)
|
||||
@@ -474,6 +648,7 @@ def get_operation_variants(spec: dict, action_name: str | None = None):
|
||||
"body": variant,
|
||||
"mode": "action",
|
||||
"min-ver": variant_spec.get("min-ver"),
|
||||
"max-ver": variant_spec.get("max-ver"),
|
||||
"mime_type": mime_type,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -12,16 +12,17 @@
|
||||
#
|
||||
import logging
|
||||
import re
|
||||
from typing import Any
|
||||
from collections.abc import Generator
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from codegenerator.common import BasePrimitiveType
|
||||
from codegenerator.common import BaseCombinedType
|
||||
from codegenerator.common import BaseCompoundType
|
||||
from codegenerator import model
|
||||
from codegenerator import common
|
||||
from codegenerator import common, model
|
||||
from codegenerator.common import (
|
||||
BaseCombinedType,
|
||||
BaseCompoundType,
|
||||
BasePrimitiveType,
|
||||
)
|
||||
|
||||
CODEBLOCK_RE = re.compile(r"```(\w*)$")
|
||||
|
||||
@@ -896,6 +897,10 @@ class TypeManager:
|
||||
)
|
||||
]
|
||||
)
|
||||
# It is forbidden in rust to use "Self" as an enum
|
||||
# kind.
|
||||
if val == "Self":
|
||||
val = "Current"
|
||||
if val and val[0].isdigit():
|
||||
val = "_" + val
|
||||
vals = variants.setdefault(val, set())
|
||||
@@ -1015,9 +1020,10 @@ class TypeManager:
|
||||
kind_description: str | None = None
|
||||
if isinstance(kind_data["model"], model.ADT):
|
||||
kind_name = self.get_model_name(kind_data["model"])
|
||||
kind_description = kind_data["model"].description
|
||||
else:
|
||||
kind_name = f"F{cnt}"
|
||||
if hasattr(kind_data["model"], "description"):
|
||||
kind_description = kind_data["model"].description
|
||||
enum_kind = enum_class._kind_type_class.get_default()(
|
||||
name=kind_name,
|
||||
description=sanitize_rust_docstrings(kind_description),
|
||||
@@ -1286,7 +1292,7 @@ class TypeManager:
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f"Model name {new_name} is already present as"
|
||||
f" {type(model_data_type)}"
|
||||
f" {type(model_data_type)}. {model_data_type}"
|
||||
)
|
||||
elif (
|
||||
name
|
||||
|
||||
+168
-83
@@ -275,7 +275,7 @@ class OpenStackServerSourceBase:
|
||||
)
|
||||
|
||||
if method.upper() != "HEAD":
|
||||
response = common.find_response_schema(
|
||||
responses = common.get_response_candidates(
|
||||
spec["responses"],
|
||||
response_key or resource_name,
|
||||
(
|
||||
@@ -285,7 +285,7 @@ class OpenStackServerSourceBase:
|
||||
),
|
||||
)
|
||||
|
||||
if response:
|
||||
if responses:
|
||||
if response_key:
|
||||
response_key = (
|
||||
response_key
|
||||
@@ -294,18 +294,21 @@ class OpenStackServerSourceBase:
|
||||
)
|
||||
else:
|
||||
response_key = resource_name
|
||||
response_def, _ = common.find_resource_schema(
|
||||
response, None, response_key
|
||||
)
|
||||
for schema, ext in responses:
|
||||
response_def, _ = common.find_resource_schema(
|
||||
schema, None, response_key
|
||||
)
|
||||
|
||||
if response_def:
|
||||
if response_def.get(
|
||||
"type", "object"
|
||||
) == "object" or (
|
||||
isinstance(response_def.get("type"), list)
|
||||
and "object" in response_def["type"]
|
||||
):
|
||||
openapi_parser.parse(response_def)
|
||||
if response_def:
|
||||
if response_def.get(
|
||||
"type", "object"
|
||||
) == "object" or (
|
||||
isinstance(
|
||||
response_def.get("type"), list
|
||||
)
|
||||
and "object" in response_def["type"]
|
||||
):
|
||||
openapi_parser.parse(response_def)
|
||||
|
||||
except Exception as ex:
|
||||
logging.exception(
|
||||
@@ -391,11 +394,7 @@ class OpenStackServerSourceBase:
|
||||
raise RuntimeError(
|
||||
f"Unsupported controller {controller} {framework}"
|
||||
)
|
||||
# logging.debug("Actions: %s, Versioned methods: %s", actions, versioned_methods)
|
||||
|
||||
# path_spec = openapi_spec.paths.setdefault(path, PathSchema())
|
||||
|
||||
# operation_spec = dict() #= getattr(path_spec, method.lower()) # , {})
|
||||
# Get Path elements
|
||||
path_elements: list[str] = list(filter(None, path.split("/")))
|
||||
if path_elements and VERSION_RE.match(path_elements[0]):
|
||||
@@ -636,7 +635,16 @@ class OpenStackServerSourceBase:
|
||||
"Versioned action %s", ver_method.func
|
||||
)
|
||||
else:
|
||||
action_impls.append((op_name, None, None))
|
||||
# An action without explicit versioned decorator, but
|
||||
# version bounds may be present (e.g.,
|
||||
# `wsgi.api_version(2.1, 2.5)`
|
||||
start_version = closurevars.nonlocals.get(
|
||||
"min_version"
|
||||
)
|
||||
end_version = closurevars.nonlocals.get("max_version")
|
||||
action_impls.append(
|
||||
(op_name, start_version, end_version)
|
||||
)
|
||||
|
||||
# Get the path/op spec only when we have
|
||||
# something to fill in
|
||||
@@ -755,7 +763,7 @@ class OpenStackServerSourceBase:
|
||||
action_name = None
|
||||
query_params_versions: set[QueryParamsSchema] = set()
|
||||
body_schemas: set[str | None] | Unset = UNSET
|
||||
expected_errors = ["404"]
|
||||
expected_errors: set[str] = {"404"}
|
||||
response_code = None
|
||||
# Version bound on an operation are set only when it is not an
|
||||
# "action"
|
||||
@@ -818,7 +826,7 @@ class OpenStackServerSourceBase:
|
||||
(
|
||||
query_params_versions,
|
||||
body_schemas,
|
||||
response_body_schema,
|
||||
response_body_schemas,
|
||||
expected_errors,
|
||||
) = self._process_decorators(
|
||||
func,
|
||||
@@ -830,6 +838,7 @@ class OpenStackServerSourceBase:
|
||||
action_name,
|
||||
operation_name,
|
||||
)
|
||||
# response_body_schema: dict | None = None
|
||||
|
||||
if hasattr(func, "_wsme_definition"):
|
||||
fdef = getattr(func, "_wsme_definition")
|
||||
@@ -900,8 +909,6 @@ 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), {"description": "Error"})
|
||||
@@ -911,6 +918,11 @@ class OpenStackServerSourceBase:
|
||||
operation_spec.deprecated = True
|
||||
if not response_code:
|
||||
response_codes = getattr(func, "wsgi_code", None)
|
||||
if not response_codes:
|
||||
if hasattr(func, "wsgi_codes"):
|
||||
# TODO: Nova exposes wsgi_codes as a tuple of (code, min_ver,
|
||||
# max_ver)
|
||||
pass
|
||||
if response_codes:
|
||||
if not isinstance(response_codes, list):
|
||||
response_codes = [response_codes]
|
||||
@@ -928,12 +940,14 @@ class OpenStackServerSourceBase:
|
||||
method, operation_spec.operationId
|
||||
)
|
||||
if response_codes:
|
||||
# Default mime_type which persists across RC iteration (glance image
|
||||
# download uses 200, 206
|
||||
mime_type = "application/json"
|
||||
for response_code in response_codes:
|
||||
rsp = responses_spec.setdefault(
|
||||
str(response_code), {"description": "Ok"}
|
||||
)
|
||||
if str(response_code) != "204" and method != "DELETE":
|
||||
# Arrange response placeholder
|
||||
schema_name = (
|
||||
"".join([x.title() for x in path_resource_names])
|
||||
+ (
|
||||
@@ -945,53 +959,58 @@ class OpenStackServerSourceBase:
|
||||
)
|
||||
+ "Response"
|
||||
)
|
||||
(schema_ref, mime_type) = self._get_schema_ref(
|
||||
openapi_spec,
|
||||
schema_name,
|
||||
description=(
|
||||
f"Response of the {operation_spec.operationId} operation"
|
||||
if not action_name
|
||||
else f"Response of the {operation_spec.operationId}:{action_name} action"
|
||||
), # noqa
|
||||
schema_def=response_body_schema,
|
||||
action_name=action_name,
|
||||
)
|
||||
|
||||
if schema_ref:
|
||||
curr_schema = (
|
||||
rsp.get("content", {})
|
||||
.get("application/json", {})
|
||||
.get("schema", {})
|
||||
if not response_body_schemas:
|
||||
(schema_ref, mime_type) = self._get_schema_ref(
|
||||
openapi_spec,
|
||||
schema_name,
|
||||
description=(
|
||||
f"Response of the {operation_spec.operationId} operation"
|
||||
if not action_name
|
||||
else f"Response of the {operation_spec.operationId}:{action_name} action"
|
||||
), # noqa
|
||||
schema_def=ser_schema,
|
||||
action_name=action_name,
|
||||
)
|
||||
if mode == "action" and curr_schema:
|
||||
if schema_ref:
|
||||
response_body_schemas = {schema_ref}
|
||||
|
||||
if response_body_schemas:
|
||||
for schema_ref in response_body_schemas:
|
||||
curr_schema = (
|
||||
rsp.get("content", {})
|
||||
.get(mime_type, {})
|
||||
.get("schema", {})
|
||||
)
|
||||
# There is existing response for the action. Need to
|
||||
# merge them
|
||||
curr_oneOf: list | None = None
|
||||
curr_ref: str | None = None
|
||||
if isinstance(curr_schema, dict):
|
||||
curr_oneOf = curr_schema.get("oneOf")
|
||||
curr_ref = curr_schema.get("$ref")
|
||||
else:
|
||||
curr_oneOf = curr_schema.oneOf
|
||||
curr_ref = curr_schema.ref
|
||||
elif isinstance(curr_schema, str):
|
||||
curr_ref = curr_schema # .get("$ref")
|
||||
if curr_oneOf:
|
||||
if schema_ref not in [
|
||||
x["$ref"] for x in curr_oneOf
|
||||
]:
|
||||
curr_oneOf.append({"$ref": schema_ref})
|
||||
if schema_ref:
|
||||
if schema_ref not in [
|
||||
x["$ref"] for x in curr_oneOf
|
||||
]:
|
||||
curr_oneOf.append({"$ref": schema_ref})
|
||||
elif curr_ref and curr_ref != schema_ref:
|
||||
rsp["content"]["application/json"][
|
||||
"schema"
|
||||
] = TypeSchema(
|
||||
oneOf=[
|
||||
rsp["content"][mime_type]["schema"] = {
|
||||
"oneOf": [
|
||||
{"$ref": curr_ref},
|
||||
{"$ref": schema_ref},
|
||||
]
|
||||
)
|
||||
else:
|
||||
rsp["content"] = {
|
||||
"application/json": {
|
||||
"schema": {"$ref": schema_ref}
|
||||
}
|
||||
}
|
||||
|
||||
else:
|
||||
if schema_ref:
|
||||
rsp["content"] = {
|
||||
mime_type: {
|
||||
"schema": {"$ref": schema_ref}
|
||||
}
|
||||
}
|
||||
|
||||
# Ensure operation tags are existing
|
||||
for tag in operation_spec.tags:
|
||||
@@ -1400,22 +1419,29 @@ class OpenStackServerSourceBase:
|
||||
action_name: str | None = None,
|
||||
operation_name: str | None = None,
|
||||
) -> tuple[
|
||||
set[QueryParamsSchema],
|
||||
set[str | None] | Unset,
|
||||
dict | Unset | None,
|
||||
list[str],
|
||||
set[QueryParamsSchema], # query params
|
||||
set[str | None] | Unset, # body schemas
|
||||
set[str] | Unset | None, # responses
|
||||
set[str], # errors
|
||||
]:
|
||||
"""Extract schemas from the decorated method."""
|
||||
# Unwrap operation decorators to access all properties
|
||||
expected_errors: list[str] = []
|
||||
expected_errors: set[str] = set()
|
||||
body_schemas: set[str | None] | Unset = UNSET
|
||||
query_params_versions: set[QueryParamsSchema] = set()
|
||||
response_body_schema: dict | Unset | None = UNSET
|
||||
response_body_schemas: set[str] | Unset | None = UNSET
|
||||
|
||||
# sometimes handler version bounds is set on the schema level, sometimes
|
||||
# on the whole handler level
|
||||
global_min_ver: str | None = None
|
||||
global_max_ver: str | None = None
|
||||
|
||||
f = func
|
||||
while hasattr(f, "__wrapped__"):
|
||||
closure = inspect.getclosurevars(f)
|
||||
closure_locals = closure.nonlocals
|
||||
min_ver = None
|
||||
max_ver = None
|
||||
min_ver = (
|
||||
closure_locals.get("min_version", start_version)
|
||||
or start_version
|
||||
@@ -1427,8 +1453,10 @@ class OpenStackServerSourceBase:
|
||||
if hasattr(min_ver, "get_string")
|
||||
else str(min_ver)
|
||||
)
|
||||
if min_ver and not start_version:
|
||||
start_version = min_ver
|
||||
# NOTE: setting start_version/min_version cause reset of the info
|
||||
# between request and response
|
||||
# if min_ver and not start_version:
|
||||
# start_version = min_ver
|
||||
|
||||
max_ver = (
|
||||
closure_locals.get("max_version", end_version) or end_version
|
||||
@@ -1439,20 +1467,26 @@ class OpenStackServerSourceBase:
|
||||
if hasattr(max_ver, "get_string")
|
||||
else str(max_ver)
|
||||
)
|
||||
if max_ver and not end_version:
|
||||
end_version = max_ver
|
||||
|
||||
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
|
||||
)
|
||||
]
|
||||
current_expected_errors = closure_locals["errors"]
|
||||
# In nova `errors` is an int or set of ints. Additionally this
|
||||
# may be duplicated for different MV. We are only interested in
|
||||
# the overall errors and not when they come.
|
||||
if isinstance(current_expected_errors, list) or isinstance(
|
||||
current_expected_errors, tuple
|
||||
):
|
||||
expected_errors.update(
|
||||
{
|
||||
str(x)
|
||||
for x in filter(
|
||||
lambda x: isinstance(x, int),
|
||||
current_expected_errors,
|
||||
)
|
||||
}
|
||||
)
|
||||
elif isinstance(expected_errors, int):
|
||||
expected_errors = [str(expected_errors)]
|
||||
expected_errors.append(str(current_expected_errors))
|
||||
if "request_body_schema" in closure_locals or hasattr(
|
||||
f, "_request_body_schema"
|
||||
):
|
||||
@@ -1489,14 +1523,18 @@ class OpenStackServerSourceBase:
|
||||
)
|
||||
)
|
||||
|
||||
if min_ver:
|
||||
if min_ver or global_min_ver:
|
||||
if not comp_schema.openstack:
|
||||
comp_schema.openstack = {}
|
||||
comp_schema.openstack["min-ver"] = min_ver
|
||||
if max_ver:
|
||||
comp_schema.openstack["min-ver"] = (
|
||||
min_ver or global_min_ver
|
||||
)
|
||||
if max_ver or global_max_ver:
|
||||
if not comp_schema.openstack:
|
||||
comp_schema.openstack = {}
|
||||
comp_schema.openstack["max-ver"] = max_ver
|
||||
comp_schema.openstack["max-ver"] = (
|
||||
max_ver or global_max_ver
|
||||
)
|
||||
if mode == "action":
|
||||
if not comp_schema.openstack:
|
||||
comp_schema.openstack = {}
|
||||
@@ -1518,7 +1556,54 @@ class OpenStackServerSourceBase:
|
||||
"response_body_schema",
|
||||
getattr(f, "_response_body_schema", {}),
|
||||
)
|
||||
response_body_schema = obj
|
||||
if response_body_schemas is UNSET:
|
||||
response_body_schemas = set()
|
||||
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()
|
||||
+ "Response"
|
||||
+ (
|
||||
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 or global_min_ver:
|
||||
if not comp_schema.openstack:
|
||||
comp_schema.openstack = {}
|
||||
comp_schema.openstack["min-ver"] = (
|
||||
min_ver or global_min_ver
|
||||
)
|
||||
if max_ver or global_max_ver:
|
||||
if not comp_schema.openstack:
|
||||
comp_schema.openstack = {}
|
||||
comp_schema.openstack["max-ver"] = (
|
||||
max_ver or global_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}"
|
||||
if isinstance(response_body_schemas, set):
|
||||
response_body_schemas.add(ref_name)
|
||||
if "query_params_schema" in closure_locals or hasattr(
|
||||
f, "_request_query_schema"
|
||||
):
|
||||
@@ -1586,7 +1671,7 @@ class OpenStackServerSourceBase:
|
||||
return (
|
||||
query_params_versions,
|
||||
body_schemas,
|
||||
response_body_schema,
|
||||
response_body_schemas,
|
||||
expected_errors,
|
||||
)
|
||||
|
||||
|
||||
@@ -13,8 +13,7 @@
|
||||
|
||||
from typing import Any
|
||||
|
||||
from codegenerator.common.schema import TypeSchema
|
||||
from codegenerator.common.schema import ParameterSchema
|
||||
from codegenerator.common.schema import ParameterSchema, TypeSchema
|
||||
|
||||
ZONE_TYPES: list[str] = ["PRIMARY", "SECONDARY", "CATALOG"]
|
||||
ZONE_ACTIONS: list[str] = ["CREATE", "DELETE", "UPDATE", "NONE"]
|
||||
@@ -847,7 +846,7 @@ def _get_schema_ref(
|
||||
]:
|
||||
openapi_spec.components.schemas[name] = TypeSchema(**ZONE_SCHEMA)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
if name in ["ZoneUpdateRequest"]:
|
||||
elif name in ["ZoneUpdateRequest"]:
|
||||
openapi_spec.components.schemas[name] = TypeSchema(
|
||||
**ZONE_UPDATE_REQUEST_SCHEMA
|
||||
)
|
||||
@@ -876,7 +875,7 @@ def _get_schema_ref(
|
||||
**NAMESERVERS_SCHEMA
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
if name in [
|
||||
elif name in [
|
||||
"ZonesShareShowResponse",
|
||||
"ZonesSharesCreateRequest",
|
||||
"ZonesSharesCreateResponse",
|
||||
|
||||
@@ -11,32 +11,37 @@
|
||||
# under the License.
|
||||
#
|
||||
import inspect
|
||||
from multiprocessing import Process
|
||||
import logging
|
||||
from multiprocessing import Process
|
||||
from pathlib import Path
|
||||
|
||||
from ruamel.yaml.scalarstring import LiteralScalarString
|
||||
|
||||
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 QueryParamsSchema
|
||||
from codegenerator.openapi.base import Unset
|
||||
from codegenerator.openapi.keystone_schemas import application_credential
|
||||
from codegenerator.openapi.keystone_schemas import auth
|
||||
from codegenerator.openapi.keystone_schemas import common
|
||||
from codegenerator.openapi.keystone_schemas import domain
|
||||
from codegenerator.openapi.keystone_schemas import endpoint
|
||||
from codegenerator.openapi.keystone_schemas import federation
|
||||
from codegenerator.openapi.keystone_schemas import group
|
||||
from codegenerator.openapi.keystone_schemas import region
|
||||
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
|
||||
from codegenerator.common.schema import (
|
||||
ParameterSchema,
|
||||
PathSchema,
|
||||
SpecSchema,
|
||||
TypeSchema,
|
||||
)
|
||||
from codegenerator.openapi.base import (
|
||||
OpenStackServerSourceBase,
|
||||
QueryParamsSchema,
|
||||
Unset,
|
||||
)
|
||||
from codegenerator.openapi.keystone_schemas import (
|
||||
application_credential,
|
||||
auth,
|
||||
common,
|
||||
domain,
|
||||
endpoint,
|
||||
federation,
|
||||
group,
|
||||
region,
|
||||
role,
|
||||
service,
|
||||
user,
|
||||
)
|
||||
from codegenerator.openapi.utils import merge_api_ref_doc, rst_to_md
|
||||
|
||||
|
||||
class KeystoneGenerator(OpenStackServerSourceBase):
|
||||
@@ -82,8 +87,8 @@ class KeystoneGenerator(OpenStackServerSourceBase):
|
||||
raise RuntimeError("Error generating Keystone OpenAPI schema")
|
||||
|
||||
def _generate(self, target_dir, args, *pargs, **kwargs):
|
||||
from keystone.server.flask import application
|
||||
from keystone import version as keystone_version
|
||||
from keystone.server.flask import application
|
||||
|
||||
self.app = application.application_factory()
|
||||
self.router = self.app.url_map
|
||||
@@ -388,11 +393,11 @@ class KeystoneGenerator(OpenStackServerSourceBase):
|
||||
|
||||
query_params_versions: set[QueryParamsSchema] = set()
|
||||
body_schemas: set[str | None] | Unset | None = set()
|
||||
expected_errors = ["404"]
|
||||
expected_errors: set[str] = {"404"}
|
||||
response_code = None
|
||||
start_version = None
|
||||
end_version = None
|
||||
ser_schema: dict | None | Unset = {}
|
||||
ser_schema: set[str] | None | Unset = set()
|
||||
|
||||
(query_params_versions, body_schemas, ser_schema, expected_errors) = (
|
||||
self._process_decorators(
|
||||
@@ -479,11 +484,15 @@ class KeystoneGenerator(OpenStackServerSourceBase):
|
||||
openapi_spec,
|
||||
schema_name,
|
||||
description=f"Response of the {operation_spec.operationId} operation",
|
||||
schema_def=ser_schema,
|
||||
# schema_def=ser_schema,
|
||||
)
|
||||
|
||||
if schema_ref:
|
||||
rsp["content"] = {mime_type: {"schema": {"$ref": schema_ref}}}
|
||||
elif ser_schema:
|
||||
rsp["content"] = {
|
||||
"application/json": {"schema": {"$ref": ser_schema.pop()}}
|
||||
}
|
||||
|
||||
if path == "/v3/auth/tokens":
|
||||
rsp_headers = rsp.setdefault("headers", {})
|
||||
|
||||
@@ -177,13 +177,13 @@ class NovaGenerator(OpenStackServerSourceBase):
|
||||
# NOTE(gtema): This must go away once schemas are merged directly to
|
||||
# Nova
|
||||
# /servers
|
||||
if name == "ServersCreateResponse":
|
||||
schema = openapi_spec.components.schemas.setdefault(
|
||||
name, TypeSchema(**nova_schemas.SERVER_CREATED_SCHEMA)
|
||||
)
|
||||
ref = f"#/components/schemas/{name}"
|
||||
# if name == "ServersCreateResponse":
|
||||
# schema = openapi_spec.components.schemas.setdefault(
|
||||
# name, TypeSchema(**nova_schemas.SERVER_CREATED_SCHEMA)
|
||||
# )
|
||||
# ref = f"#/components/schemas/{name}"
|
||||
|
||||
elif name == "ServersListResponse":
|
||||
if name == "ServersListResponse":
|
||||
schema = openapi_spec.components.schemas.setdefault(
|
||||
name, TypeSchema(**nova_schemas.SERVER_LIST_SCHEMA)
|
||||
)
|
||||
|
||||
+144
-86
@@ -933,104 +933,157 @@ class RustCliGenerator(BaseGenerator):
|
||||
|
||||
result_def: dict = {}
|
||||
response_def: dict | None = {}
|
||||
# response candidates module names (typically including the MV suffix) with
|
||||
# corresponding min-ver and max-ver.
|
||||
response_mod_candidates: list[
|
||||
tuple[
|
||||
str, # mod_name
|
||||
str | None, # min_ver
|
||||
str | None, # max_ver
|
||||
]
|
||||
] = []
|
||||
resource_header_metadata: dict = {}
|
||||
|
||||
# Process response information
|
||||
# # Prepare information about response
|
||||
if method.upper() != "HEAD":
|
||||
response = common.find_response_schema(
|
||||
responses = common.get_response_candidates(
|
||||
spec["responses"],
|
||||
args.response_key or resource_name,
|
||||
args.action_name,
|
||||
microversion,
|
||||
)
|
||||
|
||||
if response:
|
||||
response_key: str | None
|
||||
if args.response_key:
|
||||
response_key = (
|
||||
args.response_key
|
||||
if args.response_key != "null"
|
||||
else None
|
||||
)
|
||||
else:
|
||||
response_key = resource_name
|
||||
response_def, response_key = common.find_resource_schema(
|
||||
response, None, response_key
|
||||
)
|
||||
|
||||
if not response_def and response.get("type") == "string":
|
||||
response_def = response
|
||||
|
||||
if response_def:
|
||||
if response_def.get("type", "object") == "object" or (
|
||||
# BS metadata is defined with type: ["object",
|
||||
# "null"]
|
||||
isinstance(response_def.get("type"), list)
|
||||
and "object" in response_def["type"]
|
||||
):
|
||||
(root, response_types) = openapi_parser.parse(
|
||||
response_def
|
||||
if responses:
|
||||
for response, ext in responses:
|
||||
response_key: str | None
|
||||
if args.response_key:
|
||||
response_key = (
|
||||
args.response_key
|
||||
if args.response_key != "null"
|
||||
else None
|
||||
)
|
||||
if not isinstance(root, model.Dictionary):
|
||||
if method == "patch" and not request_types:
|
||||
# image patch is a jsonpatch based operation
|
||||
# where there is no request. For it we need to
|
||||
# look at the response and get writable
|
||||
# parameters as a base
|
||||
is_json_patch = True
|
||||
if not args.find_implemented_by_sdk:
|
||||
raise NotImplementedError
|
||||
additional_imports.update(
|
||||
[
|
||||
"json_patch::{Patch, diff}",
|
||||
"serde_json::json",
|
||||
]
|
||||
)
|
||||
additional_imports.add(
|
||||
f"openstack_types::"
|
||||
+ "::".join(types_mod_path)
|
||||
+ "::*"
|
||||
)
|
||||
(_, response_types) = openapi_parser.parse(
|
||||
response_def, ignore_read_only=True
|
||||
)
|
||||
type_manager.set_models(response_types)
|
||||
else:
|
||||
response_key = resource_name
|
||||
response_def, response_key = (
|
||||
common.find_resource_schema(
|
||||
response, None, response_key
|
||||
)
|
||||
)
|
||||
|
||||
elif response_def["type"] == "string":
|
||||
(root_dt, _) = openapi_parser.parse(response_def)
|
||||
if not root_dt:
|
||||
raise RuntimeError(
|
||||
"Response data can not be processed"
|
||||
)
|
||||
if (
|
||||
not response_def
|
||||
and response.get("type") == "string"
|
||||
):
|
||||
response_def = response
|
||||
|
||||
response_props = response.get("properties", {})
|
||||
if response_props and (
|
||||
(
|
||||
response_key
|
||||
and response_props.get(response_key, {}).get(
|
||||
"type"
|
||||
if response_def:
|
||||
if response_def.get(
|
||||
"type", "object"
|
||||
) == "object" or (
|
||||
# BS metadata is defined with type: ["object",
|
||||
# "null"]
|
||||
isinstance(response_def.get("type"), list)
|
||||
and "object" in response_def["type"]
|
||||
):
|
||||
(root, response_types) = openapi_parser.parse(
|
||||
response_def
|
||||
)
|
||||
if not isinstance(root, model.Dictionary):
|
||||
if method == "patch" and not request_types:
|
||||
# image patch is a jsonpatch based operation
|
||||
# where there is no request. For it we need to
|
||||
# look at the response and get writable
|
||||
# parameters as a base
|
||||
is_json_patch = True
|
||||
if not args.find_implemented_by_sdk:
|
||||
raise NotImplementedError
|
||||
additional_imports.update(
|
||||
[
|
||||
"json_patch::{Patch, diff}",
|
||||
"serde_json::json",
|
||||
]
|
||||
)
|
||||
additional_imports.add(
|
||||
f"openstack_types::"
|
||||
+ "::".join(types_mod_path)
|
||||
+ "::*"
|
||||
)
|
||||
(_, response_types) = (
|
||||
openapi_parser.parse(
|
||||
response_def,
|
||||
ignore_read_only=True,
|
||||
)
|
||||
)
|
||||
type_manager.set_models(response_types)
|
||||
|
||||
elif response_def["type"] == "string":
|
||||
(root_dt, _) = openapi_parser.parse(
|
||||
response_def
|
||||
)
|
||||
if not root_dt:
|
||||
raise RuntimeError(
|
||||
"Response data can not be processed"
|
||||
)
|
||||
|
||||
response_props = response.get("properties", {})
|
||||
if response_props and (
|
||||
(
|
||||
response_key
|
||||
and response_props.get(
|
||||
response_key, {}
|
||||
).get("type")
|
||||
== "array"
|
||||
)
|
||||
or response_props[
|
||||
list(response_props.keys())[0]
|
||||
].get("type")
|
||||
== "array"
|
||||
)
|
||||
or response_props[
|
||||
list(response_props.keys())[0]
|
||||
].get("type")
|
||||
== "array"
|
||||
):
|
||||
result_is_list = True
|
||||
):
|
||||
result_is_list = True
|
||||
|
||||
mod_response_path = "openstack_types::" + "::".join(
|
||||
[
|
||||
f"r#{x}" if x in ["trait", "type"] else x
|
||||
for x in types_mod_path
|
||||
]
|
||||
+ [response_class_name]
|
||||
)
|
||||
additional_imports.add(mod_response_path)
|
||||
else:
|
||||
response_class_name = None
|
||||
response_mod_path = types_mod_path.copy()
|
||||
response_class_name_candidate = response_class_name
|
||||
if ext and (
|
||||
"min-ver" in ext or "kind-suffix" in ext
|
||||
):
|
||||
# Combine min_ver and kind-suffix into the final
|
||||
# schema suffix
|
||||
ver_suffix = ext.get("min-ver")
|
||||
kind_suffix = ext.get("kind-suffix")
|
||||
final_suffix = "_".join(
|
||||
x for x in [ver_suffix, kind_suffix] if x
|
||||
).replace(".", "")
|
||||
response_mod_path[-1] = (
|
||||
response_mod_path[-1] + f"_{final_suffix}"
|
||||
)
|
||||
response_mod_candidates.append(
|
||||
(
|
||||
response_mod_path[-1],
|
||||
ext.get("min-ver"),
|
||||
ext.get("max-ver"),
|
||||
)
|
||||
)
|
||||
else:
|
||||
response_mod_candidates.append(
|
||||
(response_mod_path[-1], None, None)
|
||||
)
|
||||
mod_response_path = (
|
||||
"openstack_types::"
|
||||
+ "::".join(
|
||||
[
|
||||
f"r#{x}"
|
||||
if x in ["trait", "type"]
|
||||
else x
|
||||
for x in response_mod_path[:-1]
|
||||
]
|
||||
)
|
||||
)
|
||||
additional_imports.add(mod_response_path)
|
||||
else:
|
||||
response_class_name_candidate = None
|
||||
else:
|
||||
response_class_name = None
|
||||
response_class_name_candidate = None
|
||||
|
||||
mod_import_name = "openstack_sdk::api::" + "::".join(
|
||||
f"r#{x}" if x in ["trait", "type"] else x
|
||||
@@ -1132,11 +1185,15 @@ class RustCliGenerator(BaseGenerator):
|
||||
command_description: str = spec.get("description")
|
||||
command_summary: str = spec.get("summary")
|
||||
if args.operation_type == "action":
|
||||
command_description = operation_body.get(
|
||||
"description", command_description
|
||||
command_description = (
|
||||
operation_body.get("description", command_description)
|
||||
if operation_body
|
||||
else command_description
|
||||
)
|
||||
command_summary = operation_body.get(
|
||||
"summary", command_summary
|
||||
command_summary = (
|
||||
operation_body.get("summary", command_summary)
|
||||
if operation_body
|
||||
else command_summary
|
||||
)
|
||||
|
||||
if command_summary and microversion:
|
||||
@@ -1184,9 +1241,10 @@ class RustCliGenerator(BaseGenerator):
|
||||
"is_image_download": is_image_download,
|
||||
"is_json_patch": is_json_patch,
|
||||
"is_list_paginated": is_list_paginated,
|
||||
"response_class_name": response_class_name,
|
||||
"response_class_name": response_class_name_candidate,
|
||||
"types_mod_path": types_mod_path,
|
||||
"resource_key": res,
|
||||
"response_mod_candidates": response_mod_candidates,
|
||||
}
|
||||
|
||||
cli_output_path = [
|
||||
|
||||
+10
-22
@@ -531,27 +531,16 @@ class RustSdkGenerator(BaseGenerator):
|
||||
else:
|
||||
# Get basic information about response
|
||||
if method.upper() != "HEAD":
|
||||
for code, rspec in spec["responses"].items():
|
||||
if not code.startswith("2"):
|
||||
continue
|
||||
content = rspec.get("content", {})
|
||||
if "application/json" in content:
|
||||
response_spec = content["application/json"]
|
||||
try:
|
||||
(_, response_key) = (
|
||||
common.find_resource_schema(
|
||||
response_spec["schema"],
|
||||
None,
|
||||
res_name.lower(),
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
# Most likely we have response which is oneOf.
|
||||
# For the SDK it does not really harm to ignore
|
||||
# this.
|
||||
pass
|
||||
# response_def = (None,)
|
||||
response_key = None
|
||||
responses = common.get_response_candidates(
|
||||
spec["responses"], res_name.lower(), args.action_name
|
||||
)
|
||||
if len(responses) > 0:
|
||||
logging.warning(
|
||||
f"Multiple response candidates are present. For the SDK only look into the last one for detecting the response key"
|
||||
)
|
||||
(_, response_key) = common.find_resource_schema(
|
||||
responses[-1][0], None, res_name.lower()
|
||||
)
|
||||
|
||||
context = {
|
||||
"operation_id": operation_id,
|
||||
@@ -603,7 +592,6 @@ class RustSdkGenerator(BaseGenerator):
|
||||
"resource_name": resource_name,
|
||||
"service_name": service_type,
|
||||
}
|
||||
print(f"context is {context}")
|
||||
|
||||
# Generate methods for the GET resource command
|
||||
self._render_command(context, "rust_sdk/mod.rs.j2", impl_path)
|
||||
|
||||
@@ -493,7 +493,7 @@ class RustTuiGenerator(BaseGenerator):
|
||||
|
||||
# Get basic information about response
|
||||
if args.operation_type == "list":
|
||||
response = common.find_response_schema(
|
||||
response = common.get_response_candidates(
|
||||
spec["responses"],
|
||||
args.response_key or resource_name,
|
||||
(
|
||||
|
||||
+196
-151
@@ -10,9 +10,11 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
import copy
|
||||
import logging
|
||||
import re
|
||||
import subprocess
|
||||
from collections.abc import Generator
|
||||
from pathlib import Path
|
||||
|
||||
from codegenerator import common, model
|
||||
@@ -280,156 +282,46 @@ class RustTypesGenerator(BaseGenerator):
|
||||
# Remember the version prefix to discard it in the template
|
||||
ver_prefix = path_elements[0]
|
||||
|
||||
for operation_variant in operation_variants:
|
||||
logging.debug(f"Processing variant {operation_variant}")
|
||||
response_type_manager: common_rust.TypeManager = (
|
||||
ResponseTypeManager()
|
||||
# for operation_variant in operation_variants:
|
||||
# logging.debug(f"Processing variant {operation_variant}")
|
||||
response_type_manager: common_rust.TypeManager = ResponseTypeManager()
|
||||
additional_imports: set[str] = set()
|
||||
|
||||
if api_ver_matches:
|
||||
api_ver = {
|
||||
"major": api_ver_matches.group(1),
|
||||
"minor": api_ver_matches.group(3) or 0,
|
||||
}
|
||||
else:
|
||||
api_ver = {}
|
||||
|
||||
class_name = "".join(
|
||||
x.capitalize()
|
||||
for x in re.split(common.SPLIT_NAME_RE, resource_name)
|
||||
)
|
||||
response_type_manager.root_name = class_name + "Response"
|
||||
mod_name = "_".join(
|
||||
x.lower()
|
||||
for x in re.split(
|
||||
common.SPLIT_NAME_RE,
|
||||
(
|
||||
args.module_name
|
||||
or args.operation_name
|
||||
or args.operation_type.value
|
||||
or method
|
||||
),
|
||||
)
|
||||
additional_imports = set()
|
||||
)
|
||||
|
||||
if api_ver_matches:
|
||||
api_ver = {
|
||||
"major": api_ver_matches.group(1),
|
||||
"minor": api_ver_matches.group(3) or 0,
|
||||
}
|
||||
else:
|
||||
api_ver = {}
|
||||
|
||||
class_name = "".join(
|
||||
x.capitalize()
|
||||
for x in re.split(common.SPLIT_NAME_RE, resource_name)
|
||||
)
|
||||
response_type_manager.root_name = class_name + "Response"
|
||||
mod_name = "_".join(
|
||||
x.lower()
|
||||
for x in re.split(
|
||||
common.SPLIT_NAME_RE,
|
||||
(
|
||||
args.module_name
|
||||
or args.operation_name
|
||||
or args.operation_type.value
|
||||
or method
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
mod_path = common.get_rust_types_mod_path(
|
||||
args.service_type,
|
||||
args.api_version,
|
||||
args.alternative_module_path or path,
|
||||
)
|
||||
mod_path.append("response")
|
||||
|
||||
response_key: str | None = None
|
||||
response_def: dict | None = {}
|
||||
|
||||
# Get basic information about response
|
||||
if method.upper() != "HEAD":
|
||||
response = common.find_response_schema(
|
||||
spec["responses"],
|
||||
args.response_key or resource_name,
|
||||
args.action_name,
|
||||
)
|
||||
if response:
|
||||
if args.response_key:
|
||||
response_key = (
|
||||
args.response_key
|
||||
if args.response_key != "null"
|
||||
else None
|
||||
)
|
||||
else:
|
||||
response_key = resource_name
|
||||
response_def, _ = common.find_resource_schema(
|
||||
response, None, response_key
|
||||
)
|
||||
|
||||
if not response_def and response.get("type") == "string":
|
||||
response_def = response
|
||||
|
||||
if response_def:
|
||||
if response_def.get("type", "object") == "object" or (
|
||||
# BS metadata is defined with type: ["object",
|
||||
# "null"]
|
||||
isinstance(response_def.get("type"), list)
|
||||
and "object" in response_def["type"]
|
||||
):
|
||||
(root, response_types) = openapi_parser.parse(
|
||||
response_def
|
||||
)
|
||||
if isinstance(root, model.Dictionary):
|
||||
value_type: (
|
||||
common_rust.BasePrimitiveType
|
||||
| common_rust.BaseCombinedType
|
||||
| common_rust.BaseCompoundType
|
||||
| None
|
||||
) = None
|
||||
try:
|
||||
value_type = (
|
||||
response_type_manager.convert_model(
|
||||
root.value_type
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
# In rare cases we can not convert
|
||||
# value_type since it depends on different
|
||||
# types. We are here in the output
|
||||
# simplification, so just downcast it to
|
||||
# JsonValue (what is anyway our goal)
|
||||
value_type = common_rust.JsonValue()
|
||||
root_dict = common_rust.HashMapResponse(
|
||||
name=response_type_manager.root_name,
|
||||
value_type=value_type,
|
||||
)
|
||||
response_type_manager.refs[
|
||||
model.Reference(
|
||||
name=response_type_manager.root_name,
|
||||
type=common_rust.HashMapResponse,
|
||||
)
|
||||
] = root_dict
|
||||
|
||||
else:
|
||||
response_type_manager.set_models(
|
||||
response_types
|
||||
)
|
||||
|
||||
elif response_def["type"] == "string":
|
||||
(root_dt, _) = openapi_parser.parse(response_def)
|
||||
if not root_dt:
|
||||
raise RuntimeError(
|
||||
"Response data can not be processed"
|
||||
)
|
||||
field = common_rust.StructField(
|
||||
local_name="dummy",
|
||||
remote_name="dummy",
|
||||
data_type=response_type_manager.convert_model(
|
||||
root_dt
|
||||
),
|
||||
is_optional=False,
|
||||
)
|
||||
tuple_struct = common_rust.TupleStruct(
|
||||
name=response_type_manager.root_name
|
||||
)
|
||||
tuple_struct.tuple_fields.append(field)
|
||||
response_type_manager.refs[
|
||||
model.Reference(
|
||||
name=response_type_manager.root_name,
|
||||
type=common_rust.TupleStruct,
|
||||
)
|
||||
] = tuple_struct
|
||||
elif (
|
||||
response_def["type"] == "array"
|
||||
and "items" in response_def
|
||||
):
|
||||
(_, response_types) = openapi_parser.parse(
|
||||
response_def["items"]
|
||||
)
|
||||
response_type_manager.set_models(response_types)
|
||||
|
||||
else:
|
||||
return
|
||||
|
||||
additional_imports.update(response_type_manager.get_imports())
|
||||
mod_path = common.get_rust_types_mod_path(
|
||||
args.service_type,
|
||||
args.api_version,
|
||||
args.alternative_module_path or path,
|
||||
)
|
||||
mod_path.append("response")
|
||||
|
||||
# Get basic information about response
|
||||
if method.upper() != "HEAD":
|
||||
context = {
|
||||
"operation_id": operation_id,
|
||||
"operation_type": spec.get(
|
||||
@@ -451,17 +343,170 @@ class RustTypesGenerator(BaseGenerator):
|
||||
"additional_imports": additional_imports,
|
||||
}
|
||||
|
||||
mod_path = mod_path[1:]
|
||||
yield from self.generate_type_mod(
|
||||
args,
|
||||
openapi_parser,
|
||||
response_type_manager,
|
||||
context,
|
||||
class_name,
|
||||
mod_path,
|
||||
target_dir,
|
||||
mod_name,
|
||||
resource_name,
|
||||
spec,
|
||||
)
|
||||
|
||||
def generate_type_mod(
|
||||
self,
|
||||
args,
|
||||
openapi_parser,
|
||||
response_type_manager,
|
||||
context,
|
||||
class_name,
|
||||
mod_path,
|
||||
target_dir,
|
||||
mod_name,
|
||||
resource_name,
|
||||
spec,
|
||||
) -> Generator[tuple[list[str], str, str, str]]:
|
||||
"""Generate the type module"""
|
||||
|
||||
responses = common.get_response_candidates(
|
||||
spec["responses"],
|
||||
args.response_key or resource_name,
|
||||
args.action_name,
|
||||
)
|
||||
for response, schema_ext in responses:
|
||||
response_suffix: str | None = None
|
||||
local_response_type_manager = copy.deepcopy(response_type_manager)
|
||||
local_mod_path = mod_path.copy()
|
||||
|
||||
if not response:
|
||||
continue
|
||||
if schema_ext:
|
||||
min_ver = schema_ext.get("min-ver")
|
||||
if min_ver:
|
||||
response_suffix = min_ver.replace(".", "")
|
||||
if "kind-suffix" in schema_ext:
|
||||
kind_suffix = schema_ext["kind-suffix"]
|
||||
if response_suffix:
|
||||
response_suffix = f"{response_suffix}_{kind_suffix}"
|
||||
else:
|
||||
response_suffix = kind_suffix
|
||||
|
||||
if args.response_list_item_key:
|
||||
response_key = args.response_list_item_key
|
||||
elif args.response_key:
|
||||
response_key = (
|
||||
args.response_key if args.response_key != "null" else None
|
||||
)
|
||||
else:
|
||||
response_key = resource_name
|
||||
response_def, _ = common.find_resource_schema(
|
||||
response, None, response_key
|
||||
)
|
||||
|
||||
if not response_def and response.get("type") == "string":
|
||||
response_def = response
|
||||
|
||||
if response_def:
|
||||
if response_def.get("type", "object") == "object" or (
|
||||
# BS metadata is defined with type: ["object",
|
||||
# "null"]
|
||||
isinstance(response_def.get("type"), list)
|
||||
and "object" in response_def["type"]
|
||||
):
|
||||
(root, response_types) = openapi_parser.parse(response_def)
|
||||
if isinstance(root, model.Dictionary):
|
||||
value_type: (
|
||||
common_rust.BasePrimitiveType
|
||||
| common_rust.BaseCombinedType
|
||||
| common_rust.BaseCompoundType
|
||||
| None
|
||||
) = None
|
||||
try:
|
||||
value_type = (
|
||||
local_response_type_manager.convert_model(
|
||||
root.value_type
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
# In rare cases we can not convert
|
||||
# value_type since it depends on different
|
||||
# types. We are here in the output
|
||||
# simplification, so just downcast it to
|
||||
# JsonValue (what is anyway our goal)
|
||||
value_type = common_rust.JsonValue()
|
||||
root_dict = common_rust.HashMapResponse(
|
||||
name=local_response_type_manager.root_name,
|
||||
value_type=value_type,
|
||||
)
|
||||
local_response_type_manager.refs[
|
||||
model.Reference(
|
||||
name=local_response_type_manager.root_name,
|
||||
type=common_rust.HashMapResponse,
|
||||
)
|
||||
] = root_dict
|
||||
|
||||
else:
|
||||
local_response_type_manager.set_models(response_types)
|
||||
|
||||
elif response_def["type"] == "string":
|
||||
(root_dt, _) = openapi_parser.parse(response_def)
|
||||
if not root_dt:
|
||||
raise RuntimeError(
|
||||
"Response data can not be processed"
|
||||
)
|
||||
field = common_rust.StructField(
|
||||
local_name="dummy",
|
||||
remote_name="dummy",
|
||||
data_type=local_response_type_manager.convert_model(
|
||||
root_dt
|
||||
),
|
||||
is_optional=False,
|
||||
)
|
||||
tuple_struct = common_rust.TupleStruct(
|
||||
name=local_response_type_manager.root_name
|
||||
)
|
||||
tuple_struct.tuple_fields.append(field)
|
||||
local_response_type_manager.refs[
|
||||
model.Reference(
|
||||
name=local_response_type_manager.root_name,
|
||||
type=common_rust.TupleStruct,
|
||||
)
|
||||
] = tuple_struct
|
||||
elif (
|
||||
response_def["type"] == "array" and "items" in response_def
|
||||
):
|
||||
(_, response_types) = openapi_parser.parse(
|
||||
response_def["items"]
|
||||
)
|
||||
local_response_type_manager.set_models(response_types)
|
||||
context.update(
|
||||
{
|
||||
"additional_imports": local_response_type_manager.get_imports(),
|
||||
"response_type_manager": local_response_type_manager,
|
||||
}
|
||||
)
|
||||
|
||||
work_dir = Path(
|
||||
target_dir, "rust", "types", args.service_type, "src"
|
||||
)
|
||||
impl_path = Path(work_dir, "/".join(mod_path), f"{mod_name}.rs")
|
||||
# Generate methods for the GET resource command
|
||||
final_mod_name = (
|
||||
mod_name
|
||||
if not response_suffix
|
||||
else f"{mod_name}_{response_suffix}"
|
||||
)
|
||||
|
||||
impl_path = Path(
|
||||
work_dir, "/".join(mod_path[1:]), f"{final_mod_name}.rs"
|
||||
)
|
||||
# Generate type definitions for the resource command
|
||||
self._render_command(context, "rust_types/impl.rs.j2", impl_path)
|
||||
|
||||
self._format_code(impl_path)
|
||||
|
||||
yield (mod_path, mod_name, "response", class_name)
|
||||
yield (mod_path[1:], final_mod_name, "response", class_name)
|
||||
|
||||
def generate_mod(
|
||||
self, target_dir, mod_path, mod_list, url, resource_name, service_type
|
||||
|
||||
@@ -189,10 +189,20 @@ impl {{ target_class_name }}Command {
|
||||
{%- elif operation_type in ["show"] %}
|
||||
{#- Show/get implementation #}
|
||||
{%- if find_present %}
|
||||
op.output_single::<{{ response_class_name }}>(find_data)?;
|
||||
{% if response_mod_candidates|length > 0 %}
|
||||
op.output_single::<response::{{ response_mod_candidates[0].0 }}::{{ response_class_name }}>(find_data.clone())
|
||||
{%- for mod in response_mod_candidates[1:] %}
|
||||
.or_else(|_| op.output_single::<response::{{ mod.0 }}::{{ response_class_name }}>(find_data.clone()))
|
||||
{%- endfor %}?;
|
||||
{%- endif %}
|
||||
{%- else %}
|
||||
let data = ep.query_async(client).await?;
|
||||
op.output_single::<{{ response_class_name }}>(data)?;
|
||||
let data: serde_json::Value = ep.query_async(client).await?;
|
||||
{% if response_mod_candidates|length > 0 %}
|
||||
op.output_single::<response::{{ response_mod_candidates[0].0 }}::{{ response_class_name }}>(data.clone())
|
||||
{%- for mod in response_mod_candidates[1:] %}
|
||||
.or_else(|_| op.output_single::<response::{{ mod.0 }}::{{ response_class_name }}>(data.clone()))
|
||||
{%- endfor %}?;
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
|
||||
{%- elif operation_type == "create" %}
|
||||
@@ -212,10 +222,18 @@ impl {{ target_class_name }}Command {
|
||||
|
||||
{%- elif result_is_list %}
|
||||
let data: Vec<serde_json::Value> = ep.query_async(client).await?;
|
||||
op.output_list::<{{ response_class_name }}>(data)?;
|
||||
op.output_list::<response::{{ response_mod_candidates[0].0 }}::{{ response_class_name }}>(data.clone())
|
||||
{%- for mod in response_mod_candidates[1:] %}
|
||||
.or_else(|_| op.output_list::<response::{{ mod.0 }}::{{ response_class_name }}>(data.clone()))
|
||||
{%- endfor %}?;
|
||||
{%- else %}
|
||||
let data = ep.query_async(client).await?;
|
||||
op.output_single::<{{ response_class_name }}>(data)?;
|
||||
let data: serde_json::Value = ep.query_async(client).await?;
|
||||
{% if response_mod_candidates|length > 0 %}
|
||||
op.output_single::<response::{{ response_mod_candidates[0].0 }}::{{ response_class_name }}>(data.clone())
|
||||
{%- for mod in response_mod_candidates[1:] %}
|
||||
.or_else(|_| op.output_single::<response::{{ mod.0 }}::{{ response_class_name }}>(data.clone()))
|
||||
{%- endfor %}?;
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
|
||||
{%- elif operation_type not in ["delete", "download", "upload", "json"] %}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
{#- Create operation handling #}
|
||||
let data = ep.query_async(client).await?;
|
||||
op.output_single::<{{ response_class_name }}>(data)?;
|
||||
let data: serde_json::Value = ep.query_async(client).await?;
|
||||
{% if response_mod_candidates|length > 0 %}
|
||||
op.output_single::<response::{{ response_mod_candidates[0].0 }}::{{ response_class_name }}>(data.clone())
|
||||
{%- for mod in response_mod_candidates[1:] %}
|
||||
.or_else(|_| op.output_single::<response::{{ mod.0 }}::{{ response_class_name }}>(data.clone()))
|
||||
{%- endfor %}?;
|
||||
{%- endif %}
|
||||
|
||||
@@ -6,9 +6,19 @@
|
||||
{%- else %}
|
||||
let data: Vec<serde_json::Value> = ep.query_async(client).await?;
|
||||
{%- endif %}
|
||||
op.output_list::<{{ response_class_name }}>(data)?;
|
||||
{% if response_mod_candidates|length > 0 %}
|
||||
op.output_list::<response::{{ response_mod_candidates[0].0 }}::{{ response_class_name }}>(data.clone())
|
||||
{%- for mod in response_mod_candidates[1:] %}
|
||||
.or_else(|_| op.output_list::<response::{{ mod.0 }}::{{ response_class_name }}>(data.clone()))
|
||||
{%- endfor %}?;
|
||||
{%- endif %}
|
||||
|
||||
{%- else %}
|
||||
let data = ep.query_async(client).await?;
|
||||
op.output_single::<{{ response_class_name }}>(data)?;
|
||||
let data: serde_json::Value = ep.query_async(client).await?;
|
||||
{% if response_mod_candidates|length > 0 %}
|
||||
op.output_single::<response::{{ response_mod_candidates[0].0 }}::{{ response_class_name }}>(data.clone())
|
||||
{%- for mod in response_mod_candidates[1:] %}
|
||||
.or_else(|_| op.output_single::<response::{{ mod.0 }}::{{ response_class_name }}>(data.clone()))
|
||||
{%- endfor %}?;
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
{#- List operation #}
|
||||
let data: serde_json::Value = ep.query_async(client).await?;
|
||||
op.output_single::<{{ response_class_name }}>(data)?;
|
||||
{% if response_mod_candidates|length > 0 %}
|
||||
op.output_single::<response::{{ response_mod_candidates[0].0 }}::{{ response_class_name }}>(data)
|
||||
{%- for mod in response_mod_candidates[1:] %}
|
||||
.or_else(|_| op.output_single::<response::{{ mod.0 }}::{{ response_class_name }}>(data.clone()))
|
||||
{%- endfor %}?;
|
||||
{%- endif %}
|
||||
|
||||
@@ -10,11 +10,10 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
from collections import OrderedDict
|
||||
import json
|
||||
from unittest import TestCase
|
||||
|
||||
from collections import OrderedDict
|
||||
from typing import Any
|
||||
from unittest import TestCase
|
||||
|
||||
from codegenerator import common
|
||||
|
||||
@@ -171,103 +170,7 @@ class TestSortSchema(TestCase):
|
||||
)
|
||||
|
||||
|
||||
class TestFindResponseSchema(TestCase):
|
||||
FOO = {"foo": {"type": "string"}}
|
||||
|
||||
# def setUp(self):
|
||||
# super().setUp()
|
||||
# logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
def test_find_with_single_candidate(self):
|
||||
responses = {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {**self.FOO},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.assertEqual(
|
||||
responses["200"]["content"]["application/json"]["schema"],
|
||||
common.find_response_schema(responses, "foo"),
|
||||
)
|
||||
|
||||
def test_find_with_list(self):
|
||||
responses = {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"foos": {"type": "array", "items": self.FOO}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.assertEqual(
|
||||
responses["200"]["content"]["application/json"]["schema"],
|
||||
common.find_response_schema(responses, "foo"),
|
||||
)
|
||||
|
||||
def test_find_correct_action(self):
|
||||
foo_action = {
|
||||
"type": "string",
|
||||
"x-openstack": {"action-name": "foo-action"},
|
||||
}
|
||||
bar_action = {
|
||||
"type": "string",
|
||||
"x-openstack": {"action-name": "bar-action"},
|
||||
}
|
||||
responses: dict[str, Any] = {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"type": "object", "properties": self.FOO}
|
||||
}
|
||||
}
|
||||
},
|
||||
"204": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"oneOf": [foo_action, bar_action]}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
self.assertEqual(
|
||||
foo_action,
|
||||
common.find_response_schema(responses, "foo", "foo-action"),
|
||||
)
|
||||
self.assertEqual(
|
||||
bar_action,
|
||||
common.find_response_schema(responses, "foo", "bar-action"),
|
||||
)
|
||||
self.assertIsNone(
|
||||
common.find_response_schema(responses, "foo", "baz-action")
|
||||
)
|
||||
self.assertEqual(
|
||||
responses["200"]["content"]["application/json"]["schema"],
|
||||
common.find_response_schema(responses, "foo"),
|
||||
)
|
||||
|
||||
def test_no_candidates_returns_root(self):
|
||||
responses = {
|
||||
"200": {
|
||||
"content": {"application/json": {"schema": self.FOO["foo"]}}
|
||||
}
|
||||
}
|
||||
self.assertEqual(
|
||||
responses["200"]["content"]["application/json"]["schema"],
|
||||
common.find_response_schema(responses, "foo"),
|
||||
)
|
||||
|
||||
class TestPlural(TestCase):
|
||||
def test_plural(self):
|
||||
map = {
|
||||
"policy": "policies",
|
||||
@@ -300,3 +203,483 @@ class TestFindResponseSchema(TestCase):
|
||||
}
|
||||
for singular, plural in map.items():
|
||||
self.assertEqual(singular, common.get_singular_form(plural))
|
||||
|
||||
|
||||
class TestCheckMvRange(TestCase):
|
||||
def test_mv_range_filter(self):
|
||||
self.assertEqual(
|
||||
True, common.ensure_microversion_bounds(None, {"min-ver": "2.0"})
|
||||
)
|
||||
self.assertEqual(True, common.ensure_microversion_bounds(None, {}))
|
||||
self.assertEqual(
|
||||
False,
|
||||
common.ensure_microversion_bounds("2.1", {"min-ver": "2.100"}),
|
||||
)
|
||||
self.assertEqual(
|
||||
True,
|
||||
common.ensure_microversion_bounds(
|
||||
"2.1", {"min-ver": "2.0", "max-ver": "2.1"}
|
||||
),
|
||||
)
|
||||
self.assertEqual(
|
||||
True,
|
||||
common.ensure_microversion_bounds(
|
||||
"2.0", {"min-ver": "2.0", "max-ver": "2.1"}
|
||||
),
|
||||
)
|
||||
self.assertEqual(
|
||||
False,
|
||||
common.ensure_microversion_bounds(
|
||||
"2.2", {"min-ver": "2.0", "max-ver": "2.1"}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class TestGetResponseCandidates(TestCase):
|
||||
FOO = {"foo": {"type": "string"}}
|
||||
|
||||
def test_find_with_single_candidate(self):
|
||||
responses = {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {**self.FOO},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.assertEqual(
|
||||
[
|
||||
(
|
||||
responses["200"]["content"]["application/json"]["schema"],
|
||||
None,
|
||||
)
|
||||
],
|
||||
common.get_response_candidates(responses, "foo"),
|
||||
)
|
||||
|
||||
def test_find_with_list(self):
|
||||
responses = {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fooes": {"type": "array", "items": self.FOO}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.assertEqual(
|
||||
[
|
||||
(
|
||||
responses["200"]["content"]["application/json"]["schema"],
|
||||
None,
|
||||
)
|
||||
],
|
||||
common.get_response_candidates(responses, "foo"),
|
||||
)
|
||||
|
||||
def test_no_candidates_returns_root(self):
|
||||
responses = {
|
||||
"200": {
|
||||
"content": {"application/json": {"schema": self.FOO["foo"]}}
|
||||
}
|
||||
}
|
||||
self.assertEqual(
|
||||
[
|
||||
(
|
||||
responses["200"]["content"]["application/json"]["schema"],
|
||||
None,
|
||||
)
|
||||
],
|
||||
common.get_response_candidates(responses, "foo"),
|
||||
)
|
||||
|
||||
def test_server_create_response(self):
|
||||
s1 = {
|
||||
"type": "object",
|
||||
"properties": {"reservation_id": {"type": "string"}},
|
||||
}
|
||||
s2 = {"type": "object", "properties": {"server": {"type": "string"}}}
|
||||
rsp_def = {
|
||||
"201": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"oneOf": [s1, s2], "type": "object"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
candidates = common.get_response_candidates(rsp_def, "server", None)
|
||||
self.assertIn((s1, {"kind-suffix": "a"}), candidates)
|
||||
self.assertIn((s2, {"kind-suffix": "b"}), candidates)
|
||||
|
||||
def test_server_show_response(self):
|
||||
srv1 = {
|
||||
"type": "object",
|
||||
"properties": {"server": {"type": "string"}},
|
||||
"x-openstack": {"min-ver": "2.0", "max-ver": "2.1"},
|
||||
}
|
||||
srv2_a = {"type": "object", "properties": {"foo": {"type": "string"}}}
|
||||
srv2_b = {"type": "object", "properties": {"bar": {"type": "bool"}}}
|
||||
rsp_def = {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"oneOf": [
|
||||
srv1,
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"server": {"oneOf": [srv2_a, srv2_b]}
|
||||
},
|
||||
"x-openstack": {"min-ver": "2.100"},
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
candidates = common.get_response_candidates(rsp_def, "server")
|
||||
self.assertIn((srv1, {"min-ver": "2.0", "max-ver": "2.1"}), candidates)
|
||||
self.assertIn(
|
||||
(
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {"server": srv2_a},
|
||||
"x-openstack": {"min-ver": "2.100"},
|
||||
},
|
||||
{"min-ver": "2.100", "kind-suffix": "a"},
|
||||
),
|
||||
candidates,
|
||||
)
|
||||
self.assertIn(
|
||||
(
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {"server": srv2_b},
|
||||
"x-openstack": {"min-ver": "2.100"},
|
||||
},
|
||||
{"min-ver": "2.100", "kind-suffix": "b"},
|
||||
),
|
||||
candidates,
|
||||
)
|
||||
|
||||
candidates = common.get_response_candidates(
|
||||
rsp_def, "server", None, "2.101"
|
||||
)
|
||||
self.assertIn(
|
||||
(
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {"server": srv2_a},
|
||||
"x-openstack": {"min-ver": "2.100"},
|
||||
},
|
||||
{"min-ver": "2.100", "kind-suffix": "a"},
|
||||
),
|
||||
candidates,
|
||||
)
|
||||
self.assertIn(
|
||||
(
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {"server": srv2_b},
|
||||
"x-openstack": {"min-ver": "2.100"},
|
||||
},
|
||||
{"min-ver": "2.100", "kind-suffix": "b"},
|
||||
),
|
||||
candidates,
|
||||
)
|
||||
self.assertEqual(2, len(candidates))
|
||||
|
||||
candidates = common.get_response_candidates(
|
||||
rsp_def, "server", None, "2.0"
|
||||
)
|
||||
self.assertIn((srv1, {"min-ver": "2.0", "max-ver": "2.1"}), candidates)
|
||||
self.assertEqual(1, len(candidates))
|
||||
|
||||
def test_compute_service_update_response(self):
|
||||
srv1 = {
|
||||
"type": "object",
|
||||
"properties": {"service": {"type": "string"}},
|
||||
"x-openstack": {"min-ver": "2.0", "max-ver": "2.1"},
|
||||
}
|
||||
srv2_a = {"properties": {"foo": {"type": "string"}}}
|
||||
srv2_b = {"properties": {"bar": {"type": "bool"}}}
|
||||
rsp_def = {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"oneOf": [
|
||||
srv1,
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"service": {
|
||||
"oneOf": [srv2_a, srv2_b],
|
||||
"type": "object",
|
||||
}
|
||||
},
|
||||
"x-openstack": {"min-ver": "2.100"},
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
candidates = common.get_response_candidates(rsp_def, "service")
|
||||
self.assertIn((srv1, {"min-ver": "2.0", "max-ver": "2.1"}), candidates)
|
||||
self.assertIn(
|
||||
(
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {"service": {**srv2_a, "type": "object"}},
|
||||
"x-openstack": {"min-ver": "2.100"},
|
||||
},
|
||||
{"min-ver": "2.100", "kind-suffix": "a"},
|
||||
),
|
||||
candidates,
|
||||
)
|
||||
self.assertIn(
|
||||
(
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {"service": {**srv2_b, "type": "object"}},
|
||||
"x-openstack": {"min-ver": "2.100"},
|
||||
},
|
||||
{"min-ver": "2.100", "kind-suffix": "b"},
|
||||
),
|
||||
candidates,
|
||||
)
|
||||
|
||||
def test_server_list_detailed_response(self):
|
||||
srv1 = {
|
||||
"type": "object",
|
||||
"properties": {"baz": {"type": "string"}},
|
||||
"x-openstack": {"min-ver": "2.0", "max-ver": "2.1"},
|
||||
}
|
||||
srv2_a = {"type": "object", "properties": {"foo": {"type": "string"}}}
|
||||
srv2_b = {"type": "object", "properties": {"bar": {"type": "bool"}}}
|
||||
rsp_def = {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"oneOf": [
|
||||
srv1,
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"servers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"oneOf": [srv2_a, srv2_b]
|
||||
},
|
||||
}
|
||||
},
|
||||
"x-openstack": {"min-ver": "2.100"},
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
candidates = common.get_response_candidates(rsp_def, "server")
|
||||
self.assertIn((srv1, {"min-ver": "2.0", "max-ver": "2.1"}), candidates)
|
||||
self.assertIn(
|
||||
(
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"servers": {"type": "array", "items": srv2_a}
|
||||
},
|
||||
"x-openstack": {"min-ver": "2.100"},
|
||||
},
|
||||
{"min-ver": "2.100", "kind-suffix": "a"},
|
||||
),
|
||||
candidates,
|
||||
)
|
||||
self.assertIn(
|
||||
(
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"servers": {"type": "array", "items": srv2_b}
|
||||
},
|
||||
"x-openstack": {"min-ver": "2.100"},
|
||||
},
|
||||
{"min-ver": "2.100", "kind-suffix": "b"},
|
||||
),
|
||||
candidates,
|
||||
)
|
||||
|
||||
candidates = common.get_response_candidates(
|
||||
rsp_def, "server", None, "2.101"
|
||||
)
|
||||
self.assertIn(
|
||||
(
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"servers": {"type": "array", "items": srv2_a}
|
||||
},
|
||||
"x-openstack": {"min-ver": "2.100"},
|
||||
},
|
||||
{"min-ver": "2.100", "kind-suffix": "a"},
|
||||
),
|
||||
candidates,
|
||||
)
|
||||
self.assertIn(
|
||||
(
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"servers": {"type": "array", "items": srv2_b}
|
||||
},
|
||||
"x-openstack": {"min-ver": "2.100"},
|
||||
},
|
||||
{"min-ver": "2.100", "kind-suffix": "b"},
|
||||
),
|
||||
candidates,
|
||||
)
|
||||
self.assertEqual(2, len(candidates))
|
||||
|
||||
candidates = common.get_response_candidates(
|
||||
rsp_def, "server", None, "2.0"
|
||||
)
|
||||
self.assertIn((srv1, {"min-ver": "2.0", "max-ver": "2.1"}), candidates)
|
||||
self.assertEqual(1, len(candidates))
|
||||
|
||||
def test_get_image_show_candidate(self):
|
||||
responses = {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"additionalProperties": {"type": "string"},
|
||||
"properties": {
|
||||
"foo": {"type": ["null", "string"]}
|
||||
},
|
||||
"type": "object",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
[
|
||||
(
|
||||
responses["200"]["content"]["application/json"]["schema"],
|
||||
None,
|
||||
)
|
||||
],
|
||||
common.get_response_candidates(responses, "image"),
|
||||
)
|
||||
|
||||
def test_aggregate_action_response(self):
|
||||
ah1 = {
|
||||
"type": "object",
|
||||
"properties": {"aggregate": {"type": "string"}},
|
||||
"x-openstack": {"min-ver": "2.0", "action-name": "add_host"},
|
||||
}
|
||||
ah2 = {
|
||||
"type": "object",
|
||||
"properties": {"aggregate": {"type": "string"}},
|
||||
"x-openstack": {"min-ver": "2.4", "action-name": "add_host"},
|
||||
}
|
||||
rh1 = {
|
||||
"type": "object",
|
||||
"properties": {"aggregate": {"type": "string"}},
|
||||
"x-openstack": {"min-ver": "2.0", "action-name": "remove_host"},
|
||||
}
|
||||
rh2 = {
|
||||
"type": "object",
|
||||
"properties": {"aggregate": {"type": "string"}},
|
||||
"x-openstack": {"min-ver": "2.4", "action-name": "remove_host"},
|
||||
}
|
||||
rsp_def = {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"oneOf": [ah1, ah2, rh1, rh2]}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
candidates = common.get_response_candidates(
|
||||
rsp_def, "server", "add_host"
|
||||
)
|
||||
self.assertIn((ah1, ah1["x-openstack"]), candidates)
|
||||
self.assertIn((ah2, ah2["x-openstack"]), candidates)
|
||||
self.assertEqual(2, len(candidates))
|
||||
candidates = common.get_response_candidates(
|
||||
rsp_def, "server", "add_host", "2.0"
|
||||
)
|
||||
self.assertIn((ah1, ah1["x-openstack"]), candidates)
|
||||
self.assertNotIn((ah2, ah2["x-openstack"]), candidates)
|
||||
candidates = common.get_response_candidates(
|
||||
rsp_def, "server", "remove_host"
|
||||
)
|
||||
self.assertIn((rh1, rh1["x-openstack"]), candidates)
|
||||
self.assertIn((rh2, rh2["x-openstack"]), candidates)
|
||||
self.assertEqual(2, len(candidates))
|
||||
|
||||
def test_find_correct_action(self):
|
||||
foo_action = {
|
||||
"type": "string",
|
||||
"x-openstack": {"action-name": "foo-action"},
|
||||
}
|
||||
bar_action = {
|
||||
"type": "string",
|
||||
"x-openstack": {"action-name": "bar-action"},
|
||||
}
|
||||
responses: dict[str, Any] = {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"type": "object", "properties": self.FOO}
|
||||
}
|
||||
}
|
||||
},
|
||||
"204": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"oneOf": [foo_action, bar_action]}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
self.assertIn(
|
||||
(foo_action, {"action-name": "foo-action"}),
|
||||
common.get_response_candidates(responses, "foo", "foo-action"),
|
||||
)
|
||||
self.assertIn(
|
||||
(bar_action, {"action-name": "bar-action"}),
|
||||
common.get_response_candidates(responses, "foo", "bar-action"),
|
||||
)
|
||||
self.assertCountEqual(
|
||||
common.get_response_candidates(responses, "foo", "baz-action"), []
|
||||
)
|
||||
self.assertEqual(
|
||||
[
|
||||
(
|
||||
responses["200"]["content"]["application/json"]["schema"],
|
||||
None,
|
||||
)
|
||||
],
|
||||
common.get_response_candidates(responses, "foo"),
|
||||
"when no action requested and non-action response found - return it",
|
||||
)
|
||||
|
||||
@@ -13,14 +13,14 @@
|
||||
import logging
|
||||
from unittest import TestCase
|
||||
|
||||
from jinja2 import Environment
|
||||
from jinja2 import FileSystemLoader
|
||||
from jinja2 import select_autoescape
|
||||
from jinja2 import StrictUndefined
|
||||
from jinja2 import (
|
||||
Environment,
|
||||
FileSystemLoader,
|
||||
StrictUndefined,
|
||||
select_autoescape,
|
||||
)
|
||||
|
||||
from codegenerator import base
|
||||
from codegenerator import model
|
||||
from codegenerator import rust_cli
|
||||
from codegenerator import base, model, rust_cli
|
||||
|
||||
|
||||
class TestRustCliResponseManager(TestCase):
|
||||
@@ -30,7 +30,7 @@ class TestRustCliResponseManager(TestCase):
|
||||
|
||||
def test_generate_array_of_array_of_strings(self):
|
||||
expected_content = """
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
@@ -54,10 +54,12 @@ class TestRustCliResponseManager(TestCase):
|
||||
use clap::Args;
|
||||
use tracing::info;
|
||||
use eyre::{OptionExt, WrapErr};
|
||||
|
||||
use openstack_sdk::AsyncOpenStack;
|
||||
use openstack_cli_core::cli::CliArgs;
|
||||
use openstack_cli_core::output::OutputProcessor;
|
||||
use openstack_cli_core::error::OpenStackCliError;
|
||||
|
||||
use bar::import;
|
||||
use foo::import;
|
||||
|
||||
@@ -72,11 +74,13 @@ pub struct fooCommand {
|
||||
/// Path parameters
|
||||
#[command(flatten)]
|
||||
path: PathParameters,
|
||||
|
||||
/// aoaos
|
||||
///
|
||||
/// Parameter is an array, may be provided multiple times.
|
||||
#[arg(action=clap::ArgAction::Append, help_heading = "Body parameters", long, value_name="[String] as JSON", value_parser=openstack_cli_core::common::parse_json)]
|
||||
foo: Vec<Vec<String>>,
|
||||
|
||||
}
|
||||
|
||||
/// Query parameters
|
||||
@@ -84,6 +88,7 @@ pub struct fooCommand {
|
||||
struct QueryParameters {
|
||||
}
|
||||
|
||||
|
||||
/// Path parameters
|
||||
#[derive(Args)]
|
||||
struct PathParameters {
|
||||
@@ -112,13 +117,14 @@ impl fooCommand {
|
||||
.build()
|
||||
.map_err(|x| OpenStackCliError::EndpointBuild(x.to_string()))?;
|
||||
|
||||
let data = ep.query_async(client).await?;
|
||||
op.output_single::<rsp>(data)?;
|
||||
let data: serde_json::Value = ep.query_async(client).await?;
|
||||
|
||||
// Show command specific hints
|
||||
op.show_command_hint()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
"""
|
||||
schema = {
|
||||
"type": "object",
|
||||
@@ -172,7 +178,9 @@ impl fooCommand {
|
||||
response_class_name="rsp",
|
||||
result_is_list=False,
|
||||
resource_key="srv.foo",
|
||||
response_mod_candidates=[],
|
||||
)
|
||||
print(content)
|
||||
self.assertEqual(
|
||||
"".join([x.rstrip() for x in expected_content.split()]),
|
||||
"".join([x.rstrip() for x in content.split()]),
|
||||
|
||||
@@ -645,6 +645,7 @@ resources:
|
||||
sdk_mod_name: list
|
||||
rust-sdk:
|
||||
module_name: list
|
||||
response_key: keypairs
|
||||
response_list_item_key: keypair
|
||||
show:
|
||||
operation_id: os-keypairs/id:get
|
||||
@@ -657,6 +658,7 @@ resources:
|
||||
sdk_mod_name: get
|
||||
rust-sdk:
|
||||
module_name: get
|
||||
response_key: keypair
|
||||
spec_file: wrk/openapi_specs/compute/v2.yaml
|
||||
compute.limit:
|
||||
api_version: v2
|
||||
@@ -958,6 +960,7 @@ resources:
|
||||
sdk_mod_name: list_detailed
|
||||
rust-sdk:
|
||||
module_name: list_detailed
|
||||
#response_key: servers
|
||||
rust-tui:
|
||||
module_name: list_detailed
|
||||
lock:
|
||||
@@ -1971,6 +1974,7 @@ resources:
|
||||
sdk_mod_name: list
|
||||
rust-sdk:
|
||||
module_name: list
|
||||
response_key: tenant_usages
|
||||
show:
|
||||
operation_id: os-simple-tenant-usage/id:get
|
||||
operation_type: show
|
||||
@@ -1979,8 +1983,10 @@ resources:
|
||||
cli_full_command: simple-tenant-usage show
|
||||
module_name: show
|
||||
sdk_mod_name: get
|
||||
response_key: tenant_usage
|
||||
rust-sdk:
|
||||
module_name: get
|
||||
response_key: tenant_usage
|
||||
spec_file: wrk/openapi_specs/compute/v2.yaml
|
||||
compute.version:
|
||||
api_version: v2
|
||||
|
||||
@@ -11,11 +11,17 @@
|
||||
name: "."
|
||||
virtualenv: "{{ ansible_user_dir }}/.venv"
|
||||
|
||||
- name: Install openstack-codegenerator from sources
|
||||
ansible.builtin.get_url:
|
||||
url: "https://releases.openstack.org/constraints/upper/master"
|
||||
dest: "{{ ansible_user_dir }}/upper-constraints.txt"
|
||||
|
||||
- name: Install additional dependencies from sources
|
||||
ansible.builtin.pip:
|
||||
chdir: "{{ zuul.projects[zj_dep.project].src_dir }}"
|
||||
name: "{{ zj_dep.name }}"
|
||||
virtualenv: "{{ ansible_user_dir }}/.venv"
|
||||
extra_args: "-c {{ ansible_user_dir}}/upper-constraints.txt"
|
||||
loop: "{{ install_additional_projects }}"
|
||||
loop_control:
|
||||
loop_var: zj_dep
|
||||
|
||||
Reference in New Issue
Block a user