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:
parent
f3093fb583
commit
1ae47e0b0e
@ -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
|
||||
|
@ -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"""
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
|
@ -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[
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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)?;
|
||||
|
@ -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 %}
|
||||
|
@ -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)?;
|
||||
|
@ -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 %}
|
||||
|
@ -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
|
||||
|
@ -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 %}
|
@ -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 %}
|
||||
|
||||
|
@ -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 %}
|
||||
|
@ -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" %}
|
||||
|
@ -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()]),
|
||||
|
@ -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>,
|
||||
|
||||
|
111
codegenerator/tests/unit/test_rust_types.py
Normal file
111
codegenerator/tests/unit/test_rust_types.py
Normal 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()]),
|
||||
)
|
Loading…
x
Reference in New Issue
Block a user