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.
Files changed (171) hide show
  1. avrotize/__init__.py +66 -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/CMakeLists.txt.jinja +77 -0
  7. avrotize/avrotocpp/build.bat.jinja +7 -0
  8. avrotize/avrotocpp/build.sh.jinja +7 -0
  9. avrotize/avrotocpp/dataclass_body.jinja +108 -0
  10. avrotize/avrotocpp/vcpkg.json.jinja +21 -0
  11. avrotize/avrotocpp.py +483 -0
  12. avrotize/avrotocsharp/README.md.jinja +166 -0
  13. avrotize/avrotocsharp/class_test.cs.jinja +266 -0
  14. avrotize/avrotocsharp/dataclass_core.jinja +293 -0
  15. avrotize/avrotocsharp/enum_test.cs.jinja +20 -0
  16. avrotize/avrotocsharp/project.csproj.jinja +30 -0
  17. avrotize/avrotocsharp/project.sln.jinja +34 -0
  18. avrotize/avrotocsharp/run_coverage.ps1.jinja +98 -0
  19. avrotize/avrotocsharp/run_coverage.sh.jinja +149 -0
  20. avrotize/avrotocsharp/testproject.csproj.jinja +19 -0
  21. avrotize/avrotocsharp.py +1180 -0
  22. avrotize/avrotocsv.py +121 -0
  23. avrotize/avrotodatapackage.py +173 -0
  24. avrotize/avrotodb.py +1383 -0
  25. avrotize/avrotogo/go_enum.jinja +12 -0
  26. avrotize/avrotogo/go_helpers.jinja +31 -0
  27. avrotize/avrotogo/go_struct.jinja +151 -0
  28. avrotize/avrotogo/go_test.jinja +47 -0
  29. avrotize/avrotogo/go_union.jinja +38 -0
  30. avrotize/avrotogo.py +476 -0
  31. avrotize/avrotographql.py +197 -0
  32. avrotize/avrotoiceberg.py +210 -0
  33. avrotize/avrotojava/class_test.java.jinja +212 -0
  34. avrotize/avrotojava/enum_test.java.jinja +21 -0
  35. avrotize/avrotojava/testproject.pom.jinja +54 -0
  36. avrotize/avrotojava.py +2156 -0
  37. avrotize/avrotojs.py +250 -0
  38. avrotize/avrotojsons.py +481 -0
  39. avrotize/avrotojstruct.py +345 -0
  40. avrotize/avrotokusto.py +364 -0
  41. avrotize/avrotomd/README.md.jinja +49 -0
  42. avrotize/avrotomd.py +137 -0
  43. avrotize/avrotools.py +168 -0
  44. avrotize/avrotoparquet.py +208 -0
  45. avrotize/avrotoproto.py +359 -0
  46. avrotize/avrotopython/dataclass_core.jinja +241 -0
  47. avrotize/avrotopython/enum_core.jinja +87 -0
  48. avrotize/avrotopython/pyproject_toml.jinja +18 -0
  49. avrotize/avrotopython/test_class.jinja +97 -0
  50. avrotize/avrotopython/test_enum.jinja +23 -0
  51. avrotize/avrotopython.py +626 -0
  52. avrotize/avrotorust/dataclass_enum.rs.jinja +74 -0
  53. avrotize/avrotorust/dataclass_struct.rs.jinja +204 -0
  54. avrotize/avrotorust/dataclass_union.rs.jinja +105 -0
  55. avrotize/avrotorust.py +435 -0
  56. avrotize/avrotots/class_core.ts.jinja +140 -0
  57. avrotize/avrotots/class_test.ts.jinja +77 -0
  58. avrotize/avrotots/enum_core.ts.jinja +46 -0
  59. avrotize/avrotots/gitignore.jinja +34 -0
  60. avrotize/avrotots/index.ts.jinja +0 -0
  61. avrotize/avrotots/package.json.jinja +23 -0
  62. avrotize/avrotots/tsconfig.json.jinja +21 -0
  63. avrotize/avrotots.py +687 -0
  64. avrotize/avrotoxsd.py +344 -0
  65. avrotize/cddltostructure.py +1841 -0
  66. avrotize/commands.json +3496 -0
  67. avrotize/common.py +834 -0
  68. avrotize/constants.py +87 -0
  69. avrotize/csvtoavro.py +132 -0
  70. avrotize/datapackagetoavro.py +76 -0
  71. avrotize/dependencies/cpp/vcpkg/vcpkg.json +19 -0
  72. avrotize/dependencies/cs/net90/dependencies.csproj +29 -0
  73. avrotize/dependencies/go/go121/go.mod +6 -0
  74. avrotize/dependencies/java/jdk21/pom.xml +91 -0
  75. avrotize/dependencies/python/py312/requirements.txt +13 -0
  76. avrotize/dependencies/rust/stable/Cargo.toml +17 -0
  77. avrotize/dependencies/typescript/node22/package.json +16 -0
  78. avrotize/dependency_resolver.py +348 -0
  79. avrotize/dependency_version.py +432 -0
  80. avrotize/generic/generic.avsc +57 -0
  81. avrotize/jsonstoavro.py +2167 -0
  82. avrotize/jsonstostructure.py +2864 -0
  83. avrotize/jstructtoavro.py +878 -0
  84. avrotize/kstructtoavro.py +93 -0
  85. avrotize/kustotoavro.py +455 -0
  86. avrotize/openapitostructure.py +717 -0
  87. avrotize/parquettoavro.py +157 -0
  88. avrotize/proto2parser.py +498 -0
  89. avrotize/proto3parser.py +403 -0
  90. avrotize/prototoavro.py +382 -0
  91. avrotize/prototypes/any.avsc +19 -0
  92. avrotize/prototypes/api.avsc +106 -0
  93. avrotize/prototypes/duration.avsc +20 -0
  94. avrotize/prototypes/field_mask.avsc +18 -0
  95. avrotize/prototypes/struct.avsc +60 -0
  96. avrotize/prototypes/timestamp.avsc +20 -0
  97. avrotize/prototypes/type.avsc +253 -0
  98. avrotize/prototypes/wrappers.avsc +117 -0
  99. avrotize/structuretocddl.py +597 -0
  100. avrotize/structuretocpp/CMakeLists.txt.jinja +76 -0
  101. avrotize/structuretocpp/build.bat.jinja +3 -0
  102. avrotize/structuretocpp/build.sh.jinja +3 -0
  103. avrotize/structuretocpp/dataclass_body.jinja +50 -0
  104. avrotize/structuretocpp/vcpkg.json.jinja +11 -0
  105. avrotize/structuretocpp.py +697 -0
  106. avrotize/structuretocsharp/class_test.cs.jinja +180 -0
  107. avrotize/structuretocsharp/dataclass_core.jinja +156 -0
  108. avrotize/structuretocsharp/enum_test.cs.jinja +36 -0
  109. avrotize/structuretocsharp/json_structure_converters.cs.jinja +399 -0
  110. avrotize/structuretocsharp/program.cs.jinja +49 -0
  111. avrotize/structuretocsharp/project.csproj.jinja +17 -0
  112. avrotize/structuretocsharp/project.sln.jinja +34 -0
  113. avrotize/structuretocsharp/testproject.csproj.jinja +18 -0
  114. avrotize/structuretocsharp/tuple_converter.cs.jinja +121 -0
  115. avrotize/structuretocsharp.py +2295 -0
  116. avrotize/structuretocsv.py +365 -0
  117. avrotize/structuretodatapackage.py +659 -0
  118. avrotize/structuretodb.py +1125 -0
  119. avrotize/structuretogo/go_enum.jinja +12 -0
  120. avrotize/structuretogo/go_helpers.jinja +26 -0
  121. avrotize/structuretogo/go_interface.jinja +18 -0
  122. avrotize/structuretogo/go_struct.jinja +187 -0
  123. avrotize/structuretogo/go_test.jinja +70 -0
  124. avrotize/structuretogo.py +729 -0
  125. avrotize/structuretographql.py +502 -0
  126. avrotize/structuretoiceberg.py +355 -0
  127. avrotize/structuretojava/choice_core.jinja +34 -0
  128. avrotize/structuretojava/class_core.jinja +23 -0
  129. avrotize/structuretojava/enum_core.jinja +18 -0
  130. avrotize/structuretojava/equals_hashcode.jinja +30 -0
  131. avrotize/structuretojava/pom.xml.jinja +26 -0
  132. avrotize/structuretojava/tuple_core.jinja +49 -0
  133. avrotize/structuretojava.py +938 -0
  134. avrotize/structuretojs/class_core.js.jinja +33 -0
  135. avrotize/structuretojs/enum_core.js.jinja +10 -0
  136. avrotize/structuretojs/package.json.jinja +12 -0
  137. avrotize/structuretojs/test_class.js.jinja +84 -0
  138. avrotize/structuretojs/test_enum.js.jinja +58 -0
  139. avrotize/structuretojs/test_runner.js.jinja +45 -0
  140. avrotize/structuretojs.py +657 -0
  141. avrotize/structuretojsons.py +498 -0
  142. avrotize/structuretokusto.py +639 -0
  143. avrotize/structuretomd/README.md.jinja +204 -0
  144. avrotize/structuretomd.py +322 -0
  145. avrotize/structuretoproto.py +764 -0
  146. avrotize/structuretopython/dataclass_core.jinja +363 -0
  147. avrotize/structuretopython/enum_core.jinja +45 -0
  148. avrotize/structuretopython/map_alias.jinja +21 -0
  149. avrotize/structuretopython/pyproject_toml.jinja +23 -0
  150. avrotize/structuretopython/test_class.jinja +103 -0
  151. avrotize/structuretopython/test_enum.jinja +34 -0
  152. avrotize/structuretopython.py +799 -0
  153. avrotize/structuretorust/dataclass_enum.rs.jinja +63 -0
  154. avrotize/structuretorust/dataclass_struct.rs.jinja +121 -0
  155. avrotize/structuretorust/dataclass_union.rs.jinja +81 -0
  156. avrotize/structuretorust.py +714 -0
  157. avrotize/structuretots/class_core.ts.jinja +78 -0
  158. avrotize/structuretots/enum_core.ts.jinja +6 -0
  159. avrotize/structuretots/gitignore.jinja +8 -0
  160. avrotize/structuretots/index.ts.jinja +1 -0
  161. avrotize/structuretots/package.json.jinja +39 -0
  162. avrotize/structuretots/test_class.ts.jinja +35 -0
  163. avrotize/structuretots/tsconfig.json.jinja +21 -0
  164. avrotize/structuretots.py +740 -0
  165. avrotize/structuretoxsd.py +679 -0
  166. avrotize/xsdtoavro.py +413 -0
  167. avrotize-2.21.1.dist-info/METADATA +1319 -0
  168. avrotize-2.21.1.dist-info/RECORD +171 -0
  169. avrotize-2.21.1.dist-info/WHEEL +4 -0
  170. avrotize-2.21.1.dist-info/entry_points.txt +3 -0
  171. avrotize-2.21.1.dist-info/licenses/LICENSE +201 -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)
@@ -0,0 +1,140 @@
1
+ /** {{ class_name }} class. */
2
+ {%- if typed_json_annotation %}
3
+ import 'reflect-metadata';
4
+ {%- if fields | selectattr("is_array") | list | length > 0 %}
5
+ import { jsonObject, jsonMember, jsonArrayMember, TypedJSON } from 'typedjson';
6
+ {%- else %}
7
+ import { jsonObject, jsonMember, TypedJSON } from 'typedjson';
8
+ {%- endif %}
9
+ {%- endif %}
10
+ {%- if avro_annotation %}
11
+ import avro from 'avro-js';
12
+ {%- endif %}
13
+ {%- for import_type, import_path in imports.items() %}
14
+ import { {{ import_type }} } from '{{ import_path }}';
15
+ {%- endfor %}
16
+ {%- if avro_annotation or typed_json_annotation %}
17
+ import pako from 'pako';
18
+ {%- endif %}
19
+
20
+ {%- if typed_json_annotation %}
21
+ @jsonObject
22
+ {%- endif %}
23
+ export class {{ class_name }} {
24
+ {%- if avro_annotation %}
25
+ public static AvroType: avro.Type = avro.parse({{ avro_schema_json }});
26
+ {%- endif %}
27
+
28
+ {%- for field in fields %}
29
+ /** {{ field.docstring }} */
30
+ {%- if typed_json_annotation %}
31
+ {%- set field_type = field.type_no_null if not field.is_primitive else (field.type_no_null | pascal ) %}
32
+ {% if field.is_union -%}
33
+ @jsonMember({serializer: {{ field_type }}.toJSON, deserializer: {{ field_type }}.fromJSON})
34
+ {%- elif field.is_array -%}@jsonArrayMember({{ field_type | replace('[]', '') }})
35
+ {%- else -%}@jsonMember({%-if not field.is_enum-%}{{ field_type }}{%-else-%}String{%-endif-%})
36
+ {%- endif %}
37
+ {%- endif %}
38
+ public {{ field.name }}{%- if field.type.endswith('?')-%}?{%-endif-%} : {{ field.type_no_null }};
39
+ {%- endfor %}
40
+
41
+ constructor({%- for field in fields %}{{ field.name }}: {{ field.type_no_null }}{% if not loop.last %}, {% endif %}{%- endfor %}) {
42
+ {%- for field in fields %}
43
+ {%- if field.is_enum %}
44
+ if ( typeof {{ field.name }} === 'number' ) {
45
+ this.{{ field.name }} = {{ field.type_no_null }}Utils.fromOrdinal({{ field.name }});
46
+ } else {
47
+ this.{{ field.name }} = {{ field.name }};
48
+ }
49
+ {%- else %}
50
+ this.{{ field.name }} = {{ field.name }};
51
+ {%- endif %}
52
+ {%- endfor %}
53
+ }
54
+
55
+ {%- if avro_annotation or typed_json_annotation %}
56
+ public toByteArray(contentTypeString: string): Uint8Array {
57
+ const contentType = contentTypeString.split(';')[0].trim();
58
+ let result: Uint8Array | null = null;
59
+
60
+ {%- if avro_annotation %}
61
+ if (contentType.startsWith('avro/binary') || contentType.startsWith('application/vnd.apache.avro+avro')) {
62
+ result = ({{ class_name }}.AvroType.toBuffer(this) as unknown) as Uint8Array;
63
+ }
64
+ {%- endif %}
65
+
66
+ {%- if typed_json_annotation %}
67
+ if (contentType.startsWith('application/json')) {
68
+ const serializer = new TypedJSON({{ class_name }});
69
+ const jsonString = serializer.stringify(this);
70
+ result = new TextEncoder().encode(jsonString);
71
+ }
72
+ {%- endif %}
73
+
74
+ if (result && contentTypeString.endsWith('+gzip')) {
75
+ result = pako.gzip(result);
76
+ }
77
+
78
+ if (result) {
79
+ return result;
80
+ } else {
81
+ throw new Error(`Unsupported media type: ${contentTypeString}`);
82
+ }
83
+ }
84
+
85
+ public static fromData(data: any, contentTypeString: string): {{ class_name }} {
86
+ const contentType = contentTypeString.split(';')[0].trim();
87
+
88
+ if (contentTypeString.endsWith('+gzip')) {
89
+ data = pako.ungzip(data);
90
+ }
91
+
92
+ {%- if avro_annotation %}
93
+ if (contentType.startsWith('avro/binary') || contentType.startsWith('application/vnd.apache.avro+avro')) {
94
+ return {{ class_name }}.AvroType.fromBuffer(data);
95
+ }
96
+ {%- endif %}
97
+
98
+ {%- if typed_json_annotation %}
99
+ if (contentType.startsWith('application/json')) {
100
+ const serializer = new TypedJSON({{ class_name }});
101
+ const retval = serializer.parse(new TextDecoder().decode(data));
102
+ if (!(retval instanceof {{ class_name }})) {
103
+ throw new Error(`Deserialized object is not an instance of {{ class_name }}`);
104
+ }
105
+ return retval;
106
+ }
107
+ {%- endif %}
108
+
109
+ throw new Error(`Unsupported media type: ${contentTypeString}`);
110
+ }
111
+
112
+
113
+ {%- if typed_json_annotation %}
114
+ public static isJsonMatch(element: any): boolean {
115
+ {%- if fields|length == 0 %}
116
+ return true;
117
+ {%- else %}
118
+ return (
119
+ {%- for field in fields %}
120
+ {{ get_is_json_match_clause(field.name, field.type_no_null, field.is_enum) }}{%- if not loop.last %} &&
121
+ {%- endif %}
122
+ {%- endfor %}
123
+ );
124
+ {%- endif %}
125
+ }
126
+ {%- endif %}
127
+ {%- endif %}
128
+
129
+ /**
130
+ * Creates an instance of {{ class_name }} with sample data for testing.
131
+ * @returns A new {{ class_name }} instance with sample values.
132
+ */
133
+ public static createInstance(): {{ class_name }} {
134
+ return new {{ class_name }}(
135
+ {%- for field in fields %}
136
+ {{ field.test_value }}{% if not loop.last %},{% endif %} // {{ field.name }}
137
+ {%- endfor %}
138
+ );
139
+ }
140
+ }
@@ -0,0 +1,77 @@
1
+ /** Test file for {{ class_name }} */
2
+ import { {{ class_name }} } from './{{ class_name }}.js';
3
+ {%- for import_type, import_path in imports.items() %}
4
+ import { {{ import_type }} } from '{{ import_path }}';
5
+ {%- endfor %}
6
+
7
+ describe('{{ class_name }}', () => {
8
+ let instance: {{ class_name }};
9
+
10
+ beforeEach(() => {
11
+ instance = {{ class_name }}.createInstance();
12
+ });
13
+
14
+ describe('createInstance', () => {
15
+ it('should create a valid instance', () => {
16
+ expect(instance).toBeInstanceOf({{ class_name }});
17
+ });
18
+
19
+ it('should create instances with consistent values', () => {
20
+ const instance1 = {{ class_name }}.createInstance();
21
+ const instance2 = {{ class_name }}.createInstance();
22
+ {%- for field in fields %}
23
+ expect(instance1.{{ field.name }}).toEqual(instance2.{{ field.name }});
24
+ {%- endfor %}
25
+ });
26
+ });
27
+
28
+ {%- for field in fields %}
29
+
30
+ describe('{{ field.name }} property', () => {
31
+ it('should have a valid {{ field.name }} value', () => {
32
+ expect(instance.{{ field.name }}).toBeDefined();
33
+ });
34
+
35
+ it('should accept assigned values', () => {
36
+ const testValue = {{ field.test_value }};
37
+ instance.{{ field.name }} = testValue;
38
+ expect(instance.{{ field.name }}).toEqual(testValue);
39
+ });
40
+ });
41
+ {%- endfor %}
42
+
43
+ {%- if typed_json_annotation %}
44
+
45
+ describe('JSON serialization', () => {
46
+ it('should serialize to JSON and deserialize back', () => {
47
+ const bytes = instance.toByteArray('application/json');
48
+ const restored = {{ class_name }}.fromData(bytes, 'application/json');
49
+ {%- for field in fields %}
50
+ expect(restored.{{ field.name }}).toEqual(instance.{{ field.name }});
51
+ {%- endfor %}
52
+ });
53
+
54
+ it('should match JSON structure', () => {
55
+ const jsonObj = {
56
+ {%- for field in fields %}
57
+ {{ field.original_name }}: instance.{{ field.name }}{% if not loop.last %},{% endif %}
58
+ {%- endfor %}
59
+ };
60
+ expect({{ class_name }}.isJsonMatch(jsonObj)).toBe(true);
61
+ });
62
+ });
63
+ {%- endif %}
64
+
65
+ {%- if avro_annotation %}
66
+
67
+ describe('Avro serialization', () => {
68
+ it('should serialize to Avro and deserialize back', () => {
69
+ const bytes = instance.toByteArray('application/vnd.apache.avro+avro');
70
+ const restored = {{ class_name }}.fromData(bytes, 'application/vnd.apache.avro+avro');
71
+ {%- for field in fields %}
72
+ expect(restored.{{ field.name }}).toEqual(instance.{{ field.name }});
73
+ {%- endfor %}
74
+ });
75
+ });
76
+ {%- endif %}
77
+ });