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
@@ -0,0 +1,740 @@
1
+ # pylint: disable=line-too-long
2
+
3
+ """ StructureToTypeScript class for converting JSON Structure schema to TypeScript classes """
4
+
5
+ import json
6
+ import os
7
+ import random
8
+ import re
9
+ from typing import Any, Dict, List, Set, Tuple, Union, Optional
10
+
11
+ from avrotize.common import pascal, process_template
12
+
13
+ JsonNode = Dict[str, 'JsonNode'] | List['JsonNode'] | str | None
14
+
15
+ INDENT = ' '
16
+
17
+
18
+ def is_typescript_reserved_word(word: str) -> bool:
19
+ """Checks if a word is a TypeScript reserved word"""
20
+ reserved_words = [
21
+ 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger',
22
+ 'default', 'delete', 'do', 'else', 'export', 'extends', 'finally',
23
+ 'for', 'function', 'if', 'import', 'in', 'instanceof', 'new', 'return',
24
+ 'super', 'switch', 'this', 'throw', 'try', 'typeof', 'var', 'void',
25
+ 'while', 'with', 'yield', 'enum', 'string', 'number', 'boolean', 'symbol',
26
+ 'type', 'namespace', 'module', 'declare', 'abstract', 'readonly',
27
+ ]
28
+ return word in reserved_words
29
+
30
+
31
+ class StructureToTypeScript:
32
+ """ Converts JSON Structure schema to TypeScript classes """
33
+
34
+ def __init__(self, base_package: str = '', typedjson_annotation=False, avro_annotation=False) -> None:
35
+ self.base_package = base_package
36
+ self.typedjson_annotation = typedjson_annotation
37
+ self.avro_annotation = avro_annotation
38
+ self.output_dir = os.getcwd()
39
+ self.schema_doc: JsonNode = None
40
+ self.generated_types: Dict[str, str] = {}
41
+ self.generated_structure_types: Dict[str, Dict[str, Union[str, Dict, List]]] = {}
42
+ self.type_dict: Dict[str, Dict] = {}
43
+ self.definitions: Dict[str, Any] = {}
44
+ self.schema_registry: Dict[str, Dict] = {}
45
+
46
+ def get_qualified_name(self, namespace: str, name: str) -> str:
47
+ """ Concatenates namespace and name with a dot separator """
48
+ return f"{namespace}.{name}" if namespace != '' else name
49
+
50
+ def concat_namespace(self, namespace: str, name: str) -> str:
51
+ """ Concatenates namespace and name with a dot separator """
52
+ if namespace and name:
53
+ return f"{namespace}.{name}"
54
+ elif namespace:
55
+ return namespace
56
+ else:
57
+ return name
58
+
59
+ def map_primitive_to_typescript(self, structure_type: str) -> str:
60
+ """ Maps JSON Structure primitive types to TypeScript types """
61
+ mapping = {
62
+ 'null': 'null',
63
+ 'boolean': 'boolean',
64
+ 'string': 'string',
65
+ 'integer': 'number',
66
+ 'number': 'number',
67
+ 'int8': 'number',
68
+ 'uint8': 'number',
69
+ 'int16': 'number',
70
+ 'uint16': 'number',
71
+ 'int32': 'number',
72
+ 'uint32': 'number',
73
+ 'int64': 'number',
74
+ 'uint64': 'number',
75
+ 'int128': 'bigint',
76
+ 'uint128': 'bigint',
77
+ 'float8': 'number',
78
+ 'float': 'number',
79
+ 'double': 'number',
80
+ 'binary32': 'number',
81
+ 'binary64': 'number',
82
+ 'decimal': 'string',
83
+ 'binary': 'string',
84
+ 'bytes': 'string',
85
+ 'date': 'Date',
86
+ 'time': 'Date',
87
+ 'datetime': 'Date',
88
+ 'timestamp': 'Date',
89
+ 'duration': 'string',
90
+ 'uuid': 'string',
91
+ 'uri': 'string',
92
+ 'jsonpointer': 'string',
93
+ 'any': 'any'
94
+ }
95
+ qualified_class_name = self.get_qualified_name(
96
+ self.base_package.lower(), structure_type.lower())
97
+ if qualified_class_name in self.generated_types:
98
+ result = qualified_class_name
99
+ else:
100
+ result = mapping.get(structure_type, 'any')
101
+ return result
102
+
103
+ def is_typescript_primitive(self, type_name: str) -> bool:
104
+ """ Checks if a type is a TypeScript primitive type """
105
+ return type_name in ['null', 'boolean', 'number', 'bigint', 'string', 'Date', 'any']
106
+
107
+ def safe_name(self, name: str) -> str:
108
+ """Converts a name to a safe TypeScript name"""
109
+ if is_typescript_reserved_word(name):
110
+ return name + "_"
111
+ return name
112
+
113
+ def pascal_type_name(self, ref: str) -> str:
114
+ """Converts a reference to a type name"""
115
+ return '_'.join([pascal(part) for part in ref.split('.')[-1].split('_')])
116
+
117
+ def typescript_package_from_structure_type(self, namespace: str, type_name: str) -> str:
118
+ """Gets the TypeScript package from a type name"""
119
+ if '.' in type_name:
120
+ # Type name contains dots, use it as package path
121
+ type_name_package = '.'.join([part.lower() for part in type_name.split('.')])
122
+ package = type_name_package
123
+ else:
124
+ # Use namespace as package, don't add type name to package
125
+ namespace_package = '.'.join([part.lower() for part in namespace.split('.')]) if namespace else ''
126
+ package = namespace_package
127
+ if self.base_package:
128
+ package = self.base_package + ('.' if package else '') + package
129
+ return package
130
+
131
+ def typescript_type_from_structure_type(self, type_name: str) -> str:
132
+ """Gets the TypeScript class from a type name"""
133
+ return self.pascal_type_name(type_name)
134
+
135
+ def typescript_fully_qualified_name_from_structure_type(self, namespace: str, type_name: str) -> str:
136
+ """Gets the fully qualified TypeScript class name from a Structure type."""
137
+ package = self.typescript_package_from_structure_type(namespace, type_name)
138
+ return package + ('.' if package else '') + self.typescript_type_from_structure_type(type_name)
139
+
140
+ def strip_package_from_fully_qualified_name(self, fully_qualified_name: str) -> str:
141
+ """Strips the package from a fully qualified name"""
142
+ return fully_qualified_name.split('.')[-1]
143
+
144
+ def strip_nullable(self, ts_type: str) -> str:
145
+ """Strip nullable marker from TypeScript type"""
146
+ if ts_type.endswith(' | null') or ts_type.endswith('| null'):
147
+ return ts_type.replace(' | null', '').replace('| null', '')
148
+ if ts_type.endswith('| undefined') or ts_type.endswith(' | undefined'):
149
+ return ts_type.replace(' | undefined', '').replace('| undefined', '')
150
+ return ts_type
151
+
152
+ def resolve_ref(self, ref: str, context_schema: Optional[Dict] = None) -> Optional[Dict]:
153
+ """ Resolves a $ref to the actual schema definition """
154
+ if not ref.startswith('#/'):
155
+ if ref in self.schema_registry:
156
+ return self.schema_registry[ref]
157
+ return None
158
+
159
+ path = ref[2:].split('/')
160
+ schema = context_schema if context_schema else self.schema_doc
161
+ for part in path:
162
+ if not isinstance(schema, dict) or part not in schema:
163
+ return None
164
+ schema = schema[part]
165
+ return schema
166
+
167
+ def register_schema_ids(self, schema: Dict, base_uri: str = '') -> None:
168
+ """ Recursively registers schemas with $id keywords """
169
+ if not isinstance(schema, dict):
170
+ return
171
+
172
+ if '$id' in schema:
173
+ schema_id = schema['$id']
174
+ if base_uri and not schema_id.startswith(('http://', 'https://', 'urn:')):
175
+ from urllib.parse import urljoin
176
+ schema_id = urljoin(base_uri, schema_id)
177
+ self.schema_registry[schema_id] = schema
178
+ base_uri = schema_id
179
+
180
+ if 'definitions' in schema:
181
+ for def_name, def_schema in schema['definitions'].items():
182
+ if isinstance(def_schema, dict):
183
+ self.register_schema_ids(def_schema, base_uri)
184
+
185
+ if 'properties' in schema:
186
+ for prop_name, prop_schema in schema['properties'].items():
187
+ if isinstance(prop_schema, dict):
188
+ self.register_schema_ids(prop_schema, base_uri)
189
+
190
+ for key in ['items', 'values', 'additionalProperties']:
191
+ if key in schema and isinstance(schema[key], dict):
192
+ self.register_schema_ids(schema[key], base_uri)
193
+
194
+ def convert_structure_type_to_typescript(self, class_name: str, field_name: str,
195
+ structure_type: JsonNode, parent_namespace: str,
196
+ import_types: Set[str]) -> str:
197
+ """ Converts JSON Structure type to TypeScript type """
198
+ if isinstance(structure_type, str):
199
+ ts_type = self.map_primitive_to_typescript(structure_type)
200
+ return ts_type
201
+ elif isinstance(structure_type, list):
202
+ # Handle type unions
203
+ non_null_types = [t for t in structure_type if t != 'null']
204
+ if len(non_null_types) == 1:
205
+ inner_type = self.convert_structure_type_to_typescript(
206
+ class_name, field_name, non_null_types[0], parent_namespace, import_types)
207
+ if 'null' in structure_type:
208
+ return f'{inner_type} | null'
209
+ return inner_type
210
+ else:
211
+ union_types = [self.convert_structure_type_to_typescript(
212
+ class_name, field_name, t, parent_namespace, import_types) for t in non_null_types]
213
+ result = ' | '.join(union_types)
214
+ if 'null' in structure_type:
215
+ result += ' | null'
216
+ return result
217
+ elif isinstance(structure_type, dict):
218
+ # Handle $ref
219
+ if '$ref' in structure_type:
220
+ ref_schema = self.resolve_ref(structure_type['$ref'], self.schema_doc)
221
+ if ref_schema:
222
+ ref_path = structure_type['$ref'].split('/')
223
+ type_name = ref_path[-1]
224
+ ref_namespace = '.'.join(ref_path[2:-1]) if len(ref_path) > 3 else parent_namespace
225
+ ref = self.generate_class_or_choice(ref_schema, ref_namespace, write_file=True, explicit_name=type_name)
226
+ import_types.add(ref)
227
+ return self.strip_package_from_fully_qualified_name(ref)
228
+ return 'any'
229
+
230
+ # Handle enum keyword
231
+ if 'enum' in structure_type:
232
+ enum_ref = self.generate_enum(structure_type, field_name, parent_namespace, write_file=True)
233
+ import_types.add(enum_ref)
234
+ return self.strip_package_from_fully_qualified_name(enum_ref)
235
+
236
+ # Handle type keyword
237
+ if 'type' not in structure_type:
238
+ return 'any'
239
+
240
+ struct_type = structure_type['type']
241
+
242
+ # Handle complex types
243
+ if struct_type == 'object':
244
+ class_ref = self.generate_class(structure_type, parent_namespace, write_file=True)
245
+ import_types.add(class_ref)
246
+ return self.strip_package_from_fully_qualified_name(class_ref)
247
+ elif struct_type == 'array':
248
+ items_type = self.convert_structure_type_to_typescript(
249
+ class_name, field_name+'Array', structure_type.get('items', {'type': 'any'}),
250
+ parent_namespace, import_types)
251
+ return f"{items_type}[]"
252
+ elif struct_type == 'set':
253
+ items_type = self.convert_structure_type_to_typescript(
254
+ class_name, field_name+'Set', structure_type.get('items', {'type': 'any'}),
255
+ parent_namespace, import_types)
256
+ return f"Set<{items_type}>"
257
+ elif struct_type == 'map':
258
+ values_type = self.convert_structure_type_to_typescript(
259
+ class_name, field_name+'Map', structure_type.get('values', {'type': 'any'}),
260
+ parent_namespace, import_types)
261
+ return f"{{ [key: string]: {values_type} }}"
262
+ elif struct_type == 'choice':
263
+ # Generate choice returns a Union type and populates import_types with the choice types
264
+ return self.generate_choice(structure_type, parent_namespace, write_file=True, import_types=import_types)
265
+ elif struct_type == 'tuple':
266
+ tuple_ref = self.generate_tuple(structure_type, parent_namespace, write_file=True)
267
+ import_types.add(tuple_ref)
268
+ return self.strip_package_from_fully_qualified_name(tuple_ref)
269
+ else:
270
+ return self.convert_structure_type_to_typescript(class_name, field_name, struct_type, parent_namespace, import_types)
271
+ return 'any'
272
+
273
+ def generate_class_or_choice(self, structure_schema: Dict, parent_namespace: str,
274
+ write_file: bool = True, explicit_name: str = '') -> str:
275
+ """ Generates a Class or Choice """
276
+ struct_type = structure_schema.get('type', 'object')
277
+ if struct_type == 'object':
278
+ return self.generate_class(structure_schema, parent_namespace, write_file, explicit_name=explicit_name)
279
+ elif struct_type == 'choice':
280
+ return self.generate_choice(structure_schema, parent_namespace, write_file, explicit_name=explicit_name)
281
+ elif struct_type == 'tuple':
282
+ return self.generate_tuple(structure_schema, parent_namespace, write_file, explicit_name=explicit_name)
283
+ return 'any'
284
+
285
+ def generate_class(self, structure_schema: Dict, parent_namespace: str,
286
+ write_file: bool, explicit_name: str = '') -> str:
287
+ """ Generates a TypeScript class/interface from JSON Structure object type """
288
+ import_types: Set[str] = set()
289
+
290
+ # Get name and namespace
291
+ class_name = pascal(explicit_name if explicit_name else structure_schema.get('name', 'UnnamedClass'))
292
+ schema_namespace = structure_schema.get('namespace', parent_namespace)
293
+ namespace = self.concat_namespace(self.base_package, schema_namespace).lower()
294
+ typescript_qualified_name = self.typescript_fully_qualified_name_from_structure_type(schema_namespace, class_name)
295
+
296
+ if typescript_qualified_name in self.generated_types:
297
+ return typescript_qualified_name
298
+
299
+ # Check if this is an abstract type
300
+ is_abstract = structure_schema.get('abstract', False)
301
+
302
+ # Handle inheritance ($extends)
303
+ base_class = None
304
+ if '$extends' in structure_schema:
305
+ base_ref = structure_schema['$extends']
306
+ if isinstance(self.schema_doc, dict):
307
+ base_schema = self.resolve_ref(base_ref, self.schema_doc)
308
+ if base_schema:
309
+ base_class_ref = self.generate_class(base_schema, parent_namespace, write_file=True)
310
+ base_class = self.strip_package_from_fully_qualified_name(base_class_ref)
311
+ import_types.add(base_class_ref)
312
+
313
+ # Collect properties
314
+ properties = structure_schema.get('properties', {})
315
+ required_props = set(structure_schema.get('required', []))
316
+
317
+ # Handle add-ins ($uses)
318
+ if '$uses' in structure_schema and isinstance(structure_schema['$uses'], list):
319
+ for addin_ref in structure_schema['$uses']:
320
+ if isinstance(addin_ref, str):
321
+ # Resolve the add-in reference
322
+ addin_schema = self.resolve_ref(addin_ref, self.schema_doc)
323
+ if addin_schema and 'properties' in addin_schema:
324
+ properties.update(addin_schema['properties'])
325
+ if 'required' in addin_schema:
326
+ required_props.update(addin_schema['required'])
327
+
328
+ # Generate fields
329
+ fields = []
330
+ for prop_name, prop_schema in properties.items():
331
+ field_type = self.convert_structure_type_to_typescript(
332
+ class_name, prop_name, prop_schema, namespace, import_types)
333
+ is_required = prop_name in required_props
334
+ is_optional = not is_required
335
+ field_type_no_null = self.strip_nullable(field_type)
336
+
337
+ # Check if the field type is an enum
338
+ is_enum = False
339
+ for import_type in import_types:
340
+ if import_type.endswith('.' + field_type_no_null) or import_type == field_type_no_null:
341
+ if import_type in self.generated_types and self.generated_types[import_type] == 'enum':
342
+ is_enum = True
343
+ break
344
+
345
+ fields.append({
346
+ 'name': self.safe_name(prop_name),
347
+ 'original_name': prop_name,
348
+ 'type': field_type,
349
+ 'type_no_null': field_type_no_null,
350
+ 'is_required': is_required,
351
+ 'is_optional': is_optional,
352
+ 'is_primitive': self.is_typescript_primitive(field_type_no_null.replace('[]', '')),
353
+ 'is_enum': is_enum,
354
+ 'docstring': prop_schema.get('description', '') if isinstance(prop_schema, dict) else ''
355
+ })
356
+
357
+ # Build imports
358
+ imports_with_paths: Dict[str, str] = {}
359
+ for import_type in import_types:
360
+ if import_type == typescript_qualified_name:
361
+ continue
362
+ import_is_enum = import_type in self.generated_types and self.generated_types[import_type] == 'enum'
363
+ import_type_parts = import_type.split('.')
364
+ import_type_name = pascal(import_type_parts[-1])
365
+ import_path = '/'.join(import_type_parts)
366
+ current_path = '/'.join(namespace.split('.'))
367
+ relative_import_path = os.path.relpath(import_path, current_path).replace(os.sep, '/')
368
+ if not relative_import_path.startswith('.'):
369
+ relative_import_path = f'./{relative_import_path}'
370
+ imports_with_paths[import_type_name] = relative_import_path + '.js'
371
+
372
+ # Prepare required fields with test values for createInstance()
373
+ required_fields = [f for f in fields if f.get('is_required', not f.get('is_optional', False))]
374
+ for field in required_fields:
375
+ field['test_value'] = self.generate_test_value(field)
376
+
377
+ # Generate class definition using template
378
+ class_definition = process_template(
379
+ "structuretots/class_core.ts.jinja",
380
+ namespace=namespace,
381
+ class_name=class_name,
382
+ base_class=base_class,
383
+ is_abstract=is_abstract,
384
+ docstring=structure_schema.get('description', '').strip() if 'description' in structure_schema else f'A {class_name} class.',
385
+ fields=fields,
386
+ required_fields=required_fields,
387
+ imports=imports_with_paths,
388
+ typedjson_annotation=self.typedjson_annotation,
389
+ )
390
+
391
+ if write_file:
392
+ self.write_to_file(namespace, class_name, class_definition)
393
+ # Generate test class
394
+ if not is_abstract: # Don't generate tests for abstract classes
395
+ self.generate_test_class(namespace, class_name, fields)
396
+ self.generated_types[typescript_qualified_name] = 'class'
397
+ return typescript_qualified_name
398
+
399
+ def generate_enum(self, structure_schema: Dict, field_name: str, parent_namespace: str,
400
+ write_file: bool = True) -> str:
401
+ """ Generates a TypeScript enum from JSON Structure enum """
402
+ enum_name = pascal(structure_schema.get('name', field_name + 'Enum'))
403
+ schema_namespace = structure_schema.get('namespace', parent_namespace)
404
+ namespace = self.concat_namespace(self.base_package, schema_namespace).lower()
405
+ # Use schema_namespace (not parent_namespace) to match the file location
406
+ typescript_qualified_name = self.typescript_fully_qualified_name_from_structure_type(schema_namespace, enum_name)
407
+
408
+ if typescript_qualified_name in self.generated_types:
409
+ return typescript_qualified_name
410
+
411
+ symbols = structure_schema.get('enum', [])
412
+
413
+ enum_definition = process_template(
414
+ "structuretots/enum_core.ts.jinja",
415
+ namespace=namespace,
416
+ enum_name=enum_name,
417
+ docstring=structure_schema.get('description', '').strip() if 'description' in structure_schema else f'A {enum_name} enum.',
418
+ symbols=symbols,
419
+ )
420
+
421
+ if write_file:
422
+ self.write_to_file(namespace, enum_name, enum_definition)
423
+ self.generated_types[typescript_qualified_name] = 'enum'
424
+ return typescript_qualified_name
425
+
426
+ def generate_choice(self, structure_schema: Dict, parent_namespace: str,
427
+ write_file: bool = True, explicit_name: str = '',
428
+ import_types: Optional[Set[str]] = None) -> str:
429
+ """ Generates a TypeScript union type from JSON Structure choice type """
430
+ if import_types is None:
431
+ import_types = set()
432
+
433
+ choice_name = pascal(explicit_name if explicit_name else structure_schema.get('name', 'Choice'))
434
+ schema_namespace = structure_schema.get('namespace', parent_namespace)
435
+
436
+ # If the choice extends a base class, generate the base class first
437
+ if '$extends' in structure_schema:
438
+ base_ref = structure_schema['$extends']
439
+ if isinstance(self.schema_doc, dict):
440
+ base_schema = self.resolve_ref(base_ref, self.schema_doc)
441
+ if base_schema:
442
+ # Generate the base class
443
+ ref_path = base_ref.split('/')
444
+ base_name = ref_path[-1]
445
+ ref_namespace = '.'.join(ref_path[2:-1]) if len(ref_path) > 3 else parent_namespace
446
+ self.generate_class(base_schema, ref_namespace, write_file=True, explicit_name=base_name)
447
+
448
+ # Generate types for each choice
449
+ choice_types = []
450
+ choices = structure_schema.get('choices', {})
451
+
452
+ for choice_key, choice_schema in choices.items():
453
+ if isinstance(choice_schema, dict):
454
+ if '$ref' in choice_schema:
455
+ # Resolve reference and generate the type
456
+ ref_schema = self.resolve_ref(choice_schema['$ref'], self.schema_doc if isinstance(self.schema_doc, dict) else None)
457
+ if ref_schema:
458
+ ref_path = choice_schema['$ref'].split('/')
459
+ ref_name = ref_path[-1]
460
+ ref_namespace = '.'.join(ref_path[2:-1]) if len(ref_path) > 3 else parent_namespace
461
+ qualified_name = self.generate_class(ref_schema, ref_namespace, write_file=True, explicit_name=ref_name)
462
+ import_types.add(qualified_name)
463
+ choice_types.append(qualified_name.split('.')[-1])
464
+ elif 'type' in choice_schema:
465
+ # Generate inline type
466
+ ts_type = self.convert_structure_type_to_typescript(choice_name, choice_key, choice_schema, schema_namespace, import_types)
467
+ choice_types.append(ts_type)
468
+
469
+ # Return Union type
470
+ if len(choice_types) == 0:
471
+ return 'any'
472
+ elif len(choice_types) == 1:
473
+ return choice_types[0]
474
+ else:
475
+ return ' | '.join(choice_types)
476
+
477
+ def generate_tuple(self, structure_schema: Dict, parent_namespace: str,
478
+ write_file: bool = True, explicit_name: str = '') -> str:
479
+ """ Generates a TypeScript tuple type from JSON Structure tuple type """
480
+ tuple_name = pascal(explicit_name if explicit_name else structure_schema.get('name', 'Tuple'))
481
+ namespace = self.concat_namespace(self.base_package, structure_schema.get('namespace', parent_namespace)).lower()
482
+ typescript_qualified_name = self.typescript_fully_qualified_name_from_structure_type(parent_namespace, tuple_name)
483
+
484
+ if typescript_qualified_name in self.generated_types:
485
+ return typescript_qualified_name
486
+
487
+ import_types: Set[str] = set()
488
+ tuple_items = structure_schema.get('items', [])
489
+ item_types = []
490
+ for idx, item in enumerate(tuple_items):
491
+ item_type = self.convert_structure_type_to_typescript(
492
+ tuple_name, f'item{idx}', item, namespace, import_types)
493
+ item_types.append(item_type)
494
+
495
+ # TypeScript tuples are just arrays with fixed length and types
496
+ tuple_type = f"[{', '.join(item_types)}]"
497
+
498
+ # Generate type alias
499
+ tuple_definition = f"export type {tuple_name} = {tuple_type};\n"
500
+
501
+ if write_file:
502
+ self.write_to_file(namespace, tuple_name, tuple_definition)
503
+ self.generated_types[typescript_qualified_name] = 'tuple'
504
+ return typescript_qualified_name
505
+
506
+ def generate_test_value(self, field: Dict) -> str:
507
+ """Generates a test value for a given field in TypeScript"""
508
+ import random
509
+ field_type = field['type_no_null']
510
+
511
+ # Map TypeScript types to test values
512
+ test_values = {
513
+ 'string': '"test_string"',
514
+ 'number': str(random.randint(1, 100)),
515
+ 'bigint': 'BigInt(123)',
516
+ 'boolean': str(random.choice(['true', 'false'])).lower(),
517
+ 'Date': 'new Date()',
518
+ 'any': '{ test: "data" }',
519
+ 'null': 'null'
520
+ }
521
+
522
+ # Handle arrays
523
+ if field_type.endswith('[]'):
524
+ inner_type = field_type[:-2]
525
+ if inner_type in test_values:
526
+ return f"[{test_values[inner_type]}]"
527
+ else:
528
+ # For custom types, create empty array
529
+ return f"[]"
530
+
531
+ # Handle Set
532
+ if field_type.startswith('Set<'):
533
+ inner_type = field_type[4:-1]
534
+ if inner_type in test_values:
535
+ return f"new Set([{test_values[inner_type]}])"
536
+ else:
537
+ return f"new Set()"
538
+
539
+ # Handle maps (objects with string index signature)
540
+ if field_type.startswith('{ [key: string]:'):
541
+ return '{}'
542
+
543
+ # Handle enums - use first value with Object.values()
544
+ if field.get('is_enum', False):
545
+ return f'Object.values({field_type})[0] as {field_type}'
546
+
547
+ # Return test value for primitives, or call createInstance() for complex types (classes)
548
+ return test_values.get(field_type, f'{field_type}.createInstance()')
549
+
550
+ def generate_test_class(self, namespace: str, class_name: str, fields: List[Dict[str, Any]]) -> None:
551
+ """Generates a unit test class for a TypeScript class"""
552
+ # Get only required fields for the test
553
+ required_fields = [f for f in fields if f['is_required']]
554
+
555
+ # Collect enum imports needed for test file
556
+ enum_imports: Dict[str, str] = {}
557
+
558
+ # Generate test values for required fields
559
+ for field in required_fields:
560
+ field['test_value'] = self.generate_test_value(field)
561
+ # Check if this field is an enum and needs an import
562
+ if field.get('is_enum', False):
563
+ enum_type = field['type_no_null']
564
+ # Find the enum in generated_types to get its full path
565
+ for qualified_name, type_kind in self.generated_types.items():
566
+ if type_kind == 'enum' and qualified_name.endswith('.' + enum_type):
567
+ # Build import path - lowercase namespace like write_to_file does
568
+ parts = qualified_name.split('.')
569
+ enum_namespace = '.'.join(parts[:-1]).lower()
570
+ enum_import_path = enum_namespace.replace('.', '/') + '/' + enum_type
571
+ enum_imports[enum_type] = f'../src/{enum_import_path}'
572
+ break
573
+
574
+ # Determine relative path from test directory to src
575
+ namespace_path = namespace.replace('.', '/') if namespace else ''
576
+ if namespace_path:
577
+ relative_path = f"{namespace_path}/{class_name}"
578
+ else:
579
+ relative_path = class_name
580
+
581
+ test_class_definition = process_template(
582
+ "structuretots/test_class.ts.jinja",
583
+ class_name=class_name,
584
+ required_fields=required_fields,
585
+ relative_path=relative_path,
586
+ enum_imports=enum_imports
587
+ )
588
+
589
+ # Write test file
590
+ test_dir = os.path.join(self.output_dir, "test")
591
+ os.makedirs(test_dir, exist_ok=True)
592
+
593
+ test_file_path = os.path.join(test_dir, f"{class_name}.test.ts")
594
+ with open(test_file_path, 'w', encoding='utf-8') as f:
595
+ f.write(test_class_definition)
596
+
597
+ def write_to_file(self, namespace: str, type_name: str, content: str) -> None:
598
+ """ Writes generated content to a TypeScript file """
599
+ namespace_path = namespace.replace('.', '/')
600
+ file_dir = os.path.join(self.output_dir, 'src', namespace_path)
601
+ os.makedirs(file_dir, exist_ok=True)
602
+
603
+ file_path = os.path.join(file_dir, f'{type_name}.ts')
604
+ with open(file_path, 'w', encoding='utf-8') as f:
605
+ f.write(content)
606
+
607
+ def generate_package_json(self, package_name: str) -> None:
608
+ """ Generates package.json file """
609
+ package_json = process_template(
610
+ "structuretots/package.json.jinja",
611
+ package_name=package_name or 'generated-types',
612
+ )
613
+
614
+ with open(os.path.join(self.output_dir, 'package.json'), 'w', encoding='utf-8') as f:
615
+ f.write(package_json)
616
+
617
+ def generate_tsconfig(self) -> None:
618
+ """ Generates tsconfig.json file """
619
+ tsconfig = process_template("structuretots/tsconfig.json.jinja")
620
+ with open(os.path.join(self.output_dir, 'tsconfig.json'), 'w', encoding='utf-8') as f:
621
+ f.write(tsconfig)
622
+
623
+ def generate_gitignore(self) -> None:
624
+ """ Generates .gitignore file """
625
+ gitignore = process_template("structuretots/gitignore.jinja")
626
+ with open(os.path.join(self.output_dir, '.gitignore'), 'w', encoding='utf-8') as f:
627
+ f.write(gitignore)
628
+
629
+ def generate_index(self) -> None:
630
+ """ Generates index.ts that exports all generated types with aliased exports """
631
+ exports = []
632
+ for qualified_name, type_kind in self.generated_types.items():
633
+ # Split the qualified_name into parts
634
+ parts = qualified_name.split('.')
635
+ type_name = parts[-1] # The actual type name
636
+ namespace = '.'.join(parts[:-1]) # The namespace excluding the type
637
+
638
+ # Construct the relative path to the .js file
639
+ if namespace:
640
+ # Lowercase the namespace to match the directory structure created by write_to_file
641
+ relative_path = namespace.lower().replace('.', '/') + '/' + type_name
642
+ else:
643
+ relative_path = type_name
644
+
645
+ if not relative_path.startswith('./'):
646
+ relative_path = './' + relative_path
647
+
648
+ # Construct the alias name by joining all parts with underscores (PascalCase)
649
+ alias_parts = [pascal(part) for part in parts]
650
+ alias_name = '_'.join(alias_parts)
651
+
652
+ # Generate the export statement with alias (like avrotots does)
653
+ exports.append(f"export {{ {type_name} as {alias_name} }} from '{relative_path}.js';")
654
+
655
+ index_content = '\n'.join(exports) + '\n' if exports else ''
656
+
657
+ src_dir = os.path.join(self.output_dir, 'src')
658
+ os.makedirs(src_dir, exist_ok=True)
659
+ with open(os.path.join(src_dir, 'index.ts'), 'w', encoding='utf-8') as f:
660
+ f.write(index_content)
661
+
662
+ def convert(self, structure_schema_path: str, output_dir: str, package_name: str = '') -> None:
663
+ """ Converts a JSON Structure schema file to TypeScript classes """
664
+ self.output_dir = output_dir
665
+
666
+ # Load schema
667
+ with open(structure_schema_path, 'r', encoding='utf-8') as f:
668
+ schema = json.load(f)
669
+
670
+ self.convert_schema(schema, output_dir, package_name)
671
+
672
+ def convert_schema(self, schema: JsonNode, output_dir: str, package_name: str = '') -> None:
673
+ """ Converts a JSON Structure schema (or list of schemas) to TypeScript classes """
674
+ # Normalize to list
675
+ if not isinstance(schema, list):
676
+ schema = [schema]
677
+
678
+ self.output_dir = output_dir
679
+ self.schema_doc = schema
680
+
681
+ # Register schema IDs for all schemas
682
+ for s in schema:
683
+ if isinstance(s, dict):
684
+ self.register_schema_ids(s)
685
+
686
+ # Process each schema
687
+ for s in schema:
688
+ if not isinstance(s, dict):
689
+ continue
690
+
691
+ # Process definitions
692
+ if 'definitions' in s:
693
+ for def_name, def_schema in s['definitions'].items():
694
+ if isinstance(def_schema, dict):
695
+ self.generate_class_or_choice(def_schema, '', write_file=True, explicit_name=def_name)
696
+
697
+ # Process root schema if it's an object or choice
698
+ if 'type' in s:
699
+ root_namespace = s.get('namespace', '')
700
+ self.generate_class_or_choice(s, root_namespace, write_file=True)
701
+
702
+ # Generate project files
703
+ self.generate_package_json(package_name)
704
+ self.generate_tsconfig()
705
+ self.generate_gitignore()
706
+ self.generate_index()
707
+
708
+
709
+ def convert_structure_to_typescript(structure_schema_path: str, ts_file_path: str,
710
+ package_name: str = '', typedjson_annotation: bool = False,
711
+ avro_annotation: bool = False) -> None:
712
+ """
713
+ Converts a JSON Structure schema to TypeScript classes.
714
+
715
+ Args:
716
+ structure_schema_path: Path to the JSON Structure schema file
717
+ ts_file_path: Output directory for TypeScript files
718
+ package_name: Package name for the generated TypeScript project
719
+ typedjson_annotation: Whether to include TypedJSON annotations
720
+ avro_annotation: Whether to include Avro annotations
721
+ """
722
+ converter = StructureToTypeScript(package_name, typedjson_annotation, avro_annotation)
723
+ converter.convert(structure_schema_path, ts_file_path, package_name)
724
+
725
+
726
+ def convert_structure_schema_to_typescript(structure_schema: JsonNode, output_dir: str,
727
+ package_name: str = '', typedjson_annotation: bool = False,
728
+ avro_annotation: bool = False) -> None:
729
+ """
730
+ Converts a JSON Structure schema to TypeScript classes.
731
+
732
+ Args:
733
+ structure_schema: JSON Structure schema to convert
734
+ output_dir: Output directory for TypeScript files
735
+ package_name: Package name for the generated TypeScript project
736
+ typedjson_annotation: Whether to include TypedJSON annotations
737
+ avro_annotation: Whether to include Avro annotations
738
+ """
739
+ converter = StructureToTypeScript(package_name, typedjson_annotation, avro_annotation)
740
+ converter.convert_schema(structure_schema, output_dir, package_name)