structurize 3.7.3__tar.gz → 3.8.0__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 (123) hide show
  1. {structurize-3.7.3/structurize.egg-info → structurize-3.8.0}/PKG-INFO +1 -1
  2. {structurize-3.7.3 → structurize-3.8.0}/avrotize/__init__.py +2 -0
  3. {structurize-3.7.3 → structurize-3.8.0}/avrotize/_version.py +3 -3
  4. structurize-3.8.0/avrotize/avrotoasn1.py +330 -0
  5. {structurize-3.7.3 → structurize-3.8.0}/avrotize/avrotocsharp.py +11 -2
  6. {structurize-3.7.3 → structurize-3.8.0}/avrotize/avrotogo.py +15 -5
  7. {structurize-3.7.3 → structurize-3.8.0}/avrotize/commands.json +107 -2
  8. {structurize-3.7.3 → structurize-3.8.0}/avrotize/common.py +41 -0
  9. {structurize-3.7.3 → structurize-3.8.0}/avrotize/dependencies/typescript/node22/package.json +1 -1
  10. {structurize-3.7.3 → structurize-3.8.0}/avrotize/jsonstostructure.py +169 -12
  11. structurize-3.8.0/avrotize/structuretoasn1.py +467 -0
  12. {structurize-3.7.3 → structurize-3.8.0}/avrotize/structuretocsharp.py +11 -2
  13. {structurize-3.7.3 → structurize-3.8.0}/avrotize/structuretogo.py +15 -5
  14. {structurize-3.7.3 → structurize-3.8.0/structurize.egg-info}/PKG-INFO +1 -1
  15. {structurize-3.7.3 → structurize-3.8.0}/structurize.egg-info/SOURCES.txt +4 -0
  16. {structurize-3.7.3 → structurize-3.8.0}/.gitignore +0 -0
  17. {structurize-3.7.3 → structurize-3.8.0}/LICENSE +0 -0
  18. {structurize-3.7.3 → structurize-3.8.0}/MANIFEST.in +0 -0
  19. {structurize-3.7.3 → structurize-3.8.0}/README.md +0 -0
  20. {structurize-3.7.3 → structurize-3.8.0}/avrotize/__main__.py +0 -0
  21. {structurize-3.7.3 → structurize-3.8.0}/avrotize/asn1toavro.py +0 -0
  22. {structurize-3.7.3 → structurize-3.8.0}/avrotize/avrotize.py +0 -0
  23. {structurize-3.7.3 → structurize-3.8.0}/avrotize/avrotocapnproto.py +0 -0
  24. {structurize-3.7.3 → structurize-3.8.0}/avrotize/avrotocpp.py +0 -0
  25. {structurize-3.7.3 → structurize-3.8.0}/avrotize/avrotocsv.py +0 -0
  26. {structurize-3.7.3 → structurize-3.8.0}/avrotize/avrotocue.py +0 -0
  27. {structurize-3.7.3 → structurize-3.8.0}/avrotize/avrotodatapackage.py +0 -0
  28. {structurize-3.7.3 → structurize-3.8.0}/avrotize/avrotodb.py +0 -0
  29. {structurize-3.7.3 → structurize-3.8.0}/avrotize/avrotoflatbuffers.py +0 -0
  30. {structurize-3.7.3 → structurize-3.8.0}/avrotize/avrotographql.py +0 -0
  31. {structurize-3.7.3 → structurize-3.8.0}/avrotize/avrotoiceberg.py +0 -0
  32. {structurize-3.7.3 → structurize-3.8.0}/avrotize/avrotojava.py +0 -0
  33. {structurize-3.7.3 → structurize-3.8.0}/avrotize/avrotojs.py +0 -0
  34. {structurize-3.7.3 → structurize-3.8.0}/avrotize/avrotojsons.py +0 -0
  35. {structurize-3.7.3 → structurize-3.8.0}/avrotize/avrotojstruct.py +0 -0
  36. {structurize-3.7.3 → structurize-3.8.0}/avrotize/avrotojtd.py +0 -0
  37. {structurize-3.7.3 → structurize-3.8.0}/avrotize/avrotokusto.py +0 -0
  38. {structurize-3.7.3 → structurize-3.8.0}/avrotize/avrotomd.py +0 -0
  39. {structurize-3.7.3 → structurize-3.8.0}/avrotize/avrotools.py +0 -0
  40. {structurize-3.7.3 → structurize-3.8.0}/avrotize/avrotoparquet.py +0 -0
  41. {structurize-3.7.3 → structurize-3.8.0}/avrotize/avrotoproto.py +0 -0
  42. {structurize-3.7.3 → structurize-3.8.0}/avrotize/avrotopython.py +0 -0
  43. {structurize-3.7.3 → structurize-3.8.0}/avrotize/avrotoraml.py +0 -0
  44. {structurize-3.7.3 → structurize-3.8.0}/avrotize/avrotorust.py +0 -0
  45. {structurize-3.7.3 → structurize-3.8.0}/avrotize/avrotosmithy.py +0 -0
  46. {structurize-3.7.3 → structurize-3.8.0}/avrotize/avrotosurreal.py +0 -0
  47. {structurize-3.7.3 → structurize-3.8.0}/avrotize/avrotothrift.py +0 -0
  48. {structurize-3.7.3 → structurize-3.8.0}/avrotize/avrotots.py +0 -0
  49. {structurize-3.7.3 → structurize-3.8.0}/avrotize/avrototsml.py +0 -0
  50. {structurize-3.7.3 → structurize-3.8.0}/avrotize/avrotoxsd.py +0 -0
  51. {structurize-3.7.3 → structurize-3.8.0}/avrotize/avrovalidator.py +0 -0
  52. {structurize-3.7.3 → structurize-3.8.0}/avrotize/capnprototoavro.py +0 -0
  53. {structurize-3.7.3 → structurize-3.8.0}/avrotize/cddltostructure.py +0 -0
  54. {structurize-3.7.3 → structurize-3.8.0}/avrotize/choice_inference.py +0 -0
  55. {structurize-3.7.3 → structurize-3.8.0}/avrotize/constants.py +0 -0
  56. {structurize-3.7.3 → structurize-3.8.0}/avrotize/csvtoavro.py +0 -0
  57. {structurize-3.7.3 → structurize-3.8.0}/avrotize/cuetoavro.py +0 -0
  58. {structurize-3.7.3 → structurize-3.8.0}/avrotize/cuetostructure.py +0 -0
  59. {structurize-3.7.3 → structurize-3.8.0}/avrotize/datapackagetoavro.py +0 -0
  60. {structurize-3.7.3 → structurize-3.8.0}/avrotize/dependencies/cpp/vcpkg/vcpkg.json +0 -0
  61. {structurize-3.7.3 → structurize-3.8.0}/avrotize/dependency_resolver.py +0 -0
  62. {structurize-3.7.3 → structurize-3.8.0}/avrotize/dependency_version.py +0 -0
  63. {structurize-3.7.3 → structurize-3.8.0}/avrotize/flatbufferstoavro.py +0 -0
  64. {structurize-3.7.3 → structurize-3.8.0}/avrotize/flatbufferstojstruct.py +0 -0
  65. {structurize-3.7.3 → structurize-3.8.0}/avrotize/jsonstoavro.py +0 -0
  66. {structurize-3.7.3 → structurize-3.8.0}/avrotize/jsontoschema.py +0 -0
  67. {structurize-3.7.3 → structurize-3.8.0}/avrotize/jstructtoavro.py +0 -0
  68. {structurize-3.7.3 → structurize-3.8.0}/avrotize/jstructtoflatbuffers.py +0 -0
  69. {structurize-3.7.3 → structurize-3.8.0}/avrotize/jstructtoraml.py +0 -0
  70. {structurize-3.7.3 → structurize-3.8.0}/avrotize/jstructtosmithy.py +0 -0
  71. {structurize-3.7.3 → structurize-3.8.0}/avrotize/jtdtoavro.py +0 -0
  72. {structurize-3.7.3 → structurize-3.8.0}/avrotize/jtdtostructure.py +0 -0
  73. {structurize-3.7.3 → structurize-3.8.0}/avrotize/kstructtoavro.py +0 -0
  74. {structurize-3.7.3 → structurize-3.8.0}/avrotize/kustotoavro.py +0 -0
  75. {structurize-3.7.3 → structurize-3.8.0}/avrotize/kustotojstruct.py +0 -0
  76. {structurize-3.7.3 → structurize-3.8.0}/avrotize/mcp_server.py +0 -0
  77. {structurize-3.7.3 → structurize-3.8.0}/avrotize/openapitostructure.py +0 -0
  78. {structurize-3.7.3 → structurize-3.8.0}/avrotize/parquettoavro.py +0 -0
  79. {structurize-3.7.3 → structurize-3.8.0}/avrotize/proto2parser.py +0 -0
  80. {structurize-3.7.3 → structurize-3.8.0}/avrotize/proto3parser.py +0 -0
  81. {structurize-3.7.3 → structurize-3.8.0}/avrotize/prototoavro.py +0 -0
  82. {structurize-3.7.3 → structurize-3.8.0}/avrotize/ramltoavro.py +0 -0
  83. {structurize-3.7.3 → structurize-3.8.0}/avrotize/ramltojstruct.py +0 -0
  84. {structurize-3.7.3 → structurize-3.8.0}/avrotize/schema_inference.py +0 -0
  85. {structurize-3.7.3 → structurize-3.8.0}/avrotize/smithytoavro.py +0 -0
  86. {structurize-3.7.3 → structurize-3.8.0}/avrotize/smithytojstruct.py +0 -0
  87. {structurize-3.7.3 → structurize-3.8.0}/avrotize/sqltoavro.py +0 -0
  88. {structurize-3.7.3 → structurize-3.8.0}/avrotize/structuretocddl.py +0 -0
  89. {structurize-3.7.3 → structurize-3.8.0}/avrotize/structuretocpp.py +0 -0
  90. {structurize-3.7.3 → structurize-3.8.0}/avrotize/structuretocsv.py +0 -0
  91. {structurize-3.7.3 → structurize-3.8.0}/avrotize/structuretocue.py +0 -0
  92. {structurize-3.7.3 → structurize-3.8.0}/avrotize/structuretodatapackage.py +0 -0
  93. {structurize-3.7.3 → structurize-3.8.0}/avrotize/structuretodb.py +0 -0
  94. {structurize-3.7.3 → structurize-3.8.0}/avrotize/structuretographql.py +0 -0
  95. {structurize-3.7.3 → structurize-3.8.0}/avrotize/structuretoiceberg.py +0 -0
  96. {structurize-3.7.3 → structurize-3.8.0}/avrotize/structuretojava.py +0 -0
  97. {structurize-3.7.3 → structurize-3.8.0}/avrotize/structuretojs.py +0 -0
  98. {structurize-3.7.3 → structurize-3.8.0}/avrotize/structuretojsons.py +0 -0
  99. {structurize-3.7.3 → structurize-3.8.0}/avrotize/structuretojtd.py +0 -0
  100. {structurize-3.7.3 → structurize-3.8.0}/avrotize/structuretokusto.py +0 -0
  101. {structurize-3.7.3 → structurize-3.8.0}/avrotize/structuretomd.py +0 -0
  102. {structurize-3.7.3 → structurize-3.8.0}/avrotize/structuretoparquet.py +0 -0
  103. {structurize-3.7.3 → structurize-3.8.0}/avrotize/structuretoproto.py +0 -0
  104. {structurize-3.7.3 → structurize-3.8.0}/avrotize/structuretopython.py +0 -0
  105. {structurize-3.7.3 → structurize-3.8.0}/avrotize/structuretorust.py +0 -0
  106. {structurize-3.7.3 → structurize-3.8.0}/avrotize/structuretots.py +0 -0
  107. {structurize-3.7.3 → structurize-3.8.0}/avrotize/structuretotsml.py +0 -0
  108. {structurize-3.7.3 → structurize-3.8.0}/avrotize/structuretoxsd.py +0 -0
  109. {structurize-3.7.3 → structurize-3.8.0}/avrotize/surrealtoavro.py +0 -0
  110. {structurize-3.7.3 → structurize-3.8.0}/avrotize/thriftstructure.py +0 -0
  111. {structurize-3.7.3 → structurize-3.8.0}/avrotize/thrifttoavro.py +0 -0
  112. {structurize-3.7.3 → structurize-3.8.0}/avrotize/tmslvalidate.py +0 -0
  113. {structurize-3.7.3 → structurize-3.8.0}/avrotize/validate.py +0 -0
  114. {structurize-3.7.3 → structurize-3.8.0}/avrotize/xmltoschema.py +0 -0
  115. {structurize-3.7.3 → structurize-3.8.0}/avrotize/xsdtoavro.py +0 -0
  116. {structurize-3.7.3 → structurize-3.8.0}/build.ps1 +0 -0
  117. {structurize-3.7.3 → structurize-3.8.0}/build.sh +0 -0
  118. {structurize-3.7.3 → structurize-3.8.0}/pyproject.toml +0 -0
  119. {structurize-3.7.3 → structurize-3.8.0}/setup.cfg +0 -0
  120. {structurize-3.7.3 → structurize-3.8.0}/structurize.egg-info/dependency_links.txt +0 -0
  121. {structurize-3.7.3 → structurize-3.8.0}/structurize.egg-info/entry_points.txt +0 -0
  122. {structurize-3.7.3 → structurize-3.8.0}/structurize.egg-info/requires.txt +0 -0
  123. {structurize-3.7.3 → structurize-3.8.0}/structurize.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: structurize
3
- Version: 3.7.3
3
+ Version: 3.8.0
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
@@ -51,6 +51,8 @@ _mappings = {
51
51
  "convert_kafka_struct_to_avro_schema": (f"{mod}.kstructtoavro", "convert_kafka_struct_to_avro_schema"),
52
52
  "convert_kusto_to_avro": (f"{mod}.kustotoavro", "convert_kusto_to_avro"),
53
53
  "convert_asn1_to_avro": (f"{mod}.asn1toavro", "convert_asn1_to_avro"),
54
+ "convert_avro_to_asn1": (f"{mod}.avrotoasn1", "convert_avro_to_asn1"),
55
+ "convert_structure_to_asn1": (f"{mod}.structuretoasn1", "convert_structure_to_asn1"),
54
56
  "convert_xsd_to_avro": (f"{mod}.xsdtoavro", "convert_xsd_to_avro"),
55
57
  "convert_avro_to_json_schema": (f"{mod}.avrotojsons", "convert_avro_to_json_schema"),
56
58
  "convert_avro_to_kusto_file": (f"{mod}.avrotokusto", "convert_avro_to_kusto_file"),
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '3.7.3'
22
- __version_tuple__ = version_tuple = (3, 7, 3)
21
+ __version__ = version = '3.8.0'
22
+ __version_tuple__ = version_tuple = (3, 8, 0)
23
23
 
24
- __commit_id__ = commit_id = 'g4eb36ac42'
24
+ __commit_id__ = commit_id = 'geb0e0af04'
@@ -0,0 +1,330 @@
1
+ # pylint: disable=line-too-long
2
+
3
+ """ AvroToASN1 class for converting Avrotize (Avro) schema to an ASN.1 module. """
4
+
5
+ import json
6
+ import os
7
+ import re
8
+ from typing import Dict, List, Union
9
+
10
+ from avrotize.common import is_generic_avro_type
11
+
12
+ JsonNode = Union[Dict, List, str, int, float, bool, None]
13
+
14
+
15
+ class AvroToASN1:
16
+ """ Converts an Avrotize (Avro) schema to an ASN.1 (X.680) module. """
17
+
18
+ def __init__(self, module_name: str = 'AvrotizeSchema'):
19
+ self.module_name = self._to_type_reference(module_name)
20
+ # Ordered list of top-level "Name ::= ..." assignment strings.
21
+ self.assignments: List[str] = []
22
+ # ASN.1 type names already emitted (global uniqueness for typereferences).
23
+ self.known_types: Dict[str, str] = {}
24
+ # Avro full name -> ASN.1 type name.
25
+ self.type_name_map: Dict[str, str] = {}
26
+ self.used_type_names: set = set()
27
+
28
+ # ------------------------------------------------------------------ names
29
+
30
+ @staticmethod
31
+ def _sanitize(name: str) -> str:
32
+ """Turn an arbitrary name into an ASN.1-legal token body (letters, digits, hyphens)."""
33
+ if not name:
34
+ return 'x'
35
+ # Keep only the last dotted segment for namespaced names.
36
+ name = name.split('.')[-1]
37
+ # Replace any run of illegal characters with a single hyphen.
38
+ name = re.sub(r'[^A-Za-z0-9]+', '-', name)
39
+ # Collapse consecutive hyphens and strip leading/trailing hyphens.
40
+ name = re.sub(r'-+', '-', name).strip('-')
41
+ if not name:
42
+ return 'x'
43
+ # ASN.1 tokens must start with a letter.
44
+ if not name[0].isalpha():
45
+ name = 'x-' + name
46
+ return name
47
+
48
+ def _to_type_reference(self, name: str) -> str:
49
+ """ASN.1 typereference: must start with an upper-case letter."""
50
+ token = self._sanitize(name)
51
+ return token[0].upper() + token[1:]
52
+
53
+ def _to_identifier(self, name: str) -> str:
54
+ """ASN.1 identifier (member / enum value): must start with a lower-case letter."""
55
+ token = self._sanitize(name)
56
+ letters = [c for c in token if c.isalpha()]
57
+ if letters and all(c.isupper() for c in letters):
58
+ # ALL-CAPS tokens (e.g. Avro enum symbols) read best fully lower-cased.
59
+ token = token.lower()
60
+ return token[0].lower() + token[1:]
61
+
62
+ def _unique_type_name(self, name: str) -> str:
63
+ """Return a module-unique ASN.1 typereference for the given name."""
64
+ base = self._to_type_reference(name)
65
+ candidate = base
66
+ counter = 1
67
+ while candidate in self.used_type_names:
68
+ counter += 1
69
+ candidate = f"{base}{counter}"
70
+ self.used_type_names.add(candidate)
71
+ return candidate
72
+
73
+ # ------------------------------------------------------------- primitives
74
+
75
+ def _map_primitive(self, avro_type: Union[str, dict]) -> str:
76
+ """Map an Avro primitive (or logical) type to an ASN.1 built-in type."""
77
+ if isinstance(avro_type, dict):
78
+ logical_type = avro_type.get('logicalType')
79
+ base = avro_type.get('type')
80
+ if logical_type == 'date':
81
+ return 'DATE'
82
+ if logical_type in ('timestamp-millis', 'timestamp-micros',
83
+ 'local-timestamp-millis', 'local-timestamp-micros'):
84
+ return 'DATE-TIME'
85
+ if logical_type in ('time-millis', 'time-micros'):
86
+ return 'TIME-OF-DAY'
87
+ if logical_type == 'decimal':
88
+ return 'REAL'
89
+ if logical_type == 'uuid':
90
+ return 'UTF8String'
91
+ if logical_type == 'duration':
92
+ return 'UTF8String'
93
+ return self._map_primitive(base)
94
+
95
+ mapping = {
96
+ 'null': 'NULL',
97
+ 'boolean': 'BOOLEAN',
98
+ 'int': 'INTEGER',
99
+ 'long': 'INTEGER',
100
+ 'float': 'REAL',
101
+ 'double': 'REAL',
102
+ 'bytes': 'OCTET STRING',
103
+ 'string': 'UTF8String',
104
+ }
105
+ return mapping.get(avro_type, '')
106
+
107
+ @staticmethod
108
+ def _is_primitive(avro_type: Union[str, dict]) -> bool:
109
+ if isinstance(avro_type, dict):
110
+ return 'logicalType' in avro_type or avro_type.get('type') in {
111
+ 'null', 'boolean', 'int', 'long', 'float', 'double', 'bytes', 'string'}
112
+ return avro_type in {'null', 'boolean', 'int', 'long', 'float', 'double', 'bytes', 'string'}
113
+
114
+ # ------------------------------------------------------------- expressions
115
+
116
+ def type_expression(self, avro_type: JsonNode) -> str:
117
+ """Return an ASN.1 type expression for an Avro type (defining named types as a side effect)."""
118
+ if isinstance(avro_type, str):
119
+ if self._is_primitive(avro_type):
120
+ return self._map_primitive(avro_type)
121
+ # Reference to a previously defined named type.
122
+ return self.type_name_map.get(avro_type, self._to_type_reference(avro_type))
123
+
124
+ if isinstance(avro_type, list):
125
+ return self._union_expression(avro_type)
126
+
127
+ if isinstance(avro_type, dict):
128
+ if 'logicalType' in avro_type:
129
+ return self._map_primitive(avro_type)
130
+ kind = avro_type.get('type')
131
+ if kind == 'record' or kind == 'error':
132
+ return self._define_record(avro_type)
133
+ if kind == 'enum':
134
+ return self._define_enum(avro_type)
135
+ if kind == 'fixed':
136
+ return self._define_fixed(avro_type)
137
+ if kind == 'array':
138
+ items = avro_type.get('items', 'string')
139
+ return f"SEQUENCE OF {self.type_expression(items)}"
140
+ if kind == 'map':
141
+ values = avro_type.get('values', 'string')
142
+ return f"SEQUENCE OF SEQUENCE {{ key UTF8String, value {self.type_expression(values)} }}"
143
+ if self._is_primitive(avro_type):
144
+ return self._map_primitive(avro_type)
145
+ if isinstance(kind, (dict, list)):
146
+ return self.type_expression(kind)
147
+ if isinstance(kind, str):
148
+ return self.type_name_map.get(kind, self._to_type_reference(kind))
149
+ return 'UTF8String'
150
+
151
+ def _union_expression(self, union: List[JsonNode]) -> str:
152
+ """Map an Avro union to an ASN.1 type expression (nullability is handled by the caller)."""
153
+ if is_generic_avro_type(union):
154
+ return 'ANY'
155
+ non_null = [t for t in union if t != 'null']
156
+ if len(non_null) == 0:
157
+ return 'NULL'
158
+ if len(non_null) == 1:
159
+ return self.type_expression(non_null[0])
160
+ members = []
161
+ used: set = set()
162
+ for i, branch in enumerate(non_null):
163
+ alt = self._alternative_name(branch, i, used)
164
+ members.append(f"{alt} {self.type_expression(branch)}")
165
+ return "CHOICE { " + ", ".join(members) + " }"
166
+
167
+ def _alternative_name(self, branch: JsonNode, index: int, used: set) -> str:
168
+ """Derive a unique ASN.1 CHOICE alternative identifier from a union branch."""
169
+ if isinstance(branch, str):
170
+ base = self._to_identifier(branch)
171
+ elif isinstance(branch, dict):
172
+ if 'name' in branch:
173
+ base = self._to_identifier(branch['name'])
174
+ else:
175
+ base = self._to_identifier(str(branch.get('type', f'choice{index}')))
176
+ else:
177
+ base = f'choice{index}'
178
+ candidate = base
179
+ counter = 1
180
+ while candidate in used:
181
+ counter += 1
182
+ candidate = f"{base}{counter}"
183
+ used.add(candidate)
184
+ return candidate
185
+
186
+ # --------------------------------------------------------------- defining
187
+
188
+ def _define_record(self, record: dict) -> str:
189
+ """Emit an ASN.1 SEQUENCE assignment for an Avro record, returning its type name."""
190
+ full_name = self._full_name(record)
191
+ if full_name in self.type_name_map:
192
+ return self.type_name_map[full_name]
193
+
194
+ type_name = self._unique_type_name(record.get('name', 'Record'))
195
+ # Register before recursing so self-references resolve.
196
+ self.type_name_map[full_name] = type_name
197
+
198
+ fields = record.get('fields', [])
199
+ used_members: set = set()
200
+ members = [(self._member_definition(field, used_members), field.get('doc'))
201
+ for field in fields]
202
+
203
+ if members:
204
+ lines: List[str] = []
205
+ for i, (core, doc) in enumerate(members):
206
+ comma = "," if i < len(members) - 1 else ""
207
+ if doc:
208
+ lines.append(f" -- {self._comment(doc)}")
209
+ lines.append(f" {core}{comma}")
210
+ body = "SEQUENCE {\n" + "\n".join(lines) + "\n}"
211
+ else:
212
+ body = "SEQUENCE {}"
213
+ self._append_assignment(type_name, body, record.get('doc'))
214
+ return type_name
215
+
216
+ def _member_definition(self, field: dict, used_members: set) -> str:
217
+ """Build a single ASN.1 SEQUENCE member (without comment) from an Avro field."""
218
+ name = self._unique_identifier(field['name'], used_members)
219
+ field_type = field['type']
220
+ optional = isinstance(field_type, list) and 'null' in field_type
221
+ expr = self.type_expression(field_type)
222
+ member = f"{name} {expr}"
223
+ if optional:
224
+ member += " OPTIONAL"
225
+ return member
226
+
227
+ def _define_enum(self, enum: dict) -> str:
228
+ """Emit an ASN.1 ENUMERATED assignment for an Avro enum, returning its type name."""
229
+ full_name = self._full_name(enum)
230
+ if full_name in self.type_name_map:
231
+ return self.type_name_map[full_name]
232
+
233
+ type_name = self._unique_type_name(enum.get('name', 'Enum'))
234
+ self.type_name_map[full_name] = type_name
235
+
236
+ values: List[str] = []
237
+ used_symbols: set = set()
238
+ for i, symbol in enumerate(enum.get('symbols', [])):
239
+ sym = self._unique_identifier(symbol, used_symbols)
240
+ values.append(f"{sym}({i})")
241
+ body = "ENUMERATED { " + ", ".join(values) + " }" if values else "ENUMERATED { unspecified(0) }"
242
+ self._append_assignment(type_name, body, enum.get('doc'))
243
+ return type_name
244
+
245
+ def _define_fixed(self, fixed: dict) -> str:
246
+ """Emit an ASN.1 OCTET STRING (SIZE(n)) assignment for an Avro fixed type."""
247
+ full_name = self._full_name(fixed)
248
+ if full_name in self.type_name_map:
249
+ return self.type_name_map[full_name]
250
+
251
+ type_name = self._unique_type_name(fixed.get('name', 'Fixed'))
252
+ self.type_name_map[full_name] = type_name
253
+ size = fixed.get('size', 0)
254
+ self._append_assignment(type_name, f"OCTET STRING (SIZE({size}))", fixed.get('doc'))
255
+ return type_name
256
+
257
+ # ------------------------------------------------------------- utilities
258
+
259
+ def _unique_identifier(self, name: str, used: set) -> str:
260
+ base = self._to_identifier(name)
261
+ candidate = base
262
+ counter = 1
263
+ while candidate in used:
264
+ counter += 1
265
+ candidate = f"{base}-{counter}"
266
+ used.add(candidate)
267
+ return candidate
268
+
269
+ @staticmethod
270
+ def _full_name(schema: dict) -> str:
271
+ name = schema.get('name', '')
272
+ namespace = schema.get('namespace', '')
273
+ return f"{namespace}.{name}" if namespace else name
274
+
275
+ @staticmethod
276
+ def _comment(text: str) -> str:
277
+ """ASN.1 line comment body: no line breaks, no double-hyphen terminator."""
278
+ return re.sub(r'\s+', ' ', str(text)).replace('--', '- -').strip()
279
+
280
+ def _append_assignment(self, type_name: str, body: str, doc: str = None) -> None:
281
+ assignment = ''
282
+ if doc:
283
+ assignment += f"-- {self._comment(doc)}\n"
284
+ assignment += f"{type_name} ::= {body}"
285
+ self.assignments.append(assignment)
286
+ self.known_types[type_name] = body
287
+
288
+ # ----------------------------------------------------------------- render
289
+
290
+ def convert(self, avro_schema: JsonNode) -> str:
291
+ """Convert an Avro schema (dict or list of named types) to an ASN.1 module string."""
292
+ schemas = avro_schema if isinstance(avro_schema, list) else [avro_schema]
293
+ for schema in schemas:
294
+ if not isinstance(schema, dict):
295
+ continue
296
+ kind = schema.get('type')
297
+ if kind in ('record', 'error'):
298
+ self._define_record(schema)
299
+ elif kind == 'enum':
300
+ self._define_enum(schema)
301
+ elif kind == 'fixed':
302
+ self._define_fixed(schema)
303
+ return self.render()
304
+
305
+ def render(self) -> str:
306
+ """Render the accumulated assignments as a complete ASN.1 module."""
307
+ lines = [f"{self.module_name} DEFINITIONS AUTOMATIC TAGS ::= BEGIN", ""]
308
+ for assignment in self.assignments:
309
+ lines.append(assignment)
310
+ lines.append("")
311
+ lines.append("END")
312
+ return "\n".join(lines) + "\n"
313
+
314
+
315
+ def convert_avro_to_asn1(avro_schema_path: str, asn1_file_path: str, module_name: str = '') -> None:
316
+ """Convert an Avrotize schema file to an ASN.1 module file."""
317
+ with open(avro_schema_path, 'r', encoding='utf-8') as avro_file:
318
+ avro_schema = json.load(avro_file)
319
+
320
+ if not module_name:
321
+ module_name = os.path.splitext(os.path.basename(asn1_file_path))[0] or 'AvrotizeSchema'
322
+
323
+ converter = AvroToASN1(module_name)
324
+ asn1_text = converter.convert(avro_schema)
325
+
326
+ out_dir = os.path.dirname(asn1_file_path)
327
+ if out_dir and not os.path.exists(out_dir):
328
+ os.makedirs(out_dir, exist_ok=True)
329
+ with open(asn1_file_path, 'w', encoding='utf-8') as asn1_file:
330
+ asn1_file.write(asn1_text)
@@ -58,6 +58,7 @@ class AvroToCSharp:
58
58
  self.avro_annotation = False
59
59
  self.protobuf_net_annotation = False
60
60
  self.openapi_generator_compat = False
61
+ self.target_framework = ''
61
62
  self.generated_types: Dict[str,str] = {}
62
63
  self.generated_avro_types: Dict[str, Dict[str, Union[str, Dict, List]]] = {}
63
64
  self.type_dict: Dict[str, Dict] = {}
@@ -1189,6 +1190,7 @@ class AvroToCSharp:
1189
1190
  file.write(process_template(
1190
1191
  "avrotocsharp/project.csproj.jinja",
1191
1192
  project_name=project_name,
1193
+ target_framework=self.target_framework,
1192
1194
  avro_annotation=self.avro_annotation,
1193
1195
  system_xml_annotation=self.system_xml_annotation,
1194
1196
  system_text_json_annotation=self.system_text_json_annotation,
@@ -1216,6 +1218,7 @@ class AvroToCSharp:
1216
1218
  file.write(process_template(
1217
1219
  "avrotocsharp/testproject.csproj.jinja",
1218
1220
  project_name=project_name,
1221
+ target_framework=self.target_framework,
1219
1222
  avro_annotation=self.avro_annotation,
1220
1223
  system_xml_annotation=self.system_xml_annotation,
1221
1224
  system_text_json_annotation=self.system_text_json_annotation,
@@ -1279,7 +1282,8 @@ def convert_avro_to_csharp(
1279
1282
  cbor_annotation=False,
1280
1283
  avro_annotation=False,
1281
1284
  protobuf_net_annotation=False,
1282
- openapi_generator_compat=False
1285
+ openapi_generator_compat=False,
1286
+ target_framework=''
1283
1287
  ):
1284
1288
  """Converts Avro schema to C# classes
1285
1289
 
@@ -1297,6 +1301,7 @@ def convert_avro_to_csharp(
1297
1301
  avro_annotation (bool, optional): Use Avro annotations. Defaults to False.
1298
1302
  protobuf_net_annotation (bool, optional): Use protobuf-net annotations. Defaults to False.
1299
1303
  openapi_generator_compat (bool, optional): Generate OpenAPI Generator-compatible C# models. Defaults to False.
1304
+ target_framework (str, optional): Target framework moniker written into the generated .csproj (e.g. 'net8.0'). A ';'-separated value emits <TargetFrameworks> for multi-targeting. Defaults to '' (net10.0).
1300
1305
  """
1301
1306
 
1302
1307
  if not base_namespace:
@@ -1312,6 +1317,7 @@ def convert_avro_to_csharp(
1312
1317
  avrotocs.avro_annotation = avro_annotation
1313
1318
  avrotocs.protobuf_net_annotation = protobuf_net_annotation
1314
1319
  avrotocs.openapi_generator_compat = openapi_generator_compat
1320
+ avrotocs.target_framework = target_framework
1315
1321
  avrotocs.convert(avro_schema_path, cs_file_path)
1316
1322
 
1317
1323
 
@@ -1328,7 +1334,8 @@ def convert_avro_schema_to_csharp(
1328
1334
  cbor_annotation: bool = False,
1329
1335
  avro_annotation: bool = False,
1330
1336
  protobuf_net_annotation: bool = False,
1331
- openapi_generator_compat: bool = False
1337
+ openapi_generator_compat: bool = False,
1338
+ target_framework: str = ''
1332
1339
  ):
1333
1340
  """Converts Avro schema to C# classes
1334
1341
 
@@ -1345,6 +1352,7 @@ def convert_avro_schema_to_csharp(
1345
1352
  cbor_annotation (bool, optional): Use Dahomey.Cbor annotations. Defaults to False.
1346
1353
  avro_annotation (bool, optional): Use Avro annotations. Defaults to False.
1347
1354
  protobuf_net_annotation (bool, optional): Use protobuf-net annotations. Defaults to False.
1355
+ target_framework (str, optional): Target framework moniker written into the generated .csproj (e.g. 'net8.0'). A ';'-separated value emits <TargetFrameworks> for multi-targeting. Defaults to '' (net10.0).
1348
1356
  """
1349
1357
  avrotocs = AvroToCSharp(base_namespace)
1350
1358
  avrotocs.project_name = project_name
@@ -1357,4 +1365,5 @@ def convert_avro_schema_to_csharp(
1357
1365
  avrotocs.avro_annotation = avro_annotation
1358
1366
  avrotocs.protobuf_net_annotation = protobuf_net_annotation
1359
1367
  avrotocs.openapi_generator_compat = openapi_generator_compat
1368
+ avrotocs.target_framework = target_framework
1360
1369
  avrotocs.convert_schema(avro_schema, output_dir)
@@ -1,7 +1,7 @@
1
1
  import json
2
2
  import os
3
3
  from typing import Dict, List, Union, Set
4
- from avrotize.common import get_longest_namespace_prefix, is_generic_avro_type, is_any_value_type, pascal, render_template
4
+ from avrotize.common import get_longest_namespace_prefix, is_generic_avro_type, is_any_value_type, pascal, render_template, format_go_files
5
5
 
6
6
  INDENT = ' '
7
7
 
@@ -33,10 +33,19 @@ class AvroToGo:
33
33
  self.enums = []
34
34
 
35
35
  def _safe_package_name(self, name: str) -> str:
36
- """Converts a name to a safe Go package name"""
37
- if name in self.GO_RESERVED_WORDS:
38
- return f"{name}_"
39
- return name
36
+ """Converts a name to a safe Go package name (a valid lowercase identifier).
37
+
38
+ Sanitizes any character that is not valid in a Go identifier (dots,
39
+ hyphens from file names, etc.) to underscores so the emitted ``package``
40
+ clause is compilable and gofmt-parseable.
41
+ """
42
+ import re
43
+ safe = re.sub(r'[^a-zA-Z0-9_]+', '_', name).strip('_').lower()
44
+ if not safe or not re.match(r'^[a-z]', safe):
45
+ safe = f"pkg_{safe}" if safe else "pkg"
46
+ if safe in self.GO_RESERVED_WORDS:
47
+ return f"{safe}_"
48
+ return safe
40
49
 
41
50
  def safe_identifier(self, name: str) -> str:
42
51
  """Converts a name to a safe Go identifier"""
@@ -418,6 +427,7 @@ class AvroToGo:
418
427
  self.write_go_mod_file()
419
428
  self.write_modname_go_file()
420
429
  self.generate_helpers()
430
+ format_go_files(self.output_dir)
421
431
 
422
432
  def write_go_mod_file(self):
423
433
  """Writes the go.mod file for the Go project"""
@@ -1,4 +1,95 @@
1
1
  [
2
+ {
3
+ "command": "a2asn",
4
+ "description": "Convert Avrotize schema to ASN.1 schema",
5
+ "group": "1_Schemas",
6
+ "function": {
7
+ "name": "avrotize.avrotoasn1.convert_avro_to_asn1",
8
+ "args": {
9
+ "avro_schema_path": "input_file_path",
10
+ "asn1_file_path": "output_file_path",
11
+ "module_name": "args.module"
12
+ }
13
+ },
14
+ "extensions": [
15
+ ".avsc"
16
+ ],
17
+ "args": [
18
+ {
19
+ "name": "input",
20
+ "type": "str",
21
+ "nargs": "?",
22
+ "help": "Path to the Avrotize schema file (or read from stdin if omitted)",
23
+ "required": false
24
+ },
25
+ {
26
+ "name": "--out",
27
+ "type": "str",
28
+ "help": "Path to the ASN.1 schema file",
29
+ "required": false
30
+ },
31
+ {
32
+ "name": "--avsc",
33
+ "type": "str",
34
+ "help": "Deprecated: Path to the Avrotize schema file (for backcompat)",
35
+ "required": false
36
+ },
37
+ {
38
+ "name": "--module",
39
+ "type": "str",
40
+ "help": "ASN.1 module name (defaults to the output file name)",
41
+ "required": false
42
+ }
43
+ ],
44
+ "suggested_output_file_path": "{input_file_name}.asn1",
45
+ "prompts": []
46
+ },
47
+ {
48
+ "command": "s2asn",
49
+ "description": "Convert JSON Structure schema to ASN.1 schema",
50
+ "group": "1_Schemas",
51
+ "function": {
52
+ "name": "avrotize.structuretoasn1.convert_structure_to_asn1",
53
+ "args": {
54
+ "structure_schema_path": "input_file_path",
55
+ "asn1_file_path": "output_file_path",
56
+ "module_name": "args.module"
57
+ }
58
+ },
59
+ "extensions": [
60
+ ".struct.json",
61
+ ".json"
62
+ ],
63
+ "args": [
64
+ {
65
+ "name": "input",
66
+ "type": "str",
67
+ "nargs": "?",
68
+ "help": "Path to the JSON Structure schema file (or read from stdin if omitted)",
69
+ "required": false
70
+ },
71
+ {
72
+ "name": "--out",
73
+ "type": "str",
74
+ "help": "Path to the ASN.1 schema file",
75
+ "required": false
76
+ },
77
+ {
78
+ "name": "--struct",
79
+ "type": "str",
80
+ "help": "Deprecated: Path to the JSON Structure schema file (for backcompat)",
81
+ "required": false
82
+ },
83
+ {
84
+ "name": "--module",
85
+ "type": "str",
86
+ "help": "ASN.1 module name (defaults to the output file name)",
87
+ "required": false
88
+ }
89
+ ],
90
+ "suggested_output_file_path": "{input_file_name}.asn1",
91
+ "prompts": []
92
+ },
2
93
  {
3
94
  "command": "p2a",
4
95
  "description": "Convert Proto schema to Avrotize schema",
@@ -2575,7 +2666,8 @@
2575
2666
  "cbor_annotation": "args.cbor_annotation",
2576
2667
  "pascal_properties": "args.pascal_properties",
2577
2668
  "base_namespace": "args.namespace",
2578
- "openapi_generator_compat": "args.openapi_generator_compat"
2669
+ "openapi_generator_compat": "args.openapi_generator_compat",
2670
+ "target_framework": "args.target_framework"
2579
2671
  }
2580
2672
  },
2581
2673
  "extensions": [
@@ -2669,6 +2761,12 @@
2669
2761
  "help": "Generate OpenAPI Generator-compatible C# models with Option<T> optional properties and System.Text.Json converters",
2670
2762
  "default": false,
2671
2763
  "required": false
2764
+ },
2765
+ {
2766
+ "name": "--target-framework",
2767
+ "type": "str",
2768
+ "help": "Target framework moniker written into the generated .csproj (e.g. net8.0). A ';'-separated value emits <TargetFrameworks> for multi-targeting. Defaults to net10.0.",
2769
+ "required": false
2672
2770
  }
2673
2771
  ],
2674
2772
  "suggested_output_file_path": "{input_file_name}-cs",
@@ -2719,7 +2817,8 @@
2719
2817
  "pascal_properties": "args.pascal_properties",
2720
2818
  "base_namespace": "args.namespace",
2721
2819
  "project_name": "args.project_name",
2722
- "openapi_generator_compat": "args.openapi_generator_compat"
2820
+ "openapi_generator_compat": "args.openapi_generator_compat",
2821
+ "target_framework": "args.target_framework"
2723
2822
  }
2724
2823
  },
2725
2824
  "extensions": [
@@ -2799,6 +2898,12 @@
2799
2898
  "help": "Generate OpenAPI Generator-compatible C# models with Option<T> optional properties and System.Text.Json converters",
2800
2899
  "default": false,
2801
2900
  "required": false
2901
+ },
2902
+ {
2903
+ "name": "--target-framework",
2904
+ "type": "str",
2905
+ "help": "Target framework moniker written into the generated .csproj (e.g. net8.0). A ';'-separated value emits <TargetFrameworks> for multi-targeting. Defaults to net10.0.",
2906
+ "required": false
2802
2907
  }
2803
2908
  ],
2804
2909
  "suggested_output_file_path": "{input_file_name}-cs",
@@ -940,6 +940,47 @@ def render_template(template: str, output: str, **kvargs):
940
940
  f.write(out)
941
941
 
942
942
 
943
+ def format_go_files(directory: str) -> bool:
944
+ """Run ``gofmt`` over every ``.go`` file under ``directory`` to produce
945
+ canonical, gofmt-clean output.
946
+
947
+ This normalizes indentation (tabs), sorts imports, aligns struct fields and
948
+ ``const`` blocks, fixes blank-line placement, and guarantees a trailing
949
+ newline at EOF -- everything the Go emitters cannot easily express in Jinja
950
+ templates. Struct-tag spacing is fixed in the templates instead, because
951
+ ``gofmt`` never rewrites the contents of raw string literals.
952
+
953
+ Best-effort: if the Go toolchain is not installed, the generated files are
954
+ left untouched and ``False`` is returned (the emitter still produced valid,
955
+ compilable Go).
956
+
957
+ Args:
958
+ directory (str): Directory containing generated ``.go`` files. ``gofmt``
959
+ processes it recursively.
960
+
961
+ Returns:
962
+ bool: ``True`` if ``gofmt`` (or ``go fmt``) ran, ``False`` otherwise.
963
+ """
964
+ import shutil
965
+ import subprocess
966
+ gofmt = shutil.which('gofmt')
967
+ try:
968
+ if gofmt:
969
+ subprocess.run(
970
+ [gofmt, '-w', directory], check=False,
971
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
972
+ return True
973
+ go = shutil.which('go')
974
+ if go:
975
+ subprocess.run(
976
+ [go, 'fmt', './...'], cwd=directory, check=False,
977
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
978
+ return True
979
+ except OSError:
980
+ return False
981
+ return False
982
+
983
+
943
984
  def get_longest_namespace_prefix(schema):
944
985
  """ Get the longest common prefix for the namespace of all types in the schema. """
945
986
  namespaces = set(collect_namespaces(schema))
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "devDependencies": {
11
11
  "@types/node": "^26.0.0",
12
- "typescript": "^6.0.2",
12
+ "typescript": "^7.0.2",
13
13
  "jest": "^30.2.0",
14
14
  "@types/jest": "^30.0.0"
15
15
  }