Merge "Start building response types in the SDK"
This commit is contained in:
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user