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/avrotots.py ADDED
@@ -0,0 +1,687 @@
1
+ # pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring, line-too-long, too-many-locals, too-many-branches, too-many-statements
2
+
3
+ import json
4
+ import os
5
+ from typing import Dict, List, Set, Union
6
+
7
+ from avrotize.common import build_flat_type_dict, fullname, inline_avro_references, is_generic_avro_type, is_type_with_alternate, pascal, process_template, strip_alternate_type
8
+ from numpy import full
9
+
10
+
11
+ def is_typescript_reserved_word(word: str) -> bool:
12
+ """Check if word is a TypeScript reserved word."""
13
+ reserved_words = [
14
+ 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger',
15
+ 'default', 'delete', 'do', 'else', 'export', 'extends', 'finally',
16
+ 'for', 'function', 'if', 'import', 'in', 'instanceof', 'new', 'return',
17
+ 'super', 'switch', 'this', 'throw', 'try', 'typeof', 'var', 'void',
18
+ 'while', 'with', 'yield', 'enum', 'string', 'number', 'boolean', 'symbol',
19
+ 'type', 'namespace', 'module', 'declare', 'abstract', 'readonly',
20
+ ]
21
+ return word in reserved_words
22
+
23
+
24
+ class AvroToTypeScript:
25
+ """Converts Avro schema to TypeScript classes using templates with namespace support."""
26
+
27
+ def __init__(self, base_package: str = '', typed_json_annotation=False, avro_annotation=False) -> None:
28
+ self.base_package = base_package
29
+ self.typed_json_annotation = typed_json_annotation
30
+ self.avro_annotation = avro_annotation
31
+ self.output_dir = os.getcwd()
32
+ self.src_dir = os.path.join(self.output_dir, "src")
33
+ self.generated_types: Dict[str, str] = {}
34
+ self.generated_type_fields: Dict[str, List[Dict]] = {} # Store fields for test generation
35
+ self.main_schema = None
36
+ self.type_dict = None
37
+ self.INDENT = ' ' * 4
38
+
39
+ def map_primitive_to_typescript(self, avro_type: str) -> str:
40
+ """Map Avro primitive type to TypeScript type."""
41
+ mapping = {
42
+ 'null': 'null',
43
+ 'boolean': 'boolean',
44
+ 'int': 'number',
45
+ 'long': 'number',
46
+ 'float': 'number',
47
+ 'double': 'number',
48
+ 'bytes': 'string',
49
+ 'string': 'string',
50
+ }
51
+ return mapping.get(avro_type, avro_type)
52
+
53
+ def convert_logical_type_to_typescript(self, avro_type: Dict) -> str:
54
+ """Convert Avro logical type to TypeScript type."""
55
+ if 'logicalType' in avro_type:
56
+ if avro_type['logicalType'] in ['decimal', 'uuid']:
57
+ return 'string'
58
+ if avro_type['logicalType'] in ['date', 'time-millis', 'time-micros', 'timestamp-millis', 'timestamp-micros']:
59
+ return 'Date'
60
+ if avro_type['logicalType'] == 'duration':
61
+ return 'string'
62
+ return 'any'
63
+
64
+ def strip_nullable(self, ts_type: str) -> str:
65
+ """Strip nullable type from TypeScript type."""
66
+ if ts_type.endswith('?'):
67
+ return ts_type[:-1]
68
+ return ts_type
69
+
70
+ def is_typescript_primitive(self, ts_type: str) -> bool:
71
+ """Check if TypeScript type is a primitive."""
72
+ ts_type = self.strip_nullable(ts_type)
73
+ return ts_type in ['null', 'boolean', 'number', 'string', 'Date', 'any']
74
+
75
+ def is_enum_type(self, ts_type: str, namespace: str) -> bool:
76
+ """Check if TypeScript type is an enum."""
77
+ ts_type = self.strip_nullable(ts_type)
78
+ fn_type = fullname(ts_type, namespace)
79
+ return not self.is_typescript_primitive(ts_type) and fn_type in self.generated_types and self.generated_types[fn_type] == 'enum'
80
+
81
+ def safe_name(self, name: str) -> str:
82
+ """Converts a name to a safe TypeScript name."""
83
+ if is_typescript_reserved_word(name):
84
+ return name + "_"
85
+ return name
86
+
87
+ def convert_avro_type_to_typescript(self, avro_type: Union[str, Dict, List], parent_namespace: str, import_types: Set[str], class_name: str = '', field_name: str = '') -> str:
88
+ """Convert Avro type to TypeScript type with namespace support."""
89
+ if isinstance(avro_type, str):
90
+ mapped_type = self.map_primitive_to_typescript(avro_type)
91
+ if mapped_type == avro_type and not self.is_typescript_primitive(mapped_type):
92
+ full_name = self.concat_namespace(self.base_package,fullname(avro_type, parent_namespace))
93
+ import_types.add(full_name)
94
+ return pascal(avro_type.split('.')[-1])
95
+ return mapped_type
96
+ elif isinstance(avro_type, list):
97
+ if is_generic_avro_type(avro_type):
98
+ return '{ [key: string]: any }'
99
+ if 'null' in avro_type:
100
+ if len(avro_type) == 2:
101
+ return f'{self.convert_avro_type_to_typescript([t for t in avro_type if t != "null"][0], parent_namespace, import_types, class_name, field_name)}?'
102
+ return f'{self.generate_embedded_union(class_name, field_name, avro_type, parent_namespace, import_types)}?'
103
+ return self.generate_embedded_union(class_name, field_name, avro_type, parent_namespace, import_types)
104
+ elif isinstance(avro_type, dict):
105
+ if avro_type['type'] == 'record':
106
+ class_ref = self.generate_class(avro_type, parent_namespace, write_file=True)
107
+ import_types.add(class_ref)
108
+ return pascal(class_ref.split('.')[-1])
109
+ elif avro_type['type'] == 'enum':
110
+ enum_ref = self.generate_enum(avro_type, parent_namespace, write_file=True)
111
+ import_types.add(enum_ref)
112
+ return pascal(enum_ref.split('.')[-1])
113
+ elif avro_type['type'] == 'array':
114
+ return f'{self.convert_avro_type_to_typescript(avro_type["items"], parent_namespace, import_types, class_name, field_name)}[]'
115
+ elif avro_type['type'] == 'map':
116
+ return f'{{ [key: string]: {self.convert_avro_type_to_typescript(avro_type["values"], parent_namespace, import_types, class_name, field_name)} }}'
117
+ elif 'logicalType' in avro_type:
118
+ return self.convert_logical_type_to_typescript(avro_type)
119
+ return self.convert_avro_type_to_typescript(avro_type['type'], parent_namespace, import_types, class_name, field_name)
120
+ return 'any'
121
+
122
+ def get_qualified_name(self, namespace: str, name: str) -> str:
123
+ """Concatenates namespace and name with a dot separator."""
124
+ return f"{namespace}.{name}" if namespace != '' else name
125
+
126
+ def concat_namespace(self, namespace: str, name: str) -> str:
127
+ """Concatenates namespace and name with a dot separator."""
128
+ if namespace and name:
129
+ return f"{namespace}.{name}"
130
+ return namespace or name
131
+
132
+ def generate_class_or_enum(self, avro_schema: Dict, parent_namespace: str, write_file: bool = True) -> str:
133
+ """Generates a Class or Enum."""
134
+ if avro_schema['type'] == 'record':
135
+ return self.generate_class(avro_schema, parent_namespace, write_file)
136
+ elif avro_schema['type'] == 'enum':
137
+ return self.generate_enum(avro_schema, parent_namespace, write_file)
138
+ return ''
139
+
140
+ def generate_class(self, avro_schema: Dict, parent_namespace: str, write_file: bool = True) -> str:
141
+ """Generate TypeScript class from Avro record using templates with namespace support."""
142
+ import_types: Set[str] = set()
143
+ class_name = pascal(avro_schema['name'])
144
+ namespace = self.concat_namespace(self.base_package, avro_schema.get('namespace', parent_namespace))
145
+ ts_qualified_name = self.get_qualified_name(namespace, class_name)
146
+ if ts_qualified_name in self.generated_types:
147
+ return ts_qualified_name
148
+
149
+ fields = [{
150
+ 'definition': self.generate_field(field, avro_schema.get('namespace', parent_namespace), import_types, class_name),
151
+ 'docstring': field.get('doc', '')
152
+ } for field in avro_schema.get('fields', [])]
153
+
154
+ fields = [{
155
+ 'name': self.safe_name(field['definition']['name']),
156
+ 'original_name': field['definition']['name'],
157
+ 'type': field['definition']['type'],
158
+ 'type_no_null': self.strip_nullable(field['definition']['type']),
159
+ 'is_primitive': field['definition']['is_primitive'],
160
+ 'is_enum': field['definition']['is_enum'],
161
+ 'is_array': field['definition']['is_array'],
162
+ 'is_union': field['definition']['is_union'],
163
+ 'docstring': field['docstring'],
164
+ 'test_value': self.generate_test_value(field['definition']['type'], field['definition']['is_enum']),
165
+ } for field in fields]
166
+
167
+ imports_with_paths: Dict[str, str] = {}
168
+ for import_type in import_types:
169
+ if import_type == ts_qualified_name:
170
+ continue
171
+ import_is_enum = import_type in self.generated_types and self.generated_types[import_type] == 'enum'
172
+ import_type_parts = import_type.split('.')
173
+ import_type_name = pascal(import_type_parts[-1])
174
+ import_path = '/'.join(import_type_parts)
175
+ current_path = '/'.join(namespace.split('.'))
176
+ relative_import_path = os.path.relpath(import_path, current_path).replace(os.sep, '/')
177
+ if not relative_import_path.startswith('.'):
178
+ relative_import_path = f'./{relative_import_path}'
179
+ if import_is_enum:
180
+ import_type_name_and_util = f"{import_type_name}, {import_type_name}Utils"
181
+ imports_with_paths[import_type_name_and_util] = relative_import_path + '.js'
182
+ else:
183
+ imports_with_paths[import_type_name] = relative_import_path + '.js'
184
+
185
+ # Inline the schema
186
+ local_avro_schema = inline_avro_references(avro_schema.copy(), self.type_dict, parent_namespace)
187
+ avro_schema_json = json.dumps(local_avro_schema)
188
+
189
+ class_definition = process_template(
190
+ "avrotots/class_core.ts.jinja",
191
+ namespace=namespace,
192
+ class_name=class_name,
193
+ docstring=avro_schema.get('doc', '').strip() if 'doc' in avro_schema else f'A {class_name} record.',
194
+ fields=fields,
195
+ imports=imports_with_paths,
196
+ base_package=self.base_package,
197
+ avro_annotation=self.avro_annotation,
198
+ typed_json_annotation=self.typed_json_annotation,
199
+ avro_schema_json=avro_schema_json,
200
+ get_is_json_match_clause=self.get_is_json_match_clause,
201
+ )
202
+
203
+ if write_file:
204
+ self.write_to_file(namespace, class_name, class_definition)
205
+ self.generated_types[ts_qualified_name] = 'class'
206
+ self.generated_type_fields[ts_qualified_name] = fields # Store fields for test generation
207
+ return ts_qualified_name
208
+
209
+ def generate_enum(self, avro_schema: Dict, parent_namespace: str, write_file: bool = True) -> str:
210
+ """Generate TypeScript enum from Avro enum using templates with namespace support."""
211
+ enum_name = pascal(avro_schema['name'])
212
+ namespace = self.concat_namespace(self.base_package, avro_schema.get('namespace', parent_namespace))
213
+ ts_qualified_name = self.get_qualified_name(namespace, enum_name)
214
+ if ts_qualified_name in self.generated_types:
215
+ return ts_qualified_name
216
+
217
+ symbols = avro_schema.get('symbols', [])
218
+ enum_definition = process_template(
219
+ "avrotots/enum_core.ts.jinja",
220
+ namespace=namespace,
221
+ enum_name=enum_name,
222
+ docstring=avro_schema.get('doc', '').strip() if 'doc' in avro_schema else f'A {enum_name} enum.',
223
+ symbols=symbols,
224
+ )
225
+
226
+ if write_file:
227
+ self.write_to_file(namespace, enum_name, enum_definition)
228
+ self.generated_types[ts_qualified_name] = 'enum'
229
+ return ts_qualified_name
230
+
231
+ def generate_field(self, field: Dict, parent_namespace: str, import_types: Set[str], class_name: str) -> Dict:
232
+ """Generates a field for a TypeScript class."""
233
+ import_types_this = set()
234
+ field_type = self.convert_avro_type_to_typescript(
235
+ field['type'], parent_namespace, import_types_this, class_name, field['name'])
236
+ import_types.update(import_types_this)
237
+ field_name = field['name']
238
+ import_name = import_types_this.pop() if len(import_types_this) > 0 else ''
239
+ return {
240
+ 'name': field_name,
241
+ 'type': field_type,
242
+ 'is_primitive': self.is_typescript_primitive(field_type.replace('[]', '')),
243
+ 'is_array': field_type.endswith('[]'),
244
+ 'is_union': self.generated_types.get(import_name, '') == 'union',
245
+ 'is_enum': self.generated_types.get(import_name, '') == 'enum',
246
+ }
247
+
248
+ def generate_test_value(self, field_type: str, is_enum: bool = False) -> str:
249
+ """Generate a test value for a TypeScript field type."""
250
+ # Strip nullable marker
251
+ is_nullable = field_type.endswith('?')
252
+ field_type = self.strip_nullable(field_type)
253
+
254
+ # Handle arrays
255
+ if field_type.endswith('[]'):
256
+ inner_type = field_type[:-2]
257
+ inner_value = self.generate_test_value(inner_type, is_enum)
258
+ return f'[{inner_value}]'
259
+
260
+ # Handle map/dict types
261
+ if field_type.startswith('{ [key: string]:'):
262
+ return "{ 'key': 'value' }"
263
+
264
+ # Handle union types (pipe-separated)
265
+ if '|' in field_type:
266
+ first_type = field_type.split('|')[0].strip()
267
+ return self.generate_test_value(first_type, is_enum)
268
+
269
+ # Handle enums - use first value with Object.values()
270
+ if is_enum:
271
+ return f'Object.values({field_type})[0] as {field_type}'
272
+
273
+ # Handle primitive types
274
+ primitive_values = {
275
+ 'string': "'sample-string'",
276
+ 'number': '42',
277
+ 'boolean': 'true',
278
+ 'null': 'null',
279
+ 'Date': "new Date('2024-01-01T00:00:00Z')",
280
+ 'any': "{ test: 'data' }",
281
+ }
282
+
283
+ if field_type in primitive_values:
284
+ return primitive_values[field_type]
285
+
286
+ # For complex types (classes), call their createInstance method
287
+ return f'{field_type}.createInstance()'
288
+
289
+ def get_is_json_match_clause(self, field_name: str, field_type: str, field_is_enum: bool) -> str:
290
+ """Generates the isJsonMatch clause for a field."""
291
+ field_name_js = field_name.rstrip('_')
292
+ is_optional = field_type.endswith('?')
293
+ field_type = self.strip_nullable(field_type)
294
+
295
+ if '|' in field_type:
296
+ union_types = [t.strip() for t in field_type.split('|')]
297
+ union_clauses = [self.get_is_json_match_clause(field_name, union_type, False) for union_type in union_types]
298
+ clause = f"({' || '.join(union_clauses)})"
299
+ return clause
300
+
301
+ clause = f"(element.hasOwnProperty('{field_name_js}') && "
302
+
303
+ if field_is_enum:
304
+ clause += f"(typeof element['{field_name_js}'] === 'string' || typeof element['{field_name_js}'] === 'number')"
305
+ else:
306
+ if field_type == 'string':
307
+ clause += f"typeof element['{field_name_js}'] === 'string'"
308
+ elif field_type == 'number':
309
+ clause += f"typeof element['{field_name_js}'] === 'number'"
310
+ elif field_type == 'boolean':
311
+ clause += f"typeof element['{field_name_js}'] === 'boolean'"
312
+ elif field_type == 'Date':
313
+ clause += f"typeof element['{field_name_js}'] === 'string' && !isNaN(Date.parse(element['{field_name_js}']))"
314
+ elif field_type.startswith('{ [key: string]:'):
315
+ clause += f"typeof element['{field_name_js}'] === 'object' && !Array.isArray(element['{field_name_js}'])"
316
+ elif field_type.endswith('[]'):
317
+ clause += f"Array.isArray(element['{field_name_js}'])"
318
+ else:
319
+ clause += f"{field_type}.isJsonMatch(element['{field_name_js}'])"
320
+
321
+ if is_optional:
322
+ clause += f") || element['{field_name_js}'] === null"
323
+ else:
324
+ clause += ")"
325
+
326
+ return clause
327
+
328
+ def generate_embedded_union(self, class_name: str, field_name: str, avro_type: List, parent_namespace: str, parent_import_types: Set[str], write_file: bool = True) -> str:
329
+ """Generate embedded Union class for a field with namespace support."""
330
+ union_class_name = pascal(field_name) + 'Union' if field_name else pascal(class_name) + 'Union'
331
+ namespace = self.concat_namespace(self.base_package, parent_namespace)
332
+ import_types:Set[str] = set()
333
+ union_types = [self.convert_avro_type_to_typescript( t, parent_namespace, import_types) for t in avro_type if t != 'null']
334
+ if not import_types:
335
+ return '|'.join(union_types)
336
+ class_definition = ''
337
+ for import_type in import_types:
338
+ if import_type == union_class_name:
339
+ continue # Avoid importing itself
340
+ import_type_parts = import_type.split('.')
341
+ import_type_name = pascal(import_type_parts[-1])
342
+ import_path = '/'.join(import_type_parts)
343
+ current_path = '/'.join(namespace.split('.'))
344
+ relative_import_path = os.path.relpath(import_path, current_path).replace(os.sep, '/')
345
+ if not relative_import_path.startswith('.'):
346
+ relative_import_path = f'./{relative_import_path}'
347
+ class_definition += f"import {{ {import_type_name} }} from '{relative_import_path}.js';\n"
348
+
349
+ if self.typed_json_annotation:
350
+ class_definition += "import 'reflect-metadata';\n"
351
+ class_definition += "import { CustomDeserializerParams, CustomSerializerParams } from 'typedjson/lib/types/metadata.js';\n"
352
+
353
+
354
+ class_definition += f"\nexport class {union_class_name} {{\n"
355
+
356
+ class_definition += f"{self.INDENT}private value: any;\n\n"
357
+
358
+ # Constructor
359
+ class_definition += f"{self.INDENT}constructor(value: { ' | '.join(union_types) }) {{\n"
360
+ class_definition += f"{self.INDENT*2}this.value = value;\n"
361
+ class_definition += f"{self.INDENT}}}\n\n"
362
+
363
+ # Method to check which type is set
364
+ for union_type in union_types:
365
+ type_check_method = f"{self.INDENT}public is{pascal(union_type)}(): boolean {{\n"
366
+ if union_type.strip() in ['string', 'number', 'boolean']:
367
+ type_check_method += f"{self.INDENT*2}return typeof this.value === '{union_type.strip()}';\n"
368
+ elif union_type.strip() == 'Date':
369
+ type_check_method += f"{self.INDENT*2}return this.value instanceof Date;\n"
370
+ else:
371
+ type_check_method += f"{self.INDENT*2}return this.value instanceof {union_type.strip()};\n"
372
+ type_check_method += f"{self.INDENT}}}\n\n"
373
+ class_definition += type_check_method
374
+
375
+ # Method to return the current value
376
+ class_definition += f"{self.INDENT}public toJSON(): string {{\n"
377
+ class_definition += f"{self.INDENT*2}let rawJson : Uint8Array = this.value.toByteArray('application/json');\n"
378
+ class_definition += f"{self.INDENT*2}return new TextDecoder().decode(rawJson);\n"
379
+ class_definition += f"{self.INDENT}}}\n\n"
380
+
381
+ # Method to check if JSON matches any of the union types
382
+ class_definition += f"{self.INDENT}public static isJsonMatch(element: any): boolean {{\n"
383
+ match_clauses = []
384
+ for union_type in union_types:
385
+ match_clauses.append(f"({self.get_is_json_match_clause('value', union_type, False)})")
386
+ class_definition += f"{self.INDENT*2}return {' || '.join(match_clauses)};\n"
387
+ class_definition += f"{self.INDENT}}}\n\n"
388
+
389
+ # Method to deserialize from JSON
390
+ class_definition += f"{self.INDENT}public static fromData(element: any, contentTypeString: string): {union_class_name} {{\n"
391
+ class_definition += f"{self.INDENT*2}const unionTypes = [{', '.join([t.strip() for t in union_types if not self.is_typescript_primitive(t.strip())])}];\n"
392
+ class_definition += f"{self.INDENT*2}for (const type of unionTypes) {{\n"
393
+ class_definition += f"{self.INDENT*3}if (type.isJsonMatch(element)) {{\n"
394
+ class_definition += f"{self.INDENT*4}return new {union_class_name}(type.fromData(element, contentTypeString));\n"
395
+ class_definition += f"{self.INDENT*3}}}\n"
396
+ class_definition += f"{self.INDENT*2}}}\n"
397
+ class_definition += f"{self.INDENT*2}throw new Error('No matching type for union');\n"
398
+ class_definition += f"{self.INDENT}}}\n"
399
+
400
+ # Method to deserialize from JSON with custom deserializer params
401
+ class_definition += f"{self.INDENT}public static fromJSON(json: any, params: CustomDeserializerParams): {union_class_name} {{\n"
402
+ class_definition += f"{self.INDENT*2}try {{\n"
403
+ class_definition += f"{self.INDENT*3}return {union_class_name}.fromData(json, 'application/json');\n"
404
+ class_definition += f"{self.INDENT*2}}} catch (error) {{\n"
405
+ class_definition += f"{self.INDENT*3}return params.fallback(json, {union_class_name});\n"
406
+ class_definition += f"{self.INDENT*2}}}\n"
407
+ class_definition += f"{self.INDENT}}}\n\n"
408
+
409
+ # Method to serialize to JSON with custom serializer params
410
+ class_definition += f"{self.INDENT}public static toJSON(obj: any, params: CustomSerializerParams): any {{\n"
411
+ class_definition += f"{self.INDENT*2}try {{\n"
412
+ class_definition += f"{self.INDENT*3}const val = new {union_class_name}(obj);\n"
413
+ class_definition += f"{self.INDENT*3}return val.toJSON();\n"
414
+ class_definition += f"{self.INDENT*2}}} catch (error) {{\n"
415
+ class_definition += f"{self.INDENT*3}return params.fallback(this, {union_class_name});\n"
416
+ class_definition += f"{self.INDENT*2}}}\n"
417
+ class_definition += f"{self.INDENT}}}\n\n"
418
+
419
+ class_definition += "}\n"
420
+
421
+ if write_file:
422
+ self.write_to_file(namespace, union_class_name, class_definition)
423
+
424
+ parent_import_types.add(f"{namespace}.{union_class_name}")
425
+ self.generated_types[f"{namespace}.{union_class_name}"] = 'union'
426
+ return f"{union_class_name}"
427
+
428
+ def write_to_file(self, namespace: str, name: str, content: str):
429
+ """Write TypeScript class to file in the correct namespace directory."""
430
+ directory_path = os.path.join(self.src_dir, *namespace.split('.'))
431
+ if not os.path.exists(directory_path):
432
+ os.makedirs(directory_path, exist_ok=True)
433
+
434
+ file_path = os.path.join(directory_path, f"{name}.ts")
435
+ with open(file_path, 'w', encoding='utf-8') as file:
436
+ file.write(content)
437
+
438
+ def generate_index_file(self):
439
+ """Generate a root index.ts file that exports all types with aliases scoped to their modules."""
440
+ exports = []
441
+
442
+ for class_name in self.generated_types:
443
+ # Split the class_name into parts
444
+ parts = class_name.split('.')
445
+ file_name = parts[-1] # The actual type name (e.g., 'FareRules')
446
+ module_path = parts[:-1] # The module path excluding the type (e.g., ['gtfs_dash_data', 'GeneralTransitFeedStatic'])
447
+
448
+ # Construct the relative path to the .js file
449
+ # Exclude 'gtfs_dash_data' from the module path for the file path
450
+ file_relative_path = os.path.join(*(module_path[0:] + [f"{file_name}.js"])).replace(os.sep, '/')
451
+ if not file_relative_path.startswith('.'):
452
+ file_relative_path = './' + file_relative_path
453
+
454
+ # Construct the alias name by joining module parts with underscores
455
+ # Exclude 'gtfs_dash_data' for brevity
456
+ alias_parts = [pascal(part) for part in parts]
457
+ alias_name = '_'.join(alias_parts)
458
+
459
+ # Generate the export statement with alias
460
+ exports.append(f"export {{ {file_name} as {alias_name} }} from '{file_relative_path}';\n")
461
+
462
+ # Write the root index.ts file
463
+ index_file_path = os.path.join(self.src_dir, 'index.ts')
464
+ with open(index_file_path, 'w', encoding='utf-8') as f:
465
+ f.writelines(exports)
466
+
467
+ def generate_project_files(self, output_dir: str):
468
+ """Generate project files using templates."""
469
+ tsconfig_content = process_template(
470
+ "avrotots/tsconfig.json.jinja",
471
+ )
472
+
473
+ package_json_content = process_template(
474
+ "avrotots/package.json.jinja",
475
+ package_name=self.base_package,
476
+ )
477
+
478
+ gitignore_content = process_template(
479
+ "avrotots/gitignore.jinja",
480
+ )
481
+
482
+ tsconfig_path = os.path.join(output_dir, 'tsconfig.json')
483
+ package_json_path = os.path.join(output_dir, 'package.json')
484
+ gitignore_path = os.path.join(output_dir, '.gitignore')
485
+
486
+ with open(tsconfig_path, 'w', encoding='utf-8') as file:
487
+ file.write(tsconfig_content)
488
+
489
+ with open(package_json_path, 'w', encoding='utf-8') as file:
490
+ file.write(package_json_content)
491
+
492
+ with open(gitignore_path, 'w', encoding='utf-8') as file:
493
+ file.write(gitignore_content)
494
+
495
+ # Generate TypeScript type definitions for avro-js when using Avro annotations
496
+ if self.avro_annotation:
497
+ self.generate_avro_js_types(output_dir)
498
+
499
+ def generate_avro_js_types(self, output_dir: str):
500
+ """Generate TypeScript type declaration file for avro-js module."""
501
+ avro_js_types = '''declare module 'avro-js' {
502
+ /**
503
+ * Avro Type representation.
504
+ * Provides methods for encoding, decoding, and validating Avro data.
505
+ */
506
+ export class Type {
507
+ /**
508
+ * Encode a value to a Buffer.
509
+ * @param obj - Value to encode
510
+ * @returns Encoded Buffer
511
+ */
512
+ toBuffer(obj: any): Buffer;
513
+
514
+ /**
515
+ * Decode a value from a Buffer.
516
+ * @param buffer - Buffer to decode
517
+ * @returns Decoded value
518
+ */
519
+ fromBuffer(buffer: Buffer | Uint8Array): any;
520
+
521
+ /**
522
+ * Get string representation of the type or encode a value to JSON string.
523
+ * @param value - Optional value to encode
524
+ * @returns String representation
525
+ */
526
+ toString(value?: any): string;
527
+
528
+ /**
529
+ * Clone a value using the type's schema.
530
+ * @param value - Value to clone
531
+ * @param options - Clone options
532
+ * @returns Cloned value
533
+ */
534
+ clone(value: any, options?: any): any;
535
+
536
+ /**
537
+ * Compare two values according to Avro sort order.
538
+ * @param a - First value
539
+ * @param b - Second value
540
+ * @returns -1, 0, or 1
541
+ */
542
+ compare(a: any, b: any): number;
543
+
544
+ /**
545
+ * Check if a value is valid for this type.
546
+ * @param value - Value to validate
547
+ * @param options - Validation options
548
+ * @returns true if valid
549
+ */
550
+ isValid(value: any, options?: any): boolean;
551
+
552
+ /**
553
+ * Decode a value from a buffer.
554
+ * @param buffer - Buffer to decode
555
+ * @param resolver - Optional resolver for schema evolution
556
+ * @param noCheck - Skip validation
557
+ * @returns Decoded value
558
+ */
559
+ decode(buffer: Buffer, resolver?: any, noCheck?: boolean): any;
560
+
561
+ /**
562
+ * Encode a value to a buffer.
563
+ * @param value - Value to encode
564
+ * @param bufferSize - Optional buffer size
565
+ * @returns Encoded buffer
566
+ */
567
+ encode(value: any, bufferSize?: number): Buffer;
568
+
569
+ /**
570
+ * Create a resolver for schema evolution.
571
+ * @param writerType - Writer's type
572
+ * @returns Resolver
573
+ */
574
+ createResolver(writerType: Type): any;
575
+ }
576
+
577
+ /**
578
+ * Parse an Avro schema and return a Type instance.
579
+ * @param schema - Schema as string or object
580
+ * @param options - Parse options
581
+ * @returns Type instance
582
+ */
583
+ export function parse(schema: string | any, options?: any): Type;
584
+ }
585
+ '''
586
+
587
+ # Place type definitions in src directory so TypeScript can find them
588
+ src_dir = os.path.join(output_dir, 'src')
589
+ if not os.path.exists(src_dir):
590
+ os.makedirs(src_dir, exist_ok=True)
591
+
592
+ types_file_path = os.path.join(src_dir, 'avro-js.d.ts')
593
+ with open(types_file_path, 'w', encoding='utf-8') as file:
594
+ file.write(avro_js_types)
595
+
596
+ def generate_tests(self, output_dir: str):
597
+ """Generate Jest test files for all generated TypeScript classes."""
598
+ test_directory_path = os.path.join(output_dir, "test")
599
+ if not os.path.exists(test_directory_path):
600
+ os.makedirs(test_directory_path, exist_ok=True)
601
+
602
+ for qualified_name, type_kind in self.generated_types.items():
603
+ if type_kind == 'class':
604
+ self.generate_test_class(qualified_name, test_directory_path)
605
+
606
+ def generate_test_class(self, qualified_name: str, test_directory_path: str):
607
+ """Generate a Jest test file for a TypeScript class."""
608
+ parts = qualified_name.split('.')
609
+ class_name = parts[-1]
610
+ namespace = '.'.join(parts[:-1])
611
+ test_class_name = f"{class_name}.test"
612
+
613
+ fields = self.generated_type_fields.get(qualified_name, [])
614
+
615
+ # Build imports for nested types
616
+ imports_with_paths: Dict[str, str] = {}
617
+ for field in fields:
618
+ field_type = field.get('type_no_null', '')
619
+ if not self.is_typescript_primitive(field_type.replace('[]', '')):
620
+ # It's a reference type, need to import it
621
+ type_name = field_type.replace('[]', '').replace('?', '')
622
+ if type_name and type_name not in ['null', 'any', 'Date']:
623
+ # Build relative path from test dir to src dir
624
+ src_path = '/'.join(parts)
625
+ imports_with_paths[type_name] = f'../src/{src_path.rsplit("/", 1)[0]}/{type_name}.js' if '/' in src_path else f'../src/{parts[0]}/{type_name}.js'
626
+
627
+ # Calculate relative path from test directory to class file
628
+ class_path_parts = namespace.split('.') if namespace else []
629
+ relative_path = '../src/' + '/'.join(class_path_parts + [class_name]) + '.js'
630
+
631
+ test_definition = process_template(
632
+ "avrotots/class_test.ts.jinja",
633
+ class_name=class_name,
634
+ fields=fields,
635
+ imports=imports_with_paths,
636
+ typed_json_annotation=self.typed_json_annotation,
637
+ avro_annotation=self.avro_annotation,
638
+ )
639
+
640
+ # Update the import path in the generated test
641
+ test_definition = test_definition.replace(f"from './{class_name}.js'", f"from '{relative_path}'")
642
+
643
+ test_file_path = os.path.join(test_directory_path, f"{test_class_name}.ts")
644
+ with open(test_file_path, 'w', encoding='utf-8') as file:
645
+ file.write(test_definition)
646
+
647
+ def convert_schema(self, schema: Union[List[Dict], Dict], output_dir: str, write_file: bool = True):
648
+ """Convert Avro schema to TypeScript classes with namespace support."""
649
+ self.output_dir = output_dir
650
+ self.src_dir = os.path.join(self.output_dir, "src")
651
+ if isinstance(schema, dict):
652
+ schema = [schema]
653
+ self.main_schema = schema
654
+ self.type_dict = build_flat_type_dict(schema)
655
+ for avro_schema in schema:
656
+ if avro_schema['type'] == 'record':
657
+ self.generate_class(avro_schema, '', write_file)
658
+ elif avro_schema['type'] == 'enum':
659
+ self.generate_enum(avro_schema, '', write_file)
660
+ self.generate_index_file()
661
+ self.generate_project_files(output_dir)
662
+ self.generate_tests(output_dir)
663
+
664
+ def convert(self, avro_schema_path: str, output_dir: str):
665
+ """Convert Avro schema to TypeScript classes."""
666
+ with open(avro_schema_path, 'r', encoding='utf-8') as file:
667
+ schema = json.load(file)
668
+ self.convert_schema(schema, output_dir)
669
+ self.generate_project_files(output_dir)
670
+
671
+
672
+ def convert_avro_to_typescript(avro_schema_path, js_dir_path, package_name='', typedjson_annotation=False, avro_annotation=False):
673
+ """Convert Avro schema to TypeScript classes."""
674
+ if not package_name:
675
+ package_name = os.path.splitext(os.path.basename(avro_schema_path))[0].lower().replace('-', '_')
676
+
677
+ converter = AvroToTypeScript(package_name, typed_json_annotation=typedjson_annotation,
678
+ avro_annotation=avro_annotation)
679
+ converter.convert(avro_schema_path, js_dir_path)
680
+
681
+
682
+ def convert_avro_schema_to_typescript(avro_schema, js_dir_path, package_name='', typedjson_annotation=False, avro_annotation=False):
683
+ """Convert Avro schema to TypeScript classes."""
684
+ converter = AvroToTypeScript(package_name, typed_json_annotation=typedjson_annotation,
685
+ avro_annotation=avro_annotation)
686
+ converter.convert_schema(avro_schema, js_dir_path)
687
+ converter.generate_project_files(js_dir_path)