Start building response types in the SDK

Introduce openstack_types crate with certain types that might be
helpful.

In the current form those cannot be reused from cli or tui since it is
necessary to tweak StructableTable to be able to treat different
rendering of cli and tui versus the raw structs.

Change-Id: I23674e0e9119f75739139e015f44d1100d6d84e2
This commit is contained in:
Artem Goncharov 2025-04-04 13:54:41 +02:00
parent 0f870d5a5f
commit b19ca1d4d5
10 changed files with 830 additions and 36 deletions

View File

@ -33,6 +33,7 @@ from codegenerator.openapi_spec import OpenApiSchemaGenerator
# from codegenerator.osc import OSCGenerator
from codegenerator.rust_cli import RustCliGenerator
from codegenerator.rust_tui import RustTuiGenerator
from codegenerator.rust_types import RustTypesGenerator
from codegenerator.rust_sdk import RustSdkGenerator
from codegenerator.types import Metadata
@ -159,6 +160,7 @@ def main():
"rust-sdk",
"rust-cli",
"rust-tui",
"rust-types",
"openapi-spec",
"jsonschema",
"metadata",
@ -201,6 +203,7 @@ def main():
"rust-cli": RustCliGenerator(),
"rust-tui": RustTuiGenerator(),
"rust-sdk": RustSdkGenerator(),
"rust-types": RustTypesGenerator(),
"openapi-spec": OpenApiSchemaGenerator(),
"jsonschema": JsonSchemaGenerator(),
"metadata": MetadataGenerator(),
@ -226,8 +229,13 @@ def main():
continue
for op, op_data in res_data.operations.items():
logging.debug(f"Processing operation {op_data.operation_id}")
if args.target in op_data.targets:
op_args = op_data.targets[args.target]
metadata_target = (
"rust-sdk"
if args.target in ["rust-sdk", "rust-types"]
else args.target
)
if metadata_target in op_data.targets:
op_args = op_data.targets[metadata_target]
if not op_args.service_type:
op_args.service_type = res.split(".")[0]
if not op_args.api_version:
@ -254,7 +262,7 @@ def main():
):
res_mods.append((mod_path, mod_name, path, class_name))
rust_sdk_extensions = res_data.extensions.get("rust-sdk")
if rust_sdk_extensions:
if rust_sdk_extensions and args.target != "rust-types":
additional_modules = rust_sdk_extensions.setdefault(
"additional_modules", []
)
@ -273,7 +281,10 @@ def main():
)
)
if args.target in ["rust-sdk", "rust-tui"] and not args.resource:
if (
args.target in ["rust-sdk", "rust-tui", "rust-types"]
and not args.resource
):
resource_results: dict[str, dict] = {}
for mod_path, mod_name, path, class_name in res_mods:
mn = "/".join(mod_path)

View File

@ -562,6 +562,13 @@ def get_rust_sdk_mod_path(service_type: str, api_version: str, path: str):
return mod_path
def get_rust_types_mod_path(service_type: str, api_version: str, path: str):
"""Construct mod path for rust types crate"""
mod_path = [service_type.replace("-", "_"), api_version]
mod_path.extend([x.lower() for x in get_resource_names_from_url(path)])
return mod_path
def get_rust_cli_mod_path(service_type: str, api_version: str, path: str):
"""Construct mod path for rust sdk"""
mod_path = [service_type.replace("-", "_"), api_version]

View File

@ -26,6 +26,19 @@ from codegenerator import common
CODEBLOCK_RE = re.compile(r"```(\w*)$")
BASIC_FIELDS = [
"id",
"name",
"title",
"created_at",
"updated_at",
"uuid",
"state",
"status",
"operating_status",
]
class Boolean(BasePrimitiveType):
"""Basic Boolean"""
@ -264,6 +277,20 @@ class Dictionary(BaseCombinedType):
base_type: str = "dict"
value_type: BasePrimitiveType | BaseCombinedType | BaseCompoundType
@property
def imports(self):
imports: set[str] = {"std::collections::HashMap"}
imports.update(self.value_type.imports)
return imports
@property
def type_hint(self):
return f"HashMap<String, {self.value_type.type_hint}>"
@property
def lifetimes(self):
return set()
class StructField(BaseModel):
local_name: str
@ -338,6 +365,101 @@ class Struct(BaseCompoundType):
return set()
class StructFieldResponse(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 len(macros) > 0:
return f"#[serde({', '.join(sorted(macros))})]"
return ""
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(Struct):
field_type_class_: Type[StructField] = StructFieldResponse
@property
def imports(self):
imports: set[str] = {"serde::Deserialize", "serde::Serialize"}
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
@property
def static_lifetime(self):
"""Return Rust `<'lc>` lifetimes representation"""
return f"<{', '.join(self.lifetimes)}>" if self.lifetimes else ""
class EnumKind(BaseModel):
name: str
description: str | None = None
@ -346,7 +468,11 @@ class EnumKind(BaseModel):
@property
def type_hint(self):
if isinstance(self.data_type, Struct):
return self.data_type.name + self.data_type.static_lifetime
print(f"Getting type hint of {self.data_type}")
try:
return self.data_type.name + self.data_type.static_lifetime
except Exception as ex:
print(f"Error {ex}")
return self.data_type.type_hint
@property
@ -361,6 +487,14 @@ class Enum(BaseCompoundType):
original_data_type: BaseCompoundType | BaseCompoundType | None = None
_kind_type_class = EnumKind
@property
def derive_container_macros(self) -> str:
return "#[derive(Debug, Deserialize, Clone, Serialize)]"
@property
def serde_container_macros(self) -> str:
return "#[serde(untagged)]"
@property
def type_hint(self):
return self.name + (
@ -394,14 +528,18 @@ class StringEnum(BaseCompoundType):
variants: dict[str, set[str]] = {}
imports: set[str] = {"serde::Deserialize", "serde::Serialize"}
lifetimes: set[str] = set()
derive_container_macros: str = (
"#[derive(Debug, Deserialize, Clone, Serialize)]"
)
builder_container_macros: str | None = None
serde_container_macros: str | None = None # "#[serde(untagged)]"
serde_macros: set[str] | None = None
original_data_type: BaseCompoundType | BaseCompoundType | None = None
@property
def derive_container_macros(self) -> str:
return "#[derive(Debug, Deserialize, Clone, Serialize)]"
@property
def serde_container_macros(self) -> str:
return "#[serde(untagged)]"
@property
def type_hint(self):
"""Get type hint"""
@ -435,6 +573,36 @@ class StringEnum(BaseCompoundType):
return "#[serde(" + ", ".join(sorted(macros)) + ")]"
class HashMapResponse(Dictionary):
"""Wrapper around a simple dictionary to implement Display trait"""
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 TupleStruct(Struct):
"""Rust tuple struct without named fields"""
base_type: str = "struct"
tuple_fields: list[StructField] = []
@property
def imports(self):
imports: set[str] = set()
for field in self.tuple_fields:
imports.update(field.data_type.imports)
return imports
class RequestParameter(BaseModel):
"""OpenAPI request parameter in the Rust SDK form"""
@ -521,6 +689,8 @@ class TypeManager:
#: List of the models to be ignored
ignored_models: list[model.Reference] = []
root_name: str | None = "Body"
def __init__(self):
self.models = []
self.refs = {}
@ -672,7 +842,9 @@ class TypeManager:
)
if not model_ref:
model_ref = model.Reference(name="Body", type=typ.__class__)
model_ref = model.Reference(
name=self.root_name, type=typ.__class__
)
self.refs[model_ref] = typ
return typ
@ -901,8 +1073,14 @@ class TypeManager:
name = getattr(model_data_type, "name", None)
if (
name
and name in unique_models
and unique_models[name].hash_ != model_.reference.hash_
and model_.reference
and (
(
name in unique_models
and unique_models[name].hash_ != model_.reference.hash_
)
or name == self.root_name
)
):
# There is already a model with this name.
if model_.reference and model_.reference.parent:
@ -975,6 +1153,7 @@ class TypeManager:
elif (
name
and name in unique_models
and model_.reference
and unique_models[name].hash_ == model_.reference.hash_
# image.metadef.namespace have weird occurences of itself
and model_.reference != unique_models[name]
@ -993,12 +1172,12 @@ class TypeManager:
if (
k
and isinstance(v, (Enum, Struct, StringEnum))
and k.name != "Body"
and k.name != self.root_name
):
yield v
elif (
k
and k.name != "Body"
and k.name != self.root_name
and isinstance(v, self.option_type_class)
):
if isinstance(v.item_type, Enum):
@ -1007,7 +1186,7 @@ class TypeManager:
def get_root_data_type(self):
"""Get TLA type"""
for k, v in self.refs.items():
if not k or (k.name == "Body" and isinstance(v, Struct)):
if not k or (k.name == self.root_name and isinstance(v, Struct)):
if isinstance(v.fields, dict):
# There might be tuple Struct (with
# fields as list)
@ -1022,7 +1201,9 @@ class TypeManager:
)
v.fields[field_names[0]].is_optional = False
return v
elif not k or (k.name == "Body" and isinstance(v, Dictionary)):
elif not k or (
k.name == self.root_name and isinstance(v, Dictionary)
):
# Response is a free style Dictionary
return v
# No root has been found, make a dummy one

View File

@ -645,7 +645,7 @@ class RequestTypeManager(common_rust.TypeManager):
)
if not model_ref:
model_ref = model.Reference(
name="Body", type=typ.__class__
name=self.root_name, type=typ.__class__
)
if type_model.value_type.reference:
self.ignored_models.append(
@ -999,7 +999,7 @@ class ResponseTypeManager(common_rust.TypeManager):
common_rust.Array,
),
)
and k.name != "Body"
and k.name != self.root_name
):
key = v.base_type + v.type_hint
if key not in emited_data:
@ -1023,7 +1023,7 @@ class RustCliGenerator(BaseGenerator):
:param *args: Path to the code to format
"""
for path in args:
subprocess.run(["rustfmt", "--edition", "2021", path])
subprocess.run(["rustfmt", "--edition", "2024", path])
def get_parser(self, parser):
parser.add_argument(
@ -1284,7 +1284,8 @@ class RustCliGenerator(BaseGenerator):
)
response_type_manager.refs[
model.Reference(
name="Body", type=HashMapResponse
name=response_type_manager.root_name,
type=HashMapResponse,
)
] = root_dict
@ -1329,7 +1330,10 @@ class RustCliGenerator(BaseGenerator):
tuple_struct = TupleStruct(name="Response")
tuple_struct.tuple_fields.append(field)
response_type_manager.refs[
model.Reference(name="Body", type=TupleStruct)
model.Reference(
name=response_type_manager.root_name,
type=TupleStruct,
)
] = tuple_struct
elif (
response_def["type"] == "array"

View File

@ -345,7 +345,7 @@ class RustSdkGenerator(BaseGenerator):
:param *args: Path to the code to format
"""
for path in args:
subprocess.run(["rustfmt", "--edition", "2021", path])
subprocess.run(["rustfmt", "--edition", "2024", path])
def get_parser(self, parser):
parser.add_argument(

View File

@ -336,7 +336,7 @@ class TypeManager(common_rust.TypeManager):
"""Get all subtypes excluding TLA"""
for k, v in self.refs.items():
if self.sdk_type_manager:
if k.name == "Body":
if k.name == self.root_name:
sdk_type = self.sdk_type_manager.get_root_data_type()
else:
sdk_type = self.sdk_type_manager.refs[k]
@ -347,12 +347,12 @@ class TypeManager(common_rust.TypeManager):
and isinstance(
v, (common_rust.Enum, Struct, common_rust.StringEnum)
)
and k.name != "Body"
and k.name != self.root_name
):
yield (v, sdk_type)
elif (
k
and k.name != "Body"
and k.name != self.root_name
and isinstance(v, self.option_type_class)
):
if isinstance(v.item_type, common_rust.Enum):
@ -485,7 +485,7 @@ class ResponseTypeManager(common_rust.TypeManager):
common_rust.Array,
),
)
and k.name != "Body"
and k.name != self.root_name
):
key = v.base_type + v.type_hint
if key not in emited_data:

462
codegenerator/rust_types.py Normal file
View File

@ -0,0 +1,462 @@
# 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 pathlib import Path
import re
import subprocess
from typing import Type, Any
from codegenerator.base import BaseGenerator
from codegenerator.common import BasePrimitiveType
from codegenerator.common import BaseCombinedType
from codegenerator.common import BaseCompoundType
from codegenerator import common
from codegenerator import model
from codegenerator.common import BaseCompoundType
from codegenerator.common import rust as common_rust
class IntString(common.BasePrimitiveType):
"""CLI Integer or String"""
imports: set[str] = {"crate::common::IntString"}
type_hint: str = "IntString"
clap_macros: set[str] = set()
class NumString(common.BasePrimitiveType):
"""CLI Number or String"""
imports: set[str] = {"crate::common::NumString"}
type_hint: str = "NumString"
clap_macros: set[str] = set()
class BoolString(common.BasePrimitiveType):
"""CLI Boolean or String"""
imports: set[str] = {"crate::common::BoolString"}
type_hint: str = "BoolString"
clap_macros: set[str] = set()
class ResponseTypeManager(common_rust.TypeManager):
primitive_type_mapping = {}
data_type_mapping = {model.Struct: common_rust.StructResponse}
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 self.root_name or "Response"
return "".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 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("serde::Deserialize")
imports.discard("serde::Serialize")
return imports
class RustTypesGenerator(BaseGenerator):
def __init__(self):
super().__init__()
def _format_code(self, *args):
"""Format code using Rustfmt
:param *args: Path to the code to format
"""
for path in args:
subprocess.run(["rustfmt", path])
def get_parser(self, parser):
# parser.add_argument(
# "--response-key",
# help="Rust types response key (only required when normal detection does not work)",
# )
return parser
def _render_command(
self, context: dict, impl_template: str, impl_dest: Path
):
"""Render command code"""
self._render(impl_template, context, impl_dest.parent, impl_dest.name)
def generate(
self, res, target_dir, openapi_spec=None, operation_id=None, args=None
):
"""Generate code for the Rust openstack_types"""
logging.debug(
"Generating Rust Types code for %s in %s [%s]",
operation_id,
target_dir,
args,
)
if not openapi_spec:
openapi_spec = common.get_openapi_spec(args.openapi_yaml_spec)
if not operation_id:
operation_id = args.openapi_operation_id
(path, method, spec) = common.find_openapi_operation(
openapi_spec, operation_id
)
if args.operation_type == "find" or method == "HEAD":
return
# srv_name, resource_name = res.split(".") if res else (None, None)
path_resources = common.get_resource_names_from_url(path)
resource_name = common.get_resource_names_from_url(path)[-1]
mime_type = None
openapi_parser = model.OpenAPISchemaParser()
# Collect all operation parameters
# Process body information
# List of operation variants (based on the body)
operation_variants = common.get_operation_variants(
spec, args.operation_name
)
api_ver_matches: re.Match | None = None
path_elements = path.lstrip("/").split("/")
api_ver: dict[str, int] = {}
ver_prefix: str | None = None
if path_elements:
api_ver_matches = re.match(common.VERSION_RE, path_elements[0])
if api_ver_matches and api_ver_matches.groups():
# Remember the version prefix to discard it in the template
ver_prefix = path_elements[0]
for operation_variant in operation_variants:
logging.debug(f"Processing variant {operation_variant}")
response_type_manager: common_rust.TypeManager = (
ResponseTypeManager()
)
additional_imports = set()
result_is_list: bool = False
is_list_paginated: bool = False
if api_ver_matches:
api_ver = {
"major": api_ver_matches.group(1),
"minor": api_ver_matches.group(3) or 0,
}
else:
api_ver = {}
class_name = "".join(
x.capitalize()
for x in re.split(common.SPLIT_NAME_RE, resource_name)
)
response_type_manager.root_name = class_name + "Response"
operation_body = operation_variant.get("body")
mod_name = "_".join(
x.lower()
for x in re.split(
common.SPLIT_NAME_RE,
(
args.module_name
or args.operation_name
or args.operation_type.value
or method
),
)
)
mod_path = common.get_rust_types_mod_path(
args.service_type,
args.api_version,
args.alternative_module_path or path,
)
mod_path.append("response")
response_key: str | None = None
result_def: dict = {}
response_def: dict | None = {}
resource_header_metadata: dict = {}
# Get basic information about response
if method.upper() != "HEAD":
response = common.find_response_schema(
spec["responses"],
args.response_key or resource_name,
(
args.operation_name
if args.operation_type == "action"
else None
),
)
if response:
if args.response_key:
response_key = (
args.response_key
if args.response_key != "null"
else None
)
else:
response_key = resource_name
response_def, _ = common.find_resource_schema(
response, None, response_key
)
if response_def:
if response_def.get("type", "object") == "object" or (
# BS metadata is defined with type: ["object",
# "null"]
isinstance(response_def.get("type"), list)
and "object" in response_def["type"]
):
(root, response_types) = openapi_parser.parse(
response_def
)
if isinstance(root, model.Dictionary):
value_type: (
common_rust.BasePrimitiveType
| common_rust.BaseCombinedType
| common_rust.BaseCompoundType
| None
) = None
try:
value_type = (
response_type_manager.convert_model(
root.value_type
)
)
except Exception:
# In rare cases we can not 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 = common_rust.JsonValue()
# 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
)
response_type_manager.refs[
model.Reference(
name=response_type_manager.root_name,
type=common_rust.HashMapResponse,
)
] = root_dict
else:
response_type_manager.set_models(
response_types
)
elif response_def["type"] == "string":
(root_dt, _) = openapi_parser.parse(response_def)
if not root_dt:
raise RuntimeError(
"Response data can not be processed"
)
field = common_rust.StructField(
local_name="dummy",
remote_name="dummy",
data_type=response_type_manager.convert_model(
root_dt
),
is_optional=False,
)
tuple_struct = common_rust.TupleStruct(
name=class_name
)
tuple_struct.tuple_fields.append(field)
response_type_manager.refs[
model.Reference(
name=response_type_manager.root_name,
type=common_rust.TupleStruct,
)
] = tuple_struct
elif (
response_def["type"] == "array"
and "items" in response_def
):
(_, response_types) = openapi_parser.parse(
response_def["items"]
)
response_type_manager.set_models(response_types)
response_props = response.get("properties", {})
if (
response_props
and response_props[
list(response_props.keys())[0]
].get("type")
== "array"
):
result_is_list = True
else:
return
additional_imports.update(response_type_manager.get_imports())
context = {
"operation_id": operation_id,
"operation_type": spec.get(
"x-openstack-operation-type", args.operation_type
),
"command_description": common_rust.sanitize_rust_docstrings(
common.make_ascii_string(spec.get("description"))
),
"class_name": class_name,
"sdk_service_name": common.get_rust_service_type_from_str(
args.service_type
),
"url": path.lstrip("/").lstrip(ver_prefix).lstrip("/"),
"method": method,
"response_type_manager": response_type_manager,
"api_ver": api_ver,
"target_class_name": class_name,
"resource_name": resource_name,
"additional_imports": additional_imports,
}
work_dir = Path(target_dir, "rust", "openstack_types", "src")
impl_path = Path(work_dir, "/".join(mod_path), f"{mod_name}.rs")
# Generate methods for the GET resource command
self._render_command(context, "rust_types/impl.rs.j2", impl_path)
self._format_code(impl_path)
yield (mod_path, mod_name, "response", class_name)
def generate_mod(
self, target_dir, mod_path, mod_list, url, resource_name, service_name
):
"""Generate collection module (include individual modules)"""
work_dir = Path(target_dir, "rust", "openstack_types", "src")
impl_path = Path(
work_dir, "/".join(mod_path[0:-1]), f"{mod_path[-1]}.rs"
)
context = {
"mod_list": mod_list,
"mod_path": mod_path,
"url": url,
"resource_name": resource_name,
"service_name": service_name,
}
# Generate methods for the GET resource command
self._render_command(context, "rust_types/mod.rs.j2", impl_path)
self._format_code(impl_path)

View File

@ -0,0 +1,99 @@
// 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`.
{% import 'rust_macros.j2' as macros with context -%}
//! Response type for the {{ method }} {{ url }} operation
use serde::{Deserialize, Serialize};
{% for mod in additional_imports | sort -%}
use {{ mod }};
{% endfor %}
{% 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(Clone, Deserialize, Serialize)]
struct {{ data_type.name }} {
{%- for k, v in data_type.fields | dictsort %}
{% if not (operation_type == "list" and k in ["links"]) %}
{{ macros.docstring(v.description, indent=4) }}
{% if v.serde_macros -%}
{{ v.serde_macros }}
{% endif -%}
{{ v.local_name }}: {{ v.type_hint }},
{%- endif %}
{%- endfor %}
}
{%- else %}
{#- No response data at all #}
{%- 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 {{ class_name }}(
{%- for field in data_type.tuple_fields %}
{{ field.type_hint }},
{%- endfor %}
);
{%- elif data_type.__class__.__name__ == "HashMapResponse" %}
/// Response data as HashMap type
#[derive(Deserialize, Serialize)]
struct {{ class_name }}(HashMap<String, {{ data_type.value_type.type_hint }}>);
{%- endif %}
{%- endwith %}
{%- for subtype in response_type_manager.get_subtypes() %}
{%- if subtype["fields"] is defined %}
{{ macros.docstring(subtype.description, indent=0) }}
/// `{{ subtype.name }}` type
#[derive(Clone, Debug)]
#[derive(Deserialize, Serialize)]
{{ subtype.base_type }} {{ subtype.name }} {
{%- for k, v in subtype.fields | dictsort %}
{{ v.local_name }}: {{ v.type_hint }},
{%- endfor %}
}
{%- elif subtype.base_type == "enum" and subtype.__class__.__name__ == "StringEnum" %}
{{ macros.docstring(subtype.description, indent=0) }}
{{ subtype.derive_container_macros }}
{{ subtype.serde_container_macros }}
pub enum {{ subtype.name }} {
{% for kind in subtype.variants %}
// {{ kind or "Empty" }}
{{ subtype.variant_serde_macros(kind) }}
{{ kind or "Empty" }},
{% endfor %}
}
{%- elif subtype.base_type == "enum" %}
{{ macros.docstring(subtype.description, indent=0) }}
{{ subtype.derive_container_macros }}
{{ subtype.serde_container_macros }}
pub enum {{ subtype.name }} {
{% for kind, def in subtype.kinds.items() %}
// {{ kind }}
{{ kind }}({{ def.type_hint }}),
{%- endfor %}
}
{%- endif %}
{%- endfor %}

View File

@ -0,0 +1,30 @@
// 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`.
{% if mod_path|length > 2 %}
//! `{{ url }}` REST operations of {{ service_name }}
{%- else %}
//! `{{ service_name|capitalize }}` Service bindings
{%- endif %}
{%- for mod in mod_list|sort %}
{%- if mod in ["trait", "type"] %}
pub mod r#{{ mod }};
{%- else %}
pub mod {{ mod }};
{%- endif %}
{%- endfor %}

View File

@ -14,34 +14,34 @@
codegenerator_service_metadata_target_map:
- service: "block-storage"
metadata: "metadata/block-storage_metadata.yaml"
targets: ["rust-sdk", "rust-cli", "rust-tui"]
targets: ["rust-sdk", "rust-cli", "rust-tui", "rust-types"]
- service: "compute"
metadata: "metadata/compute_metadata.yaml"
targets: ["rust-sdk", "rust-cli", "rust-tui"]
targets: ["rust-sdk", "rust-cli", "rust-tui", "rust-types"]
- service: "container-infrastructure-management"
metadata: "metadata/container-infrastructure-management_metadata.yaml"
targets: ["rust-sdk", "rust-cli"]
targets: ["rust-sdk", "rust-cli", "rust-types"]
- service: "dns"
metadata: "metadata/dns_metadata.yaml"
targets: ["rust-sdk", "rust-cli", "rust-tui"]
targets: ["rust-sdk", "rust-cli", "rust-tui", "rust-types"]
- service: "identity"
metadata: "metadata/identity_metadata.yaml"
targets: ["rust-sdk", "rust-cli", "rust-tui"]
targets: ["rust-sdk", "rust-cli", "rust-tui", "rust-types"]
- service: "image"
metadata: "metadata/image_metadata.yaml"
targets: ["rust-sdk", "rust-cli", "rust-tui"]
targets: ["rust-sdk", "rust-cli", "rust-tui", "rust-types"]
- service: "load-balancer"
metadata: "metadata/load-balancer_metadata.yaml"
targets: ["rust-sdk", "rust-cli", "rust-tui"]
targets: ["rust-sdk", "rust-cli", "rust-tui", "rust-types"]
- service: "network"
metadata: "metadata/network_metadata.yaml"
targets: ["rust-sdk", "rust-cli", "rust-tui"]
targets: ["rust-sdk", "rust-cli", "rust-tui", "rust-types"]
- service: "object-store"
metadata: "metadata/object-store_metadata.yaml"
targets: ["rust-sdk"]
targets: ["rust-sdk", "rust-types"]
- service: "placement"
metadata: "metadata/placement_metadata.yaml"
targets: ["rust-sdk", "rust-cli"]
targets: ["rust-sdk", "rust-cli", "rust-types"]
# - service: "shared-file-system"
# metadata: "metadata/shared-file-system_metadata.yaml"
# targets: ["rust-sdk"]