avrotize 2.21.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- avrotize/__init__.py +66 -0
- avrotize/__main__.py +6 -0
- avrotize/_version.py +34 -0
- avrotize/asn1toavro.py +160 -0
- avrotize/avrotize.py +152 -0
- avrotize/avrotocpp/CMakeLists.txt.jinja +77 -0
- avrotize/avrotocpp/build.bat.jinja +7 -0
- avrotize/avrotocpp/build.sh.jinja +7 -0
- avrotize/avrotocpp/dataclass_body.jinja +108 -0
- avrotize/avrotocpp/vcpkg.json.jinja +21 -0
- avrotize/avrotocpp.py +483 -0
- avrotize/avrotocsharp/README.md.jinja +166 -0
- avrotize/avrotocsharp/class_test.cs.jinja +266 -0
- avrotize/avrotocsharp/dataclass_core.jinja +293 -0
- avrotize/avrotocsharp/enum_test.cs.jinja +20 -0
- avrotize/avrotocsharp/project.csproj.jinja +30 -0
- avrotize/avrotocsharp/project.sln.jinja +34 -0
- avrotize/avrotocsharp/run_coverage.ps1.jinja +98 -0
- avrotize/avrotocsharp/run_coverage.sh.jinja +149 -0
- avrotize/avrotocsharp/testproject.csproj.jinja +19 -0
- avrotize/avrotocsharp.py +1180 -0
- avrotize/avrotocsv.py +121 -0
- avrotize/avrotodatapackage.py +173 -0
- avrotize/avrotodb.py +1383 -0
- avrotize/avrotogo/go_enum.jinja +12 -0
- avrotize/avrotogo/go_helpers.jinja +31 -0
- avrotize/avrotogo/go_struct.jinja +151 -0
- avrotize/avrotogo/go_test.jinja +47 -0
- avrotize/avrotogo/go_union.jinja +38 -0
- avrotize/avrotogo.py +476 -0
- avrotize/avrotographql.py +197 -0
- avrotize/avrotoiceberg.py +210 -0
- avrotize/avrotojava/class_test.java.jinja +212 -0
- avrotize/avrotojava/enum_test.java.jinja +21 -0
- avrotize/avrotojava/testproject.pom.jinja +54 -0
- avrotize/avrotojava.py +2156 -0
- avrotize/avrotojs.py +250 -0
- avrotize/avrotojsons.py +481 -0
- avrotize/avrotojstruct.py +345 -0
- avrotize/avrotokusto.py +364 -0
- avrotize/avrotomd/README.md.jinja +49 -0
- avrotize/avrotomd.py +137 -0
- avrotize/avrotools.py +168 -0
- avrotize/avrotoparquet.py +208 -0
- avrotize/avrotoproto.py +359 -0
- avrotize/avrotopython/dataclass_core.jinja +241 -0
- avrotize/avrotopython/enum_core.jinja +87 -0
- avrotize/avrotopython/pyproject_toml.jinja +18 -0
- avrotize/avrotopython/test_class.jinja +97 -0
- avrotize/avrotopython/test_enum.jinja +23 -0
- avrotize/avrotopython.py +626 -0
- avrotize/avrotorust/dataclass_enum.rs.jinja +74 -0
- avrotize/avrotorust/dataclass_struct.rs.jinja +204 -0
- avrotize/avrotorust/dataclass_union.rs.jinja +105 -0
- avrotize/avrotorust.py +435 -0
- avrotize/avrotots/class_core.ts.jinja +140 -0
- avrotize/avrotots/class_test.ts.jinja +77 -0
- avrotize/avrotots/enum_core.ts.jinja +46 -0
- avrotize/avrotots/gitignore.jinja +34 -0
- avrotize/avrotots/index.ts.jinja +0 -0
- avrotize/avrotots/package.json.jinja +23 -0
- avrotize/avrotots/tsconfig.json.jinja +21 -0
- avrotize/avrotots.py +687 -0
- avrotize/avrotoxsd.py +344 -0
- avrotize/cddltostructure.py +1841 -0
- avrotize/commands.json +3496 -0
- avrotize/common.py +834 -0
- avrotize/constants.py +87 -0
- avrotize/csvtoavro.py +132 -0
- avrotize/datapackagetoavro.py +76 -0
- avrotize/dependencies/cpp/vcpkg/vcpkg.json +19 -0
- avrotize/dependencies/cs/net90/dependencies.csproj +29 -0
- avrotize/dependencies/go/go121/go.mod +6 -0
- avrotize/dependencies/java/jdk21/pom.xml +91 -0
- avrotize/dependencies/python/py312/requirements.txt +13 -0
- avrotize/dependencies/rust/stable/Cargo.toml +17 -0
- avrotize/dependencies/typescript/node22/package.json +16 -0
- avrotize/dependency_resolver.py +348 -0
- avrotize/dependency_version.py +432 -0
- avrotize/generic/generic.avsc +57 -0
- avrotize/jsonstoavro.py +2167 -0
- avrotize/jsonstostructure.py +2864 -0
- avrotize/jstructtoavro.py +878 -0
- avrotize/kstructtoavro.py +93 -0
- avrotize/kustotoavro.py +455 -0
- avrotize/openapitostructure.py +717 -0
- avrotize/parquettoavro.py +157 -0
- avrotize/proto2parser.py +498 -0
- avrotize/proto3parser.py +403 -0
- avrotize/prototoavro.py +382 -0
- avrotize/prototypes/any.avsc +19 -0
- avrotize/prototypes/api.avsc +106 -0
- avrotize/prototypes/duration.avsc +20 -0
- avrotize/prototypes/field_mask.avsc +18 -0
- avrotize/prototypes/struct.avsc +60 -0
- avrotize/prototypes/timestamp.avsc +20 -0
- avrotize/prototypes/type.avsc +253 -0
- avrotize/prototypes/wrappers.avsc +117 -0
- avrotize/structuretocddl.py +597 -0
- avrotize/structuretocpp/CMakeLists.txt.jinja +76 -0
- avrotize/structuretocpp/build.bat.jinja +3 -0
- avrotize/structuretocpp/build.sh.jinja +3 -0
- avrotize/structuretocpp/dataclass_body.jinja +50 -0
- avrotize/structuretocpp/vcpkg.json.jinja +11 -0
- avrotize/structuretocpp.py +697 -0
- avrotize/structuretocsharp/class_test.cs.jinja +180 -0
- avrotize/structuretocsharp/dataclass_core.jinja +156 -0
- avrotize/structuretocsharp/enum_test.cs.jinja +36 -0
- avrotize/structuretocsharp/json_structure_converters.cs.jinja +399 -0
- avrotize/structuretocsharp/program.cs.jinja +49 -0
- avrotize/structuretocsharp/project.csproj.jinja +17 -0
- avrotize/structuretocsharp/project.sln.jinja +34 -0
- avrotize/structuretocsharp/testproject.csproj.jinja +18 -0
- avrotize/structuretocsharp/tuple_converter.cs.jinja +121 -0
- avrotize/structuretocsharp.py +2295 -0
- avrotize/structuretocsv.py +365 -0
- avrotize/structuretodatapackage.py +659 -0
- avrotize/structuretodb.py +1125 -0
- avrotize/structuretogo/go_enum.jinja +12 -0
- avrotize/structuretogo/go_helpers.jinja +26 -0
- avrotize/structuretogo/go_interface.jinja +18 -0
- avrotize/structuretogo/go_struct.jinja +187 -0
- avrotize/structuretogo/go_test.jinja +70 -0
- avrotize/structuretogo.py +729 -0
- avrotize/structuretographql.py +502 -0
- avrotize/structuretoiceberg.py +355 -0
- avrotize/structuretojava/choice_core.jinja +34 -0
- avrotize/structuretojava/class_core.jinja +23 -0
- avrotize/structuretojava/enum_core.jinja +18 -0
- avrotize/structuretojava/equals_hashcode.jinja +30 -0
- avrotize/structuretojava/pom.xml.jinja +26 -0
- avrotize/structuretojava/tuple_core.jinja +49 -0
- avrotize/structuretojava.py +938 -0
- avrotize/structuretojs/class_core.js.jinja +33 -0
- avrotize/structuretojs/enum_core.js.jinja +10 -0
- avrotize/structuretojs/package.json.jinja +12 -0
- avrotize/structuretojs/test_class.js.jinja +84 -0
- avrotize/structuretojs/test_enum.js.jinja +58 -0
- avrotize/structuretojs/test_runner.js.jinja +45 -0
- avrotize/structuretojs.py +657 -0
- avrotize/structuretojsons.py +498 -0
- avrotize/structuretokusto.py +639 -0
- avrotize/structuretomd/README.md.jinja +204 -0
- avrotize/structuretomd.py +322 -0
- avrotize/structuretoproto.py +764 -0
- avrotize/structuretopython/dataclass_core.jinja +363 -0
- avrotize/structuretopython/enum_core.jinja +45 -0
- avrotize/structuretopython/map_alias.jinja +21 -0
- avrotize/structuretopython/pyproject_toml.jinja +23 -0
- avrotize/structuretopython/test_class.jinja +103 -0
- avrotize/structuretopython/test_enum.jinja +34 -0
- avrotize/structuretopython.py +799 -0
- avrotize/structuretorust/dataclass_enum.rs.jinja +63 -0
- avrotize/structuretorust/dataclass_struct.rs.jinja +121 -0
- avrotize/structuretorust/dataclass_union.rs.jinja +81 -0
- avrotize/structuretorust.py +714 -0
- avrotize/structuretots/class_core.ts.jinja +78 -0
- avrotize/structuretots/enum_core.ts.jinja +6 -0
- avrotize/structuretots/gitignore.jinja +8 -0
- avrotize/structuretots/index.ts.jinja +1 -0
- avrotize/structuretots/package.json.jinja +39 -0
- avrotize/structuretots/test_class.ts.jinja +35 -0
- avrotize/structuretots/tsconfig.json.jinja +21 -0
- avrotize/structuretots.py +740 -0
- avrotize/structuretoxsd.py +679 -0
- avrotize/xsdtoavro.py +413 -0
- avrotize-2.21.1.dist-info/METADATA +1319 -0
- avrotize-2.21.1.dist-info/RECORD +171 -0
- avrotize-2.21.1.dist-info/WHEEL +4 -0
- avrotize-2.21.1.dist-info/entry_points.txt +3 -0
- avrotize-2.21.1.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
# pylint: disable=line-too-long
|
|
2
|
+
|
|
3
|
+
""" StructureToRust class for converting JSON Structure schema to Rust structs """
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
from typing import Any, Dict, List, Set, Tuple, Union, Optional
|
|
9
|
+
|
|
10
|
+
from avrotize.common import pascal, snake, render_template
|
|
11
|
+
|
|
12
|
+
JsonNode = Dict[str, 'JsonNode'] | List['JsonNode'] | str | None
|
|
13
|
+
|
|
14
|
+
INDENT = ' '
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class StructureToRust:
|
|
18
|
+
""" Converts JSON Structure schema to Rust structs """
|
|
19
|
+
|
|
20
|
+
def __init__(self, base_package: str = '', serde_annotation: bool = False) -> None:
|
|
21
|
+
self.base_package = base_package.replace('.', '/').lower()
|
|
22
|
+
self.serde_annotation = serde_annotation
|
|
23
|
+
self.output_dir = os.getcwd()
|
|
24
|
+
self.schema_doc: JsonNode = None
|
|
25
|
+
self.generated_types_rust_package: Dict[str, str] = {}
|
|
26
|
+
self.generated_structure_types: Dict[str, Dict[str, Union[str, Dict, List]]] = {}
|
|
27
|
+
self.type_dict: Dict[str, Dict] = {}
|
|
28
|
+
self.definitions: Dict[str, Any] = {}
|
|
29
|
+
self.schema_registry: Dict[str, Dict] = {}
|
|
30
|
+
|
|
31
|
+
reserved_words = [
|
|
32
|
+
'as', 'break', 'const', 'continue', 'crate', 'else', 'enum', 'extern', 'false', 'fn', 'for', 'if', 'impl',
|
|
33
|
+
'in', 'let', 'loop', 'match', 'mod', 'move', 'mut', 'pub', 'ref', 'return', 'self', 'Self', 'static',
|
|
34
|
+
'struct', 'super', 'trait', 'true', 'type', 'unsafe', 'use', 'where', 'while', 'async', 'await', 'dyn',
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
def safe_identifier(self, name: str) -> str:
|
|
38
|
+
"""Converts a name to a safe Rust identifier"""
|
|
39
|
+
if name in StructureToRust.reserved_words:
|
|
40
|
+
return f"{name}_"
|
|
41
|
+
return name
|
|
42
|
+
|
|
43
|
+
def escaped_identifier(self, name: str) -> str:
|
|
44
|
+
"""Converts a name to a safe Rust identifier with a leading r# prefix"""
|
|
45
|
+
if name != "crate" and name in StructureToRust.reserved_words:
|
|
46
|
+
return f"r#{name}"
|
|
47
|
+
return name
|
|
48
|
+
|
|
49
|
+
def safe_package(self, package: str) -> str:
|
|
50
|
+
"""Converts a package name to a safe Rust package name"""
|
|
51
|
+
elements = package.split('::')
|
|
52
|
+
return '::'.join([self.escaped_identifier(element) for element in elements])
|
|
53
|
+
|
|
54
|
+
def get_qualified_name(self, namespace: str, name: str) -> str:
|
|
55
|
+
""" Concatenates namespace and name with a dot separator """
|
|
56
|
+
return f"{namespace}.{name}" if namespace != '' else name
|
|
57
|
+
|
|
58
|
+
def sanitize_namespace(self, namespace: str) -> str:
|
|
59
|
+
"""Converts a namespace to a valid Rust module path by replacing dots with underscores"""
|
|
60
|
+
return namespace.replace('.', '_')
|
|
61
|
+
|
|
62
|
+
def concat_namespace(self, namespace: str, name: str) -> str:
|
|
63
|
+
""" Concatenates namespace and name with a dot separator """
|
|
64
|
+
if namespace and name:
|
|
65
|
+
return f"{namespace}.{name}"
|
|
66
|
+
elif namespace:
|
|
67
|
+
return namespace
|
|
68
|
+
else:
|
|
69
|
+
return name
|
|
70
|
+
|
|
71
|
+
def concat_package(self, package: str, name: str) -> str:
|
|
72
|
+
"""Concatenates package and name using a double colon separator"""
|
|
73
|
+
return f"crate::{package.lower()}::{name.lower()}::{name}" if package else name
|
|
74
|
+
|
|
75
|
+
def map_primitive_to_rust(self, structure_type: str, is_optional: bool = False) -> str:
|
|
76
|
+
""" Maps JSON Structure primitive types to Rust types """
|
|
77
|
+
optional_mapping = {
|
|
78
|
+
'null': 'None',
|
|
79
|
+
'boolean': 'Option<bool>',
|
|
80
|
+
'string': 'Option<String>',
|
|
81
|
+
'integer': 'Option<i32>',
|
|
82
|
+
'number': 'Option<f64>',
|
|
83
|
+
'int8': 'Option<i8>',
|
|
84
|
+
'uint8': 'Option<u8>',
|
|
85
|
+
'int16': 'Option<i16>',
|
|
86
|
+
'uint16': 'Option<u16>',
|
|
87
|
+
'int32': 'Option<i32>',
|
|
88
|
+
'uint32': 'Option<u32>',
|
|
89
|
+
'int64': 'Option<i64>',
|
|
90
|
+
'uint64': 'Option<u64>',
|
|
91
|
+
'int128': 'Option<i128>',
|
|
92
|
+
'uint128': 'Option<u128>',
|
|
93
|
+
'float8': 'Option<f32>',
|
|
94
|
+
'float': 'Option<f32>',
|
|
95
|
+
'double': 'Option<f64>',
|
|
96
|
+
'binary32': 'Option<f32>',
|
|
97
|
+
'binary64': 'Option<f64>',
|
|
98
|
+
'decimal': 'Option<f64>',
|
|
99
|
+
'binary': 'Option<Vec<u8>>',
|
|
100
|
+
'date': 'Option<chrono::NaiveDate>',
|
|
101
|
+
'time': 'Option<chrono::NaiveTime>',
|
|
102
|
+
'datetime': 'Option<chrono::DateTime<chrono::Utc>>',
|
|
103
|
+
'timestamp': 'Option<chrono::DateTime<chrono::Utc>>',
|
|
104
|
+
'duration': 'Option<chrono::Duration>',
|
|
105
|
+
'uuid': 'Option<uuid::Uuid>',
|
|
106
|
+
'uri': 'Option<String>',
|
|
107
|
+
'jsonpointer': 'Option<String>',
|
|
108
|
+
'any': 'Option<serde_json::Value>',
|
|
109
|
+
}
|
|
110
|
+
required_mapping = {
|
|
111
|
+
'null': 'None',
|
|
112
|
+
'boolean': 'bool',
|
|
113
|
+
'string': 'String',
|
|
114
|
+
'integer': 'i32',
|
|
115
|
+
'number': 'f64',
|
|
116
|
+
'int8': 'i8',
|
|
117
|
+
'uint8': 'u8',
|
|
118
|
+
'int16': 'i16',
|
|
119
|
+
'uint16': 'u16',
|
|
120
|
+
'int32': 'i32',
|
|
121
|
+
'uint32': 'u32',
|
|
122
|
+
'int64': 'i64',
|
|
123
|
+
'uint64': 'u64',
|
|
124
|
+
'int128': 'i128',
|
|
125
|
+
'uint128': 'u128',
|
|
126
|
+
'float8': 'f32',
|
|
127
|
+
'float': 'f32',
|
|
128
|
+
'double': 'f64',
|
|
129
|
+
'binary32': 'f32',
|
|
130
|
+
'binary64': 'f64',
|
|
131
|
+
'decimal': 'f64',
|
|
132
|
+
'binary': 'Vec<u8>',
|
|
133
|
+
'date': 'chrono::NaiveDate',
|
|
134
|
+
'time': 'chrono::NaiveTime',
|
|
135
|
+
'datetime': 'chrono::DateTime<chrono::Utc>',
|
|
136
|
+
'timestamp': 'chrono::DateTime<chrono::Utc>',
|
|
137
|
+
'duration': 'chrono::Duration',
|
|
138
|
+
'uuid': 'uuid::Uuid',
|
|
139
|
+
'uri': 'String',
|
|
140
|
+
'jsonpointer': 'String',
|
|
141
|
+
'any': 'serde_json::Value',
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
rust_fullname = structure_type
|
|
145
|
+
if '.' in rust_fullname:
|
|
146
|
+
type_name = pascal(structure_type.split('.')[-1])
|
|
147
|
+
package_name = '::'.join(structure_type.split('.')[:-1]).lower()
|
|
148
|
+
rust_fullname = self.safe_package(self.concat_package(package_name, type_name))
|
|
149
|
+
|
|
150
|
+
if rust_fullname in self.generated_types_rust_package:
|
|
151
|
+
return rust_fullname
|
|
152
|
+
else:
|
|
153
|
+
return required_mapping.get(structure_type, 'serde_json::Value') if not is_optional else optional_mapping.get(structure_type, 'Option<serde_json::Value>')
|
|
154
|
+
|
|
155
|
+
def resolve_ref(self, ref: str, context_schema: Optional[Dict] = None) -> Optional[Dict]:
|
|
156
|
+
""" Resolves a $ref to the actual schema definition """
|
|
157
|
+
if not ref.startswith('#/'):
|
|
158
|
+
if ref in self.schema_registry:
|
|
159
|
+
return self.schema_registry[ref]
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
path = ref[2:].split('/')
|
|
163
|
+
schema = context_schema if context_schema else self.schema_doc
|
|
164
|
+
for part in path:
|
|
165
|
+
if not isinstance(schema, dict) or part not in schema:
|
|
166
|
+
return None
|
|
167
|
+
schema = schema[part]
|
|
168
|
+
return schema
|
|
169
|
+
|
|
170
|
+
def register_schema_ids(self, schema: Dict, base_uri: str = '') -> None:
|
|
171
|
+
""" Recursively registers schemas with $id keywords """
|
|
172
|
+
if not isinstance(schema, dict):
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
if '$id' in schema:
|
|
176
|
+
schema_id = schema['$id']
|
|
177
|
+
if base_uri and not schema_id.startswith(('http://', 'https://', 'urn:')):
|
|
178
|
+
from urllib.parse import urljoin
|
|
179
|
+
schema_id = urljoin(base_uri, schema_id)
|
|
180
|
+
self.schema_registry[schema_id] = schema
|
|
181
|
+
base_uri = schema_id
|
|
182
|
+
|
|
183
|
+
if 'definitions' in schema:
|
|
184
|
+
for def_name, def_schema in schema['definitions'].items():
|
|
185
|
+
if isinstance(def_schema, dict):
|
|
186
|
+
self.register_schema_ids(def_schema, base_uri)
|
|
187
|
+
|
|
188
|
+
if 'properties' in schema:
|
|
189
|
+
for prop_name, prop_schema in schema['properties'].items():
|
|
190
|
+
if isinstance(prop_schema, dict):
|
|
191
|
+
self.register_schema_ids(prop_schema, base_uri)
|
|
192
|
+
|
|
193
|
+
for key in ['items', 'values', 'additionalProperties']:
|
|
194
|
+
if key in schema and isinstance(schema[key], dict):
|
|
195
|
+
self.register_schema_ids(schema[key], base_uri)
|
|
196
|
+
|
|
197
|
+
def convert_structure_type_to_rust(self, class_name: str, field_name: str, structure_type: JsonNode, parent_namespace: str, nullable: bool = False) -> str:
|
|
198
|
+
""" Converts JSON Structure type to Rust type """
|
|
199
|
+
ns = self.sanitize_namespace(parent_namespace).replace('.', '::').lower()
|
|
200
|
+
|
|
201
|
+
if isinstance(structure_type, str):
|
|
202
|
+
return self.map_primitive_to_rust(structure_type, nullable)
|
|
203
|
+
elif isinstance(structure_type, list):
|
|
204
|
+
# Handle type unions
|
|
205
|
+
non_null_types = [t for t in structure_type if t != 'null']
|
|
206
|
+
has_null = 'null' in structure_type
|
|
207
|
+
|
|
208
|
+
if len(non_null_types) == 1:
|
|
209
|
+
inner_type = self.convert_structure_type_to_rust(class_name, field_name, non_null_types[0], parent_namespace, False)
|
|
210
|
+
if has_null:
|
|
211
|
+
if inner_type.startswith('Option<'):
|
|
212
|
+
return inner_type
|
|
213
|
+
return f'Option<{inner_type}>'
|
|
214
|
+
return inner_type
|
|
215
|
+
else:
|
|
216
|
+
# Multiple non-null types - generate a union enum
|
|
217
|
+
return self.generate_union_enum(field_name, structure_type, parent_namespace)
|
|
218
|
+
elif isinstance(structure_type, dict):
|
|
219
|
+
# Handle $ref
|
|
220
|
+
if '$ref' in structure_type:
|
|
221
|
+
ref_schema = self.resolve_ref(structure_type['$ref'], self.schema_doc if isinstance(self.schema_doc, dict) else None)
|
|
222
|
+
if ref_schema:
|
|
223
|
+
ref_path = structure_type['$ref'].split('/')
|
|
224
|
+
type_name = ref_path[-1]
|
|
225
|
+
ref_namespace = '.'.join(ref_path[2:-1]) if len(ref_path) > 3 else parent_namespace
|
|
226
|
+
return self.generate_class_or_choice(ref_schema, ref_namespace, write_file=True, explicit_name=type_name)
|
|
227
|
+
return 'serde_json::Value'
|
|
228
|
+
|
|
229
|
+
# Handle enum keyword
|
|
230
|
+
if 'enum' in structure_type:
|
|
231
|
+
return self.generate_enum(structure_type, field_name, parent_namespace, write_file=True)
|
|
232
|
+
|
|
233
|
+
# Handle type keyword
|
|
234
|
+
if 'type' not in structure_type:
|
|
235
|
+
return 'serde_json::Value'
|
|
236
|
+
|
|
237
|
+
struct_type = structure_type['type']
|
|
238
|
+
|
|
239
|
+
# Handle complex types
|
|
240
|
+
if struct_type == 'object':
|
|
241
|
+
return self.generate_class(structure_type, parent_namespace, write_file=True)
|
|
242
|
+
elif struct_type == 'array':
|
|
243
|
+
items_type = self.convert_structure_type_to_rust(
|
|
244
|
+
class_name, field_name+'Item', structure_type.get('items', {'type': 'any'}), parent_namespace)
|
|
245
|
+
return f"Vec<{items_type}>"
|
|
246
|
+
elif struct_type == 'set':
|
|
247
|
+
items_type = self.convert_structure_type_to_rust(
|
|
248
|
+
class_name, field_name+'Item', structure_type.get('items', {'type': 'any'}), parent_namespace)
|
|
249
|
+
return f"std::collections::HashSet<{items_type}>"
|
|
250
|
+
elif struct_type == 'map':
|
|
251
|
+
values_type = self.convert_structure_type_to_rust(
|
|
252
|
+
class_name, field_name+'Value', structure_type.get('values', {'type': 'any'}), parent_namespace)
|
|
253
|
+
return f"std::collections::HashMap<String, {values_type}>"
|
|
254
|
+
elif struct_type == 'choice':
|
|
255
|
+
return self.generate_choice(structure_type, parent_namespace, write_file=True)
|
|
256
|
+
elif struct_type == 'tuple':
|
|
257
|
+
return self.generate_tuple(structure_type, parent_namespace, write_file=True)
|
|
258
|
+
else:
|
|
259
|
+
return self.convert_structure_type_to_rust(class_name, field_name, struct_type, parent_namespace, nullable)
|
|
260
|
+
|
|
261
|
+
return 'serde_json::Value'
|
|
262
|
+
|
|
263
|
+
def generate_class_or_choice(self, structure_schema: Dict, parent_namespace: str, write_file: bool = True, explicit_name: str = '') -> str:
|
|
264
|
+
""" Generates a Class or Choice """
|
|
265
|
+
struct_type = structure_schema.get('type', 'object')
|
|
266
|
+
if struct_type == 'object':
|
|
267
|
+
return self.generate_class(structure_schema, parent_namespace, write_file, explicit_name=explicit_name)
|
|
268
|
+
elif struct_type == 'choice':
|
|
269
|
+
return self.generate_choice(structure_schema, parent_namespace, write_file, explicit_name=explicit_name)
|
|
270
|
+
elif struct_type == 'tuple':
|
|
271
|
+
return self.generate_tuple(structure_schema, parent_namespace, write_file, explicit_name=explicit_name)
|
|
272
|
+
return 'serde_json::Value'
|
|
273
|
+
|
|
274
|
+
def generate_class(self, structure_schema: Dict, parent_namespace: str, write_file: bool, explicit_name: str = '') -> str:
|
|
275
|
+
""" Generates a Rust struct from JSON Structure object type """
|
|
276
|
+
# Get name and namespace
|
|
277
|
+
class_name = pascal(explicit_name if explicit_name else structure_schema.get('name', 'UnnamedClass'))
|
|
278
|
+
schema_namespace = structure_schema.get('namespace', parent_namespace)
|
|
279
|
+
namespace = self.sanitize_namespace(schema_namespace.lower())
|
|
280
|
+
|
|
281
|
+
qualified_struct_name = self.safe_package(self.concat_package(namespace, class_name))
|
|
282
|
+
if qualified_struct_name in self.generated_types_rust_package:
|
|
283
|
+
return qualified_struct_name
|
|
284
|
+
|
|
285
|
+
# Check if this is an abstract type
|
|
286
|
+
is_abstract = structure_schema.get('abstract', False)
|
|
287
|
+
|
|
288
|
+
# Handle inheritance ($extends)
|
|
289
|
+
base_class = None
|
|
290
|
+
if '$extends' in structure_schema:
|
|
291
|
+
base_ref = structure_schema['$extends']
|
|
292
|
+
if isinstance(self.schema_doc, dict):
|
|
293
|
+
base_schema = self.resolve_ref(base_ref, self.schema_doc)
|
|
294
|
+
if base_schema:
|
|
295
|
+
ref_path = base_ref.split('/')
|
|
296
|
+
base_name = ref_path[-1]
|
|
297
|
+
ref_namespace = '.'.join(ref_path[2:-1]) if len(ref_path) > 3 else parent_namespace
|
|
298
|
+
base_class = self.generate_class(base_schema, ref_namespace, write_file=True, explicit_name=base_name)
|
|
299
|
+
|
|
300
|
+
# Generate properties
|
|
301
|
+
properties = structure_schema.get('properties', {})
|
|
302
|
+
required_props = structure_schema.get('required', [])
|
|
303
|
+
|
|
304
|
+
fields = []
|
|
305
|
+
for prop_name, prop_schema in properties.items():
|
|
306
|
+
# Skip const fields for now (they would need to be class constants)
|
|
307
|
+
if 'const' in prop_schema:
|
|
308
|
+
continue
|
|
309
|
+
|
|
310
|
+
original_field_name = prop_name
|
|
311
|
+
field_name = self.safe_identifier(snake(prop_name))
|
|
312
|
+
|
|
313
|
+
# Determine if required
|
|
314
|
+
is_required = prop_name in required_props if not isinstance(required_props, list) or \
|
|
315
|
+
len(required_props) == 0 or not isinstance(required_props[0], list) else \
|
|
316
|
+
any(prop_name in req_set for req_set in required_props)
|
|
317
|
+
|
|
318
|
+
# Get property type
|
|
319
|
+
prop_type = self.convert_structure_type_to_rust(class_name, field_name, prop_schema, schema_namespace, not is_required)
|
|
320
|
+
|
|
321
|
+
# Add Option wrapper if not required and doesn't already have it
|
|
322
|
+
if not is_required and not prop_type.startswith('Option<'):
|
|
323
|
+
prop_type = f'Option<{prop_type}>'
|
|
324
|
+
|
|
325
|
+
serde_rename = field_name != original_field_name
|
|
326
|
+
|
|
327
|
+
fields.append({
|
|
328
|
+
'original_name': original_field_name,
|
|
329
|
+
'name': field_name,
|
|
330
|
+
'type': prop_type,
|
|
331
|
+
'serde_rename': serde_rename,
|
|
332
|
+
'random_value': self.generate_random_value(prop_type)
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
# Get docstring
|
|
336
|
+
doc = structure_schema.get('description', structure_schema.get('doc', class_name))
|
|
337
|
+
|
|
338
|
+
# Prepare context for template
|
|
339
|
+
context = {
|
|
340
|
+
'serde_annotation': self.serde_annotation,
|
|
341
|
+
'doc': doc,
|
|
342
|
+
'struct_name': self.safe_identifier(class_name),
|
|
343
|
+
'fields': fields,
|
|
344
|
+
'is_abstract': is_abstract,
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
file_name = self.to_file_name(qualified_struct_name)
|
|
348
|
+
target_file = os.path.join(self.output_dir, "src", file_name + ".rs")
|
|
349
|
+
render_template('structuretorust/dataclass_struct.rs.jinja', target_file, **context)
|
|
350
|
+
self.write_mod_rs(namespace)
|
|
351
|
+
|
|
352
|
+
self.generated_types_rust_package[qualified_struct_name] = "struct"
|
|
353
|
+
self.generated_structure_types[qualified_struct_name] = structure_schema
|
|
354
|
+
|
|
355
|
+
return qualified_struct_name
|
|
356
|
+
|
|
357
|
+
def generate_enum(self, structure_schema: Dict, field_name: str, parent_namespace: str, write_file: bool) -> str:
|
|
358
|
+
""" Generates a Rust enum from JSON Structure enum keyword """
|
|
359
|
+
enum_values = structure_schema.get('enum', [])
|
|
360
|
+
if not enum_values:
|
|
361
|
+
return 'serde_json::Value'
|
|
362
|
+
|
|
363
|
+
# Determine enum name from field name
|
|
364
|
+
enum_name = pascal(structure_schema.get('name', field_name + 'Enum'))
|
|
365
|
+
schema_namespace = structure_schema.get('namespace', parent_namespace)
|
|
366
|
+
namespace = self.sanitize_namespace(schema_namespace.lower())
|
|
367
|
+
|
|
368
|
+
qualified_enum_name = self.safe_package(self.concat_package(namespace, enum_name))
|
|
369
|
+
if qualified_enum_name in self.generated_types_rust_package:
|
|
370
|
+
return qualified_enum_name
|
|
371
|
+
|
|
372
|
+
# Convert enum values to valid Rust identifiers
|
|
373
|
+
symbols = []
|
|
374
|
+
for value in enum_values:
|
|
375
|
+
if isinstance(value, str):
|
|
376
|
+
# Convert to PascalCase and make it a valid Rust identifier
|
|
377
|
+
symbol = pascal(value.replace('-', '_').replace(' ', '_'))
|
|
378
|
+
symbols.append({'name': symbol, 'value': value})
|
|
379
|
+
else:
|
|
380
|
+
# For numeric values, use Value prefix
|
|
381
|
+
symbols.append({'name': f"Value{value}", 'value': str(value)})
|
|
382
|
+
|
|
383
|
+
doc = structure_schema.get('description', structure_schema.get('doc', enum_name))
|
|
384
|
+
|
|
385
|
+
context = {
|
|
386
|
+
'serde_annotation': self.serde_annotation,
|
|
387
|
+
'enum_name': self.safe_identifier(enum_name),
|
|
388
|
+
'symbols': symbols,
|
|
389
|
+
'doc': doc,
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
file_name = self.to_file_name(qualified_enum_name)
|
|
393
|
+
target_file = os.path.join(self.output_dir, "src", file_name + ".rs")
|
|
394
|
+
render_template('structuretorust/dataclass_enum.rs.jinja', target_file, **context)
|
|
395
|
+
self.write_mod_rs(namespace)
|
|
396
|
+
|
|
397
|
+
self.generated_types_rust_package[qualified_enum_name] = "enum"
|
|
398
|
+
self.generated_structure_types[qualified_enum_name] = structure_schema
|
|
399
|
+
|
|
400
|
+
return qualified_enum_name
|
|
401
|
+
|
|
402
|
+
def generate_choice(self, structure_schema: Dict, parent_namespace: str, write_file: bool, explicit_name: str = '') -> str:
|
|
403
|
+
""" Generates a discriminated union (choice) type """
|
|
404
|
+
choice_name = explicit_name if explicit_name else structure_schema.get('name', 'UnnamedChoice')
|
|
405
|
+
schema_namespace = structure_schema.get('namespace', parent_namespace)
|
|
406
|
+
namespace = self.sanitize_namespace(schema_namespace.lower())
|
|
407
|
+
|
|
408
|
+
qualified_name = self.safe_package(self.concat_package(namespace, pascal(choice_name)))
|
|
409
|
+
if qualified_name in self.generated_types_rust_package:
|
|
410
|
+
return qualified_name
|
|
411
|
+
|
|
412
|
+
choices = structure_schema.get('choices', {})
|
|
413
|
+
|
|
414
|
+
# Generate types for each choice
|
|
415
|
+
choice_types = []
|
|
416
|
+
for choice_key, choice_schema in choices.items():
|
|
417
|
+
if isinstance(choice_schema, dict):
|
|
418
|
+
if '$ref' in choice_schema:
|
|
419
|
+
# Resolve reference and generate the type
|
|
420
|
+
ref_schema = self.resolve_ref(choice_schema['$ref'], self.schema_doc if isinstance(self.schema_doc, dict) else None)
|
|
421
|
+
if ref_schema:
|
|
422
|
+
ref_path = choice_schema['$ref'].split('/')
|
|
423
|
+
ref_name = ref_path[-1]
|
|
424
|
+
ref_namespace = '.'.join(ref_path[2:-1]) if len(ref_path) > 3 else parent_namespace
|
|
425
|
+
type_ref = self.generate_class(ref_schema, ref_namespace, write_file=True, explicit_name=ref_name)
|
|
426
|
+
type_name = type_ref.split('::')[-1]
|
|
427
|
+
choice_types.append({
|
|
428
|
+
'variant_name': pascal(choice_key),
|
|
429
|
+
'type': type_name,
|
|
430
|
+
'tag': choice_key
|
|
431
|
+
})
|
|
432
|
+
elif 'type' in choice_schema:
|
|
433
|
+
# Generate inline type
|
|
434
|
+
rust_type = self.convert_structure_type_to_rust(choice_name, choice_key, choice_schema, schema_namespace)
|
|
435
|
+
choice_types.append({
|
|
436
|
+
'variant_name': pascal(choice_key),
|
|
437
|
+
'type': rust_type,
|
|
438
|
+
'tag': choice_key
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
doc = structure_schema.get('description', structure_schema.get('doc', choice_name))
|
|
442
|
+
|
|
443
|
+
context = {
|
|
444
|
+
'serde_annotation': self.serde_annotation,
|
|
445
|
+
'union_enum_name': self.safe_identifier(pascal(choice_name)),
|
|
446
|
+
'variants': choice_types,
|
|
447
|
+
'doc': doc,
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
file_name = self.to_file_name(qualified_name)
|
|
451
|
+
target_file = os.path.join(self.output_dir, "src", file_name + ".rs")
|
|
452
|
+
render_template('structuretorust/dataclass_union.rs.jinja', target_file, **context)
|
|
453
|
+
self.write_mod_rs(namespace)
|
|
454
|
+
|
|
455
|
+
self.generated_types_rust_package[qualified_name] = "choice"
|
|
456
|
+
self.generated_structure_types[qualified_name] = structure_schema
|
|
457
|
+
|
|
458
|
+
return qualified_name
|
|
459
|
+
|
|
460
|
+
def generate_tuple(self, structure_schema: Dict, parent_namespace: str, write_file: bool, explicit_name: str = '') -> str:
|
|
461
|
+
""" Generates a Rust tuple type from JSON Structure tuple """
|
|
462
|
+
# For tuples, we generate a struct with numbered fields
|
|
463
|
+
tuple_name = explicit_name if explicit_name else structure_schema.get('name', 'UnnamedTuple')
|
|
464
|
+
return self.generate_class(structure_schema, parent_namespace, write_file, explicit_name=tuple_name)
|
|
465
|
+
|
|
466
|
+
def generate_union_enum(self, field_name: str, structure_type: List, namespace: str) -> str:
|
|
467
|
+
"""Generates a union enum for Rust"""
|
|
468
|
+
ns = self.sanitize_namespace(namespace.replace('.', '::').lower())
|
|
469
|
+
union_enum_name = pascal(field_name) + 'Union'
|
|
470
|
+
|
|
471
|
+
non_null_types = [t for t in structure_type if t != 'null']
|
|
472
|
+
has_null = 'null' in structure_type
|
|
473
|
+
|
|
474
|
+
union_types = []
|
|
475
|
+
for i, t in enumerate(non_null_types):
|
|
476
|
+
type_name = self.convert_structure_type_to_rust(field_name + "Option" + str(i), field_name + "Option" + str(i), t, namespace)
|
|
477
|
+
variant_name = pascal(type_name.rsplit('::',1)[-1].replace('<', '').replace('>', '').replace(',', ''))
|
|
478
|
+
union_types.append({
|
|
479
|
+
'variant_name': variant_name,
|
|
480
|
+
'type': type_name,
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
qualified_union_enum_name = self.safe_package(self.concat_package(ns, union_enum_name))
|
|
484
|
+
|
|
485
|
+
context = {
|
|
486
|
+
'serde_annotation': self.serde_annotation,
|
|
487
|
+
'union_enum_name': union_enum_name,
|
|
488
|
+
'variants': union_types,
|
|
489
|
+
'doc': f'Union type for {field_name}',
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
file_name = self.to_file_name(qualified_union_enum_name)
|
|
493
|
+
target_file = os.path.join(self.output_dir, "src", file_name + ".rs").lower()
|
|
494
|
+
render_template('structuretorust/dataclass_union.rs.jinja', target_file, **context)
|
|
495
|
+
self.generated_types_rust_package[qualified_union_enum_name] = "union"
|
|
496
|
+
self.write_mod_rs(namespace)
|
|
497
|
+
|
|
498
|
+
return qualified_union_enum_name
|
|
499
|
+
|
|
500
|
+
def to_file_name(self, qualified_name):
|
|
501
|
+
"""Converts a qualified union enum name to a file name"""
|
|
502
|
+
if qualified_name.startswith('crate::'):
|
|
503
|
+
qualified_name = qualified_name[(len('crate::')):]
|
|
504
|
+
qualified_name = qualified_name.replace('r#', '')
|
|
505
|
+
return qualified_name.rsplit('::',1)[0].replace('::', os.sep).lower()
|
|
506
|
+
|
|
507
|
+
def generate_random_value(self, rust_type: str) -> str:
|
|
508
|
+
"""Generates a random value for a given Rust type"""
|
|
509
|
+
if rust_type == 'String' or rust_type == 'Option<String>':
|
|
510
|
+
return 'format!("random_string_{}", rand::Rng::gen::<u32>(&mut rng))'
|
|
511
|
+
elif rust_type == 'bool' or rust_type == 'Option<bool>':
|
|
512
|
+
return 'rand::Rng::gen::<bool>(&mut rng)'
|
|
513
|
+
elif rust_type in ['i8', 'Option<i8>']:
|
|
514
|
+
return 'rand::Rng::gen_range(&mut rng, 1..101) as i8'
|
|
515
|
+
elif rust_type in ['u8', 'Option<u8>']:
|
|
516
|
+
return 'rand::Rng::gen_range(&mut rng, 1..101) as u8'
|
|
517
|
+
elif rust_type in ['i16', 'Option<i16>']:
|
|
518
|
+
return 'rand::Rng::gen_range(&mut rng, 1..101) as i16'
|
|
519
|
+
elif rust_type in ['u16', 'Option<u16>']:
|
|
520
|
+
return 'rand::Rng::gen_range(&mut rng, 1..101) as u16'
|
|
521
|
+
elif rust_type in ['i32', 'Option<i32>']:
|
|
522
|
+
return 'rand::Rng::gen_range(&mut rng, 1..101)'
|
|
523
|
+
elif rust_type in ['u32', 'Option<u32>']:
|
|
524
|
+
return 'rand::Rng::gen_range(&mut rng, 1..101) as u32'
|
|
525
|
+
elif rust_type in ['i64', 'Option<i64>']:
|
|
526
|
+
return 'rand::Rng::gen_range(&mut rng, 1..101) as i64'
|
|
527
|
+
elif rust_type in ['u64', 'Option<u64>']:
|
|
528
|
+
return 'rand::Rng::gen_range(&mut rng, 1..101) as u64'
|
|
529
|
+
elif rust_type in ['i128', 'Option<i128>']:
|
|
530
|
+
return 'rand::Rng::gen_range(&mut rng, 1..101) as i128'
|
|
531
|
+
elif rust_type in ['u128', 'Option<u128>']:
|
|
532
|
+
return 'rand::Rng::gen_range(&mut rng, 1..101) as u128'
|
|
533
|
+
elif rust_type in ['f32', 'Option<f32>']:
|
|
534
|
+
return '(rand::Rng::gen::<f32>(&mut rng)*1000.0).round()/1000.0'
|
|
535
|
+
elif rust_type in ['f64', 'Option<f64>']:
|
|
536
|
+
return '(rand::Rng::gen::<f64>(&mut rng)*1000.0).round()/1000.0'
|
|
537
|
+
elif rust_type == 'Vec<u8>' or rust_type == 'Option<Vec<u8>>':
|
|
538
|
+
return 'vec![rand::Rng::gen::<u8>(&mut rng); 10]'
|
|
539
|
+
elif rust_type == 'chrono::NaiveDate' or rust_type == 'Option<chrono::NaiveDate>':
|
|
540
|
+
return 'chrono::NaiveDate::from_ymd_opt(rand::Rng::gen_range(&mut rng, 2000..2023), rand::Rng::gen_range(&mut rng, 1..13), rand::Rng::gen_range(&mut rng, 1..29)).unwrap()'
|
|
541
|
+
elif rust_type == 'chrono::NaiveTime' or rust_type == 'Option<chrono::NaiveTime>':
|
|
542
|
+
return 'chrono::NaiveTime::from_hms_opt(rand::Rng::gen_range(&mut rng, 0..24),rand::Rng::gen_range(&mut rng, 0..60), rand::Rng::gen_range(&mut rng, 0..60)).unwrap()'
|
|
543
|
+
elif rust_type == 'chrono::DateTime<chrono::Utc>' or rust_type == 'Option<chrono::DateTime<chrono::Utc>>':
|
|
544
|
+
return 'chrono::Utc::now()'
|
|
545
|
+
elif rust_type == 'chrono::Duration' or rust_type == 'Option<chrono::Duration>':
|
|
546
|
+
return 'chrono::Duration::seconds(rand::Rng::gen_range(&mut rng, 0..86400))'
|
|
547
|
+
elif rust_type == 'uuid::Uuid' or rust_type == 'Option<uuid::Uuid>':
|
|
548
|
+
return 'uuid::Uuid::new_v4()'
|
|
549
|
+
elif rust_type.startswith('std::collections::HashMap<String, '):
|
|
550
|
+
inner_type = rust_type.split(', ')[1][:-1]
|
|
551
|
+
return f'(0..3).map(|_| (format!("key_{{}}", rand::Rng::gen::<u32>(&mut rng)), {self.generate_random_value(inner_type)})).collect()'
|
|
552
|
+
elif rust_type.startswith('std::collections::HashSet<'):
|
|
553
|
+
inner_type = rust_type[27:-1]
|
|
554
|
+
return f'(0..3).map(|_| {self.generate_random_value(inner_type)}).collect()'
|
|
555
|
+
elif rust_type.startswith('Vec<'):
|
|
556
|
+
inner_type = rust_type[4:-1]
|
|
557
|
+
return f'(0..3).map(|_| {self.generate_random_value(inner_type)}).collect()'
|
|
558
|
+
elif rust_type in self.generated_types_rust_package:
|
|
559
|
+
return f'{rust_type}::generate_random_instance()'
|
|
560
|
+
else:
|
|
561
|
+
return 'Default::default()'
|
|
562
|
+
|
|
563
|
+
def write_mod_rs(self, namespace: str):
|
|
564
|
+
"""Writes the mod.rs file for a Rust module"""
|
|
565
|
+
# Sanitize namespace to replace dots with underscores
|
|
566
|
+
sanitized_namespace = self.sanitize_namespace(namespace)
|
|
567
|
+
directories = sanitized_namespace.split('.')
|
|
568
|
+
for i in range(len(directories)):
|
|
569
|
+
sub_package = '::'.join(directories[:i + 1])
|
|
570
|
+
directory_path = os.path.join(
|
|
571
|
+
self.output_dir, "src", sub_package.replace('.', os.sep).replace('::', os.sep))
|
|
572
|
+
if not os.path.exists(directory_path):
|
|
573
|
+
os.makedirs(directory_path, exist_ok=True)
|
|
574
|
+
mod_rs_path = os.path.join(directory_path, "mod.rs")
|
|
575
|
+
|
|
576
|
+
types = [file.replace('.rs', '') for file in os.listdir(directory_path) if file.endswith('.rs') and file != "mod.rs"]
|
|
577
|
+
mod_statements = '\n'.join(f'pub mod {self.escaped_identifier(typ.lower())};' for typ in types)
|
|
578
|
+
mods = [dir for dir in os.listdir(directory_path) if os.path.isdir(os.path.join(directory_path, dir))]
|
|
579
|
+
mod_statements += '\n' + '\n'.join(f'pub mod {self.escaped_identifier(mod.lower())};' for mod in mods)
|
|
580
|
+
|
|
581
|
+
with open(mod_rs_path, 'w', encoding='utf-8') as file:
|
|
582
|
+
file.write(mod_statements)
|
|
583
|
+
|
|
584
|
+
def write_cargo_toml(self):
|
|
585
|
+
"""Writes the Cargo.toml file for the Rust project"""
|
|
586
|
+
dependencies = []
|
|
587
|
+
if self.serde_annotation:
|
|
588
|
+
dependencies.append('serde = { version = "1.0", features = ["derive"] }')
|
|
589
|
+
dependencies.append('serde_json = "1.0"')
|
|
590
|
+
dependencies.append('chrono = { version = "0.4", features = ["serde"] }')
|
|
591
|
+
dependencies.append('uuid = { version = "1.11", features = ["serde", "v4"] }')
|
|
592
|
+
dependencies.append('flate2 = "1.0"')
|
|
593
|
+
dependencies.append('rand = "0.8"')
|
|
594
|
+
|
|
595
|
+
cargo_toml_content = f"[package]\n"
|
|
596
|
+
cargo_toml_content += f"name = \"{self.base_package.replace('/', '_')}\"\n"
|
|
597
|
+
cargo_toml_content += f"version = \"0.1.0\"\n"
|
|
598
|
+
cargo_toml_content += f"edition = \"2021\"\n\n"
|
|
599
|
+
cargo_toml_content += f"[dependencies]\n"
|
|
600
|
+
cargo_toml_content += "\n".join(f"{dependency}" for dependency in dependencies)
|
|
601
|
+
cargo_toml_path = os.path.join(self.output_dir, "Cargo.toml")
|
|
602
|
+
with open(cargo_toml_path, 'w', encoding='utf-8') as file:
|
|
603
|
+
file.write(cargo_toml_content)
|
|
604
|
+
|
|
605
|
+
def write_lib_rs(self):
|
|
606
|
+
"""Writes the lib.rs file for the Rust project"""
|
|
607
|
+
modules = {name[(len('crate::')):].split('::')[0].replace('.', '_') for name in self.generated_types_rust_package if name.startswith('crate::')}
|
|
608
|
+
mod_statements = '\n'.join(f'pub mod {self.escaped_identifier(module)};' for module in sorted(modules))
|
|
609
|
+
|
|
610
|
+
lib_rs_content = f"""
|
|
611
|
+
// This is the library entry point
|
|
612
|
+
|
|
613
|
+
{mod_statements}
|
|
614
|
+
"""
|
|
615
|
+
lib_rs_path = os.path.join(self.output_dir, "src", "lib.rs")
|
|
616
|
+
if not os.path.exists(os.path.dirname(lib_rs_path)):
|
|
617
|
+
os.makedirs(os.path.dirname(lib_rs_path), exist_ok=True)
|
|
618
|
+
with open(lib_rs_path, 'w', encoding='utf-8') as file:
|
|
619
|
+
file.write(lib_rs_content)
|
|
620
|
+
|
|
621
|
+
def process_definitions(self, definitions: Dict, namespace_path: str) -> None:
|
|
622
|
+
""" Processes the definitions section recursively """
|
|
623
|
+
for name, definition in definitions.items():
|
|
624
|
+
if isinstance(definition, dict):
|
|
625
|
+
if 'type' in definition:
|
|
626
|
+
# This is a type definition
|
|
627
|
+
current_namespace = self.concat_namespace(namespace_path, '')
|
|
628
|
+
check_namespace = self.sanitize_namespace(current_namespace.lower())
|
|
629
|
+
check_name = pascal(name)
|
|
630
|
+
check_ref = self.safe_package(self.concat_package(check_namespace, check_name))
|
|
631
|
+
if check_ref not in self.generated_types_rust_package:
|
|
632
|
+
self.generate_class_or_choice(definition, current_namespace, write_file=True, explicit_name=name)
|
|
633
|
+
else:
|
|
634
|
+
# This is a namespace
|
|
635
|
+
new_namespace = self.concat_namespace(namespace_path, name)
|
|
636
|
+
self.process_definitions(definition, new_namespace)
|
|
637
|
+
|
|
638
|
+
def convert_schema(self, schema: JsonNode, output_dir: str):
|
|
639
|
+
"""Converts JSON Structure schema to Rust"""
|
|
640
|
+
if not isinstance(schema, list):
|
|
641
|
+
schema = [schema]
|
|
642
|
+
if not os.path.exists(output_dir):
|
|
643
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
644
|
+
self.output_dir = output_dir
|
|
645
|
+
|
|
646
|
+
# Register all schema IDs first
|
|
647
|
+
for structure_schema in (s for s in schema if isinstance(s, dict)):
|
|
648
|
+
self.register_schema_ids(structure_schema)
|
|
649
|
+
|
|
650
|
+
# Process each schema
|
|
651
|
+
for structure_schema in (s for s in schema if isinstance(s, dict)):
|
|
652
|
+
self.schema_doc = structure_schema
|
|
653
|
+
|
|
654
|
+
# Store definitions for later use
|
|
655
|
+
if 'definitions' in structure_schema:
|
|
656
|
+
self.definitions = structure_schema['definitions']
|
|
657
|
+
|
|
658
|
+
# Process root type FIRST
|
|
659
|
+
if 'type' in structure_schema:
|
|
660
|
+
self.generate_class_or_choice(structure_schema, '', write_file=True)
|
|
661
|
+
elif '$root' in structure_schema:
|
|
662
|
+
root_ref = structure_schema['$root']
|
|
663
|
+
root_schema = self.resolve_ref(root_ref, structure_schema)
|
|
664
|
+
if root_schema:
|
|
665
|
+
ref_path = root_ref.split('/')
|
|
666
|
+
type_name = ref_path[-1]
|
|
667
|
+
ref_namespace = '.'.join(ref_path[2:-1]) if len(ref_path) > 3 else ''
|
|
668
|
+
self.generate_class_or_choice(root_schema, ref_namespace, write_file=True, explicit_name=type_name)
|
|
669
|
+
|
|
670
|
+
# Process definitions
|
|
671
|
+
if 'definitions' in structure_schema:
|
|
672
|
+
self.process_definitions(self.definitions, '')
|
|
673
|
+
|
|
674
|
+
self.write_cargo_toml()
|
|
675
|
+
self.write_lib_rs()
|
|
676
|
+
|
|
677
|
+
def convert(self, structure_schema_path: str, output_dir: str):
|
|
678
|
+
"""Converts JSON Structure schema to Rust"""
|
|
679
|
+
with open(structure_schema_path, 'r', encoding='utf-8') as file:
|
|
680
|
+
schema = json.load(file)
|
|
681
|
+
self.convert_schema(schema, output_dir)
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def convert_structure_to_rust(structure_schema_path: str, rust_file_path: str, package_name: str = '', serde_annotation: bool = False):
|
|
685
|
+
"""Converts JSON Structure schema to Rust structs
|
|
686
|
+
|
|
687
|
+
Args:
|
|
688
|
+
structure_schema_path (str): JSON Structure input schema path
|
|
689
|
+
rust_file_path (str): Output Rust file path
|
|
690
|
+
package_name (str): Base package name
|
|
691
|
+
serde_annotation (bool): Include Serde annotations
|
|
692
|
+
"""
|
|
693
|
+
if not package_name:
|
|
694
|
+
package_name = os.path.splitext(os.path.basename(structure_schema_path))[0].lower().replace('-', '_')
|
|
695
|
+
|
|
696
|
+
structtorust = StructureToRust()
|
|
697
|
+
structtorust.base_package = package_name
|
|
698
|
+
structtorust.serde_annotation = serde_annotation
|
|
699
|
+
structtorust.convert(structure_schema_path, rust_file_path)
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def convert_structure_schema_to_rust(structure_schema: JsonNode, output_dir: str, package_name: str = '', serde_annotation: bool = False):
|
|
703
|
+
"""Converts JSON Structure schema to Rust structs
|
|
704
|
+
|
|
705
|
+
Args:
|
|
706
|
+
structure_schema (JsonNode): JSON Structure schema as a dictionary or list of dictionaries
|
|
707
|
+
output_dir (str): Output directory path
|
|
708
|
+
package_name (str): Base package name
|
|
709
|
+
serde_annotation (bool): Include Serde annotations
|
|
710
|
+
"""
|
|
711
|
+
structtorust = StructureToRust()
|
|
712
|
+
structtorust.base_package = package_name
|
|
713
|
+
structtorust.serde_annotation = serde_annotation
|
|
714
|
+
structtorust.convert_schema(structure_schema, output_dir)
|