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.
- avrotize/__init__.py +63 -63
- avrotize/__main__.py +5 -5
- avrotize/_version.py +34 -34
- avrotize/asn1toavro.py +160 -160
- avrotize/avrotize.py +152 -152
- avrotize/avrotocpp.py +483 -483
- avrotize/avrotocsharp.py +992 -992
- avrotize/avrotocsv.py +121 -121
- avrotize/avrotodatapackage.py +173 -173
- avrotize/avrotodb.py +1383 -1383
- avrotize/avrotogo.py +476 -476
- avrotize/avrotographql.py +197 -197
- avrotize/avrotoiceberg.py +210 -210
- avrotize/avrotojava.py +1023 -1023
- avrotize/avrotojs.py +250 -250
- avrotize/avrotojsons.py +481 -481
- avrotize/avrotojstruct.py +345 -345
- avrotize/avrotokusto.py +363 -363
- avrotize/avrotomd.py +137 -137
- avrotize/avrotools.py +168 -168
- avrotize/avrotoparquet.py +208 -208
- avrotize/avrotoproto.py +358 -358
- avrotize/avrotopython.py +622 -622
- avrotize/avrotorust.py +435 -435
- avrotize/avrotots.py +598 -598
- avrotize/avrotoxsd.py +344 -344
- avrotize/commands.json +2493 -2433
- avrotize/common.py +828 -828
- avrotize/constants.py +4 -4
- avrotize/csvtoavro.py +131 -131
- avrotize/datapackagetoavro.py +76 -76
- avrotize/dependency_resolver.py +348 -348
- avrotize/jsonstoavro.py +1698 -1698
- avrotize/jsonstostructure.py +2642 -2642
- avrotize/jstructtoavro.py +878 -878
- avrotize/kstructtoavro.py +93 -93
- avrotize/kustotoavro.py +455 -455
- avrotize/parquettoavro.py +157 -157
- avrotize/proto2parser.py +497 -497
- avrotize/proto3parser.py +402 -402
- avrotize/prototoavro.py +382 -382
- avrotize/structuretocsharp.py +2005 -2005
- avrotize/structuretojsons.py +498 -498
- avrotize/structuretopython.py +772 -772
- avrotize/structuretots.py +653 -0
- avrotize/xsdtoavro.py +413 -413
- {structurize-2.16.2.dist-info → structurize-2.16.5.dist-info}/METADATA +848 -805
- structurize-2.16.5.dist-info/RECORD +52 -0
- {structurize-2.16.2.dist-info → structurize-2.16.5.dist-info}/licenses/LICENSE +200 -200
- structurize-2.16.2.dist-info/RECORD +0 -51
- {structurize-2.16.2.dist-info → structurize-2.16.5.dist-info}/WHEEL +0 -0
- {structurize-2.16.2.dist-info → structurize-2.16.5.dist-info}/entry_points.txt +0 -0
- {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)
|