structurize 2.16.2__py3-none-any.whl → 2.16.5__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 (53) hide show
  1. avrotize/__init__.py +63 -63
  2. avrotize/__main__.py +5 -5
  3. avrotize/_version.py +34 -34
  4. avrotize/asn1toavro.py +160 -160
  5. avrotize/avrotize.py +152 -152
  6. avrotize/avrotocpp.py +483 -483
  7. avrotize/avrotocsharp.py +992 -992
  8. avrotize/avrotocsv.py +121 -121
  9. avrotize/avrotodatapackage.py +173 -173
  10. avrotize/avrotodb.py +1383 -1383
  11. avrotize/avrotogo.py +476 -476
  12. avrotize/avrotographql.py +197 -197
  13. avrotize/avrotoiceberg.py +210 -210
  14. avrotize/avrotojava.py +1023 -1023
  15. avrotize/avrotojs.py +250 -250
  16. avrotize/avrotojsons.py +481 -481
  17. avrotize/avrotojstruct.py +345 -345
  18. avrotize/avrotokusto.py +363 -363
  19. avrotize/avrotomd.py +137 -137
  20. avrotize/avrotools.py +168 -168
  21. avrotize/avrotoparquet.py +208 -208
  22. avrotize/avrotoproto.py +358 -358
  23. avrotize/avrotopython.py +622 -622
  24. avrotize/avrotorust.py +435 -435
  25. avrotize/avrotots.py +598 -598
  26. avrotize/avrotoxsd.py +344 -344
  27. avrotize/commands.json +2493 -2433
  28. avrotize/common.py +828 -828
  29. avrotize/constants.py +4 -4
  30. avrotize/csvtoavro.py +131 -131
  31. avrotize/datapackagetoavro.py +76 -76
  32. avrotize/dependency_resolver.py +348 -348
  33. avrotize/jsonstoavro.py +1698 -1698
  34. avrotize/jsonstostructure.py +2642 -2642
  35. avrotize/jstructtoavro.py +878 -878
  36. avrotize/kstructtoavro.py +93 -93
  37. avrotize/kustotoavro.py +455 -455
  38. avrotize/parquettoavro.py +157 -157
  39. avrotize/proto2parser.py +497 -497
  40. avrotize/proto3parser.py +402 -402
  41. avrotize/prototoavro.py +382 -382
  42. avrotize/structuretocsharp.py +2005 -2005
  43. avrotize/structuretojsons.py +498 -498
  44. avrotize/structuretopython.py +772 -772
  45. avrotize/structuretots.py +653 -0
  46. avrotize/xsdtoavro.py +413 -413
  47. {structurize-2.16.2.dist-info → structurize-2.16.5.dist-info}/METADATA +848 -805
  48. structurize-2.16.5.dist-info/RECORD +52 -0
  49. {structurize-2.16.2.dist-info → structurize-2.16.5.dist-info}/licenses/LICENSE +200 -200
  50. structurize-2.16.2.dist-info/RECORD +0 -51
  51. {structurize-2.16.2.dist-info → structurize-2.16.5.dist-info}/WHEEL +0 -0
  52. {structurize-2.16.2.dist-info → structurize-2.16.5.dist-info}/entry_points.txt +0 -0
  53. {structurize-2.16.2.dist-info → structurize-2.16.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,653 @@
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
+
336
+ fields.append({
337
+ 'name': self.safe_name(prop_name),
338
+ 'original_name': prop_name,
339
+ 'type': field_type,
340
+ 'type_no_null': self.strip_nullable(field_type),
341
+ 'is_required': is_required,
342
+ 'is_optional': is_optional,
343
+ 'is_primitive': self.is_typescript_primitive(self.strip_nullable(field_type).replace('[]', '')),
344
+ 'docstring': prop_schema.get('description', '') if isinstance(prop_schema, dict) else ''
345
+ })
346
+
347
+ # Build imports
348
+ imports_with_paths: Dict[str, str] = {}
349
+ for import_type in import_types:
350
+ if import_type == typescript_qualified_name:
351
+ continue
352
+ import_is_enum = import_type in self.generated_types and self.generated_types[import_type] == 'enum'
353
+ import_type_parts = import_type.split('.')
354
+ import_type_name = pascal(import_type_parts[-1])
355
+ import_path = '/'.join(import_type_parts)
356
+ current_path = '/'.join(namespace.split('.'))
357
+ relative_import_path = os.path.relpath(import_path, current_path).replace(os.sep, '/')
358
+ if not relative_import_path.startswith('.'):
359
+ relative_import_path = f'./{relative_import_path}'
360
+ imports_with_paths[import_type_name] = relative_import_path + '.js'
361
+
362
+ # Generate class definition using template
363
+ class_definition = process_template(
364
+ "structuretots/class_core.ts.jinja",
365
+ namespace=namespace,
366
+ class_name=class_name,
367
+ base_class=base_class,
368
+ is_abstract=is_abstract,
369
+ docstring=structure_schema.get('description', '').strip() if 'description' in structure_schema else f'A {class_name} class.',
370
+ fields=fields,
371
+ imports=imports_with_paths,
372
+ typedjson_annotation=self.typedjson_annotation,
373
+ )
374
+
375
+ if write_file:
376
+ self.write_to_file(namespace, class_name, class_definition)
377
+ # Generate test class
378
+ if not is_abstract: # Don't generate tests for abstract classes
379
+ self.generate_test_class(namespace, class_name, fields)
380
+ self.generated_types[typescript_qualified_name] = 'class'
381
+ return typescript_qualified_name
382
+
383
+ def generate_enum(self, structure_schema: Dict, field_name: str, parent_namespace: str,
384
+ write_file: bool = True) -> str:
385
+ """ Generates a TypeScript enum from JSON Structure enum """
386
+ enum_name = pascal(structure_schema.get('name', field_name + 'Enum'))
387
+ namespace = self.concat_namespace(self.base_package, structure_schema.get('namespace', parent_namespace)).lower()
388
+ typescript_qualified_name = self.typescript_fully_qualified_name_from_structure_type(parent_namespace, enum_name)
389
+
390
+ if typescript_qualified_name in self.generated_types:
391
+ return typescript_qualified_name
392
+
393
+ symbols = structure_schema.get('enum', [])
394
+
395
+ enum_definition = process_template(
396
+ "structuretots/enum_core.ts.jinja",
397
+ namespace=namespace,
398
+ enum_name=enum_name,
399
+ docstring=structure_schema.get('description', '').strip() if 'description' in structure_schema else f'A {enum_name} enum.',
400
+ symbols=symbols,
401
+ )
402
+
403
+ if write_file:
404
+ self.write_to_file(namespace, enum_name, enum_definition)
405
+ self.generated_types[typescript_qualified_name] = 'enum'
406
+ return typescript_qualified_name
407
+
408
+ def generate_choice(self, structure_schema: Dict, parent_namespace: str,
409
+ write_file: bool = True, explicit_name: str = '',
410
+ import_types: Optional[Set[str]] = None) -> str:
411
+ """ Generates a TypeScript union type from JSON Structure choice type """
412
+ if import_types is None:
413
+ import_types = set()
414
+
415
+ choice_name = pascal(explicit_name if explicit_name else structure_schema.get('name', 'Choice'))
416
+ schema_namespace = structure_schema.get('namespace', parent_namespace)
417
+
418
+ # If the choice extends a base class, generate the base class first
419
+ if '$extends' in structure_schema:
420
+ base_ref = structure_schema['$extends']
421
+ if isinstance(self.schema_doc, dict):
422
+ base_schema = self.resolve_ref(base_ref, self.schema_doc)
423
+ if base_schema:
424
+ # Generate the base class
425
+ ref_path = base_ref.split('/')
426
+ base_name = ref_path[-1]
427
+ ref_namespace = '.'.join(ref_path[2:-1]) if len(ref_path) > 3 else parent_namespace
428
+ self.generate_class(base_schema, ref_namespace, write_file=True, explicit_name=base_name)
429
+
430
+ # Generate types for each choice
431
+ choice_types = []
432
+ choices = structure_schema.get('choices', {})
433
+
434
+ for choice_key, choice_schema in choices.items():
435
+ if isinstance(choice_schema, dict):
436
+ if '$ref' in choice_schema:
437
+ # Resolve reference and generate the type
438
+ ref_schema = self.resolve_ref(choice_schema['$ref'], self.schema_doc if isinstance(self.schema_doc, dict) else None)
439
+ if ref_schema:
440
+ ref_path = choice_schema['$ref'].split('/')
441
+ ref_name = ref_path[-1]
442
+ ref_namespace = '.'.join(ref_path[2:-1]) if len(ref_path) > 3 else parent_namespace
443
+ qualified_name = self.generate_class(ref_schema, ref_namespace, write_file=True, explicit_name=ref_name)
444
+ import_types.add(qualified_name)
445
+ choice_types.append(qualified_name.split('.')[-1])
446
+ elif 'type' in choice_schema:
447
+ # Generate inline type
448
+ ts_type = self.convert_structure_type_to_typescript(choice_name, choice_key, choice_schema, schema_namespace, import_types)
449
+ choice_types.append(ts_type)
450
+
451
+ # Return Union type
452
+ if len(choice_types) == 0:
453
+ return 'any'
454
+ elif len(choice_types) == 1:
455
+ return choice_types[0]
456
+ else:
457
+ return ' | '.join(choice_types)
458
+
459
+ def generate_tuple(self, structure_schema: Dict, parent_namespace: str,
460
+ write_file: bool = True, explicit_name: str = '') -> str:
461
+ """ Generates a TypeScript tuple type from JSON Structure tuple type """
462
+ tuple_name = pascal(explicit_name if explicit_name else structure_schema.get('name', 'Tuple'))
463
+ namespace = self.concat_namespace(self.base_package, structure_schema.get('namespace', parent_namespace)).lower()
464
+ typescript_qualified_name = self.typescript_fully_qualified_name_from_structure_type(parent_namespace, tuple_name)
465
+
466
+ if typescript_qualified_name in self.generated_types:
467
+ return typescript_qualified_name
468
+
469
+ import_types: Set[str] = set()
470
+ tuple_items = structure_schema.get('items', [])
471
+ item_types = []
472
+ for idx, item in enumerate(tuple_items):
473
+ item_type = self.convert_structure_type_to_typescript(
474
+ tuple_name, f'item{idx}', item, namespace, import_types)
475
+ item_types.append(item_type)
476
+
477
+ # TypeScript tuples are just arrays with fixed length and types
478
+ tuple_type = f"[{', '.join(item_types)}]"
479
+
480
+ # Generate type alias
481
+ tuple_definition = f"export type {tuple_name} = {tuple_type};\n"
482
+
483
+ if write_file:
484
+ self.write_to_file(namespace, tuple_name, tuple_definition)
485
+ self.generated_types[typescript_qualified_name] = 'tuple'
486
+ return typescript_qualified_name
487
+
488
+ def generate_test_value(self, field: Dict) -> str:
489
+ """Generates a test value for a given field in TypeScript"""
490
+ import random
491
+ field_type = field['type_no_null']
492
+
493
+ # Map TypeScript types to test values
494
+ test_values = {
495
+ 'string': '"test_string"',
496
+ 'number': str(random.randint(1, 100)),
497
+ 'bigint': 'BigInt(123)',
498
+ 'boolean': str(random.choice(['true', 'false'])).lower(),
499
+ 'Date': 'new Date()',
500
+ 'any': '{ test: "data" }',
501
+ 'null': 'null'
502
+ }
503
+
504
+ # Handle arrays
505
+ if field_type.endswith('[]'):
506
+ inner_type = field_type[:-2]
507
+ if inner_type in test_values:
508
+ return f"[{test_values[inner_type]}]"
509
+ else:
510
+ # For custom types, create empty array
511
+ return f"[]"
512
+
513
+ # Handle Set
514
+ if field_type.startswith('Set<'):
515
+ inner_type = field_type[4:-1]
516
+ if inner_type in test_values:
517
+ return f"new Set([{test_values[inner_type]}])"
518
+ else:
519
+ return f"new Set()"
520
+
521
+ # Handle maps (objects with string index signature)
522
+ if field_type.startswith('{ [key: string]:'):
523
+ return '{}'
524
+
525
+ # Return test value or construct object for custom types
526
+ return test_values.get(field_type, f'{{}} as {field_type}')
527
+
528
+ def generate_test_class(self, namespace: str, class_name: str, fields: List[Dict[str, Any]]) -> None:
529
+ """Generates a unit test class for a TypeScript class"""
530
+ # Get only required fields for the test
531
+ required_fields = [f for f in fields if f['is_required']]
532
+
533
+ # Generate test values for required fields
534
+ for field in required_fields:
535
+ field['test_value'] = self.generate_test_value(field)
536
+
537
+ # Determine relative path from test directory to src
538
+ namespace_path = namespace.replace('.', '/') if namespace else ''
539
+ if namespace_path:
540
+ relative_path = f"{namespace_path}/{class_name}"
541
+ else:
542
+ relative_path = class_name
543
+
544
+ test_class_definition = process_template(
545
+ "structuretots/test_class.ts.jinja",
546
+ class_name=class_name,
547
+ required_fields=required_fields,
548
+ relative_path=relative_path
549
+ )
550
+
551
+ # Write test file
552
+ test_dir = os.path.join(self.output_dir, "test")
553
+ os.makedirs(test_dir, exist_ok=True)
554
+
555
+ test_file_path = os.path.join(test_dir, f"{class_name}.test.ts")
556
+ with open(test_file_path, 'w', encoding='utf-8') as f:
557
+ f.write(test_class_definition)
558
+
559
+ def write_to_file(self, namespace: str, type_name: str, content: str) -> None:
560
+ """ Writes generated content to a TypeScript file """
561
+ namespace_path = namespace.replace('.', '/')
562
+ file_dir = os.path.join(self.output_dir, 'src', namespace_path)
563
+ os.makedirs(file_dir, exist_ok=True)
564
+
565
+ file_path = os.path.join(file_dir, f'{type_name}.ts')
566
+ with open(file_path, 'w', encoding='utf-8') as f:
567
+ f.write(content)
568
+
569
+ def generate_package_json(self, package_name: str) -> None:
570
+ """ Generates package.json file """
571
+ package_json = process_template(
572
+ "structuretots/package.json.jinja",
573
+ package_name=package_name or 'generated-types',
574
+ )
575
+
576
+ with open(os.path.join(self.output_dir, 'package.json'), 'w', encoding='utf-8') as f:
577
+ f.write(package_json)
578
+
579
+ def generate_tsconfig(self) -> None:
580
+ """ Generates tsconfig.json file """
581
+ tsconfig = process_template("structuretots/tsconfig.json.jinja")
582
+ with open(os.path.join(self.output_dir, 'tsconfig.json'), 'w', encoding='utf-8') as f:
583
+ f.write(tsconfig)
584
+
585
+ def generate_gitignore(self) -> None:
586
+ """ Generates .gitignore file """
587
+ gitignore = process_template("structuretots/gitignore.jinja")
588
+ with open(os.path.join(self.output_dir, '.gitignore'), 'w', encoding='utf-8') as f:
589
+ f.write(gitignore)
590
+
591
+ def generate_index(self) -> None:
592
+ """ Generates index.ts that exports all generated types """
593
+ exports = []
594
+ for qualified_name, type_kind in self.generated_types.items():
595
+ type_name = qualified_name.split('.')[-1]
596
+ namespace = '.'.join(qualified_name.split('.')[:-1])
597
+ if namespace:
598
+ relative_path = namespace.replace('.', '/') + '/' + type_name
599
+ else:
600
+ relative_path = type_name
601
+ exports.append(f"export * from './{relative_path}.js';")
602
+
603
+ index_content = '\n'.join(exports) + '\n' if exports else ''
604
+
605
+ src_dir = os.path.join(self.output_dir, 'src')
606
+ os.makedirs(src_dir, exist_ok=True)
607
+ with open(os.path.join(src_dir, 'index.ts'), 'w', encoding='utf-8') as f:
608
+ f.write(index_content)
609
+
610
+ def convert(self, structure_schema_path: str, output_dir: str, package_name: str = '') -> None:
611
+ """ Main conversion method """
612
+ self.output_dir = output_dir
613
+
614
+ # Load schema
615
+ with open(structure_schema_path, 'r', encoding='utf-8') as f:
616
+ self.schema_doc = json.load(f)
617
+
618
+ # Register schema IDs
619
+ self.register_schema_ids(self.schema_doc)
620
+
621
+ # Process definitions
622
+ if 'definitions' in self.schema_doc:
623
+ for def_name, def_schema in self.schema_doc['definitions'].items():
624
+ if isinstance(def_schema, dict):
625
+ self.generate_class_or_choice(def_schema, '', write_file=True, explicit_name=def_name)
626
+
627
+ # Process root schema if it's an object or choice
628
+ if 'type' in self.schema_doc:
629
+ root_namespace = self.schema_doc.get('namespace', '')
630
+ self.generate_class_or_choice(self.schema_doc, root_namespace, write_file=True)
631
+
632
+ # Generate project files
633
+ self.generate_package_json(package_name)
634
+ self.generate_tsconfig()
635
+ self.generate_gitignore()
636
+ self.generate_index()
637
+
638
+
639
+ def convert_structure_to_typescript(structure_schema_path: str, ts_file_path: str,
640
+ package_name: str = '', typedjson_annotation: bool = False,
641
+ avro_annotation: bool = False) -> None:
642
+ """
643
+ Converts a JSON Structure schema to TypeScript classes.
644
+
645
+ Args:
646
+ structure_schema_path: Path to the JSON Structure schema file
647
+ ts_file_path: Output directory for TypeScript files
648
+ package_name: Package name for the generated TypeScript project
649
+ typedjson_annotation: Whether to include TypedJSON annotations
650
+ avro_annotation: Whether to include Avro annotations
651
+ """
652
+ converter = StructureToTypeScript(package_name, typedjson_annotation, avro_annotation)
653
+ converter.convert(structure_schema_path, ts_file_path, package_name)