structurize 2.20.5__tar.gz → 2.21.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. {structurize-2.20.5/structurize.egg-info → structurize-2.21.1}/PKG-INFO +1 -1
  2. {structurize-2.20.5 → structurize-2.21.1}/avrotize/_version.py +3 -3
  3. {structurize-2.20.5 → structurize-2.21.1}/avrotize/avrotopython.py +4 -2
  4. {structurize-2.20.5 → structurize-2.21.1}/avrotize/structuretocsharp.py +2 -2
  5. {structurize-2.20.5 → structurize-2.21.1}/avrotize/structuretogo.py +9 -0
  6. {structurize-2.20.5 → structurize-2.21.1}/avrotize/structuretojava.py +86 -1
  7. {structurize-2.20.5 → structurize-2.21.1}/avrotize/structuretopython.py +41 -14
  8. {structurize-2.20.5 → structurize-2.21.1}/avrotize/structuretots.py +86 -24
  9. {structurize-2.20.5 → structurize-2.21.1/structurize.egg-info}/PKG-INFO +1 -1
  10. {structurize-2.20.5 → structurize-2.21.1}/.gitignore +0 -0
  11. {structurize-2.20.5 → structurize-2.21.1}/LICENSE +0 -0
  12. {structurize-2.20.5 → structurize-2.21.1}/MANIFEST.in +0 -0
  13. {structurize-2.20.5 → structurize-2.21.1}/README.md +0 -0
  14. {structurize-2.20.5 → structurize-2.21.1}/avrotize/__init__.py +0 -0
  15. {structurize-2.20.5 → structurize-2.21.1}/avrotize/__main__.py +0 -0
  16. {structurize-2.20.5 → structurize-2.21.1}/avrotize/asn1toavro.py +0 -0
  17. {structurize-2.20.5 → structurize-2.21.1}/avrotize/avrotize.py +0 -0
  18. {structurize-2.20.5 → structurize-2.21.1}/avrotize/avrotocpp.py +0 -0
  19. {structurize-2.20.5 → structurize-2.21.1}/avrotize/avrotocsharp.py +0 -0
  20. {structurize-2.20.5 → structurize-2.21.1}/avrotize/avrotocsv.py +0 -0
  21. {structurize-2.20.5 → structurize-2.21.1}/avrotize/avrotodatapackage.py +0 -0
  22. {structurize-2.20.5 → structurize-2.21.1}/avrotize/avrotodb.py +0 -0
  23. {structurize-2.20.5 → structurize-2.21.1}/avrotize/avrotogo.py +0 -0
  24. {structurize-2.20.5 → structurize-2.21.1}/avrotize/avrotographql.py +0 -0
  25. {structurize-2.20.5 → structurize-2.21.1}/avrotize/avrotoiceberg.py +0 -0
  26. {structurize-2.20.5 → structurize-2.21.1}/avrotize/avrotojava.py +0 -0
  27. {structurize-2.20.5 → structurize-2.21.1}/avrotize/avrotojs.py +0 -0
  28. {structurize-2.20.5 → structurize-2.21.1}/avrotize/avrotojsons.py +0 -0
  29. {structurize-2.20.5 → structurize-2.21.1}/avrotize/avrotojstruct.py +0 -0
  30. {structurize-2.20.5 → structurize-2.21.1}/avrotize/avrotokusto.py +0 -0
  31. {structurize-2.20.5 → structurize-2.21.1}/avrotize/avrotomd.py +0 -0
  32. {structurize-2.20.5 → structurize-2.21.1}/avrotize/avrotools.py +0 -0
  33. {structurize-2.20.5 → structurize-2.21.1}/avrotize/avrotoparquet.py +0 -0
  34. {structurize-2.20.5 → structurize-2.21.1}/avrotize/avrotoproto.py +0 -0
  35. {structurize-2.20.5 → structurize-2.21.1}/avrotize/avrotorust.py +0 -0
  36. {structurize-2.20.5 → structurize-2.21.1}/avrotize/avrotots.py +0 -0
  37. {structurize-2.20.5 → structurize-2.21.1}/avrotize/avrotoxsd.py +0 -0
  38. {structurize-2.20.5 → structurize-2.21.1}/avrotize/cddltostructure.py +0 -0
  39. {structurize-2.20.5 → structurize-2.21.1}/avrotize/commands.json +0 -0
  40. {structurize-2.20.5 → structurize-2.21.1}/avrotize/common.py +0 -0
  41. {structurize-2.20.5 → structurize-2.21.1}/avrotize/constants.py +0 -0
  42. {structurize-2.20.5 → structurize-2.21.1}/avrotize/csvtoavro.py +0 -0
  43. {structurize-2.20.5 → structurize-2.21.1}/avrotize/datapackagetoavro.py +0 -0
  44. {structurize-2.20.5 → structurize-2.21.1}/avrotize/dependencies/cpp/vcpkg/vcpkg.json +0 -0
  45. {structurize-2.20.5 → structurize-2.21.1}/avrotize/dependencies/typescript/node22/package.json +0 -0
  46. {structurize-2.20.5 → structurize-2.21.1}/avrotize/dependency_resolver.py +0 -0
  47. {structurize-2.20.5 → structurize-2.21.1}/avrotize/dependency_version.py +0 -0
  48. {structurize-2.20.5 → structurize-2.21.1}/avrotize/jsonstoavro.py +0 -0
  49. {structurize-2.20.5 → structurize-2.21.1}/avrotize/jsonstostructure.py +0 -0
  50. {structurize-2.20.5 → structurize-2.21.1}/avrotize/jstructtoavro.py +0 -0
  51. {structurize-2.20.5 → structurize-2.21.1}/avrotize/kstructtoavro.py +0 -0
  52. {structurize-2.20.5 → structurize-2.21.1}/avrotize/kustotoavro.py +0 -0
  53. {structurize-2.20.5 → structurize-2.21.1}/avrotize/openapitostructure.py +0 -0
  54. {structurize-2.20.5 → structurize-2.21.1}/avrotize/parquettoavro.py +0 -0
  55. {structurize-2.20.5 → structurize-2.21.1}/avrotize/proto2parser.py +0 -0
  56. {structurize-2.20.5 → structurize-2.21.1}/avrotize/proto3parser.py +0 -0
  57. {structurize-2.20.5 → structurize-2.21.1}/avrotize/prototoavro.py +0 -0
  58. {structurize-2.20.5 → structurize-2.21.1}/avrotize/structuretocddl.py +0 -0
  59. {structurize-2.20.5 → structurize-2.21.1}/avrotize/structuretocpp.py +0 -0
  60. {structurize-2.20.5 → structurize-2.21.1}/avrotize/structuretocsv.py +0 -0
  61. {structurize-2.20.5 → structurize-2.21.1}/avrotize/structuretodatapackage.py +0 -0
  62. {structurize-2.20.5 → structurize-2.21.1}/avrotize/structuretodb.py +0 -0
  63. {structurize-2.20.5 → structurize-2.21.1}/avrotize/structuretographql.py +0 -0
  64. {structurize-2.20.5 → structurize-2.21.1}/avrotize/structuretoiceberg.py +0 -0
  65. {structurize-2.20.5 → structurize-2.21.1}/avrotize/structuretojs.py +0 -0
  66. {structurize-2.20.5 → structurize-2.21.1}/avrotize/structuretojsons.py +0 -0
  67. {structurize-2.20.5 → structurize-2.21.1}/avrotize/structuretokusto.py +0 -0
  68. {structurize-2.20.5 → structurize-2.21.1}/avrotize/structuretomd.py +0 -0
  69. {structurize-2.20.5 → structurize-2.21.1}/avrotize/structuretoproto.py +0 -0
  70. {structurize-2.20.5 → structurize-2.21.1}/avrotize/structuretorust.py +0 -0
  71. {structurize-2.20.5 → structurize-2.21.1}/avrotize/structuretoxsd.py +0 -0
  72. {structurize-2.20.5 → structurize-2.21.1}/avrotize/xsdtoavro.py +0 -0
  73. {structurize-2.20.5 → structurize-2.21.1}/build.ps1 +0 -0
  74. {structurize-2.20.5 → structurize-2.21.1}/build.sh +0 -0
  75. {structurize-2.20.5 → structurize-2.21.1}/pyproject.toml +0 -0
  76. {structurize-2.20.5 → structurize-2.21.1}/setup.cfg +0 -0
  77. {structurize-2.20.5 → structurize-2.21.1}/structurize.egg-info/SOURCES.txt +0 -0
  78. {structurize-2.20.5 → structurize-2.21.1}/structurize.egg-info/dependency_links.txt +0 -0
  79. {structurize-2.20.5 → structurize-2.21.1}/structurize.egg-info/entry_points.txt +0 -0
  80. {structurize-2.20.5 → structurize-2.21.1}/structurize.egg-info/requires.txt +0 -0
  81. {structurize-2.20.5 → structurize-2.21.1}/structurize.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: structurize
3
- Version: 2.20.5
3
+ Version: 2.21.1
4
4
  Summary: Tools to convert from and to JSON Structure from various other schema languages.
5
5
  Author-email: Clemens Vasters <clemensv@microsoft.com>
6
6
  Classifier: Programming Language :: Python :: 3
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '2.20.5'
32
- __version_tuple__ = version_tuple = (2, 20, 5)
31
+ __version__ = version = '2.21.1'
32
+ __version_tuple__ = version_tuple = (2, 21, 1)
33
33
 
34
- __commit_id__ = commit_id = 'g4811363b1'
34
+ __commit_id__ = commit_id = 'g9b9841dd3'
@@ -327,7 +327,8 @@ class AvroToPython:
327
327
  def generate_test_class(self, package_name: str, class_name: str, fields: List[Dict[str, str]], import_types: Set[str]) -> None:
328
328
  """Generates a unit test class for a Python data class"""
329
329
  test_class_name = f"Test_{class_name}"
330
- tests_package_name = "test_"+package_name.replace('.', '_').lower()
330
+ flat_package = package_name.replace('.', '_').lower()
331
+ tests_package_name = flat_package if flat_package.startswith('test_') else f"test_{flat_package}"
331
332
  test_class_definition = process_template(
332
333
  "avrotopython/test_class.jinja",
333
334
  package_name=package_name,
@@ -348,7 +349,8 @@ class AvroToPython:
348
349
  def generate_test_enum(self, package_name: str, class_name: str, symbols: List[str]) -> None:
349
350
  """Generates a unit test class for a Python enum"""
350
351
  test_class_name = f"Test_{class_name}"
351
- tests_package_name = "test_"+package_name.replace('.', '_').lower()
352
+ flat_package = package_name.replace('.', '_').lower()
353
+ tests_package_name = flat_package if flat_package.startswith('test_') else f"test_{flat_package}"
352
354
  test_class_definition = process_template(
353
355
  "avrotopython/test_enum.jinja",
354
356
  package_name=package_name,
@@ -2072,8 +2072,8 @@ class StructureToCSharp:
2072
2072
  if enum_values:
2073
2073
  for value in enum_values:
2074
2074
  if isinstance(value, str):
2075
- # Convert to PascalCase enum member name
2076
- symbol_name = ''.join(word.capitalize() for word in re.split(r'[_\-\s]+', value))
2075
+ # Convert to PascalCase enum member name - must match generate_enum logic
2076
+ symbol_name = pascal(str(value).replace('-', '_').replace(' ', '_'))
2077
2077
  symbols.append(symbol_name)
2078
2078
  else:
2079
2079
  # For numeric enums, use Value1, Value2, etc.
@@ -523,6 +523,10 @@ class StructureToGo:
523
523
  v = f'float64({random.uniform(-100, 100)})'
524
524
  elif go_type == '[]byte':
525
525
  v = '[]byte("' + ''.join(random.choices(string.ascii_letters + string.digits, k=10)) + '")'
526
+ elif go_type == 'time.Time':
527
+ v = 'time.Now()'
528
+ elif go_type == 'time.Duration':
529
+ v = 'time.Hour'
526
530
  elif go_type.startswith('[]'):
527
531
  inner_type = go_type[2:]
528
532
  v = f'{go_type}{{{self.random_value(inner_type)}}}'
@@ -549,10 +553,15 @@ class StructureToGo:
549
553
  'enums': self.enums,
550
554
  'base_package': self.base_package,
551
555
  }
556
+ needs_time_import = False
552
557
  for struct in context['structs']:
553
558
  for field in struct['fields']:
554
559
  if 'value' not in field:
555
560
  field['value'] = self.random_value(field['type'])
561
+ # Check if time package is needed
562
+ if 'time.Time' in field['type'] or 'time.Duration' in field['type']:
563
+ needs_time_import = True
564
+ context['needs_time_import'] = needs_time_import
556
565
  helpers_file_name = os.path.join(self.output_dir, 'pkg', self.base_package, f"{self.base_package}_helpers.go")
557
566
  render_template('structuretogo/go_helpers.jinja', helpers_file_name, **context)
558
567
 
@@ -384,7 +384,12 @@ class StructureToJava:
384
384
  field_count=len(field_names)
385
385
  )
386
386
 
387
- class_definition = class_definition.rstrip() + equals_hashcode + "\n}\n"
387
+ # Generate createTestInstance() method for testing (only for non-abstract classes)
388
+ create_test_instance = ''
389
+ if not is_abstract:
390
+ create_test_instance = self.generate_create_test_instance_method(class_name, fields, schema_namespace)
391
+
392
+ class_definition = class_definition.rstrip() + create_test_instance + equals_hashcode + "\n}\n"
388
393
 
389
394
  if write_file:
390
395
  self.write_to_file(package, class_name, class_definition)
@@ -455,6 +460,86 @@ class StructureToJava:
455
460
  return str(value)
456
461
  return f"/* unsupported const value */"
457
462
 
463
+ def get_test_value(self, java_type: str) -> str:
464
+ """ Get a test value for a Java type """
465
+ # Handle arrays/lists
466
+ if java_type.startswith("List<") or java_type.startswith("ArrayList<"):
467
+ return "new java.util.ArrayList<>()"
468
+ if java_type.startswith("Map<"):
469
+ return "new java.util.HashMap<>()"
470
+ if java_type.endswith("[]"):
471
+ return f"new {java_type[:-2]}[0]"
472
+
473
+ # Primitive test values
474
+ test_values = {
475
+ 'String': '"test-string"',
476
+ 'string': '"test-string"',
477
+ 'int': '42',
478
+ 'Integer': '42',
479
+ 'long': '42L',
480
+ 'Long': '42L',
481
+ 'double': '3.14',
482
+ 'Double': '3.14',
483
+ 'float': '3.14f',
484
+ 'Float': '3.14f',
485
+ 'boolean': 'true',
486
+ 'Boolean': 'true',
487
+ 'byte': '(byte)0',
488
+ 'Byte': '(byte)0',
489
+ 'short': '(short)0',
490
+ 'Short': '(short)0',
491
+ 'BigInteger': 'java.math.BigInteger.ZERO',
492
+ 'BigDecimal': 'java.math.BigDecimal.ZERO',
493
+ 'byte[]': 'new byte[0]',
494
+ 'LocalDate': 'java.time.LocalDate.now()',
495
+ 'LocalTime': 'java.time.LocalTime.now()',
496
+ 'Instant': 'java.time.Instant.now()',
497
+ 'Duration': 'java.time.Duration.ZERO',
498
+ 'UUID': 'java.util.UUID.randomUUID()',
499
+ 'URI': 'java.net.URI.create("http://example.com")',
500
+ 'Object': 'new Object()',
501
+ 'void': 'null',
502
+ 'Void': 'null',
503
+ }
504
+
505
+ if java_type in test_values:
506
+ return test_values[java_type]
507
+
508
+ # Check if it's a generated type (enum or class)
509
+ if java_type in self.generated_types_java_package:
510
+ type_kind = self.generated_types_java_package[java_type]
511
+ if type_kind == "enum":
512
+ return f'{java_type}.values()[0]'
513
+ elif type_kind == "class":
514
+ return f'{java_type}.createTestInstance()'
515
+
516
+ # Default: try to instantiate
517
+ return f'new {java_type}()'
518
+
519
+ def generate_create_test_instance_method(self, class_name: str, fields: List[Dict], parent_package: str) -> str:
520
+ """ Generates a static createTestInstance method that creates a fully initialized instance """
521
+ method = f"\n{INDENT}/**\n{INDENT} * Creates a test instance with all required fields populated\n{INDENT} * @return a fully initialized test instance\n{INDENT} */\n"
522
+ method += f"{INDENT}public static {class_name} createTestInstance() {{\n"
523
+ method += f"{INDENT*2}{class_name} instance = new {class_name}();\n"
524
+
525
+ for field in fields:
526
+ # Skip const fields
527
+ if field.get('is_const', False):
528
+ continue
529
+
530
+ field_name = field['name']
531
+ field_type = field['type']
532
+
533
+ # Get a test value for this field
534
+ test_value = self.get_test_value(field_type)
535
+
536
+ # Setter name: set{Pascal(field_name)}
537
+ method += f"{INDENT*2}instance.set{pascal(field_name)}({test_value});\n"
538
+
539
+ method += f"{INDENT*2}return instance;\n"
540
+ method += f"{INDENT}}}\n"
541
+ return method
542
+
458
543
  def generate_enum(self, structure_schema: Dict, field_name: str, parent_package: str, write_file: bool) -> JavaType:
459
544
  """ Generates a Java enum from JSON Structure enum schema """
460
545
 
@@ -39,6 +39,7 @@ class StructureToPython:
39
39
  self.schema_doc: JsonNode = None
40
40
  self.generated_types: Dict[str, str] = {}
41
41
  self.generated_structure_types: Dict[str, Dict[str, Union[str, Dict, List]]] = {}
42
+ self.generated_enum_symbols: Dict[str, List[str]] = {}
42
43
  self.type_dict: Dict[str, Dict] = {}
43
44
  self.definitions: Dict[str, Any] = {}
44
45
  self.schema_registry: Dict[str, Dict] = {}
@@ -285,6 +286,7 @@ class StructureToPython:
285
286
  class_name = pascal(explicit_name if explicit_name else structure_schema.get('name', 'UnnamedClass'))
286
287
  schema_namespace = structure_schema.get('namespace', parent_namespace)
287
288
  namespace = self.concat_namespace(self.base_package, schema_namespace).lower()
289
+ package_name = self.python_package_from_structure_type(schema_namespace, class_name)
288
290
  python_qualified_name = self.python_fully_qualified_name_from_structure_type(schema_namespace, class_name)
289
291
 
290
292
  if python_qualified_name in self.generated_types:
@@ -357,8 +359,8 @@ class StructureToPython:
357
359
  )
358
360
 
359
361
  if write_file:
360
- self.write_to_file(namespace, class_name, class_definition)
361
- self.generate_test_class(namespace, class_name, field_docstrings, import_types)
362
+ self.write_to_file(package_name, class_name, class_definition)
363
+ self.generate_test_class(package_name, class_name, field_docstrings, import_types)
362
364
 
363
365
  self.generated_types[python_qualified_name] = 'class'
364
366
  self.generated_structure_types[python_qualified_name] = structure_schema
@@ -423,6 +425,7 @@ class StructureToPython:
423
425
  class_name = pascal(structure_schema.get('name', field_name + 'Enum'))
424
426
  schema_namespace = structure_schema.get('namespace', parent_namespace)
425
427
  namespace = self.concat_namespace(self.base_package, schema_namespace).lower()
428
+ package_name = self.python_package_from_structure_type(schema_namespace, class_name)
426
429
  python_qualified_name = self.python_fully_qualified_name_from_structure_type(schema_namespace, class_name)
427
430
 
428
431
  if python_qualified_name in self.generated_types:
@@ -441,10 +444,11 @@ class StructureToPython:
441
444
  )
442
445
 
443
446
  if write_file:
444
- self.write_to_file(namespace, class_name, enum_definition)
445
- self.generate_test_enum(namespace, class_name, symbols)
447
+ self.write_to_file(package_name, class_name, enum_definition)
448
+ self.generate_test_enum(package_name, class_name, symbols)
446
449
 
447
450
  self.generated_types[python_qualified_name] = 'enum'
451
+ self.generated_enum_symbols[python_qualified_name] = symbols
448
452
  return python_qualified_name
449
453
 
450
454
  def generate_choice(self, structure_schema: Dict, parent_namespace: str,
@@ -511,6 +515,7 @@ class StructureToPython:
511
515
  class_name = pascal(structure_schema.get('name', 'UnnamedMap'))
512
516
  schema_namespace = structure_schema.get('namespace', parent_namespace)
513
517
  namespace = self.concat_namespace(self.base_package, schema_namespace).lower()
518
+ package_name = self.python_package_from_structure_type(schema_namespace, class_name)
514
519
  python_qualified_name = self.python_fully_qualified_name_from_structure_type(schema_namespace, class_name)
515
520
 
516
521
  if python_qualified_name in self.generated_types:
@@ -535,7 +540,7 @@ class StructureToPython:
535
540
  )
536
541
 
537
542
  if write_file:
538
- self.write_to_file(namespace, class_name, map_definition)
543
+ self.write_to_file(package_name, class_name, map_definition)
539
544
 
540
545
  self.generated_types[python_qualified_name] = 'map'
541
546
  return python_qualified_name
@@ -596,7 +601,24 @@ class StructureToPython:
596
601
  elif field_type.startswith('typing.Union['):
597
602
  field_type = resolve(field_type)
598
603
  return generate_value(field_type)
599
- return test_values.get(field_type, 'Test_' + field_type + '.create_instance()')
604
+ if field_type in test_values:
605
+ return test_values[field_type]
606
+ # Check if this is an enum type - use first symbol value
607
+ # Look up by fully qualified name or by short name (class name only)
608
+ enum_symbols = None
609
+ if field_type in self.generated_enum_symbols:
610
+ enum_symbols = self.generated_enum_symbols[field_type]
611
+ else:
612
+ # Try to find by short name (the field type might be just the class name)
613
+ for qualified_name, symbols in self.generated_enum_symbols.items():
614
+ if qualified_name.endswith('.' + field_type) or qualified_name == field_type:
615
+ enum_symbols = symbols
616
+ break
617
+ if enum_symbols:
618
+ return f"{field_type.split('.')[-1]}.{enum_symbols[0]}"
619
+ # For complex types, use None since fields are typically optional
620
+ # This avoids needing to construct nested objects with required args
621
+ return 'None'
600
622
 
601
623
  return generate_value(field_type)
602
624
 
@@ -604,7 +626,8 @@ class StructureToPython:
604
626
  import_types: Set[str]) -> None:
605
627
  """Generates a unit test class for a Python dataclass"""
606
628
  test_class_name = f"Test_{class_name}"
607
- tests_package_name = "test_" + package_name.replace('.', '_').lower()
629
+ # Use a simpler file naming scheme based on class name only
630
+ test_file_name = f"test_{class_name.lower()}"
608
631
  test_class_definition = process_template(
609
632
  "structuretopython/test_class.jinja",
610
633
  package_name=package_name,
@@ -617,7 +640,7 @@ class StructureToPython:
617
640
  )
618
641
 
619
642
  base_dir = os.path.join(self.output_dir, "tests")
620
- test_file_path = os.path.join(base_dir, f"{tests_package_name.replace('.', '_').lower()}.py")
643
+ test_file_path = os.path.join(base_dir, f"{test_file_name}.py")
621
644
  if not os.path.exists(os.path.dirname(test_file_path)):
622
645
  os.makedirs(os.path.dirname(test_file_path), exist_ok=True)
623
646
  with open(test_file_path, 'w', encoding='utf-8') as file:
@@ -626,7 +649,8 @@ class StructureToPython:
626
649
  def generate_test_enum(self, package_name: str, class_name: str, symbols: List[str]) -> None:
627
650
  """Generates a unit test class for a Python enum"""
628
651
  test_class_name = f"Test_{class_name}"
629
- tests_package_name = "test_" + package_name.replace('.', '_').lower()
652
+ # Use a simpler file naming scheme based on class name only
653
+ test_file_name = f"test_{class_name.lower()}"
630
654
  test_class_definition = process_template(
631
655
  "structuretopython/test_enum.jinja",
632
656
  package_name=package_name,
@@ -635,7 +659,7 @@ class StructureToPython:
635
659
  symbols=symbols
636
660
  )
637
661
  base_dir = os.path.join(self.output_dir, "tests")
638
- test_file_path = os.path.join(base_dir, f"{tests_package_name.replace('.', '_').lower()}.py")
662
+ test_file_path = os.path.join(base_dir, f"{test_file_name}.py")
639
663
  if not os.path.exists(os.path.dirname(test_file_path)):
640
664
  os.makedirs(os.path.dirname(test_file_path), exist_ok=True)
641
665
  with open(test_file_path, 'w', encoding='utf-8') as file:
@@ -643,9 +667,8 @@ class StructureToPython:
643
667
 
644
668
  def write_to_file(self, package: str, class_name: str, python_code: str):
645
669
  """Writes a Python class to a file"""
646
- # Add 'struct' module to the package path
647
- full_package = f"{package}.struct"
648
- parent_package_name = '.'.join(full_package.split('.')[:-1])
670
+ # The containing directory is the parent package (matches avrotopython.py)
671
+ parent_package_name = '.'.join(package.split('.')[:-1])
649
672
  parent_package_path = os.sep.join(parent_package_name.split('.')).lower()
650
673
  directory_path = os.path.join(self.output_dir, "src", parent_package_path)
651
674
  if not os.path.exists(directory_path):
@@ -758,7 +781,11 @@ class StructureToPython:
758
781
  def convert_structure_to_python(structure_schema_path, py_file_path, package_name='', dataclasses_json_annotation=False, avro_annotation=False):
759
782
  """Converts JSON Structure schema to Python dataclasses"""
760
783
  if not package_name:
761
- package_name = os.path.splitext(os.path.basename(structure_schema_path))[0].lower().replace('-', '_')
784
+ # Strip .json extension, then also strip .struct suffix if present (*.struct.json naming convention)
785
+ base_name = os.path.splitext(os.path.basename(structure_schema_path))[0]
786
+ if base_name.endswith('.struct'):
787
+ base_name = base_name[:-7] # Remove '.struct' suffix
788
+ package_name = base_name.lower().replace('-', '_')
762
789
 
763
790
  structure_to_python = StructureToPython(package_name, dataclasses_json_annotation=dataclasses_json_annotation, avro_annotation=avro_annotation)
764
791
  structure_to_python.convert(structure_schema_path, py_file_path)
@@ -332,15 +332,25 @@ class StructureToTypeScript:
332
332
  class_name, prop_name, prop_schema, namespace, import_types)
333
333
  is_required = prop_name in required_props
334
334
  is_optional = not is_required
335
+ field_type_no_null = self.strip_nullable(field_type)
336
+
337
+ # Check if the field type is an enum
338
+ is_enum = False
339
+ for import_type in import_types:
340
+ if import_type.endswith('.' + field_type_no_null) or import_type == field_type_no_null:
341
+ if import_type in self.generated_types and self.generated_types[import_type] == 'enum':
342
+ is_enum = True
343
+ break
335
344
 
336
345
  fields.append({
337
346
  'name': self.safe_name(prop_name),
338
347
  'original_name': prop_name,
339
348
  'type': field_type,
340
- 'type_no_null': self.strip_nullable(field_type),
349
+ 'type_no_null': field_type_no_null,
341
350
  'is_required': is_required,
342
351
  'is_optional': is_optional,
343
- 'is_primitive': self.is_typescript_primitive(self.strip_nullable(field_type).replace('[]', '')),
352
+ 'is_primitive': self.is_typescript_primitive(field_type_no_null.replace('[]', '')),
353
+ 'is_enum': is_enum,
344
354
  'docstring': prop_schema.get('description', '') if isinstance(prop_schema, dict) else ''
345
355
  })
346
356
 
@@ -359,6 +369,11 @@ class StructureToTypeScript:
359
369
  relative_import_path = f'./{relative_import_path}'
360
370
  imports_with_paths[import_type_name] = relative_import_path + '.js'
361
371
 
372
+ # Prepare required fields with test values for createInstance()
373
+ required_fields = [f for f in fields if f.get('is_required', not f.get('is_optional', False))]
374
+ for field in required_fields:
375
+ field['test_value'] = self.generate_test_value(field)
376
+
362
377
  # Generate class definition using template
363
378
  class_definition = process_template(
364
379
  "structuretots/class_core.ts.jinja",
@@ -368,6 +383,7 @@ class StructureToTypeScript:
368
383
  is_abstract=is_abstract,
369
384
  docstring=structure_schema.get('description', '').strip() if 'description' in structure_schema else f'A {class_name} class.',
370
385
  fields=fields,
386
+ required_fields=required_fields,
371
387
  imports=imports_with_paths,
372
388
  typedjson_annotation=self.typedjson_annotation,
373
389
  )
@@ -384,8 +400,10 @@ class StructureToTypeScript:
384
400
  write_file: bool = True) -> str:
385
401
  """ Generates a TypeScript enum from JSON Structure enum """
386
402
  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)
403
+ schema_namespace = structure_schema.get('namespace', parent_namespace)
404
+ namespace = self.concat_namespace(self.base_package, schema_namespace).lower()
405
+ # Use schema_namespace (not parent_namespace) to match the file location
406
+ typescript_qualified_name = self.typescript_fully_qualified_name_from_structure_type(schema_namespace, enum_name)
389
407
 
390
408
  if typescript_qualified_name in self.generated_types:
391
409
  return typescript_qualified_name
@@ -522,17 +540,36 @@ class StructureToTypeScript:
522
540
  if field_type.startswith('{ [key: string]:'):
523
541
  return '{}'
524
542
 
525
- # Return test value or construct object for custom types
526
- return test_values.get(field_type, f'{{}} as {field_type}')
543
+ # Handle enums - use first value with Object.values()
544
+ if field.get('is_enum', False):
545
+ return f'Object.values({field_type})[0] as {field_type}'
546
+
547
+ # Return test value for primitives, or call createInstance() for complex types (classes)
548
+ return test_values.get(field_type, f'{field_type}.createInstance()')
527
549
 
528
550
  def generate_test_class(self, namespace: str, class_name: str, fields: List[Dict[str, Any]]) -> None:
529
551
  """Generates a unit test class for a TypeScript class"""
530
552
  # Get only required fields for the test
531
553
  required_fields = [f for f in fields if f['is_required']]
532
554
 
555
+ # Collect enum imports needed for test file
556
+ enum_imports: Dict[str, str] = {}
557
+
533
558
  # Generate test values for required fields
534
559
  for field in required_fields:
535
560
  field['test_value'] = self.generate_test_value(field)
561
+ # Check if this field is an enum and needs an import
562
+ if field.get('is_enum', False):
563
+ enum_type = field['type_no_null']
564
+ # Find the enum in generated_types to get its full path
565
+ for qualified_name, type_kind in self.generated_types.items():
566
+ if type_kind == 'enum' and qualified_name.endswith('.' + enum_type):
567
+ # Build import path - lowercase namespace like write_to_file does
568
+ parts = qualified_name.split('.')
569
+ enum_namespace = '.'.join(parts[:-1]).lower()
570
+ enum_import_path = enum_namespace.replace('.', '/') + '/' + enum_type
571
+ enum_imports[enum_type] = f'../src/{enum_import_path}'
572
+ break
536
573
 
537
574
  # Determine relative path from test directory to src
538
575
  namespace_path = namespace.replace('.', '/') if namespace else ''
@@ -545,7 +582,8 @@ class StructureToTypeScript:
545
582
  "structuretots/test_class.ts.jinja",
546
583
  class_name=class_name,
547
584
  required_fields=required_fields,
548
- relative_path=relative_path
585
+ relative_path=relative_path,
586
+ enum_imports=enum_imports
549
587
  )
550
588
 
551
589
  # Write test file
@@ -589,17 +627,30 @@ class StructureToTypeScript:
589
627
  f.write(gitignore)
590
628
 
591
629
  def generate_index(self) -> None:
592
- """ Generates index.ts that exports all generated types """
630
+ """ Generates index.ts that exports all generated types with aliased exports """
593
631
  exports = []
594
632
  for qualified_name, type_kind in self.generated_types.items():
595
- type_name = qualified_name.split('.')[-1]
596
- namespace = '.'.join(qualified_name.split('.')[:-1])
633
+ # Split the qualified_name into parts
634
+ parts = qualified_name.split('.')
635
+ type_name = parts[-1] # The actual type name
636
+ namespace = '.'.join(parts[:-1]) # The namespace excluding the type
637
+
638
+ # Construct the relative path to the .js file
597
639
  if namespace:
598
640
  # Lowercase the namespace to match the directory structure created by write_to_file
599
641
  relative_path = namespace.lower().replace('.', '/') + '/' + type_name
600
642
  else:
601
643
  relative_path = type_name
602
- exports.append(f"export * from './{relative_path}.js';")
644
+
645
+ if not relative_path.startswith('./'):
646
+ relative_path = './' + relative_path
647
+
648
+ # Construct the alias name by joining all parts with underscores (PascalCase)
649
+ alias_parts = [pascal(part) for part in parts]
650
+ alias_name = '_'.join(alias_parts)
651
+
652
+ # Generate the export statement with alias (like avrotots does)
653
+ exports.append(f"export {{ {type_name} as {alias_name} }} from '{relative_path}.js';")
603
654
 
604
655
  index_content = '\n'.join(exports) + '\n' if exports else ''
605
656
 
@@ -619,23 +670,34 @@ class StructureToTypeScript:
619
670
  self.convert_schema(schema, output_dir, package_name)
620
671
 
621
672
  def convert_schema(self, schema: JsonNode, output_dir: str, package_name: str = '') -> None:
622
- """ Converts a JSON Structure schema to TypeScript classes """
673
+ """ Converts a JSON Structure schema (or list of schemas) to TypeScript classes """
674
+ # Normalize to list
675
+ if not isinstance(schema, list):
676
+ schema = [schema]
677
+
623
678
  self.output_dir = output_dir
624
679
  self.schema_doc = schema
625
680
 
626
- # Register schema IDs
627
- self.register_schema_ids(self.schema_doc)
681
+ # Register schema IDs for all schemas
682
+ for s in schema:
683
+ if isinstance(s, dict):
684
+ self.register_schema_ids(s)
628
685
 
629
- # Process definitions
630
- if 'definitions' in self.schema_doc:
631
- for def_name, def_schema in self.schema_doc['definitions'].items():
632
- if isinstance(def_schema, dict):
633
- self.generate_class_or_choice(def_schema, '', write_file=True, explicit_name=def_name)
634
-
635
- # Process root schema if it's an object or choice
636
- if 'type' in self.schema_doc:
637
- root_namespace = self.schema_doc.get('namespace', '')
638
- self.generate_class_or_choice(self.schema_doc, root_namespace, write_file=True)
686
+ # Process each schema
687
+ for s in schema:
688
+ if not isinstance(s, dict):
689
+ continue
690
+
691
+ # Process definitions
692
+ if 'definitions' in s:
693
+ for def_name, def_schema in s['definitions'].items():
694
+ if isinstance(def_schema, dict):
695
+ self.generate_class_or_choice(def_schema, '', write_file=True, explicit_name=def_name)
696
+
697
+ # Process root schema if it's an object or choice
698
+ if 'type' in s:
699
+ root_namespace = s.get('namespace', '')
700
+ self.generate_class_or_choice(s, root_namespace, write_file=True)
639
701
 
640
702
  # Generate project files
641
703
  self.generate_package_json(package_name)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: structurize
3
- Version: 2.20.5
3
+ Version: 2.21.1
4
4
  Summary: Tools to convert from and to JSON Structure from various other schema languages.
5
5
  Author-email: Clemens Vasters <clemensv@microsoft.com>
6
6
  Classifier: Programming Language :: Python :: 3
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes