Switch cli to openstack_types

There are lot of changes related to the deduplication caused mostly be
lot of data simplifactions done on the cli side for the responses which
are not done on openstack_types. This changes were applied manually to
the target repository since the changes require also bumping structable
version.

Some other minor enum sorting and docstring formatting fixes has been
applied.

Change-Id: Ifc508c54c01bd39af12e3023668e698085829a05
This commit is contained in:
Artem Goncharov 2025-04-17 12:31:42 +02:00
parent f3093fb583
commit 1ae47e0b0e
20 changed files with 512 additions and 1013 deletions

View File

@ -235,6 +235,8 @@ def find_resource_schema(
return (schema, None)
else:
return (schema, None)
# elif schema_type == "string":
# return (schema, None)
except Exception as ex:
logging.exception(
f"Caught exception {ex} during processing of {schema}"
@ -321,6 +323,7 @@ def find_response_schema(
in schema.get("properties", [])
)
)
or schema.get("type") == "string"
)
):
return schema

View File

@ -273,19 +273,20 @@ class BTreeSet(BaseCombinedType):
return imports
class Dictionary(BaseCombinedType):
class Dictionary(BaseCompoundType):
base_type: str = "dict"
value_type: BasePrimitiveType | BaseCombinedType | BaseCompoundType
@property
def imports(self):
imports: set[str] = {"std::collections::HashMap"}
imports: set[str] = {"std::collections::BTreeMap"}
imports.update(self.value_type.imports)
imports.add("structable::{StructTable, StructTableOptions}")
return imports
@property
def type_hint(self):
return f"HashMap<String, {self.value_type.type_hint}>"
return f"BTreeMap<String, {self.value_type.type_hint}>"
@property
def lifetimes(self):
@ -325,6 +326,9 @@ class Struct(BaseCompoundType):
additional_fields_type: (
BasePrimitiveType | BaseCombinedType | BaseCompoundType | None
) = None
pattern_properties: (
BasePrimitiveType | BaseCombinedType | BaseCompoundType | None
) = None
@property
def type_hint(self):
@ -382,6 +386,8 @@ class StructFieldResponse(StructField):
macros.update(self.data_type.get_serde_macros(self.is_optional))
except Exception:
pass
if self.is_optional:
macros.add("default")
if self.local_name != self.remote_name:
macros.add(f'rename="{self.remote_name}"')
if len(macros) > 0:
@ -538,6 +544,7 @@ class StringEnum(BaseCompoundType):
lifetimes: set[str] = set()
builder_container_macros: str | None = None
original_data_type: BaseCompoundType | BaseCompoundType | None = None
allows_arbitrary_value: bool = False
@property
def derive_container_macros(self) -> str | None:
@ -545,6 +552,8 @@ class StringEnum(BaseCompoundType):
@property
def serde_container_macros(self) -> str | None:
if self.allows_arbitrary_value:
return "#[serde(untagged)]"
return None
@property
@ -583,6 +592,7 @@ class StringEnum(BaseCompoundType):
class HashMapResponse(Dictionary):
"""Wrapper around a simple dictionary to implement Display trait"""
# name: str | None = None
lifetimes: set[str] = set()
@property
@ -592,7 +602,8 @@ class HashMapResponse(Dictionary):
@property
def imports(self):
imports = self.value_type.imports
imports.add("std::collections::HashMap")
imports.add("std::collections::BTreeMap")
imports.add("structable::{StructTable, StructTableOptions}")
return imports
@ -607,6 +618,7 @@ class TupleStruct(Struct):
imports: set[str] = set()
for field in self.tuple_fields:
imports.update(field.data_type.imports)
imports.add("structable::{StructTable, StructTableOptions}")
return imports
@ -781,7 +793,8 @@ class TypeManager:
typ = self._get_one_of_type(type_model)
elif isinstance(type_model, model.Dictionary):
typ = self.data_type_mapping[model.Dictionary](
value_type=self.convert_model(type_model.value_type)
name=self.get_model_name(type_model.reference),
value_type=self.convert_model(type_model.value_type),
)
elif isinstance(type_model, model.CommaSeparatedList):
typ = self.data_type_mapping[model.CommaSeparatedList](
@ -1001,6 +1014,7 @@ class TypeManager:
integer_klass = self.primitive_type_mapping[model.ConstraintInteger]
boolean_klass = self.primitive_type_mapping[model.PrimitiveBoolean]
dict_klass = self.data_type_mapping[model.Dictionary]
option_klass = self.option_type_class
enum_name = type_model.reference.name if type_model.reference else None
if string_klass in kinds_classes and number_klass in kinds_classes:
# oneOf [string, number] => string
@ -1062,6 +1076,15 @@ class TypeManager:
bck = kinds[0].copy()
kinds.clear()
kinds.append(bck)
elif (
self.string_enum_class in kinds_classes
and option_klass in kinds_classes
):
option = next(x for x in kinds if isinstance(x["local"], Option))
enum = next(x for x in kinds if isinstance(x["local"], StringEnum))
if option and isinstance(option["local"].item_type, String):
enum["local"].allows_arbitrary_value = True
kinds.remove(option)
def set_models(self, models):
"""Process (translate) ADT models into Rust models"""

View File

@ -437,7 +437,7 @@ class JsonSchemaParser:
# `"type": "object", "pattern_properties": ...`
if len(list(pattern_props.values())) == 1:
obj = Dictionary(
value_type=list(pattern_props.values())[0]
name=name, value_type=list(pattern_props.values())[0]
)
else:
obj = Struct(pattern_properties=pattern_props)

View File

@ -69,47 +69,18 @@ class SecretString(String):
pass
class IntString(common.BasePrimitiveType):
"""CLI Integer or String"""
imports: set[str] = {"openstack_sdk::types::IntString"}
type_hint: str = "IntString"
clap_macros: set[str] = set()
class NumString(common.BasePrimitiveType):
"""CLI Number or String"""
imports: set[str] = {"openstack_sdk::types::NumString"}
type_hint: str = "NumString"
clap_macros: set[str] = set()
class BoolString(common.BasePrimitiveType):
"""CLI Boolean or String"""
imports: set[str] = {"openstack_sdk::types::BoolString"}
type_hint: str = "BoolString"
clap_macros: set[str] = set()
class VecString(common.BasePrimitiveType):
"""CLI Vector of strings"""
imports: set[str] = {"crate::common::VecString"}
type_hint: str = "VecString"
clap_macros: set[str] = set()
class JsonValue(common_rust.JsonValue):
"""Arbitrary JSON value"""
clap_macros: set[str] = {'value_name="JSON"', "value_parser=parse_json"}
clap_macros: set[str] = {
'value_name="JSON"',
"value_parser=crate::common::parse_json",
}
original_data_type: BaseCombinedType | BaseCompoundType | None = None
@property
def imports(self):
imports: set[str] = {"crate::common::parse_json", "serde_json::Value"}
imports: set[str] = {"serde_json::Value"}
if self.original_data_type and isinstance(
self.original_data_type, common_rust.Dictionary
):
@ -240,108 +211,6 @@ class EnumGroupStruct(common_rust.Struct):
reference: model.Reference | None = None
class StructFieldResponse(common_rust.StructField):
"""Response Structure Field"""
@property
def type_hint(self):
typ_hint = self.data_type.type_hint
if self.is_optional and not typ_hint.startswith("Option<"):
typ_hint = f"Option<{typ_hint}>"
return typ_hint
@property
def serde_macros(self):
macros = set()
if self.local_name != self.remote_name:
macros.add(f'rename="{self.remote_name}"')
return f"#[serde({', '.join(sorted(macros))})]"
def get_structable_macros(
self,
struct: "StructResponse",
service_name: str,
resource_name: str,
operation_type: str,
):
macros = set()
if self.is_optional or self.data_type.type_hint.startswith("Option<"):
macros.add("optional")
if self.local_name != self.remote_name:
macros.add(f'title="{self.remote_name}"')
# Fully Qualified Attribute Name
fqan: str = ".".join(
[service_name, resource_name, self.remote_name]
).lower()
# Check the known alias of the field by FQAN
alias = common.FQAN_ALIAS_MAP.get(fqan)
if operation_type in ["list", "list_from_struct"]:
if (
"id" in struct.fields.keys()
and not (
self.local_name in BASIC_FIELDS or alias in BASIC_FIELDS
)
) or (
"id" not in struct.fields.keys()
and (self.local_name not in list(struct.fields.keys())[-10:])
and not (
self.local_name in BASIC_FIELDS or alias in BASIC_FIELDS
)
):
# Only add "wide" flag if field is not in the basic fields AND
# there is at least "id" field existing in the struct OR the
# field is not in the first 10
macros.add("wide")
if (
self.local_name == "state"
and "status" not in struct.fields.keys()
):
macros.add("status")
elif (
self.local_name == "operating_status"
and "status" not in struct.fields.keys()
):
macros.add("status")
if self.data_type.type_hint in [
"Value",
"Option<Value>",
"Vec<Value>",
"Option<Vec<Value>>",
]:
macros.add("pretty")
return f"#[structable({', '.join(sorted(macros))})]"
class StructResponse(common_rust.Struct):
field_type_class_: Type[common_rust.StructField] = StructFieldResponse
@property
def imports(self):
imports: set[str] = {"serde::Deserialize"}
for field in self.fields.values():
imports.update(field.data_type.imports)
# In difference to the SDK and Input we do not currently handle
# additional_fields of the struct in response
# if self.additional_fields_type:
# imports.add("std::collections::BTreeMap")
# imports.update(self.additional_fields_type.imports)
return imports
class TupleStruct(common_rust.Struct):
"""Rust tuple struct without named fields"""
base_type: str = "struct"
tuple_fields: list[common_rust.StructField] = []
@property
def imports(self):
imports: set[str] = set()
for field in self.tuple_fields:
imports.update(field.data_type.imports)
return imports
class DictionaryInput(common_rust.Dictionary):
lifetimes: set[str] = set()
original_data_type: BaseCompoundType | BaseCompoundType | None = None
@ -392,37 +261,13 @@ class ArrayInput(common_rust.Array):
macros: set[str] = {"long", "action=clap::ArgAction::Append"}
macros.update(self.item_type.clap_macros)
if isinstance(self.item_type, ArrayInput):
macros.add("value_parser=parse_json")
macros.add("value_parser=crate::common::parse_json")
macros.add(
f'value_name="[{self.item_type.item_type.type_hint}] as JSON"'
)
return macros
class ArrayResponse(common_rust.Array):
"""Vector of data for the Reponse
in the reponse need to be converted to own type to implement Display"""
@property
def type_hint(self):
return f"Vec{self.item_type.type_hint}"
class HashMapResponse(common_rust.Dictionary):
lifetimes: set[str] = set()
@property
def type_hint(self):
return f"HashMapString{self.value_type.type_hint.replace('<', '').replace('>', '')}"
@property
def imports(self):
imports = self.value_type.imports
imports.add("std::collections::HashMap")
return imports
class CommaSeparatedList(common_rust.CommaSeparatedList):
@property
def type_hint(self):
@ -627,7 +472,8 @@ class RequestTypeManager(common_rust.TypeManager):
original_data_type = self.convert_model(type_model.value_type)
typ = JsonValue(
original_data_type=DictionaryInput(
value_type=original_data_type
name=self.get_model_name(type_model.reference),
value_type=original_data_type,
)
)
else:
@ -638,6 +484,7 @@ class RequestTypeManager(common_rust.TypeManager):
type_model.value_type
)
typ = DictionaryInput(
name=self.get_model_name(type_model.reference),
description=type_model.value_type.description,
value_type=JsonValue(
original_data_type=original_data_type
@ -821,198 +668,6 @@ class RequestTypeManager(common_rust.TypeManager):
self.parameters[k] = param
class ResponseTypeManager(common_rust.TypeManager):
primitive_type_mapping: dict[
Type[model.PrimitiveType], Type[BasePrimitiveType]
] = {
model.PrimitiveString: common_rust.String,
model.ConstraintString: common_rust.String,
}
data_type_mapping = {
model.Struct: StructResponse,
model.Array: JsonValue,
model.Dictionary: JsonValue,
}
def get_model_name(self, model_ref: model.Reference | None) -> str:
"""Get the localized model type name
In order to avoid collision between structures in request and
response we prefix all types with `Response`
:returns str: Type name
"""
if not model_ref:
return "Response"
return "Response" + "".join(
x.capitalize()
for x in re.split(common.SPLIT_NAME_RE, model_ref.name)
)
def convert_model(
self, type_model: model.PrimitiveType | model.ADT | model.Reference
) -> BasePrimitiveType | BaseCombinedType | BaseCompoundType:
"""Get local destination type from the ModelType"""
model_ref: model.Reference | None = None
typ: BasePrimitiveType | BaseCombinedType | BaseCompoundType | None = (
None
)
if isinstance(type_model, model.Reference):
model_ref = type_model
type_model = self._get_adt_by_reference(model_ref)
elif isinstance(type_model, model.ADT):
# Direct composite type
model_ref = type_model.reference
# CLI response PRE hacks
if isinstance(type_model, model.Array):
item_type = type_model.item_type
if isinstance(item_type, String):
# Array of string is replaced by `VecString` type
typ = VecString()
elif (
model_ref
and model_ref.name == "links"
and model_ref.type == model.Array
):
# Array of "links" is replaced by Json Value
typ = common_rust.JsonValue()
self.ignored_models.append(type_model.item_type)
elif (
isinstance(item_type, model.Reference)
and type_model.item_type.type == model.Struct
):
# Array of complex Structs is replaced on output by Json Value
typ = common_rust.JsonValue()
self.ignored_models.append(item_type)
if typ:
if model_ref:
self.refs[model_ref] = typ
else:
# Not hacked anything, invoke superior method
typ = super().convert_model(type_model)
# POST hacks
if typ and isinstance(typ, common_rust.StringEnum):
# There is no sense of Enum in the output. Convert to the plain
# string
typ = String(
description=common_rust.sanitize_rust_docstrings(
typ.description
)
)
if (
typ
and isinstance(typ, ArrayResponse)
and isinstance(typ.item_type, common_rust.Enum)
):
# Array of complex Enums is replaced on output by Json Value
self.ignored_models.append(typ.item_type)
typ = common_rust.JsonValue()
return typ
def _simplify_oneof_combinations(self, type_model, kinds):
"""Simplify certain known oneOf combinations"""
kinds_classes = [x["class"] for x in kinds]
if (
common_rust.String in kinds_classes
and common_rust.Number in kinds_classes
):
# oneOf [string, number] => NumString
kinds.clear()
kinds.append({"local": NumString(), "class": NumString})
elif (
common_rust.String in kinds_classes
and common_rust.Integer in kinds_classes
):
# oneOf [string, integer] => NumString
kinds.clear()
kinds.append({"local": IntString(), "class": IntString})
elif (
common_rust.String in kinds_classes
and common_rust.Boolean in kinds_classes
):
# oneOf [string, boolean] => String
kinds.clear()
kinds.append({"local": BoolString(), "class": BoolString})
super()._simplify_oneof_combinations(type_model, kinds)
def _get_struct_type(self, type_model: model.Struct) -> common_rust.Struct:
"""Convert model.Struct into Rust `Struct`"""
struct_class = self.data_type_mapping[model.Struct]
mod = struct_class(
name=self.get_model_name(type_model.reference),
description=common_rust.sanitize_rust_docstrings(
type_model.description
),
)
field_class = mod.field_type_class_
for field_name, field in type_model.fields.items():
is_nullable: bool = False
field_data_type = self.convert_model(field.data_type)
if isinstance(field_data_type, self.option_type_class):
# Unwrap Option into "is_nullable" NOTE: but perhaps
# Option<Option> is better (not set vs set explicitly to None
# )
is_nullable = True
if isinstance(field_data_type.item_type, common_rust.Array):
# Unwrap Option<Option<Vec...>>
field_data_type = field_data_type.item_type
elif isinstance(field_data_type, struct_class):
field_data_type = JsonValue(**field_data_type.model_dump())
self.ignored_models.append(field.data_type)
f = field_class(
local_name=self.get_local_attribute_name(field_name),
remote_name=self.get_remote_attribute_name(field_name),
description=common_rust.sanitize_rust_docstrings(
field.description
),
data_type=field_data_type,
is_optional=not field.is_required,
is_nullable=is_nullable,
)
mod.fields[field_name] = f
if type_model.additional_fields:
definition = type_model.additional_fields
# Structure allows additional fields
if isinstance(definition, bool):
mod.additional_fields_type = self.primitive_type_mapping[
model.PrimitiveAny
]
else:
mod.additional_fields_type = self.convert_model(definition)
return mod
def get_subtypes(self):
"""Get all subtypes excluding TLA"""
emited_data: set[str] = set()
for k, v in self.refs.items():
if (
k
and isinstance(
v,
(
common_rust.Enum,
common_rust.Struct,
common_rust.StringEnum,
common_rust.Dictionary,
common_rust.Array,
),
)
and k.name != self.root_name
):
key = v.base_type + v.type_hint
if key not in emited_data:
emited_data.add(key)
yield v
def get_imports(self):
"""Get complete set of additional imports required by all models in scope"""
imports: set[str] = super().get_imports()
imports.discard("crate::common::parse_json")
return imports
class RustCliGenerator(BaseGenerator):
def __init__(self):
super().__init__()
@ -1095,7 +750,17 @@ class RustCliGenerator(BaseGenerator):
cli_mod_path = common.get_rust_cli_mod_path(
args.service_type, args.api_version, args.module_path or path
)
types_mod_path = common.get_rust_types_mod_path(
args.service_type, args.api_version, args.module_path or path
)
target_class_name = resource_name
response_class_name: str | None = (
"".join(
x.capitalize()
for x in re.split(common.SPLIT_NAME_RE, resource_name)
)
+ "Response"
)
is_image_download: bool = False
is_json_patch: bool = False
global_additional_imports: set[str] = set()
@ -1154,9 +819,6 @@ class RustCliGenerator(BaseGenerator):
logging.debug(f"Processing variant {operation_variant}")
additional_imports = set(global_additional_imports)
type_manager: common_rust.TypeManager = RequestTypeManager()
response_type_manager: common_rust.TypeManager = (
ResponseTypeManager()
)
result_is_list: bool = False
is_list_paginated: bool = False
if operation_params:
@ -1214,6 +876,11 @@ class RustCliGenerator(BaseGenerator):
sdk_mod_path: list[str] = sdk_mod_path_base.copy()
sdk_mod_path.append((args.sdk_mod_name or mod_name) + mod_suffix)
types_mod_path = common.get_rust_types_mod_path(
args.service_type, args.api_version, args.module_path or path
)
types_mod_path.append("response")
types_mod_path.append(args.sdk_mod_name or mod_name)
mod_name += mod_suffix
result_def: dict = {}
@ -1247,6 +914,9 @@ class RustCliGenerator(BaseGenerator):
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",
@ -1257,43 +927,7 @@ class RustCliGenerator(BaseGenerator):
(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 conter
# 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 = JsonValue()
# if not isinstance(value_type, common_rust.BasePrimitiveType):
# value_type = JsonValue(original_data_type=value_type)
root_dict = HashMapResponse(
value_type=value_type
)
response_type_manager.refs[
model.Reference(
name=response_type_manager.root_name,
type=HashMapResponse,
)
] = root_dict
else:
response_type_manager.set_models(
response_types
)
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
@ -1308,6 +942,11 @@ class RustCliGenerator(BaseGenerator):
"serde_json::json",
]
)
additional_imports.add(
f"openstack_types::"
+ "::".join(types_mod_path)
+ "::*"
)
(_, response_types) = openapi_parser.parse(
response_def, ignore_read_only=True
)
@ -1319,30 +958,6 @@ class RustCliGenerator(BaseGenerator):
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 = TupleStruct(name="Response")
tuple_struct.tuple_fields.append(field)
response_type_manager.refs[
model.Reference(
name=response_type_manager.root_name,
type=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)
response_props = response.get("properties", {})
if (
@ -1354,13 +969,23 @@ class RustCliGenerator(BaseGenerator):
):
result_is_list = True
root_type = response_type_manager.get_root_data_type()
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
else:
response_class_name = None
mod_import_name = "openstack_sdk::api::" + "::".join(
f"r#{x}" if x in ["trait", "type"] else x
for x in sdk_mod_path
)
if not (
args.find_implemented_by_sdk
and args.operation_type in ["show", "download"]
@ -1405,26 +1030,13 @@ class RustCliGenerator(BaseGenerator):
additional_imports.add(
"crate::common::build_upload_asyncread"
)
if (
(
isinstance(root_type, StructResponse)
and root_type.fields
)
or (
isinstance(root_type, TupleStruct)
and root_type.tuple_fields
)
or (isinstance(root_type, common_rust.Dictionary))
):
additional_imports.add("openstack_sdk::api::QueryAsync")
else:
additional_imports.add("openstack_sdk::api::RawQueryAsync")
additional_imports.add("openstack_sdk::api::QueryAsync")
if resource_header_metadata:
additional_imports.add("openstack_sdk::api::RawQueryAsync")
additional_imports.add("http::Response")
additional_imports.add("bytes::Bytes")
if isinstance(root_type, StructResponse):
additional_imports.add("structable_derive::StructTable")
if resource_header_metadata:
additional_imports.add(
"crate::common::HashMapStringString"
@ -1442,16 +1054,10 @@ class RustCliGenerator(BaseGenerator):
):
additional_imports.add("regex::Regex")
for st in response_type_manager.get_subtypes():
if isinstance(st, StructResponse) or getattr(
st, "base_type", None
) in ["vec", "dict"]:
additional_imports.add("std::fmt")
break
if is_image_download:
additional_imports.add("openstack_sdk::api::find")
additional_imports.add("openstack_sdk::api::QueryAsync")
additional_imports.add("openstack_sdk::api::RawQueryAsync")
additional_imports.add(
"::".join(
[
@ -1466,7 +1072,7 @@ class RustCliGenerator(BaseGenerator):
additional_imports.discard("bytes::Bytes")
additional_imports.update(type_manager.get_imports())
additional_imports.update(response_type_manager.get_imports())
# additional_imports.update(response_type_manager.get_imports())
# Deserialize is already in template since it is uncoditionally required
additional_imports.discard("serde::Deserialize")
additional_imports.discard("serde::Serialize")
@ -1498,7 +1104,6 @@ class RustCliGenerator(BaseGenerator):
),
"type_manager": type_manager,
"resource_name": resource_name,
"response_type_manager": response_type_manager,
"target_class_name": "".join(
x.title() for x in target_class_name.split("_")
),
@ -1524,6 +1129,8 @@ 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,
"types_mod_path": types_mod_path,
}
if not args.cli_mod_path:

View File

@ -75,12 +75,18 @@ class StructField(common_rust.StructField):
@property
def builder_macros(self):
macros: set[str] = set()
if not isinstance(self.data_type, BaseCompoundType):
macros.update(self.data_type.builder_macros)
elif not isinstance(self.data_type, common_rust.StringEnum):
macros.add("setter(into)")
# if not isinstance(self.data_type, BaseCompoundType):
macros.update(self.data_type.builder_macros)
setter_params: set[str] = set()
if "setter(into)" in macros:
macros.remove("setter(into)")
setter_params.add("into")
if not isinstance(self.data_type, common_rust.StringEnum):
setter_params.add("into")
if "private" in macros:
macros.add(f'setter(name="_{self.local_name}")')
setter_params.add(f'name="_{self.local_name}"')
if setter_params:
macros.add(f"setter({','.join(sorted(setter_params))})")
if self.is_optional:
default_set: bool = False
for macro in macros:

View File

@ -213,83 +213,6 @@ class Struct(rust_sdk.Struct):
return result
class StructFieldResponse(common_rust.StructField):
"""Response Structure Field"""
@property
def type_hint(self):
typ_hint = self.data_type.type_hint
if self.is_optional and not typ_hint.startswith("Option<"):
typ_hint = f"Option<{typ_hint}>"
return typ_hint
@property
def serde_macros(self):
macros = set()
if self.local_name != self.remote_name:
macros.add(f'rename="{self.remote_name}"')
if self.is_optional or self.data_type.type_hint.startswith("Option<"):
macros.add("default")
return f"#[serde({', '.join(sorted(macros))})]"
def get_structable_macros(
self,
struct: "StructResponse",
service_name: str,
resource_name: str,
operation_type: str,
):
macros = set()
if self.is_optional or self.data_type.type_hint.startswith("Option<"):
macros.add("optional")
macros.add(f'title="{self.remote_name.upper()}"')
# Fully Qualified Attribute Name
fqan: str = ".".join(
[service_name, resource_name, self.remote_name]
).lower()
# Check the known alias of the field by FQAN
alias = common.FQAN_ALIAS_MAP.get(fqan)
if operation_type in ["list", "list_from_struct"]:
if (
"id" in struct.fields.keys()
and not (
self.local_name in BASIC_FIELDS or alias in BASIC_FIELDS
)
) or (
"id" not in struct.fields.keys()
and (self.local_name not in list(struct.fields.keys())[-10:])
and not (
self.local_name in BASIC_FIELDS or alias in BASIC_FIELDS
)
):
# Only add "wide" flag if field is not in the basic fields AND
# there is at least "id" field existing in the struct OR the
# field is not in the first 10
macros.add("wide")
if (
self.local_name == "state"
and "status" not in struct.fields.keys()
):
macros.add("status")
elif (
self.local_name == "operating_status"
and "status" not in struct.fields.keys()
):
macros.add("status")
return f"#[structable({', '.join(sorted(macros))})]"
class StructResponse(common_rust.Struct):
field_type_class_: Type[common_rust.StructField] = StructFieldResponse
@property
def imports(self):
imports: set[str] = {"serde::Deserialize"}
for field in self.fields.values():
imports.update(field.data_type.imports)
return imports
class TypeManager(common_rust.TypeManager):
"""Rust SDK type manager
@ -359,146 +282,6 @@ class TypeManager(common_rust.TypeManager):
yield (v.item_type, sdk_type)
class ResponseTypeManager(common_rust.TypeManager):
primitive_type_mapping: dict[
Type[model.PrimitiveType], Type[BasePrimitiveType]
] = {
model.PrimitiveString: common_rust.String,
model.ConstraintString: common_rust.String,
}
data_type_mapping = {
model.Struct: StructResponse,
model.Array: common_rust.JsonValue,
model.Dictionary: common_rust.JsonValue,
}
def get_model_name(self, model_ref: model.Reference | None) -> str:
"""Get the localized model type name
In order to avoid collision between structures in request and
response we prefix all types with `Response`
:returns str: Type name
"""
if not model_ref:
return "Response"
return "Response" + "".join(
x.capitalize()
for x in re.split(common.SPLIT_NAME_RE, model_ref.name)
)
def _simplify_oneof_combinations(self, type_model, kinds):
"""Simplify certain known oneOf combinations"""
kinds_classes = [x["class"] for x in kinds]
if (
common_rust.String in kinds_classes
and common_rust.Number in kinds_classes
):
# oneOf [string, number] => NumString
kinds.clear()
kinds.append({"local": NumString(), "class": NumString})
elif (
common_rust.String in kinds_classes
and common_rust.Integer in kinds_classes
):
# oneOf [string, integer] => NumString
kinds.clear()
kinds.append({"local": IntString(), "class": IntString})
elif (
common_rust.String in kinds_classes
and common_rust.Boolean in kinds_classes
):
# oneOf [string, boolean] => String
kinds.clear()
kinds.append({"local": BoolString(), "class": BoolString})
super()._simplify_oneof_combinations(type_model, kinds)
def _get_struct_type(self, type_model: model.Struct) -> common_rust.Struct:
"""Convert model.Struct into Rust `Struct`"""
struct_class = self.data_type_mapping[model.Struct]
mod = struct_class(
name=self.get_model_name(type_model.reference),
description=common_rust.sanitize_rust_docstrings(
type_model.description
),
)
field_class = mod.field_type_class_
for field_name, field in type_model.fields.items():
is_nullable: bool = False
field_data_type = self.convert_model(field.data_type)
if isinstance(field_data_type, self.option_type_class):
# Unwrap Option into "is_nullable"
# NOTE: but perhaps Option<Option> is better (not set vs set
# explicitly to None)
is_nullable = True
if isinstance(field_data_type.item_type, common_rust.Array):
# Unwrap Option<Option<Vec...>>
field_data_type = field_data_type.item_type
elif not isinstance(
field_data_type.item_type, BasePrimitiveType
):
# Everything more complex than a primitive goes to Value
field_data_type = common_rust.JsonValue(
**field_data_type.model_dump()
)
self.ignored_models.append(field.data_type)
elif not isinstance(field_data_type, BasePrimitiveType):
field_data_type = common_rust.JsonValue(
**field_data_type.model_dump()
)
self.ignored_models.append(field.data_type)
f = field_class(
local_name=self.get_local_attribute_name(field_name),
remote_name=self.get_remote_attribute_name(field_name),
description=common_rust.sanitize_rust_docstrings(
field.description
),
data_type=field_data_type,
is_optional=not field.is_required,
is_nullable=is_nullable,
)
mod.fields[field_name] = f
if type_model.additional_fields:
definition = type_model.additional_fields
# Structure allows additional fields
if isinstance(definition, bool):
mod.additional_fields_type = self.primitive_type_mapping[
model.PrimitiveAny
]
else:
mod.additional_fields_type = self.convert_model(definition)
return mod
def get_subtypes(self):
"""Get all subtypes excluding TLA"""
emited_data: set[str] = set()
for k, v in self.refs.items():
if (
k
and isinstance(
v,
(
common_rust.Enum,
common_rust.Struct,
common_rust.StringEnum,
common_rust.Dictionary,
common_rust.Array,
),
)
and k.name != self.root_name
):
key = v.base_type + v.type_hint
if key not in emited_data:
emited_data.add(key)
yield v
def get_imports(self):
"""Get complete set of additional imports required by all models in scope"""
imports: set[str] = super().get_imports()
imports.discard("crate::common::parse_json")
return imports
class RustTuiGenerator(BaseGenerator):
def __init__(self):
super().__init__()
@ -619,9 +402,6 @@ class RustTuiGenerator(BaseGenerator):
type_manager = TypeManager()
sdk_type_manager = SdkTypeManager()
type_manager.set_parameters(operation_params)
response_type_manager: common_rust.TypeManager = (
ResponseTypeManager()
)
sdk_type_manager.set_parameters(operation_params)
mod_name = "_".join(
@ -702,21 +482,6 @@ class RustTuiGenerator(BaseGenerator):
)
else:
response_key = resource_name
response_def, _ = common.find_resource_schema(
response, None, response_key
)
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
)
response_type_manager.set_models(response_types)
additional_imports.add("serde_json::Value")
@ -745,9 +510,6 @@ class RustTuiGenerator(BaseGenerator):
additional_imports.add(
"openstack_sdk::api::{paged, Pagination}"
)
additional_imports.add("structable_derive::StructTable")
additional_imports.add("crate::utils::StructTable")
additional_imports.add("crate::utils::OutputConfig")
elif args.operation_type == "delete":
additional_imports.add("openstack_sdk::api::ignore")
@ -755,7 +517,6 @@ class RustTuiGenerator(BaseGenerator):
"crate::cloud_worker::ConfirmableRequest"
)
additional_imports.update(response_type_manager.get_imports())
# Deserialize is already in template since it is uncoditionally required
additional_imports.discard("serde::Deserialize")
additional_imports.discard("serde::Serialize")
@ -773,7 +534,6 @@ class RustTuiGenerator(BaseGenerator):
"response_class_name": response_class_name,
"sdk_service_name": service_name,
"resource_name": resource_name,
"response_type_manager": response_type_manager,
"url": path.lstrip("/").lstrip(ver_prefix).lstrip("/"),
"method": method,
"type_manager": type_manager,

View File

@ -27,51 +27,57 @@ from codegenerator.common import rust as common_rust
class IntString(common.BasePrimitiveType):
"""CLI Integer or String"""
"""Integer or Integer as String"""
type_hint: str = "i64"
imports: set[str] = {
"crate::common::deser_num_str",
"crate::common::deser_num_str_opt",
}
imports: set[str] = set()
clap_macros: set[str] = set()
serde_macros: set[str] = {'deserialize_with="deser_num_str"'}
serde_macros: set[str] = {
'deserialize_with="crate::common::deser_num_str"'
}
def get_serde_macros(self, optional: bool = False) -> set[str]:
deser: str = "deser_num_str" if not optional else "deser_num_str_opt"
return {f'deserialize_with="{deser}"'}
deser_name: str = "crate::common::deser_num_str"
if not optional:
return {f'deserialize_with="{deser_name}"'}
else:
return {"default", f'deserialize_with="{deser_name}_opt"'}
class NumString(common.BasePrimitiveType):
"""CLI Number or String"""
"""Number or Number as String"""
type_hint: str = "f64"
imports: set[str] = {
"crate::common::deser_num_str",
"crate::common::deser_num_str_opt",
}
imports: set[str] = set()
clap_macros: set[str] = set()
serde_macros: set[str] = {'deserialize_with="deser_num_str"'}
serde_macros: set[str] = {
'deserialize_with="crate::common::deser_num_str"'
}
def get_serde_macros(self, optional: bool = False) -> set[str]:
deser: str = "deser_num_str" if not optional else "deser_num_str_opt"
return {f'deserialize_with="{deser}"'}
deser_name: str = "crate::common::deser_num_str"
if not optional:
return {f'deserialize_with="{deser_name}"'}
else:
return {"default", f'deserialize_with="{deser_name}_opt"'}
class BoolString(common.BasePrimitiveType):
"""CLI Boolean or String"""
"""Boolean or Boolean as String"""
type_hint: str = "bool"
imports: set[str] = {
"crate::common::deser_bool_str",
"crate::common::deser_bool_str_opt",
}
imports: set[str] = set()
clap_macros: set[str] = set()
serde_macros: set[str] = {'deserialize_with="deser_bool_str"'}
serde_macros: set[str] = {
'deserialize_with="crate::common::deser_bool_str"'
}
def get_serde_macros(self, optional: bool = False) -> set[str]:
deser: str = "deser_bool_str" if not optional else "deser_bool_str_opt"
return {f'deserialize_with="{deser}"'}
deser_name: str = "crate::common::deser_bool_str"
if not optional:
return {f'deserialize_with="{deser_name}"'}
else:
return {"default", f'deserialize_with="{deser_name}_opt"'}
class Enum(common_rust.Enum):
@ -172,6 +178,12 @@ class ResponseTypeManager(common_rust.TypeManager):
]
else:
mod.additional_fields_type = self.convert_model(definition)
if type_model.pattern_properties:
if type_model.fields and type_model.pattern_properties:
# no way to have normal struct with additional props -> Value it
mod = self.data_type_mapping[model.Dictionary](
value_type=common_rust.JsonValue
)
return mod
def get_subtypes(self):
@ -348,6 +360,9 @@ class RustTypesGenerator(BaseGenerator):
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",
@ -381,7 +396,8 @@ class RustTypesGenerator(BaseGenerator):
# if not isinstance(value_type, common_rust.BasePrimitiveType):
# value_type = JsonValue(original_data_type=value_type)
root_dict = common_rust.HashMapResponse(
value_type=value_type
name=response_type_manager.root_name,
value_type=value_type,
)
response_type_manager.refs[
model.Reference(
@ -410,7 +426,7 @@ class RustTypesGenerator(BaseGenerator):
is_optional=False,
)
tuple_struct = common_rust.TupleStruct(
name=class_name
name=response_type_manager.root_name
)
tuple_struct.tuple_fields.append(field)
response_type_manager.refs[

View File

@ -22,15 +22,12 @@
{% import 'rust_macros.j2' as macros with context -%}
use clap::Args;
use serde::{Deserialize, Serialize};
use tracing::info;
use openstack_sdk::AsyncOpenStack;
use crate::output::OutputProcessor;
use crate::Cli;
use crate::OutputConfig;
use crate::StructTable;
use crate::OpenStackCliError;
{% for mod in additional_imports | sort %}
@ -107,8 +104,6 @@ struct {{ type.name }} {
{%- endif %}
{% endfor %}
{%- include 'rust_cli/response_struct.j2' %}
impl {{ target_class_name }}Command {
/// Perform command action
pub async fn take_action(
@ -158,49 +153,56 @@ impl {{ target_class_name }}Command {
.map_err(|x| OpenStackCliError::EndpointBuild(x.to_string()))?;
{%- endif %}
{# Response #}
{%- with data_type = response_type_manager.get_root_data_type() %}
{%- if (data_type.__class__.__name__ == "StructResponse" and data_type.fields) or data_type.__class__.__name__ == "TupleStruct" or data_type.__class__.__name__ == "HashMapResponse" %}
{#- there is result structure meand we can render response #}
{%- if operation_type == "upload" %}
{%- include 'rust_cli/invoke_upload.j2' %}
{%- elif response_class_name %}
{# Response #}
{%- if operation_type == "list" %}
{% include 'rust_cli/invoke_list.j2' %}
{% include 'rust_cli/invoke_list.j2' %}
{%- elif operation_type == "list_from_struct" %}
{% include 'rust_cli/invoke_list_from_struct.j2' %}
{% include 'rust_cli/invoke_list_from_struct.j2' %}
{%- elif operation_type in ["show"] %}
{#- Show/get implementation #}
{%- if find_present %}
op.output_single::<ResponseData>(find_data)?;
{%- else %}
let data = ep.query_async(client).await?;
op.output_single::<ResponseData>(data)?;
{%- endif %}
{#- Show/get implementation #}
{%- if find_present %}
op.output_single::<{{ response_class_name }}>(find_data)?;
{%- else %}
let data = ep.query_async(client).await?;
op.output_single::<{{ response_class_name }}>(data)?;
{%- endif %}
{%- elif operation_type == "create" %}
{% include 'rust_cli/invoke_create.j2' %}
{% include 'rust_cli/invoke_create.j2' %}
{%- elif operation_type == "set" and method == "patch" and is_json_patch %}
{#- Patch implementation #}
{% include 'rust_cli/invoke_patch.j2' %}
{#- Patch implementation #}
{% include 'rust_cli/invoke_patch.j2' %}
{%- elif operation_type == "delete" %}
let _rsp: Response<Bytes> = ep.raw_query_async(client).await?;
{%- elif operation_type == "download" %}
{%- include 'rust_cli/invoke_download.j2' %}
{%- elif operation_type == "json" %}
let data: serde_json::Value = ep.query_async(client).await?;
op.output_machine(data)?;
{%- elif result_is_list %}
let data: Vec<serde_json::Value> = ep.query_async(client).await?;
op.output_list::<{{ response_class_name }}>(data)?;
{%- else %}
{%- if result_is_list %}
let data: Vec<serde_json::Value> = ep.query_async(client).await?;
op.output_list::<ResponseData>(data)?;
{%- else %}
let data = ep.query_async(client).await?;
op.output_single::<ResponseData>(data)?;
{%- endif %}
let data = ep.query_async(client).await?;
op.output_single::<{{ response_class_name }}>(data)?;
{%- endif %}
{%- elif operation_type not in ["delete", "download", "upload", "json"] %}
{#- there is no result structure - raw mode #}
let _rsp: Response<Bytes> = ep.raw_query_async(client).await?;
{%- if resource_header_metadata %}
let rsp: Response<Bytes> = ep.raw_query_async(client).await?;
{#- metadata from headers for now can be only returned when there is no response struct #}
let mut metadata: HashMap<String, String> = HashMap::new();
let headers = _rsp.headers();
let headers = rsp.headers();
let mut regexes: Vec<Regex> = vec![
{%- for hdr, spec in resource_header_metadata.items() %}
@ -231,28 +233,15 @@ impl {{ target_class_name }}Command {
}
}
}
let data = ResponseData {metadata: metadata.into()};
let data = {{ response_class_name }} {metadata: metadata.into()};
op.output_human::<{{ response_class_name }}>(&data)?;
{%- else %}
let data = ResponseData {};
openstack_sdk::api::ignore(ep).query_async(client).await?;
{%- endif %}
// Maybe output some headers metadata
op.output_human::<ResponseData>(&data)?;
{%- elif operation_type == "delete" %}
let _rsp: Response<Bytes> = ep.raw_query_async(client).await?;
{%- elif operation_type == "download" %}
{%- include 'rust_cli/invoke_download.j2' %}
{%- elif operation_type == "upload" %}
{%- include 'rust_cli/invoke_upload.j2' %}
{%- elif operation_type == "json" %}
let rsp: Response<Bytes> = ep.raw_query_async(client).await?;
let data: serde_json::Value = serde_json::from_slice(rsp.body())?;
op.output_machine(data)?;
{%- else %}
// not implemented
openstack_sdk::api::ignore(ep).query_async(client).await?;
{%- endif %}
{%- endwith %}
{%- endif %} {#- specialities #}
Ok(())
}

View File

@ -1,3 +1,3 @@
{#- Create operation handling #}
let data = ep.query_async(client).await?;
op.output_single::<ResponseData>(data)?;
op.output_single::<{{ response_class_name }}>(data)?;

View File

@ -1,15 +1,14 @@
{#- List operation #}
{%- if data_type.__class__.__name__ in ["StructResponse", "TupleStruct"] %}
{%- if result_is_list %}
{%- if is_list_paginated %}
{#- paginated list #}
let data: Vec<serde_json::Value> = paged(ep, Pagination::Limit(self.max_items)).query_async(client).await?;
{%- else %}
let data: Vec<serde_json::Value> = ep.query_async(client).await?;
{%- endif %}
op.output_list::<{{ response_class_name }}>(data)?;
op.output_list::<ResponseData>(data)?;
{%- elif data_type.__class__.__name__ == "HashMapResponse" %}
{%- else %}
let data = ep.query_async(client).await?;
op.output_single::<ResponseData>(data)?;
op.output_single::<{{ response_class_name }}>(data)?;
{%- endif %}

View File

@ -1,25 +1,3 @@
{#- List operation #}
{%- if data_type.__class__.__name__ in ["StructResponse", "TupleStruct"] %}
let data: serde_json::Value = ep.query_async(client).await?;
let split: Vec<Value> = data
.as_object()
.expect("API response is not an object")
.iter()
.map(|(k, v)| {
let mut new = v.clone();
new.as_object_mut()
.expect("Object item is an object")
.entry("name".to_string())
.or_insert(serde_json::json!(k));
new
})
.collect();
op.output_list::<ResponseData>(split)?;
{%- elif data_type.__class__.__name__ in ["HashMapResponse"] %}
let data: serde_json::Value = ep.query_async(client).await?;
op.output_single::<ResponseData>(data)?;
{%- else %}
let data: serde_json::Value = ep.query_async(client).await?;
op.output_list::<ResponseData>(data)?;
{%- endif %}
op.output_single::<{{ response_class_name }}>(data)?;

View File

@ -6,7 +6,7 @@
.expect("Resource ID is a string")
.to_string();
let data: ResponseData = serde_json::from_value(find_data)?;
let data: {{ response_class_name }} = serde_json::from_value(find_data)?;
let mut new = data.clone();
{%- for attr_name, field in root.fields.items() %}
@ -31,7 +31,10 @@
}
{%- endfor %}
};
new.{{ field.local_name }} = Some(tmp.to_string());
new.{{ field.local_name }} = Some(
tmp.parse()
.map_err(|_| eyre::eyre!("unsupported value for {{ field.local_name }}"))?
);
{%- elif "Option" in field.type_hint %}
new.{{ field.local_name }} = Some(val.into());
@ -65,6 +68,6 @@
.build()
.map_err(|x| OpenStackCliError::EndpointBuild(x.to_string()))?;
let new_data = patch_ep.query_async(client).await?;
op.output_single::<ResponseData>(new_data)?;
op.output_single::<{{ response_class_name }}>(new_data)?;
{%- endwith %}

View File

@ -1,5 +1,5 @@
let dst = self.file.clone();
let data = build_upload_asyncread(dst).await?;
let _rsp: Response<Bytes> = ep.raw_query_read_body_async(client, data).await?;
let _rsp = ep.raw_query_read_body_async(client, data).await?;
// TODO: what if there is an interesting response

View File

@ -1,170 +0,0 @@
{%- import 'rust_macros.j2' as macros with context -%}
{%- with data_type = response_type_manager.get_root_data_type() %}
{%- if data_type.__class__.__name__ == "StructResponse" %}
{%- if data_type.fields %}
/// {{ target_class_name }} response representation
#[derive(Deserialize, Serialize)]
#[derive(Clone, StructTable)]
struct ResponseData {
{%- for k, v in data_type.fields | dictsort %}
{% if not (operation_type == "list" and k in ["links"]) %}
{{ macros.docstring(v.description, indent=4) }}
{{ v.serde_macros }}
{{ v.get_structable_macros(data_type, sdk_service_name, resource_name, operation_type) }}
{{ v.local_name }}: {{ v.type_hint }},
{%- endif %}
{%- endfor %}
}
{%- else %}
{#- No response data at all #}
/// {{ target_class_name }} response representation
#[derive(Deserialize, Serialize)]
#[derive(Clone, StructTable)]
struct ResponseData {}
{%- endif %}
{%- elif data_type.__class__.__name__ == "TupleStruct" %}
{#- tuple struct requires custom implementation of StructTable #}
/// {{ target_class_name }} response representation
#[derive(Deserialize, Serialize)]
#[derive(Clone)]
struct ResponseData(
{%- for field in data_type.tuple_fields %}
{{ field.type_hint }},
{%- endfor %}
);
impl StructTable for ResponseData {
fn build(&self, _: &OutputConfig) -> (Vec<String>,
Vec<Vec<String>>) {
let headers: Vec<String> = Vec::from(["Value".to_string()]);
let res: Vec<Vec<String>> = Vec::from([Vec::from([self.0.
to_string()])]);
(headers, res)
}
}
impl StructTable for Vec<ResponseData> {
fn build(&self, _: &OutputConfig) -> (Vec<String>,
Vec<Vec<String>>) {
let headers: Vec<String> = Vec::from(["Values".to_string()]);
let res: Vec<Vec<String>> =
Vec::from([Vec::from([self.into_iter().map(|v| v.0.
to_string()).collect::<Vec<_>>().join(", ")])]);
(headers, res)
}
}
{%- elif data_type.__class__.__name__ == "HashMapResponse" %}
/// Response data as HashMap type
#[derive(Deserialize, Serialize)]
struct ResponseData(HashMap<String, {{ data_type.value_type.type_hint }}>);
impl StructTable for ResponseData {
fn build(&self, _options: &OutputConfig) -> (Vec<String>, Vec<Vec<String>>) {
let headers: Vec<String> = Vec::from(["Name".to_string(), "Value".to_string()]);
let mut rows: Vec<Vec<String>> = Vec::new();
rows.extend(
self.0
.iter()
{%- if data_type.value_type.type_hint == "Value" %}
.map(|(k, v)| Vec::from([k.clone(), serde_json::to_string(&v).expect("Is a valid data")])),
{%- elif data_type.value_type.type_hint == "String" %}
.map(|(k, v)| Vec::from([k.clone(), v.clone()])),
{%- elif data_type.value_type.__class__.__name__ == "Option" %}
.map(|(k, v)| Vec::from([k.clone(), v.clone().unwrap_or(String::new()).to_string()])),
{%- else %}
.map(|(k, v)| Vec::from([k.clone(), v.to_string()])),
{%- endif %}
);
(headers, rows)
}
}
{%- endif %}
{%- endwith %}
{%- for subtype in response_type_manager.get_subtypes() %}
{%- if subtype["fields"] is defined %}
/// `{{ subtype.base_type }}` response type
#[derive(Default)]
#[derive(Clone)]
#[derive(Deserialize, Serialize)]
{{ subtype.base_type }} {{ subtype.name }} {
{%- for k, v in subtype.fields | dictsort %}
{{ v.local_name }}: {{ v.type_hint }},
{%- endfor %}
}
impl fmt::Display for {{ subtype.name }} {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let data = Vec::from([
{%- for k, v in subtype.fields | dictsort %}
format!(
"{{v.local_name}}={}",
self
.{{ v.local_name }}
{%- if v.type_hint.startswith("Option") %}
{%- if v.type_hint not in ["Option<i32>", "Option<i64>", "Option<f32>", "Option<f64>", "Option<bool>"] %}
.clone()
{%- endif %}
.map_or(String::new(), |v| v.to_string())
{%- endif %}
),
{%- endfor %}
]);
write!(
f,
"{}",
data
.join(";")
)
}
}
{%- elif subtype.base_type == "vec" %}
/// Vector of `{{ subtype.item_type.type_hint}}` response type
#[derive(Default)]
#[derive(Clone)]
#[derive(Deserialize, Serialize)]
struct Vec{{ subtype.item_type.type_hint}}(Vec<{{subtype.item_type.type_hint}}>);
impl fmt::Display for Vec{{ subtype.item_type.type_hint }} {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"[{}]",
self.0
.iter()
.map(|v| v.to_string() )
.collect::<Vec<String>>()
.join(",")
)
}
}
{%- elif subtype.base_type == "dict" %}
/// HashMap of `{{ subtype.value_type.type_hint }}` response type
#[derive(Default)]
#[derive(Clone)]
#[derive(Deserialize, Serialize)]
struct {{ subtype.type_hint }}(HashMap<String, {{ subtype.value_type.type_hint }}>);
impl fmt::Display for {{ subtype.type_hint }} {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{{ '{{{}}}' }}",
self.0
.iter()
{%- if subtype.value_type.__class__.__name__ == "Option" %}
.map(|v| format!("{}={}", v.0, v.1.clone().unwrap_or(String::new())))
{%- else %}
.map(|v| format!("{}={}", v.0, v.1))
{%- endif %}
.collect::<Vec<String>>()
.join("\n")
)
}
}
{%- endif %}
{%- endfor %}

View File

@ -7,7 +7,7 @@
{%- macro docstring(doc, indent=0) %}
{#- docstring for an element #}
{%- if doc %}
{{ (' ' * indent) }}/// {{ doc | trim("\n") | wrap_markdown(79-indent-4) | replace('\n', '\n' + (' ' * indent) + '/// ') }}
{{ (' ' * indent) }}/// {{ doc | wrap_markdown(79-indent-4) | trim("\n") | replace('\n', '\n' + (' ' * indent) + '/// ') }}
{%- endif %}
{%- endmacro %}

View File

@ -220,24 +220,3 @@ impl ConfirmableRequest for {{ class_name }} {
{%- endif %}
{%- endwith %}
{%- with data_type = response_type_manager.get_root_data_type() %}
{%- if data_type.__class__.__name__ == "StructResponse" %}
{%- if data_type.fields %}
/// {{ response_class_name }} response representation
#[derive(Deserialize, Serialize)]
#[derive(Clone, StructTable)]
pub struct {{ response_class_name }} {
{%- for k, v in data_type.fields | dictsort %}
{% if not (operation_type == "list" and k in ["links"]) %}
{{ macros.docstring(v.description, indent=4) }}
{{ v.serde_macros }}
{{ v.get_structable_macros(data_type, sdk_service_name, resource_name, operation_type) }}
pub {{ v.local_name }}: {{ v.type_hint }},
{%- endif %}
{%- endfor %}
}
{%- endif %}
{%- endif %}
{%- endwith %}

View File

@ -48,16 +48,104 @@ use {{ mod }};
{#- tuple struct requires custom implementation of StructTable #}
/// {{ target_class_name }} response representation
#[derive(Clone, Deserialize, Serialize)]
pub struct {{ class_name }}(
pub struct {{ data_type.name }}(
{%- for field in data_type.tuple_fields %}
{{ field.type_hint }},
{%- endfor %}
);
{%- elif data_type.__class__.__name__ == "HashMapResponse" %}
impl StructTable for {{ data_type.name }} {
fn class_headers<O: StructTableOptions>(
_options: &O,
) -> Option<Vec<String>> {
Some(Vec::from(["Value".to_string()]))
}
fn data<O: StructTableOptions>(
&self,
_options: &O,
) -> ::std::vec::Vec<Option<::std::string::String>> {
Vec::from([Some(self.0.to_string())])
}
}
impl StructTable for &{{ data_type.name }} {
fn class_headers<O: StructTableOptions>(
_options: &O,
) -> Option<Vec<String>> {
Some(Vec::from(["Value".to_string()]))
}
fn data<O: StructTableOptions>(
&self,
_options: &O,
) -> ::std::vec::Vec<Option<::std::string::String>> {
Vec::from([Some(self.0.to_string())])
}
}
{%- elif data_type.__class__.__name__ in ["HashMapResponse", "Dictionary"] %}
/// Response data as HashMap type
#[derive(Deserialize, Serialize)]
pub struct {{ class_name }}(HashMap<String, {{ data_type.value_type.type_hint }}>);
pub struct {{ data_type.name }}(BTreeMap<String, {{ data_type.value_type.type_hint }}>);
impl StructTable for {{ data_type.name }} {
fn instance_headers<O: StructTableOptions>(
&self,
_options: &O,
) -> Option<Vec<String>> {
Some(self.0.keys().map(Into::into).collect())
}
fn data<O: StructTableOptions>(
&self,
_options: &O,
) -> Vec<Option<String>> {
Vec::from_iter(
self.0
.values()
{%- if data_type.value_type.type_hint == "Value" %}
.map(|v| serde_json::to_string(&v).ok()),
{%- elif data_type.value_type.type_hint == "String" %}
.map(|v| Some(v.clone())),
{%- elif data_type.value_type.__class__.__name__ == "Option" %}
.map(|v| v.clone().map(|x| x.to_string())),
{%- else %}
.map(|v| Some(v.to_string())),
{%- endif %}
)
}
}
impl StructTable for &{{ data_type.name }} {
fn instance_headers<O: StructTableOptions>(
&self,
_options: &O,
) -> Option<Vec<String>> {
Some(self.0.keys().map(Into::into).collect())
}
fn data<O: StructTableOptions>(
&self,
_options: &O,
) -> Vec<Option<String>> {
Vec::from_iter(
self.0
.values()
{%- if data_type.value_type.type_hint == "Value" %}
.map(|v| serde_json::to_string(&v).ok()),
{%- elif data_type.value_type.type_hint == "String" %}
.map(|v| Some(v.clone())),
{%- elif data_type.value_type.__class__.__name__ == "Option" %}
.map(|v| v.clone().map(|x| x.to_string())),
{%- else %}
.map(|v| Some(v.to_string())),
{%- endif %}
)
}
}
{%- else %}
// Something may appear here
{%- endif %}
{%- endwith %}
@ -78,11 +166,33 @@ use {{ mod }};
{{ subtype.derive_container_macros }}
{%- if subtype.serde_container_macros %}{{ subtype.serde_container_macros }}{% endif %}
pub enum {{ subtype.name }} {
{% for kind, v in subtype.variants | dictsort %}
{% for kind in subtype.variants | sort %}
// {{ kind or "Empty" }}
{{ subtype.variant_serde_macros(kind) }}
{{ kind or "Empty" }},
{% endfor %}
{%- if subtype.allows_arbitrary_value %}
Other(Option<String>),
{%- endif %}
}
impl std::str::FromStr for {{ subtype.name }} {
type Err = ();
fn from_str(input: &str) -> Result<Self, Self::Err> {
match input {
{%- for kind, vals in subtype.variants | dictsort %}
{%- for val in vals | sort %}
"{{ val }}" => Ok(Self::{{ kind or "Empty" }}),
{%- endfor %}
{%- endfor %}
{%- if subtype.allows_arbitrary_value %}
other => Ok(Self::Other(Some(other.into()))),
{%- else %}
_ => Err(()),
{%- endif %}
}
}
}
{%- elif subtype.base_type == "enum" %}

View File

@ -28,17 +28,95 @@ class TestRustCliResponseManager(TestCase):
super().setUp()
logging.basicConfig(level=logging.DEBUG)
def test_parse_array_of_array_of_strings(self):
def test_generate_array_of_array_of_strings(self):
expected_content = """
/// foo response representation
#[derive(Deserialize, Serialize)]
#[derive(Clone, StructTable)]
struct ResponseData {
// 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0
//
// WARNING: This file is automatically generated from OpenAPI schema using
// `openstack-codegenerator`.
//! Dummy foo command
//!
//! Wraps invoking of the `/url` with `NONE` method
use clap::Args;
use tracing::info;
use openstack_sdk::AsyncOpenStack;
use crate::output::OutputProcessor;
use crate::Cli;
use crate::OpenStackCliError;
use bar::import;
use foo::import;
/// description
#[derive(Args)]
#[command(about = "summary")]
pub struct fooCommand {
/// Request Query parameters
#[command(flatten)]
query: QueryParameters,
/// Path parameters
#[command(flatten)]
path: PathParameters,
/// aoaos
///
#[serde()]
#[structable(optional,pretty)]
foo: Option<Value>,
/// 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=crate::common::parse_json)]
foo: Vec<Vec<String>>,
}
/// Query parameters
#[derive(Args)]
struct QueryParameters {
}
/// Path parameters
#[derive(Args)]
struct PathParameters {
}
impl fooCommand {
/// Perform command action
pub async fn take_action(
&self,
parsed_args: &Cli,
client: &mut AsyncOpenStack,
) -> Result<(), OpenStackCliError> {
info!("Dummy foo");
let op = OutputProcessor::from_args(parsed_args);
op.validate_args(parsed_args)?;
let mut ep_builder = srv::Request::builder();
// Set path parameters
// Set query parameters
// Set body parameters
// Set Request.foo data
ep_builder.foo(self.foo.into_iter());
let ep = ep_builder
.build()
.map_err(|x| OpenStackCliError::EndpointBuild(x.to_string()))?;
let data = ep.query_async(client).await?;
op.output_single::<rsp>(data)?;
Ok(())
}
}
"""
schema = {
@ -61,7 +139,7 @@ struct ResponseData {
parser = model.OpenAPISchemaParser()
(_, all_models) = parser.parse(schema)
cli_rm = rust_cli.ResponseTypeManager()
cli_rm = rust_cli.RequestTypeManager()
cli_rm.set_models(all_models)
env = Environment(
@ -70,17 +148,26 @@ struct ResponseData {
undefined=StrictUndefined,
)
env.filters["wrap_markdown"] = base.wrap_markdown
template = env.get_template("rust_cli/response_struct.j2")
template = env.get_template("rust_cli/impl.rs.j2")
content = template.render(
target_class_name="foo",
response_type_manager=cli_rm,
type_manager=cli_rm,
method=None,
params={},
is_json_patch=False,
sdk_service_name="srv",
resource_name="res",
operation_type="dummy",
microversion=None,
url="/url",
additional_imports={"foo::import", "bar::import"},
command_description="description",
command_summary="summary",
find_present=False,
sdk_mod_path=["openstack_sdk", "api", "srv"],
response_class_name="rsp",
result_is_list=False,
)
self.assertEqual(
"".join([x.rstrip() for x in expected_content.split()]),

View File

@ -170,12 +170,10 @@ pub struct Request<'a> {
pub(crate) os_sch_hnt_scheduler_hints: Option<OsSchHntSchedulerHints<'a>>,
/// scheduler hints description
///
#[builder(default, setter(into))]
pub(crate) os_scheduler_hints: Option<OsSchedulerHints<'a>>,
/// A `server` object.
///
#[builder(setter(into))]
pub(crate) server: Server<'a>,

View File

@ -0,0 +1,111 @@
# 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
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
import logging
from unittest import TestCase
from jinja2 import Environment
from jinja2 import FileSystemLoader
from jinja2 import select_autoescape
from jinja2 import StrictUndefined
from codegenerator import base
from codegenerator import model
from codegenerator import rust_types
class TestRustTypesResponseManager(TestCase):
def setUp(self):
super().setUp()
logging.basicConfig(level=logging.DEBUG)
def test_parse_array_of_array_of_strings(self):
expected_content = """
// 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0
//
// WARNING: This file is automatically generated from OpenAPI schema using
// `openstack-codegenerator`.
//! Response type for the NONE `/url` operation
use serde::{Deserialize, Serialize};
use bar::import;
use foo::import;
/// foo response representation
#[derive(Clone, Deserialize, Serialize, StructTable)]
pub struct Body {
/// aoaos
#[structable(serialize)]
pub foo: Vec<Vec<String>>,
}
"""
schema = {
"type": "object",
"properties": {
"foo": {
"type": ["array", "null"],
"description": "aoaos",
"items": {
"type": "array",
"description": "aos",
"items": {"type": "string"},
"minItems": 1,
"uniqueItems": True,
},
"uniqueItems": True,
}
},
}
parser = model.OpenAPISchemaParser()
(_, all_models) = parser.parse(schema)
rm = rust_types.ResponseTypeManager()
rm.set_models(all_models)
env = Environment(
loader=FileSystemLoader("codegenerator/templates"),
autoescape=select_autoescape(),
undefined=StrictUndefined,
)
env.filters["wrap_markdown"] = base.wrap_markdown
template = env.get_template("rust_types/impl.rs.j2")
content = template.render(
target_class_name="foo",
response_type_manager=rm,
method=None,
params={},
sdk_service_name="srv",
resource_name="res",
operation_type="dummy",
additional_imports={"foo::import", "bar::import"},
url="/url",
)
self.assertEqual(
"".join([x.rstrip() for x in expected_content.split()]),
"".join([x.rstrip() for x in content.split()]),
)