structurize 2.16.2__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 +63 -0
- avrotize/__main__.py +6 -0
- avrotize/_version.py +34 -0
- avrotize/asn1toavro.py +160 -0
- avrotize/avrotize.py +152 -0
- avrotize/avrotocpp.py +483 -0
- avrotize/avrotocsharp.py +992 -0
- avrotize/avrotocsv.py +121 -0
- avrotize/avrotodatapackage.py +173 -0
- avrotize/avrotodb.py +1383 -0
- avrotize/avrotogo.py +476 -0
- avrotize/avrotographql.py +197 -0
- avrotize/avrotoiceberg.py +210 -0
- avrotize/avrotojava.py +1023 -0
- avrotize/avrotojs.py +250 -0
- avrotize/avrotojsons.py +481 -0
- avrotize/avrotojstruct.py +345 -0
- avrotize/avrotokusto.py +364 -0
- avrotize/avrotomd.py +137 -0
- avrotize/avrotools.py +168 -0
- avrotize/avrotoparquet.py +208 -0
- avrotize/avrotoproto.py +359 -0
- avrotize/avrotopython.py +622 -0
- avrotize/avrotorust.py +435 -0
- avrotize/avrotots.py +598 -0
- avrotize/avrotoxsd.py +344 -0
- avrotize/commands.json +2433 -0
- avrotize/common.py +829 -0
- avrotize/constants.py +5 -0
- avrotize/csvtoavro.py +132 -0
- avrotize/datapackagetoavro.py +76 -0
- avrotize/dependency_resolver.py +348 -0
- avrotize/jsonstoavro.py +1698 -0
- avrotize/jsonstostructure.py +2642 -0
- avrotize/jstructtoavro.py +878 -0
- avrotize/kstructtoavro.py +93 -0
- avrotize/kustotoavro.py +455 -0
- avrotize/parquettoavro.py +157 -0
- avrotize/proto2parser.py +498 -0
- avrotize/proto3parser.py +403 -0
- avrotize/prototoavro.py +382 -0
- avrotize/structuretocsharp.py +2005 -0
- avrotize/structuretojsons.py +498 -0
- avrotize/structuretopython.py +772 -0
- avrotize/xsdtoavro.py +413 -0
- structurize-2.16.2.dist-info/METADATA +805 -0
- structurize-2.16.2.dist-info/RECORD +51 -0
- structurize-2.16.2.dist-info/WHEEL +5 -0
- structurize-2.16.2.dist-info/entry_points.txt +2 -0
- structurize-2.16.2.dist-info/licenses/LICENSE +201 -0
- structurize-2.16.2.dist-info/top_level.txt +1 -0
avrotize/avrotorust.py
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from typing import Dict, List, Union
|
|
4
|
+
from avrotize.common import is_generic_avro_type, render_template, pascal, camel, snake
|
|
5
|
+
|
|
6
|
+
INDENT = ' '
|
|
7
|
+
|
|
8
|
+
JsonNode = Dict[str, 'JsonNode'] | List['JsonNode'] | str | None
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AvroToRust:
|
|
12
|
+
"""Converts Avro schema to Rust structs, including Serde and Avro marshalling methods"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, base_package: str = '') -> None:
|
|
15
|
+
self.base_package = base_package.replace('.', '/').lower()
|
|
16
|
+
self.output_dir = os.getcwd()
|
|
17
|
+
self.generated_types_avro_namespace: Dict[str, str] = {}
|
|
18
|
+
self.generated_types_rust_package: Dict[str, str] = {}
|
|
19
|
+
self.avro_annotation = False
|
|
20
|
+
self.serde_annotation = False
|
|
21
|
+
|
|
22
|
+
reserved_words = [
|
|
23
|
+
'as', 'break', 'const', 'continue', 'crate', 'else', 'enum', 'extern', 'false', 'fn', 'for', 'if', 'impl',
|
|
24
|
+
'in', 'let', 'loop', 'match', 'mod', 'move', 'mut', 'pub', 'ref', 'return', 'self', 'Self', 'static',
|
|
25
|
+
'struct', 'super', 'trait', 'true', 'type', 'unsafe', 'use', 'where', 'while', 'async', 'await', 'dyn',
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
def safe_identifier(self, name: str) -> str:
|
|
29
|
+
"""Converts a name to a safe Rust identifier"""
|
|
30
|
+
if name in AvroToRust.reserved_words:
|
|
31
|
+
return f"{name}_"
|
|
32
|
+
return name
|
|
33
|
+
|
|
34
|
+
def escaped_identifier(self, name: str) -> str:
|
|
35
|
+
"""Converts a name to a safe Rust identifier with a leading r# prefix"""
|
|
36
|
+
if name != "crate" and name in AvroToRust.reserved_words:
|
|
37
|
+
return f"r#{name}"
|
|
38
|
+
return name
|
|
39
|
+
|
|
40
|
+
def safe_package(self, package: str) -> str:
|
|
41
|
+
"""Converts a package name to a safe Rust package name"""
|
|
42
|
+
elements = package.split('::')
|
|
43
|
+
return '::'.join([self.escaped_identifier(element) for element in elements])
|
|
44
|
+
|
|
45
|
+
def map_primitive_to_rust(self, avro_fullname: str, is_optional: bool) -> str:
|
|
46
|
+
"""Maps Avro primitive types to Rust types"""
|
|
47
|
+
optional_mapping = {
|
|
48
|
+
'null': 'None',
|
|
49
|
+
'boolean': 'Option<bool>',
|
|
50
|
+
'int': 'Option<i32>',
|
|
51
|
+
'long': 'Option<i64>',
|
|
52
|
+
'float': 'Option<f32>',
|
|
53
|
+
'double': 'Option<f64>',
|
|
54
|
+
'bytes': 'Option<Vec<u8>>',
|
|
55
|
+
'string': 'Option<String>',
|
|
56
|
+
}
|
|
57
|
+
required_mapping = {
|
|
58
|
+
'null': 'None',
|
|
59
|
+
'boolean': 'bool',
|
|
60
|
+
'int': 'i32',
|
|
61
|
+
'long': 'i64',
|
|
62
|
+
'float': 'f32',
|
|
63
|
+
'double': 'f64',
|
|
64
|
+
'bytes': 'Vec<u8>',
|
|
65
|
+
'string': 'String',
|
|
66
|
+
}
|
|
67
|
+
rust_fullname = avro_fullname
|
|
68
|
+
if '.' in rust_fullname:
|
|
69
|
+
type_name = pascal(avro_fullname.split('.')[-1])
|
|
70
|
+
package_name = '::'.join(avro_fullname.split('.')[:-1]).lower()
|
|
71
|
+
rust_fullname = self.safe_package(self.concat_package(package_name, type_name))
|
|
72
|
+
if rust_fullname in self.generated_types_rust_package:
|
|
73
|
+
return rust_fullname
|
|
74
|
+
else:
|
|
75
|
+
return required_mapping.get(avro_fullname, avro_fullname) if not is_optional else optional_mapping.get(avro_fullname, avro_fullname)
|
|
76
|
+
|
|
77
|
+
def concat_package(self, package: str, name: str) -> str:
|
|
78
|
+
"""Concatenates package and name using a double colon separator"""
|
|
79
|
+
return f"crate::{package.lower()}::{name.lower()}::{name}" if package else name
|
|
80
|
+
|
|
81
|
+
def convert_avro_type_to_rust(self, field_name: str, avro_type: Union[str, Dict, List], namespace: str, nullable: bool = False) -> str:
|
|
82
|
+
"""Converts Avro type to Rust type"""
|
|
83
|
+
ns = namespace.replace('.', '::').lower()
|
|
84
|
+
type_name = ''
|
|
85
|
+
if isinstance(avro_type, str):
|
|
86
|
+
type_name = self.map_primitive_to_rust(avro_type, nullable)
|
|
87
|
+
elif isinstance(avro_type, list):
|
|
88
|
+
if is_generic_avro_type(avro_type):
|
|
89
|
+
return 'serde_json::Value' if self.serde_annotation else 'std::collections::HashMap<String, String>'
|
|
90
|
+
non_null_types = [t for t in avro_type if t != 'null']
|
|
91
|
+
if len(non_null_types) == 1:
|
|
92
|
+
# Rust apache-avro has a bug in the union type handling, so we need to swap the types
|
|
93
|
+
# if the first type is not null
|
|
94
|
+
if avro_type[0] != 'null':
|
|
95
|
+
avro_type[1] = avro_type[0]
|
|
96
|
+
avro_type[0] = 'null'
|
|
97
|
+
if isinstance(non_null_types[0], str):
|
|
98
|
+
type_name = self.map_primitive_to_rust(non_null_types[0], True)
|
|
99
|
+
else:
|
|
100
|
+
type_name = self.convert_avro_type_to_rust(field_name, non_null_types[0], namespace)
|
|
101
|
+
else:
|
|
102
|
+
type_name = self.generate_union_enum(field_name, avro_type, namespace)
|
|
103
|
+
elif isinstance(avro_type, dict):
|
|
104
|
+
if avro_type['type'] in ['record', 'enum']:
|
|
105
|
+
type_name = self.generate_class_or_enum(avro_type, namespace)
|
|
106
|
+
elif avro_type['type'] == 'fixed' or avro_type['type'] == 'bytes' and 'logicalType' in avro_type:
|
|
107
|
+
if avro_type['logicalType'] == 'decimal':
|
|
108
|
+
return 'f64'
|
|
109
|
+
elif avro_type['type'] == 'array':
|
|
110
|
+
item_type = self.convert_avro_type_to_rust(field_name, avro_type['items'], namespace)
|
|
111
|
+
return f"Vec<{item_type}>"
|
|
112
|
+
elif avro_type['type'] == 'map':
|
|
113
|
+
values_type = self.convert_avro_type_to_rust(field_name, avro_type['values'], namespace)
|
|
114
|
+
return f"std::collections::HashMap<String, {values_type}>"
|
|
115
|
+
elif 'logicalType' in avro_type:
|
|
116
|
+
if avro_type['logicalType'] == 'date':
|
|
117
|
+
return 'chrono::NaiveDate'
|
|
118
|
+
elif avro_type['logicalType'] == 'time-millis' or avro_type['logicalType'] == 'time-micros':
|
|
119
|
+
return 'chrono::NaiveTime'
|
|
120
|
+
elif avro_type['logicalType'] == 'timestamp-millis' or avro_type['logicalType'] == 'timestamp-micros':
|
|
121
|
+
return 'chrono::NaiveDateTime'
|
|
122
|
+
elif avro_type['logicalType'] == 'uuid':
|
|
123
|
+
return 'uuid::Uuid'
|
|
124
|
+
else:
|
|
125
|
+
type_name = self.convert_avro_type_to_rust(field_name, avro_type['type'], namespace)
|
|
126
|
+
if type_name:
|
|
127
|
+
return type_name
|
|
128
|
+
return 'serde_json::Value' if self.serde_annotation else 'std::collections::HashMap<String, String>'
|
|
129
|
+
|
|
130
|
+
def generate_class_or_enum(self, avro_schema: Dict, parent_namespace: str = '') -> str:
|
|
131
|
+
"""Generates a Rust struct or enum from an Avro schema"""
|
|
132
|
+
namespace = avro_schema.get('namespace', parent_namespace).lower()
|
|
133
|
+
if avro_schema['type'] == 'record':
|
|
134
|
+
return self.generate_struct(avro_schema, namespace)
|
|
135
|
+
elif avro_schema['type'] == 'enum':
|
|
136
|
+
return self.generate_enum(avro_schema, namespace)
|
|
137
|
+
return 'serde_json::Value'
|
|
138
|
+
|
|
139
|
+
def generate_struct(self, avro_schema: Dict, parent_namespace: str) -> str:
|
|
140
|
+
"""Generates a Rust struct from an Avro record schema"""
|
|
141
|
+
fields = []
|
|
142
|
+
for field in avro_schema.get('fields', []):
|
|
143
|
+
original_field_name = field['name']
|
|
144
|
+
field_name = self.safe_identifier(snake(original_field_name))
|
|
145
|
+
field_type = self.convert_avro_type_to_rust(field_name, field['type'], parent_namespace)
|
|
146
|
+
serde_rename = field_name != original_field_name
|
|
147
|
+
fields.append({
|
|
148
|
+
'original_name': original_field_name,
|
|
149
|
+
'name': field_name,
|
|
150
|
+
'type': field_type,
|
|
151
|
+
'serde_rename': serde_rename,
|
|
152
|
+
'random_value': self.generate_random_value(field_type)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
struct_name = self.safe_identifier(pascal(avro_schema['name']))
|
|
156
|
+
ns = parent_namespace.replace('.', '::').lower()
|
|
157
|
+
qualified_struct_name = self.safe_package(self.concat_package(ns, struct_name))
|
|
158
|
+
if not 'namespace' in avro_schema:
|
|
159
|
+
avro_schema['namespace'] = parent_namespace
|
|
160
|
+
avro_schema_str = json.dumps(avro_schema)
|
|
161
|
+
avro_schema_str = avro_schema_str.replace('"', '§')
|
|
162
|
+
avro_schema_str = f"\",\n{INDENT*2}\"".join(
|
|
163
|
+
[avro_schema_str[i:i+80] for i in range(0, len(avro_schema_str), 80)])
|
|
164
|
+
avro_schema_str = avro_schema_str.replace('§', '\\"')
|
|
165
|
+
avro_schema_str = f"concat!(\"{avro_schema_str}\")"
|
|
166
|
+
|
|
167
|
+
context = {
|
|
168
|
+
'avro_annotation': self.avro_annotation,
|
|
169
|
+
'serde_annotation': self.serde_annotation,
|
|
170
|
+
'doc': avro_schema.get('doc', ''),
|
|
171
|
+
'struct_name': struct_name,
|
|
172
|
+
'fields': fields,
|
|
173
|
+
'avro_schema': avro_schema_str,
|
|
174
|
+
'json_match_predicates': [self.get_is_json_match_clause(f['original_name'], f['type']) for f in fields]
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
file_name = self.to_file_name(qualified_struct_name)
|
|
178
|
+
target_file = os.path.join(self.output_dir, "src", file_name + ".rs")
|
|
179
|
+
render_template('avrotorust/dataclass_struct.rs.jinja', target_file, **context)
|
|
180
|
+
self.write_mod_rs(parent_namespace)
|
|
181
|
+
|
|
182
|
+
self.generated_types_avro_namespace[qualified_struct_name] = "struct"
|
|
183
|
+
self.generated_types_rust_package[qualified_struct_name] = "struct"
|
|
184
|
+
|
|
185
|
+
return qualified_struct_name
|
|
186
|
+
|
|
187
|
+
def get_is_json_match_clause(self, field_name: str, field_type: str, for_union=False) -> str:
|
|
188
|
+
"""Generates the is_json_match clause for a field"""
|
|
189
|
+
ref = f'node[\"{field_name}\"]' if not for_union else 'node'
|
|
190
|
+
if field_type == 'String' or field_type == 'Option<String>':
|
|
191
|
+
return f"{ref}.is_string()"
|
|
192
|
+
elif field_type == 'bool' or field_type == 'Option<bool>':
|
|
193
|
+
return f"{ref}.is_boolean()"
|
|
194
|
+
elif field_type == 'i32' or field_type == 'Option<i32>':
|
|
195
|
+
return f"{ref}.is_i64()"
|
|
196
|
+
elif field_type == 'i64' or field_type == 'Option<i64>':
|
|
197
|
+
return f"{ref}.is_i64()"
|
|
198
|
+
elif field_type == 'f32' or field_type == 'Option<f32>':
|
|
199
|
+
return f"{ref}.is_f64()"
|
|
200
|
+
elif field_type == 'f64' or field_type == 'Option<f64>':
|
|
201
|
+
return f"{ref}.is_f64()"
|
|
202
|
+
elif field_type == 'Vec<u8>' or field_type == 'Option<Vec<u8>>':
|
|
203
|
+
return f"{ref}.is_array()"
|
|
204
|
+
elif field_type == 'serde_json::Value' or field_type == 'std::collections::HashMap<String, String>':
|
|
205
|
+
return f"{ref}.is_object()"
|
|
206
|
+
elif field_type.startswith('std::collections::HashMap<String, '):
|
|
207
|
+
return f"{ref}.is_object()"
|
|
208
|
+
elif field_type.startswith('Vec<'):
|
|
209
|
+
return f"{ref}.is_array()"
|
|
210
|
+
else:
|
|
211
|
+
return f"{field_type}::is_json_match(&{ref})"
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def generate_enum(self, avro_schema: Dict, parent_namespace: str) -> str:
|
|
215
|
+
"""Generates a Rust enum from an Avro enum schema"""
|
|
216
|
+
symbols = avro_schema.get('symbols', [])
|
|
217
|
+
enum_name = self.safe_identifier(pascal(avro_schema['name']))
|
|
218
|
+
ns = parent_namespace.replace('.', '::').lower()
|
|
219
|
+
qualified_enum_name = self.safe_package(self.concat_package(ns, enum_name))
|
|
220
|
+
|
|
221
|
+
if not 'namespace' in avro_schema:
|
|
222
|
+
avro_schema['namespace'] = parent_namespace
|
|
223
|
+
avro_schema_str = json.dumps(avro_schema)
|
|
224
|
+
avro_schema_str = avro_schema_str.replace('"', '§')
|
|
225
|
+
avro_schema_str = f"\",\n{INDENT*2}\"".join(
|
|
226
|
+
[avro_schema_str[i:i+80] for i in range(0, len(avro_schema_str), 80)])
|
|
227
|
+
avro_schema_str = avro_schema_str.replace('§', '\\"')
|
|
228
|
+
avro_schema_str = f"concat!(\"{avro_schema_str}\")"
|
|
229
|
+
|
|
230
|
+
context = {
|
|
231
|
+
'avro_annotation': self.avro_annotation,
|
|
232
|
+
'serde_annotation': self.serde_annotation,
|
|
233
|
+
'enum_name': enum_name,
|
|
234
|
+
'symbols': symbols,
|
|
235
|
+
'avro_schema': avro_schema_str,
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
file_name = self.to_file_name(qualified_enum_name)
|
|
239
|
+
target_file = os.path.join(self.output_dir, "src", file_name + ".rs")
|
|
240
|
+
render_template('avrotorust/dataclass_enum.rs.jinja', target_file, **context)
|
|
241
|
+
self.write_mod_rs(parent_namespace)
|
|
242
|
+
|
|
243
|
+
self.generated_types_avro_namespace[qualified_enum_name] = "enum"
|
|
244
|
+
self.generated_types_rust_package[qualified_enum_name] = "enum"
|
|
245
|
+
|
|
246
|
+
return qualified_enum_name
|
|
247
|
+
|
|
248
|
+
def generate_union_enum(self, field_name: str, avro_type: List, namespace: str) -> str:
|
|
249
|
+
"""Generates a union enum for Rust"""
|
|
250
|
+
ns = namespace.replace('.', '::').lower()
|
|
251
|
+
union_enum_name = pascal(field_name) + 'Union'
|
|
252
|
+
union_types = [self.convert_avro_type_to_rust(field_name + "Option" + str(i), t, namespace) for i, t in enumerate(avro_type) if t != 'null']
|
|
253
|
+
union_fields = [
|
|
254
|
+
{
|
|
255
|
+
'name': pascal(t.rsplit('::',1)[-1]),
|
|
256
|
+
'type': t,
|
|
257
|
+
'random_value': self.generate_random_value(t),
|
|
258
|
+
'default_value': 'Default::default()',
|
|
259
|
+
'json_match_predicate': self.get_is_json_match_clause(field_name, t, for_union=True),
|
|
260
|
+
} for i, t in enumerate(union_types)]
|
|
261
|
+
qualified_union_enum_name = self.safe_package(self.concat_package(ns, union_enum_name))
|
|
262
|
+
context = {
|
|
263
|
+
'serde_annotation': self.serde_annotation,
|
|
264
|
+
'union_enum_name': union_enum_name,
|
|
265
|
+
'union_fields': union_fields,
|
|
266
|
+
'json_match_predicates': [self.get_is_json_match_clause(f['name'], f['type'], for_union=True) for f in union_fields]
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
file_name = self.to_file_name(qualified_union_enum_name)
|
|
270
|
+
target_file = os.path.join(self.output_dir, "src", file_name + ".rs").lower()
|
|
271
|
+
render_template('avrotorust/dataclass_union.rs.jinja', target_file, **context)
|
|
272
|
+
self.generated_types_avro_namespace[qualified_union_enum_name] = "union"
|
|
273
|
+
self.generated_types_rust_package[qualified_union_enum_name] = "union"
|
|
274
|
+
self.write_mod_rs(namespace)
|
|
275
|
+
|
|
276
|
+
return qualified_union_enum_name
|
|
277
|
+
|
|
278
|
+
def to_file_name(self, qualified_name):
|
|
279
|
+
"""Converts a qualified union enum name to a file name"""
|
|
280
|
+
if qualified_name.startswith('crate::'):
|
|
281
|
+
qualified_name = qualified_name[(len('crate::')):]
|
|
282
|
+
qualified_name = qualified_name.replace('r#', '')
|
|
283
|
+
return qualified_name.rsplit('::',1)[0].replace('::', os.sep).lower()
|
|
284
|
+
|
|
285
|
+
def generate_random_value(self, rust_type: str) -> str:
|
|
286
|
+
"""Generates a random value for a given Rust type"""
|
|
287
|
+
if rust_type == 'String' or rust_type == 'Option<String>':
|
|
288
|
+
return 'format!("random_string_{}", rand::Rng::gen::<u32>(&mut rng))'
|
|
289
|
+
elif rust_type == 'bool' or rust_type == 'Option<bool>':
|
|
290
|
+
return 'rand::Rng::gen::<bool>(&mut rng)'
|
|
291
|
+
elif rust_type == 'i32' or rust_type == 'Option<i32>':
|
|
292
|
+
return 'rand::Rng::gen_range(&mut rng, 0..100)'
|
|
293
|
+
elif rust_type == 'i64' or rust_type == 'Option<i64>':
|
|
294
|
+
return 'rand::Rng::gen_range(&mut rng, 0..100) as i64'
|
|
295
|
+
elif rust_type == 'f32' or rust_type == 'Option<f32>':
|
|
296
|
+
return '(rand::Rng::gen::<f32>(&mut rng)*1000.0).round()/1000.0'
|
|
297
|
+
elif rust_type == 'f64' or rust_type == 'Option<f64>':
|
|
298
|
+
return '(rand::Rng::gen::<f64>(&mut rng)*1000.0).round()/1000.0'
|
|
299
|
+
elif rust_type == 'Vec<u8>' or rust_type == 'Option<Vec<u8>>':
|
|
300
|
+
return 'vec![rand::Rng::gen::<u8>(&mut rng); 10]'
|
|
301
|
+
elif rust_type == 'chrono::NaiveDate':
|
|
302
|
+
return 'chrono::NaiveDate::from_ymd(rand::Rng::gen_range(&mut rng, 2000..2023), rand::Rng::gen_range(&mut rng, 1..13), rand::Rng::gen_range(&mut rng, 1..29))'
|
|
303
|
+
elif rust_type == 'chrono::NaiveTime':
|
|
304
|
+
return 'chrono::NaiveTime::from_hms(rand::Rng::gen_range(&mut rng, 0..24),rand::Rng::gen_range(&mut rng, 0..60), rand::Rng::gen_range(&mut rng, 0..60))'
|
|
305
|
+
elif rust_type == 'chrono::NaiveDateTime':
|
|
306
|
+
return 'chrono::NaiveDateTime::new(chrono::NaiveDate::from_ymd(rand::Rng::gen_range(&mut rng, 2000..2023), rand::Rng::gen_range(&mut rng, 1..13), rand::Rng::gen_range(&mut rng, 1..29)), chrono::NaiveTime::from_hms(rand::Rng::gen_range(&mut rng, 0..24), rand::Rng::gen_range(&mut rng, 0..60), rand::Rng::gen_range(&mut rng, 0..60)))'
|
|
307
|
+
elif rust_type == 'uuid::Uuid':
|
|
308
|
+
return 'uuid::Uuid::new_v4()'
|
|
309
|
+
elif rust_type.startswith('std::collections::HashMap<String, '):
|
|
310
|
+
inner_type = rust_type.split(', ')[1][:-1]
|
|
311
|
+
return f'(0..3).map(|_| (format!("key_{{}}", rand::Rng::gen::<u32>(&mut rng)), {self.generate_random_value(inner_type)})).collect()'
|
|
312
|
+
elif rust_type.startswith('Vec<'):
|
|
313
|
+
inner_type = rust_type[4:-1]
|
|
314
|
+
return f'(0..3).map(|_| {self.generate_random_value(inner_type)}).collect()'
|
|
315
|
+
elif rust_type in self.generated_types_rust_package:
|
|
316
|
+
return f'{rust_type}::generate_random_instance()'
|
|
317
|
+
else:
|
|
318
|
+
return 'Default::default()'
|
|
319
|
+
|
|
320
|
+
def write_mod_rs(self, namespace: str):
|
|
321
|
+
"""Writes the mod.rs file for a Rust module"""
|
|
322
|
+
directories = namespace.split('.')
|
|
323
|
+
for i in range(len(directories)):
|
|
324
|
+
sub_package = '::'.join(directories[:i + 1])
|
|
325
|
+
directory_path = os.path.join(
|
|
326
|
+
self.output_dir, "src", sub_package.replace('.', os.sep).replace('::', os.sep))
|
|
327
|
+
if not os.path.exists(directory_path):
|
|
328
|
+
os.makedirs(directory_path, exist_ok=True)
|
|
329
|
+
mod_rs_path = os.path.join(directory_path, "mod.rs")
|
|
330
|
+
|
|
331
|
+
types = [file.replace('.rs', '') for file in os.listdir(directory_path) if file.endswith('.rs') and file != "mod.rs"]
|
|
332
|
+
mod_statements = '\n'.join(f'pub mod {self.escaped_identifier(typ.lower())};' for typ in types)
|
|
333
|
+
mods = [dir for dir in os.listdir(directory_path) if os.path.isdir(os.path.join(directory_path, dir))]
|
|
334
|
+
mod_statements += '\n' + '\n'.join(f'pub mod {self.escaped_identifier(mod.lower())};' for mod in mods)
|
|
335
|
+
|
|
336
|
+
with open(mod_rs_path, 'w', encoding='utf-8') as file:
|
|
337
|
+
file.write(mod_statements)
|
|
338
|
+
|
|
339
|
+
def write_cargo_toml(self):
|
|
340
|
+
"""Writes the Cargo.toml file for the Rust project"""
|
|
341
|
+
dependencies = []
|
|
342
|
+
if self.serde_annotation or self.avro_annotation:
|
|
343
|
+
dependencies.append('serde = { version = "1.0", features = ["derive"] }')
|
|
344
|
+
dependencies.append('serde_json = "1.0"')
|
|
345
|
+
dependencies.append('chrono = { version = "0.4", features = ["serde"] }')
|
|
346
|
+
dependencies.append('uuid = { version = "1.11", features = ["serde", "v4"] }')
|
|
347
|
+
if self.avro_annotation or self.serde_annotation:
|
|
348
|
+
dependencies.append('flate2 = "1.0"')
|
|
349
|
+
if self.avro_annotation:
|
|
350
|
+
dependencies.append('apache-avro = "0.17"')
|
|
351
|
+
dependencies.append('lazy_static = "1.4"')
|
|
352
|
+
dependencies.append('rand = "0.8"')
|
|
353
|
+
|
|
354
|
+
cargo_toml_content = f"[package]\n"
|
|
355
|
+
cargo_toml_content += f"name = \"{self.base_package.replace('/', '_')}\"\n"
|
|
356
|
+
cargo_toml_content += f"version = \"0.1.0\"\n"
|
|
357
|
+
cargo_toml_content += f"edition = \"2021\"\n\n"
|
|
358
|
+
cargo_toml_content += f"[dependencies]\n"
|
|
359
|
+
cargo_toml_content += "\n".join(f"{dependency}" for dependency in dependencies)
|
|
360
|
+
cargo_toml_path = os.path.join(self.output_dir, "Cargo.toml")
|
|
361
|
+
with open(cargo_toml_path, 'w', encoding='utf-8') as file:
|
|
362
|
+
file.write(cargo_toml_content)
|
|
363
|
+
|
|
364
|
+
def write_lib_rs(self):
|
|
365
|
+
"""Writes the lib.rs file for the Rust project"""
|
|
366
|
+
modules = {name[(len('crate::')):].split('::')[0] for name in self.generated_types_rust_package}
|
|
367
|
+
mod_statements = '\n'.join(f'pub mod {module};' for module in modules)
|
|
368
|
+
|
|
369
|
+
lib_rs_content = f"""
|
|
370
|
+
// This is the library entry point
|
|
371
|
+
|
|
372
|
+
{mod_statements}
|
|
373
|
+
"""
|
|
374
|
+
lib_rs_path = os.path.join(self.output_dir, "src", "lib.rs")
|
|
375
|
+
if not os.path.exists(os.path.dirname(lib_rs_path)):
|
|
376
|
+
os.makedirs(os.path.dirname(lib_rs_path), exist_ok=True)
|
|
377
|
+
with open(lib_rs_path, 'w', encoding='utf-8') as file:
|
|
378
|
+
file.write(lib_rs_content)
|
|
379
|
+
|
|
380
|
+
def convert_schema(self, schema: JsonNode, output_dir: str):
|
|
381
|
+
"""Converts Avro schema to Rust"""
|
|
382
|
+
if not isinstance(schema, list):
|
|
383
|
+
schema = [schema]
|
|
384
|
+
if not os.path.exists(output_dir):
|
|
385
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
386
|
+
self.output_dir = output_dir
|
|
387
|
+
for avro_schema in (x for x in schema if isinstance(x, dict)):
|
|
388
|
+
self.generate_class_or_enum(avro_schema)
|
|
389
|
+
|
|
390
|
+
self.write_cargo_toml()
|
|
391
|
+
self.write_lib_rs()
|
|
392
|
+
|
|
393
|
+
def convert(self, avro_schema_path: str, output_dir: str):
|
|
394
|
+
"""Converts Avro schema to Rust"""
|
|
395
|
+
with open(avro_schema_path, 'r', encoding='utf-8') as file:
|
|
396
|
+
schema = json.load(file)
|
|
397
|
+
self.convert_schema(schema, output_dir)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def convert_avro_to_rust(avro_schema_path, rust_file_path, package_name='', avro_annotation=False, serde_annotation=False):
|
|
401
|
+
"""Converts Avro schema to Rust structs
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
avro_schema_path (str): Avro input schema path
|
|
405
|
+
rust_file_path (str): Output Rust file path
|
|
406
|
+
package_name (str): Base package name
|
|
407
|
+
avro_annotation (bool): Include Avro annotations
|
|
408
|
+
serde_annotation (bool): Include Serde annotations
|
|
409
|
+
"""
|
|
410
|
+
|
|
411
|
+
if not package_name:
|
|
412
|
+
package_name = os.path.splitext(os.path.basename(avro_schema_path))[0].lower().replace('-', '_')
|
|
413
|
+
|
|
414
|
+
avrotorust = AvroToRust()
|
|
415
|
+
avrotorust.base_package = package_name
|
|
416
|
+
avrotorust.avro_annotation = avro_annotation
|
|
417
|
+
avrotorust.serde_annotation = serde_annotation
|
|
418
|
+
avrotorust.convert(avro_schema_path, rust_file_path)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def convert_avro_schema_to_rust(avro_schema: JsonNode, output_dir: str, package_name='', avro_annotation=False, serde_annotation=False):
|
|
422
|
+
"""Converts Avro schema to Rust structs
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
avro_schema (JsonNode): Avro schema as a dictionary or list of dictionaries
|
|
426
|
+
output_dir (str): Output directory path
|
|
427
|
+
package_name (str): Base package name
|
|
428
|
+
avro_annotation (bool): Include Avro annotations
|
|
429
|
+
serde_annotation (bool): Include Serde annotations
|
|
430
|
+
"""
|
|
431
|
+
avrotorust = AvroToRust()
|
|
432
|
+
avrotorust.base_package = package_name
|
|
433
|
+
avrotorust.avro_annotation = avro_annotation
|
|
434
|
+
avrotorust.serde_annotation = serde_annotation
|
|
435
|
+
avrotorust.convert_schema(avro_schema, output_dir)
|