structurize 2.19.0__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.
Files changed (70) hide show
  1. avrotize/__init__.py +64 -0
  2. avrotize/__main__.py +6 -0
  3. avrotize/_version.py +34 -0
  4. avrotize/asn1toavro.py +160 -0
  5. avrotize/avrotize.py +152 -0
  6. avrotize/avrotocpp.py +483 -0
  7. avrotize/avrotocsharp.py +1075 -0
  8. avrotize/avrotocsv.py +121 -0
  9. avrotize/avrotodatapackage.py +173 -0
  10. avrotize/avrotodb.py +1383 -0
  11. avrotize/avrotogo.py +476 -0
  12. avrotize/avrotographql.py +197 -0
  13. avrotize/avrotoiceberg.py +210 -0
  14. avrotize/avrotojava.py +2156 -0
  15. avrotize/avrotojs.py +250 -0
  16. avrotize/avrotojsons.py +481 -0
  17. avrotize/avrotojstruct.py +345 -0
  18. avrotize/avrotokusto.py +364 -0
  19. avrotize/avrotomd.py +137 -0
  20. avrotize/avrotools.py +168 -0
  21. avrotize/avrotoparquet.py +208 -0
  22. avrotize/avrotoproto.py +359 -0
  23. avrotize/avrotopython.py +624 -0
  24. avrotize/avrotorust.py +435 -0
  25. avrotize/avrotots.py +598 -0
  26. avrotize/avrotoxsd.py +344 -0
  27. avrotize/cddltostructure.py +1841 -0
  28. avrotize/commands.json +3337 -0
  29. avrotize/common.py +834 -0
  30. avrotize/constants.py +72 -0
  31. avrotize/csvtoavro.py +132 -0
  32. avrotize/datapackagetoavro.py +76 -0
  33. avrotize/dependencies/cpp/vcpkg/vcpkg.json +19 -0
  34. avrotize/dependencies/typescript/node22/package.json +16 -0
  35. avrotize/dependency_resolver.py +348 -0
  36. avrotize/dependency_version.py +432 -0
  37. avrotize/jsonstoavro.py +2167 -0
  38. avrotize/jsonstostructure.py +2642 -0
  39. avrotize/jstructtoavro.py +878 -0
  40. avrotize/kstructtoavro.py +93 -0
  41. avrotize/kustotoavro.py +455 -0
  42. avrotize/parquettoavro.py +157 -0
  43. avrotize/proto2parser.py +498 -0
  44. avrotize/proto3parser.py +403 -0
  45. avrotize/prototoavro.py +382 -0
  46. avrotize/structuretocddl.py +597 -0
  47. avrotize/structuretocpp.py +697 -0
  48. avrotize/structuretocsharp.py +2295 -0
  49. avrotize/structuretocsv.py +365 -0
  50. avrotize/structuretodatapackage.py +659 -0
  51. avrotize/structuretodb.py +1125 -0
  52. avrotize/structuretogo.py +720 -0
  53. avrotize/structuretographql.py +502 -0
  54. avrotize/structuretoiceberg.py +355 -0
  55. avrotize/structuretojava.py +853 -0
  56. avrotize/structuretojsons.py +498 -0
  57. avrotize/structuretokusto.py +639 -0
  58. avrotize/structuretomd.py +322 -0
  59. avrotize/structuretoproto.py +764 -0
  60. avrotize/structuretopython.py +772 -0
  61. avrotize/structuretorust.py +714 -0
  62. avrotize/structuretots.py +653 -0
  63. avrotize/structuretoxsd.py +679 -0
  64. avrotize/xsdtoavro.py +413 -0
  65. structurize-2.19.0.dist-info/METADATA +107 -0
  66. structurize-2.19.0.dist-info/RECORD +70 -0
  67. structurize-2.19.0.dist-info/WHEEL +5 -0
  68. structurize-2.19.0.dist-info/entry_points.txt +2 -0
  69. structurize-2.19.0.dist-info/licenses/LICENSE +201 -0
  70. structurize-2.19.0.dist-info/top_level.txt +1 -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)