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:
parent
0f870d5a5f
commit
b19ca1d4d5
@ -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)
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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(
|
||||
|
@ -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
462
codegenerator/rust_types.py
Normal 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)
|
99
codegenerator/templates/rust_types/impl.rs.j2
Normal file
99
codegenerator/templates/rust_types/impl.rs.j2
Normal 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 %}
|
30
codegenerator/templates/rust_types/mod.rs.j2
Normal file
30
codegenerator/templates/rust_types/mod.rs.j2
Normal 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 %}
|
||||
|
@ -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"]
|
||||
|
Loading…
x
Reference in New Issue
Block a user