structurize 2.20.3__py3-none-any.whl → 2.20.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 +2 -0
- avrotize/_version.py +3 -3
- avrotize/avrotocsharp.py +121 -16
- avrotize/commands.json +168 -9
- avrotize/constants.py +15 -0
- avrotize/jsonstostructure.py +234 -12
- avrotize/openapitostructure.py +717 -0
- avrotize/structuretojs.py +657 -0
- avrotize/structuretots.py +28 -3
- {structurize-2.20.3.dist-info → structurize-2.20.5.dist-info}/METADATA +1 -1
- {structurize-2.20.3.dist-info → structurize-2.20.5.dist-info}/RECORD +15 -13
- {structurize-2.20.3.dist-info → structurize-2.20.5.dist-info}/WHEEL +0 -0
- {structurize-2.20.3.dist-info → structurize-2.20.5.dist-info}/entry_points.txt +0 -0
- {structurize-2.20.3.dist-info → structurize-2.20.5.dist-info}/licenses/LICENSE +0 -0
- {structurize-2.20.3.dist-info → structurize-2.20.5.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
# pylint: disable=line-too-long
|
|
2
|
+
|
|
3
|
+
""" StructureToJavaScript class for converting JSON Structure schema to JavaScript classes """
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from typing import Any, Dict, List, Set, Union, Optional
|
|
8
|
+
|
|
9
|
+
from avrotize.common import pascal, process_template
|
|
10
|
+
|
|
11
|
+
JsonNode = Dict[str, 'JsonNode'] | List['JsonNode'] | str | None
|
|
12
|
+
|
|
13
|
+
INDENT = ' '
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def is_javascript_reserved_word(word: str) -> bool:
|
|
17
|
+
"""Check if word is a JavaScript reserved word"""
|
|
18
|
+
reserved_words = [
|
|
19
|
+
'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger',
|
|
20
|
+
'default', 'delete', 'do', 'else', 'export', 'extends', 'finally',
|
|
21
|
+
'for', 'function', 'if', 'import', 'in', 'instanceof', 'new', 'return',
|
|
22
|
+
'super', 'switch', 'this', 'throw', 'try', 'typeof', 'var', 'void',
|
|
23
|
+
'while', 'with', 'yield', 'let', 'static', 'enum', 'await', 'async',
|
|
24
|
+
'implements', 'interface', 'package', 'private', 'protected', 'public'
|
|
25
|
+
]
|
|
26
|
+
return word in reserved_words
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def is_javascript_primitive(word: str) -> bool:
|
|
30
|
+
"""Check if word is a JavaScript primitive"""
|
|
31
|
+
primitives = ['null', 'boolean', 'number', 'string', 'Date', 'Array', 'Object']
|
|
32
|
+
return word in primitives
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class StructureToJavaScript:
|
|
36
|
+
"""Convert JSON Structure schema to JavaScript classes"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, base_package: str = '', avro_annotation=False) -> None:
|
|
39
|
+
self.base_package = base_package
|
|
40
|
+
self.avro_annotation = avro_annotation
|
|
41
|
+
self.output_dir = os.getcwd()
|
|
42
|
+
self.schema_doc: JsonNode = None
|
|
43
|
+
self.generated_types: Dict[str, str] = {}
|
|
44
|
+
self.generated_structure_types: Dict[str, Dict[str, Union[str, Dict, List]]] = {}
|
|
45
|
+
self.type_dict: Dict[str, Dict] = {}
|
|
46
|
+
self.definitions: Dict[str, Any] = {}
|
|
47
|
+
self.schema_registry: Dict[str, Dict] = {}
|
|
48
|
+
|
|
49
|
+
def get_qualified_name(self, namespace: str, name: str) -> str:
|
|
50
|
+
"""Concatenates namespace and name with a dot separator"""
|
|
51
|
+
return f"{namespace}.{name}" if namespace != '' else name
|
|
52
|
+
|
|
53
|
+
def concat_namespace(self, namespace: str, name: str) -> str:
|
|
54
|
+
"""Concatenates namespace and name with a dot separator"""
|
|
55
|
+
if namespace and name:
|
|
56
|
+
return f"{namespace}.{name}"
|
|
57
|
+
elif namespace:
|
|
58
|
+
return namespace
|
|
59
|
+
else:
|
|
60
|
+
return name
|
|
61
|
+
|
|
62
|
+
def map_primitive_to_javascript(self, structure_type: str) -> str:
|
|
63
|
+
"""Maps JSON Structure primitive types to JavaScript types"""
|
|
64
|
+
mapping = {
|
|
65
|
+
'null': 'null',
|
|
66
|
+
'boolean': 'boolean',
|
|
67
|
+
'string': 'string',
|
|
68
|
+
'integer': 'number',
|
|
69
|
+
'number': 'number',
|
|
70
|
+
'int8': 'number',
|
|
71
|
+
'uint8': 'number',
|
|
72
|
+
'int16': 'number',
|
|
73
|
+
'uint16': 'number',
|
|
74
|
+
'int32': 'number',
|
|
75
|
+
'uint32': 'number',
|
|
76
|
+
'int64': 'number',
|
|
77
|
+
'uint64': 'number',
|
|
78
|
+
'int128': 'bigint',
|
|
79
|
+
'uint128': 'bigint',
|
|
80
|
+
'float8': 'number',
|
|
81
|
+
'float': 'number',
|
|
82
|
+
'double': 'number',
|
|
83
|
+
'binary32': 'number',
|
|
84
|
+
'binary64': 'number',
|
|
85
|
+
'decimal': 'string', # JavaScript doesn't have native decimal
|
|
86
|
+
'binary': 'string', # Base64 encoded
|
|
87
|
+
'date': 'Date',
|
|
88
|
+
'time': 'string', # ISO 8601 time string
|
|
89
|
+
'datetime': 'Date',
|
|
90
|
+
'timestamp': 'Date',
|
|
91
|
+
'duration': 'string', # ISO 8601 duration string
|
|
92
|
+
'uuid': 'string',
|
|
93
|
+
'uri': 'string',
|
|
94
|
+
'jsonpointer': 'string',
|
|
95
|
+
'any': 'any'
|
|
96
|
+
}
|
|
97
|
+
qualified_class_name = self.get_qualified_name(self.base_package, pascal(structure_type))
|
|
98
|
+
if qualified_class_name in self.generated_types:
|
|
99
|
+
result = qualified_class_name
|
|
100
|
+
else:
|
|
101
|
+
result = mapping.get(structure_type, 'any')
|
|
102
|
+
return result
|
|
103
|
+
|
|
104
|
+
def is_javascript_primitive_type(self, js_type: str) -> bool:
|
|
105
|
+
"""Checks if a type is a JavaScript primitive type"""
|
|
106
|
+
return js_type in ['null', 'boolean', 'number', 'string', 'Date', 'bigint', 'any']
|
|
107
|
+
|
|
108
|
+
def safe_name(self, name: str) -> str:
|
|
109
|
+
"""Converts a name to a safe JavaScript name"""
|
|
110
|
+
if is_javascript_reserved_word(name):
|
|
111
|
+
return name + "_"
|
|
112
|
+
return name
|
|
113
|
+
|
|
114
|
+
def pascal_type_name(self, ref: str) -> str:
|
|
115
|
+
"""Converts a reference to a type name"""
|
|
116
|
+
return pascal(ref.split('.')[-1])
|
|
117
|
+
|
|
118
|
+
def resolve_ref(self, ref: str, context_schema: Optional[Dict] = None) -> Optional[Dict]:
|
|
119
|
+
"""Resolves a $ref to the actual schema definition"""
|
|
120
|
+
if not ref.startswith('#/'):
|
|
121
|
+
if ref in self.schema_registry:
|
|
122
|
+
return self.schema_registry[ref]
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
path = ref[2:].split('/')
|
|
126
|
+
schema = context_schema if context_schema else self.schema_doc
|
|
127
|
+
for part in path:
|
|
128
|
+
if not isinstance(schema, dict) or part not in schema:
|
|
129
|
+
return None
|
|
130
|
+
schema = schema[part]
|
|
131
|
+
return schema
|
|
132
|
+
|
|
133
|
+
def register_schema_ids(self, schema: Dict, base_uri: str = '') -> None:
|
|
134
|
+
"""Recursively registers schemas with $id keywords"""
|
|
135
|
+
if not isinstance(schema, dict):
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
if '$id' in schema:
|
|
139
|
+
schema_id = schema['$id']
|
|
140
|
+
if base_uri and not schema_id.startswith(('http://', 'https://', 'urn:')):
|
|
141
|
+
from urllib.parse import urljoin
|
|
142
|
+
schema_id = urljoin(base_uri, schema_id)
|
|
143
|
+
self.schema_registry[schema_id] = schema
|
|
144
|
+
base_uri = schema_id
|
|
145
|
+
|
|
146
|
+
if 'definitions' in schema:
|
|
147
|
+
for def_name, def_schema in schema['definitions'].items():
|
|
148
|
+
if isinstance(def_schema, dict):
|
|
149
|
+
self.register_schema_ids(def_schema, base_uri)
|
|
150
|
+
|
|
151
|
+
if 'properties' in schema:
|
|
152
|
+
for prop_name, prop_schema in schema['properties'].items():
|
|
153
|
+
if isinstance(prop_schema, dict):
|
|
154
|
+
self.register_schema_ids(prop_schema, base_uri)
|
|
155
|
+
|
|
156
|
+
for key in ['items', 'values', 'additionalProperties']:
|
|
157
|
+
if key in schema and isinstance(schema[key], dict):
|
|
158
|
+
self.register_schema_ids(schema[key], base_uri)
|
|
159
|
+
|
|
160
|
+
def convert_structure_type_to_javascript(self, class_name: str, field_name: str,
|
|
161
|
+
structure_type: JsonNode, parent_namespace: str,
|
|
162
|
+
import_types: Set[str]) -> str:
|
|
163
|
+
"""Converts JSON Structure type to JavaScript type"""
|
|
164
|
+
if isinstance(structure_type, str):
|
|
165
|
+
js_type = self.map_primitive_to_javascript(structure_type)
|
|
166
|
+
if js_type == structure_type and not self.is_javascript_primitive_type(js_type):
|
|
167
|
+
import_types.add(js_type)
|
|
168
|
+
return self.pascal_type_name(js_type)
|
|
169
|
+
return js_type
|
|
170
|
+
elif isinstance(structure_type, list):
|
|
171
|
+
# Handle type unions
|
|
172
|
+
non_null_types = [t for t in structure_type if t != 'null']
|
|
173
|
+
if len(non_null_types) == 1:
|
|
174
|
+
inner_type = self.convert_structure_type_to_javascript(
|
|
175
|
+
class_name, field_name, non_null_types[0], parent_namespace, import_types)
|
|
176
|
+
if 'null' in structure_type:
|
|
177
|
+
return f'{inner_type}|null'
|
|
178
|
+
return inner_type
|
|
179
|
+
else:
|
|
180
|
+
union_types = [self.convert_structure_type_to_javascript(
|
|
181
|
+
class_name, field_name, t, parent_namespace, import_types) for t in non_null_types]
|
|
182
|
+
return '|'.join(union_types)
|
|
183
|
+
elif isinstance(structure_type, dict):
|
|
184
|
+
# Handle $ref
|
|
185
|
+
if '$ref' in structure_type:
|
|
186
|
+
ref_schema = self.resolve_ref(structure_type['$ref'], self.schema_doc)
|
|
187
|
+
if ref_schema:
|
|
188
|
+
ref_path = structure_type['$ref'].split('/')
|
|
189
|
+
type_name = ref_path[-1]
|
|
190
|
+
ref_namespace = '.'.join(ref_path[2:-1]) if len(ref_path) > 3 else parent_namespace
|
|
191
|
+
ref = self.generate_class_or_choice(ref_schema, ref_namespace, write_file=True, explicit_name=type_name)
|
|
192
|
+
import_types.add(ref)
|
|
193
|
+
return ref.split('.')[-1]
|
|
194
|
+
return 'any'
|
|
195
|
+
|
|
196
|
+
# Handle enum keyword
|
|
197
|
+
if 'enum' in structure_type:
|
|
198
|
+
enum_ref = self.generate_enum(structure_type, field_name, parent_namespace, write_file=True)
|
|
199
|
+
import_types.add(enum_ref)
|
|
200
|
+
return enum_ref.split('.')[-1]
|
|
201
|
+
|
|
202
|
+
# Handle type keyword
|
|
203
|
+
if 'type' not in structure_type:
|
|
204
|
+
return 'any'
|
|
205
|
+
|
|
206
|
+
struct_type = structure_type['type']
|
|
207
|
+
|
|
208
|
+
# Handle complex types
|
|
209
|
+
if struct_type == 'object':
|
|
210
|
+
class_ref = self.generate_class(structure_type, parent_namespace, write_file=True)
|
|
211
|
+
import_types.add(class_ref)
|
|
212
|
+
return class_ref.split('.')[-1]
|
|
213
|
+
elif struct_type == 'array':
|
|
214
|
+
items_type = self.convert_structure_type_to_javascript(
|
|
215
|
+
class_name, field_name+'Item', structure_type.get('items', {'type': 'any'}),
|
|
216
|
+
parent_namespace, import_types)
|
|
217
|
+
return f'Array<{items_type}>'
|
|
218
|
+
elif struct_type == 'set':
|
|
219
|
+
items_type = self.convert_structure_type_to_javascript(
|
|
220
|
+
class_name, field_name+'Item', structure_type.get('items', {'type': 'any'}),
|
|
221
|
+
parent_namespace, import_types)
|
|
222
|
+
return f'Set<{items_type}>'
|
|
223
|
+
elif struct_type == 'map':
|
|
224
|
+
values_type = self.convert_structure_type_to_javascript(
|
|
225
|
+
class_name, field_name+'Value', structure_type.get('values', {'type': 'any'}),
|
|
226
|
+
parent_namespace, import_types)
|
|
227
|
+
return f'Object<string, {values_type}>'
|
|
228
|
+
elif struct_type == 'choice':
|
|
229
|
+
return self.generate_choice(structure_type, parent_namespace, write_file=True, import_types=import_types)
|
|
230
|
+
elif struct_type == 'tuple':
|
|
231
|
+
return self.generate_tuple(structure_type, parent_namespace, write_file=True, import_types=import_types)
|
|
232
|
+
else:
|
|
233
|
+
return self.convert_structure_type_to_javascript(class_name, field_name, struct_type, parent_namespace, import_types)
|
|
234
|
+
return 'any'
|
|
235
|
+
|
|
236
|
+
def generate_class_or_choice(self, structure_schema: Dict, parent_namespace: str,
|
|
237
|
+
write_file: bool = True, explicit_name: str = '') -> str:
|
|
238
|
+
"""Generates a Class or Choice"""
|
|
239
|
+
struct_type = structure_schema.get('type', 'object')
|
|
240
|
+
if struct_type == 'object':
|
|
241
|
+
return self.generate_class(structure_schema, parent_namespace, write_file, explicit_name=explicit_name)
|
|
242
|
+
elif struct_type == 'choice':
|
|
243
|
+
return self.generate_choice(structure_schema, parent_namespace, write_file, explicit_name=explicit_name)
|
|
244
|
+
elif struct_type == 'tuple':
|
|
245
|
+
return self.generate_tuple(structure_schema, parent_namespace, write_file, explicit_name=explicit_name)
|
|
246
|
+
return 'any'
|
|
247
|
+
|
|
248
|
+
def generate_class(self, structure_schema: Dict, parent_namespace: str,
|
|
249
|
+
write_file: bool, explicit_name: str = '') -> str:
|
|
250
|
+
"""Generates a JavaScript class from JSON Structure object type"""
|
|
251
|
+
import_types: Set[str] = set()
|
|
252
|
+
|
|
253
|
+
# Get name and namespace
|
|
254
|
+
class_name = pascal(explicit_name if explicit_name else structure_schema.get('name', 'UnnamedClass'))
|
|
255
|
+
schema_namespace = structure_schema.get('namespace', parent_namespace)
|
|
256
|
+
namespace = self.concat_namespace(self.base_package, schema_namespace)
|
|
257
|
+
qualified_name = self.get_qualified_name(namespace, class_name)
|
|
258
|
+
|
|
259
|
+
if qualified_name in self.generated_types:
|
|
260
|
+
return qualified_name
|
|
261
|
+
|
|
262
|
+
# Check if this is an abstract type
|
|
263
|
+
is_abstract = structure_schema.get('abstract', False)
|
|
264
|
+
|
|
265
|
+
# Handle inheritance ($extends)
|
|
266
|
+
base_class = None
|
|
267
|
+
base_class_name = None
|
|
268
|
+
if '$extends' in structure_schema:
|
|
269
|
+
base_ref = structure_schema['$extends']
|
|
270
|
+
if isinstance(self.schema_doc, dict):
|
|
271
|
+
base_schema = self.resolve_ref(base_ref, self.schema_doc)
|
|
272
|
+
if base_schema:
|
|
273
|
+
ref_path = base_ref.split('/')
|
|
274
|
+
base_name = ref_path[-1]
|
|
275
|
+
ref_namespace = '.'.join(ref_path[2:-1]) if len(ref_path) > 3 else parent_namespace
|
|
276
|
+
base_class = self.generate_class(base_schema, ref_namespace, write_file=True, explicit_name=base_name)
|
|
277
|
+
import_types.add(base_class)
|
|
278
|
+
base_class_name = pascal(base_name)
|
|
279
|
+
|
|
280
|
+
# Generate properties
|
|
281
|
+
properties = structure_schema.get('properties', {})
|
|
282
|
+
required_props = structure_schema.get('required', [])
|
|
283
|
+
|
|
284
|
+
# Collect fields for template
|
|
285
|
+
fields = []
|
|
286
|
+
static_fields = []
|
|
287
|
+
|
|
288
|
+
for prop_name, prop_schema in properties.items():
|
|
289
|
+
field_name = self.safe_name(prop_name)
|
|
290
|
+
field_doc = prop_schema.get('description', prop_schema.get('doc', ''))
|
|
291
|
+
|
|
292
|
+
# Check if this is a const field
|
|
293
|
+
if 'const' in prop_schema:
|
|
294
|
+
# Const fields are static properties
|
|
295
|
+
const_value = self.format_const_value(prop_schema['const'])
|
|
296
|
+
static_fields.append({
|
|
297
|
+
'name': field_name,
|
|
298
|
+
'value': const_value,
|
|
299
|
+
'docstring': field_doc
|
|
300
|
+
})
|
|
301
|
+
continue
|
|
302
|
+
|
|
303
|
+
# Determine if required
|
|
304
|
+
is_required = prop_name in required_props if not isinstance(required_props, list) or \
|
|
305
|
+
len(required_props) == 0 or not isinstance(required_props[0], list) else \
|
|
306
|
+
any(prop_name in req_set for req_set in required_props)
|
|
307
|
+
|
|
308
|
+
# Get property type
|
|
309
|
+
prop_type = self.convert_structure_type_to_javascript(
|
|
310
|
+
class_name, field_name, prop_schema, schema_namespace, import_types)
|
|
311
|
+
|
|
312
|
+
# Add default value if present
|
|
313
|
+
if 'default' in prop_schema:
|
|
314
|
+
default_val = self.format_default_value(prop_schema['default'], prop_type)
|
|
315
|
+
elif not is_required:
|
|
316
|
+
default_val = 'null'
|
|
317
|
+
else:
|
|
318
|
+
default_val = 'undefined'
|
|
319
|
+
|
|
320
|
+
fields.append({
|
|
321
|
+
'name': field_name,
|
|
322
|
+
'type': prop_type,
|
|
323
|
+
'default_value': default_val,
|
|
324
|
+
'docstring': field_doc
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
# Get docstring
|
|
328
|
+
doc = structure_schema.get('description', structure_schema.get('doc', class_name))
|
|
329
|
+
|
|
330
|
+
# Generate imports dictionary
|
|
331
|
+
imports = {}
|
|
332
|
+
for import_type in import_types:
|
|
333
|
+
import_type_package = import_type.rsplit('.', 1)[0]
|
|
334
|
+
import_type_type = pascal(import_type.split('.')[-1])
|
|
335
|
+
import_type_package = import_type_package.replace('.', '/')
|
|
336
|
+
namespace_path = namespace.replace('.', '/')
|
|
337
|
+
|
|
338
|
+
if import_type_package:
|
|
339
|
+
import_type_package = os.path.relpath(import_type_package, namespace_path).replace(os.sep, '/')
|
|
340
|
+
if not import_type_package.startswith('.'):
|
|
341
|
+
import_type_package = f'./{import_type_package}'
|
|
342
|
+
imports[import_type_type] = f'{import_type_package}/{import_type_type}'
|
|
343
|
+
else:
|
|
344
|
+
imports[import_type_type] = f'./{import_type_type}'
|
|
345
|
+
|
|
346
|
+
# Generate class definition using template
|
|
347
|
+
class_definition = process_template(
|
|
348
|
+
"structuretojs/class_core.js.jinja",
|
|
349
|
+
class_name=class_name,
|
|
350
|
+
docstring=doc,
|
|
351
|
+
fields=fields,
|
|
352
|
+
static_fields=static_fields,
|
|
353
|
+
imports=imports,
|
|
354
|
+
is_abstract=is_abstract,
|
|
355
|
+
base_class_name=base_class_name
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
if write_file:
|
|
359
|
+
self.write_to_file(namespace, class_name, class_definition)
|
|
360
|
+
# Generate test file for this class
|
|
361
|
+
self.generate_test_class(namespace, class_name, fields, import_types, prop_schema=structure_schema)
|
|
362
|
+
|
|
363
|
+
self.generated_types[qualified_name] = 'class'
|
|
364
|
+
self.generated_structure_types[qualified_name] = structure_schema
|
|
365
|
+
return qualified_name
|
|
366
|
+
|
|
367
|
+
def generate_enum(self, structure_schema: Dict, field_name: str, parent_namespace: str,
|
|
368
|
+
write_file: bool) -> str:
|
|
369
|
+
"""Generates a JavaScript enum from JSON Structure enum"""
|
|
370
|
+
class_name = pascal(structure_schema.get('name', field_name + 'Enum'))
|
|
371
|
+
schema_namespace = structure_schema.get('namespace', parent_namespace)
|
|
372
|
+
namespace = self.concat_namespace(self.base_package, schema_namespace)
|
|
373
|
+
qualified_name = self.get_qualified_name(namespace, class_name)
|
|
374
|
+
|
|
375
|
+
if qualified_name in self.generated_types:
|
|
376
|
+
return qualified_name
|
|
377
|
+
|
|
378
|
+
enum_values = structure_schema.get('enum', [])
|
|
379
|
+
symbols = [str(symbol) if not is_javascript_reserved_word(str(symbol)) else str(symbol) + "_"
|
|
380
|
+
for symbol in enum_values]
|
|
381
|
+
symbol_values = [str(val) for val in enum_values]
|
|
382
|
+
|
|
383
|
+
doc = structure_schema.get('description', structure_schema.get('doc', f'A {class_name} enum.'))
|
|
384
|
+
|
|
385
|
+
enum_definition = process_template(
|
|
386
|
+
"structuretojs/enum_core.js.jinja",
|
|
387
|
+
class_name=class_name,
|
|
388
|
+
docstring=doc,
|
|
389
|
+
symbols=symbols,
|
|
390
|
+
symbol_values=symbol_values,
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
if write_file:
|
|
394
|
+
self.write_to_file(namespace, class_name, enum_definition)
|
|
395
|
+
# Generate test file for this enum
|
|
396
|
+
self.generate_test_enum(namespace, class_name, enum_values)
|
|
397
|
+
|
|
398
|
+
self.generated_types[qualified_name] = 'enum'
|
|
399
|
+
self.generated_structure_types[qualified_name] = structure_schema
|
|
400
|
+
return qualified_name
|
|
401
|
+
|
|
402
|
+
def generate_choice(self, structure_schema: Dict, parent_namespace: str,
|
|
403
|
+
write_file: bool, explicit_name: str = '', import_types: Optional[Set[str]] = None) -> str:
|
|
404
|
+
"""Generates a JavaScript Union type from JSON Structure choice"""
|
|
405
|
+
if import_types is None:
|
|
406
|
+
import_types = set()
|
|
407
|
+
|
|
408
|
+
# Generate types for each choice
|
|
409
|
+
choice_types = []
|
|
410
|
+
choices = structure_schema.get('choices', {})
|
|
411
|
+
|
|
412
|
+
for choice_key, choice_schema in choices.items():
|
|
413
|
+
if isinstance(choice_schema, dict):
|
|
414
|
+
if '$ref' in choice_schema:
|
|
415
|
+
ref_schema = self.resolve_ref(choice_schema['$ref'], self.schema_doc)
|
|
416
|
+
if ref_schema:
|
|
417
|
+
ref_path = choice_schema['$ref'].split('/')
|
|
418
|
+
ref_name = ref_path[-1]
|
|
419
|
+
ref_namespace = '.'.join(ref_path[2:-1]) if len(ref_path) > 3 else parent_namespace
|
|
420
|
+
qualified_name = self.generate_class(ref_schema, ref_namespace, write_file=True, explicit_name=ref_name)
|
|
421
|
+
import_types.add(qualified_name)
|
|
422
|
+
choice_types.append(qualified_name.split('.')[-1])
|
|
423
|
+
elif 'type' in choice_schema:
|
|
424
|
+
js_type = self.convert_structure_type_to_javascript(
|
|
425
|
+
explicit_name, choice_key, choice_schema, parent_namespace, import_types)
|
|
426
|
+
choice_types.append(js_type)
|
|
427
|
+
|
|
428
|
+
# Return union type string
|
|
429
|
+
if len(choice_types) == 0:
|
|
430
|
+
return 'any'
|
|
431
|
+
elif len(choice_types) == 1:
|
|
432
|
+
return choice_types[0]
|
|
433
|
+
else:
|
|
434
|
+
return '|'.join(choice_types)
|
|
435
|
+
|
|
436
|
+
def generate_tuple(self, structure_schema: Dict, parent_namespace: str,
|
|
437
|
+
write_file: bool, explicit_name: str = '', import_types: Optional[Set[str]] = None) -> str:
|
|
438
|
+
"""Generates a JavaScript Array type for tuples"""
|
|
439
|
+
if import_types is None:
|
|
440
|
+
import_types = set()
|
|
441
|
+
|
|
442
|
+
# Tuples in JavaScript are represented as arrays
|
|
443
|
+
# For now, return Array<any> as tuples need special handling
|
|
444
|
+
return 'Array<any>'
|
|
445
|
+
|
|
446
|
+
def format_const_value(self, value: Any) -> str:
|
|
447
|
+
"""Formats a const value for JavaScript"""
|
|
448
|
+
if value is None:
|
|
449
|
+
return 'null'
|
|
450
|
+
elif isinstance(value, bool):
|
|
451
|
+
return 'true' if value else 'false'
|
|
452
|
+
elif isinstance(value, str):
|
|
453
|
+
return f'"{value}"'
|
|
454
|
+
elif isinstance(value, (int, float)):
|
|
455
|
+
return str(value)
|
|
456
|
+
elif isinstance(value, list):
|
|
457
|
+
items = ', '.join([self.format_const_value(v) for v in value])
|
|
458
|
+
return f'[{items}]'
|
|
459
|
+
elif isinstance(value, dict):
|
|
460
|
+
items = ', '.join([f'"{k}": {self.format_const_value(v)}' for k, v in value.items()])
|
|
461
|
+
return f'{{{items}}}'
|
|
462
|
+
return 'null'
|
|
463
|
+
|
|
464
|
+
def format_default_value(self, value: Any, js_type: str) -> str:
|
|
465
|
+
"""Formats a default value for JavaScript"""
|
|
466
|
+
return self.format_const_value(value)
|
|
467
|
+
|
|
468
|
+
def write_to_file(self, namespace: str, name: str, content: str):
|
|
469
|
+
"""Writes JavaScript class to file"""
|
|
470
|
+
directory_path = os.path.join(self.output_dir, namespace.replace('.', os.sep), 'src')
|
|
471
|
+
if not os.path.exists(directory_path):
|
|
472
|
+
os.makedirs(directory_path, exist_ok=True)
|
|
473
|
+
|
|
474
|
+
file_path = os.path.join(directory_path, f"{name}.js")
|
|
475
|
+
with open(file_path, 'w', encoding='utf-8') as file:
|
|
476
|
+
file.write(content)
|
|
477
|
+
|
|
478
|
+
def write_test_to_file(self, namespace: str, name: str, content: str):
|
|
479
|
+
"""Writes test content to a file in the test directory"""
|
|
480
|
+
directory_path = os.path.join(self.output_dir, namespace.replace('.', os.sep), 'test')
|
|
481
|
+
if not os.path.exists(directory_path):
|
|
482
|
+
os.makedirs(directory_path, exist_ok=True)
|
|
483
|
+
|
|
484
|
+
file_path = os.path.join(directory_path, f'test_{name}.js')
|
|
485
|
+
with open(file_path, 'w', encoding='utf-8') as file:
|
|
486
|
+
file.write(content)
|
|
487
|
+
|
|
488
|
+
def generate_test_value(self, field_type: str, field_name: str) -> str:
|
|
489
|
+
"""Generate appropriate test value based on field type"""
|
|
490
|
+
if field_type == 'string':
|
|
491
|
+
return f'"test_{field_name}"'
|
|
492
|
+
elif field_type in ['number', 'int', 'integer', 'float', 'double']:
|
|
493
|
+
return '42'
|
|
494
|
+
elif field_type == 'boolean':
|
|
495
|
+
return 'true'
|
|
496
|
+
elif field_type == 'Date':
|
|
497
|
+
return 'new Date("2024-01-01")'
|
|
498
|
+
elif field_type == 'null':
|
|
499
|
+
return 'null'
|
|
500
|
+
elif field_type.startswith('Array'):
|
|
501
|
+
return '[]'
|
|
502
|
+
elif field_type.startswith('Set'):
|
|
503
|
+
return 'new Set()'
|
|
504
|
+
elif field_type.startswith('Map') or field_type == 'Object':
|
|
505
|
+
return '{}'
|
|
506
|
+
else:
|
|
507
|
+
return f'new {field_type}()'
|
|
508
|
+
|
|
509
|
+
def generate_test_class(self, namespace: str, class_name: str, fields: List[Dict],
|
|
510
|
+
import_types: Set[str], prop_schema: Dict):
|
|
511
|
+
"""Generate test file for a class"""
|
|
512
|
+
test_imports = {}
|
|
513
|
+
for import_type in import_types:
|
|
514
|
+
import_type_type = pascal(import_type.split('.')[-1])
|
|
515
|
+
import_type_package = import_type.rsplit('.', 1)[0].replace('.', '/')
|
|
516
|
+
namespace_path = namespace.replace('.', '/')
|
|
517
|
+
|
|
518
|
+
if import_type_package:
|
|
519
|
+
rel_path = os.path.relpath(import_type_package, namespace_path).replace(os.sep, '/')
|
|
520
|
+
if not rel_path.startswith('.'):
|
|
521
|
+
rel_path = f'./{rel_path}'
|
|
522
|
+
test_imports[import_type_type] = f'../{rel_path}/{import_type_type}'
|
|
523
|
+
else:
|
|
524
|
+
test_imports[import_type_type] = f'./{import_type_type}'
|
|
525
|
+
|
|
526
|
+
test_fields = []
|
|
527
|
+
for field in fields:
|
|
528
|
+
test_value = self.generate_test_value(field['type'], field['name'])
|
|
529
|
+
test_fields.append({
|
|
530
|
+
'name': field['name'],
|
|
531
|
+
'type': field['type'],
|
|
532
|
+
'test_value': test_value,
|
|
533
|
+
'docstring': field.get('docstring', '')
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
# Class path should just be the class name since src/test structure mirrors each other
|
|
537
|
+
class_path = class_name
|
|
538
|
+
|
|
539
|
+
test_definition = process_template(
|
|
540
|
+
"structuretojs/test_class.js.jinja",
|
|
541
|
+
class_name=class_name,
|
|
542
|
+
class_path=class_path,
|
|
543
|
+
fields=test_fields,
|
|
544
|
+
test_imports=test_imports
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
self.write_test_to_file(namespace, class_name, test_definition)
|
|
548
|
+
|
|
549
|
+
def generate_test_enum(self, namespace: str, enum_name: str, enum_values: List):
|
|
550
|
+
"""Generate test file for an enum"""
|
|
551
|
+
expected_values = ', '.join([f'"{val}"' for val in enum_values])
|
|
552
|
+
|
|
553
|
+
# Enum path should just be the enum name
|
|
554
|
+
enum_path = enum_name
|
|
555
|
+
|
|
556
|
+
test_definition = process_template(
|
|
557
|
+
"structuretojs/test_enum.js.jinja",
|
|
558
|
+
enum_name=enum_name,
|
|
559
|
+
enum_path=enum_path,
|
|
560
|
+
expected_values=expected_values
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
self.write_test_to_file(namespace, enum_name, test_definition)
|
|
564
|
+
|
|
565
|
+
def process_definitions(self, definitions: Dict, namespace_path: str) -> None:
|
|
566
|
+
"""Processes the definitions section recursively"""
|
|
567
|
+
for name, definition in definitions.items():
|
|
568
|
+
if isinstance(definition, dict):
|
|
569
|
+
if 'type' in definition:
|
|
570
|
+
current_namespace = self.concat_namespace(namespace_path, '')
|
|
571
|
+
check_namespace = self.concat_namespace(self.base_package, current_namespace)
|
|
572
|
+
check_name = pascal(name)
|
|
573
|
+
check_ref = self.get_qualified_name(check_namespace, check_name)
|
|
574
|
+
if check_ref not in self.generated_types:
|
|
575
|
+
self.generate_class_or_choice(definition, current_namespace, write_file=True, explicit_name=name)
|
|
576
|
+
else:
|
|
577
|
+
new_namespace = self.concat_namespace(namespace_path, name)
|
|
578
|
+
self.process_definitions(definition, new_namespace)
|
|
579
|
+
|
|
580
|
+
def convert_schemas(self, structure_schemas: List, output_dir: str):
|
|
581
|
+
"""Converts JSON Structure schemas to JavaScript classes"""
|
|
582
|
+
self.output_dir = output_dir
|
|
583
|
+
if not os.path.exists(self.output_dir):
|
|
584
|
+
os.makedirs(self.output_dir, exist_ok=True)
|
|
585
|
+
|
|
586
|
+
# Register all schema IDs first
|
|
587
|
+
for structure_schema in structure_schemas:
|
|
588
|
+
if isinstance(structure_schema, dict):
|
|
589
|
+
self.register_schema_ids(structure_schema)
|
|
590
|
+
|
|
591
|
+
for structure_schema in structure_schemas:
|
|
592
|
+
if not isinstance(structure_schema, dict):
|
|
593
|
+
continue
|
|
594
|
+
|
|
595
|
+
self.schema_doc = structure_schema
|
|
596
|
+
|
|
597
|
+
if 'definitions' in structure_schema:
|
|
598
|
+
self.definitions = structure_schema['definitions']
|
|
599
|
+
|
|
600
|
+
# Process root type first
|
|
601
|
+
if 'type' in structure_schema:
|
|
602
|
+
self.generate_class_or_choice(structure_schema, '', write_file=True)
|
|
603
|
+
elif '$root' in structure_schema:
|
|
604
|
+
root_ref = structure_schema['$root']
|
|
605
|
+
root_schema = self.resolve_ref(root_ref, structure_schema)
|
|
606
|
+
if root_schema:
|
|
607
|
+
ref_path = root_ref.split('/')
|
|
608
|
+
type_name = ref_path[-1]
|
|
609
|
+
ref_namespace = '.'.join(ref_path[2:-1]) if len(ref_path) > 3 else ''
|
|
610
|
+
self.generate_class_or_choice(root_schema, ref_namespace, write_file=True, explicit_name=type_name)
|
|
611
|
+
|
|
612
|
+
# Process definitions
|
|
613
|
+
if 'definitions' in structure_schema:
|
|
614
|
+
self.process_definitions(self.definitions, '')
|
|
615
|
+
|
|
616
|
+
# Generate test runner after all classes and enums are generated
|
|
617
|
+
self.generate_test_runner()
|
|
618
|
+
|
|
619
|
+
def generate_test_runner(self):
|
|
620
|
+
"""Generate the test runner script"""
|
|
621
|
+
test_runner = process_template(
|
|
622
|
+
"structuretojs/test_runner.js.jinja"
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
# Write test runner to root test directory
|
|
626
|
+
test_dir = os.path.join(self.output_dir, self.base_package.replace('.', os.sep), 'test')
|
|
627
|
+
if not os.path.exists(test_dir):
|
|
628
|
+
os.makedirs(test_dir, exist_ok=True)
|
|
629
|
+
|
|
630
|
+
test_runner_path = os.path.join(test_dir, 'test_runner.js')
|
|
631
|
+
with open(test_runner_path, 'w', encoding='utf-8') as file:
|
|
632
|
+
file.write(test_runner)
|
|
633
|
+
|
|
634
|
+
def convert(self, structure_schema_path: str, output_dir: str):
|
|
635
|
+
"""Converts JSON Structure schema to JavaScript classes"""
|
|
636
|
+
with open(structure_schema_path, 'r', encoding='utf-8') as file:
|
|
637
|
+
schema = json.load(file)
|
|
638
|
+
if isinstance(schema, dict):
|
|
639
|
+
schema = [schema]
|
|
640
|
+
return self.convert_schemas(schema, output_dir)
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def convert_structure_to_javascript(structure_schema_path, js_file_path, package_name='', avro_annotation=False):
|
|
644
|
+
"""Converts JSON Structure schema to JavaScript classes"""
|
|
645
|
+
if not package_name:
|
|
646
|
+
package_name = os.path.splitext(os.path.basename(structure_schema_path))[0].lower().replace('-', '_')
|
|
647
|
+
|
|
648
|
+
structure_to_js = StructureToJavaScript(package_name, avro_annotation=avro_annotation)
|
|
649
|
+
structure_to_js.convert(structure_schema_path, js_file_path)
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def convert_structure_schema_to_javascript(structure_schema, js_file_path, package_name='', avro_annotation=False):
|
|
653
|
+
"""Converts JSON Structure schema to JavaScript classes"""
|
|
654
|
+
structure_to_js = StructureToJavaScript(package_name, avro_annotation=avro_annotation)
|
|
655
|
+
if isinstance(structure_schema, dict):
|
|
656
|
+
structure_schema = [structure_schema]
|
|
657
|
+
structure_to_js.convert_schemas(structure_schema, js_file_path)
|