From 8800529ad42a4e723ecb836dea38657a3497d779 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Sun, 12 Jan 2025 16:44:34 +0100 Subject: [PATCH] Generate TUI response structure Change-Id: I2b9f37ebb5e08e36b969aee148bc571ed587c7ae --- codegenerator/rust_tui.py | 289 +++++++++++++++++--- codegenerator/templates/rust_tui/impl.rs.j2 | 21 ++ 2 files changed, 277 insertions(+), 33 deletions(-) diff --git a/codegenerator/rust_tui.py b/codegenerator/rust_tui.py index b2ccb87..5c732c4 100644 --- a/codegenerator/rust_tui.py +++ b/codegenerator/rust_tui.py @@ -20,11 +20,24 @@ from codegenerator.base import BaseGenerator from codegenerator import common from codegenerator import model from codegenerator.common import BaseCompoundType +from codegenerator.common import BaseCombinedType +from codegenerator.common import BasePrimitiveType from codegenerator.common import rust as common_rust from codegenerator.rust_sdk import TypeManager as SdkTypeManager from codegenerator import rust_sdk +BASIC_FIELDS = [ + "name", + "title", + "created_at", + "updated_at", + "state", + "status", + "operating_status", +] + + class String(common_rust.String): type_hint: str = "String" @@ -172,6 +185,83 @@ 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 @@ -241,6 +331,112 @@ 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 _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