structurize 2.20.2__py3-none-any.whl → 2.20.4__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.
@@ -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)