structurize 3.7.4__tar.gz → 3.9.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 (125) hide show
  1. {structurize-3.7.4/structurize.egg-info → structurize-3.9.0}/PKG-INFO +1 -1
  2. {structurize-3.7.4 → structurize-3.9.0}/avrotize/__init__.py +2 -0
  3. {structurize-3.7.4 → structurize-3.9.0}/avrotize/_version.py +3 -3
  4. structurize-3.9.0/avrotize/avrotoasn1.py +330 -0
  5. {structurize-3.7.4 → structurize-3.9.0}/avrotize/avrotocsharp.py +11 -2
  6. {structurize-3.7.4 → structurize-3.9.0}/avrotize/avrotogo.py +63 -19
  7. {structurize-3.7.4 → structurize-3.9.0}/avrotize/avrotojava.py +243 -37
  8. structurize-3.9.0/avrotize/avrotojs.py +375 -0
  9. {structurize-3.7.4 → structurize-3.9.0}/avrotize/avrotopython.py +58 -8
  10. {structurize-3.7.4 → structurize-3.9.0}/avrotize/avrotorust.py +209 -9
  11. {structurize-3.7.4 → structurize-3.9.0}/avrotize/avrotots.py +86 -11
  12. {structurize-3.7.4 → structurize-3.9.0}/avrotize/commands.json +288 -14
  13. {structurize-3.7.4 → structurize-3.9.0}/avrotize/common.py +84 -0
  14. {structurize-3.7.4 → structurize-3.9.0}/avrotize/constants.py +5 -0
  15. {structurize-3.7.4 → structurize-3.9.0}/avrotize/dependencies/typescript/node22/package.json +1 -1
  16. structurize-3.9.0/avrotize/rust_xml.py +21 -0
  17. structurize-3.9.0/avrotize/structuretoasn1.py +467 -0
  18. {structurize-3.7.4 → structurize-3.9.0}/avrotize/structuretocsharp.py +11 -2
  19. {structurize-3.7.4 → structurize-3.9.0}/avrotize/structuretogo.py +48 -11
  20. {structurize-3.7.4 → structurize-3.9.0}/avrotize/structuretojava.py +276 -15
  21. {structurize-3.7.4 → structurize-3.9.0}/avrotize/structuretojs.py +123 -17
  22. {structurize-3.7.4 → structurize-3.9.0}/avrotize/structuretopython.py +65 -7
  23. {structurize-3.7.4 → structurize-3.9.0}/avrotize/structuretorust.py +214 -12
  24. {structurize-3.7.4 → structurize-3.9.0}/avrotize/structuretots.py +90 -5
  25. {structurize-3.7.4 → structurize-3.9.0/structurize.egg-info}/PKG-INFO +1 -1
  26. {structurize-3.7.4 → structurize-3.9.0}/structurize.egg-info/SOURCES.txt +15 -0
  27. structurize-3.7.4/avrotize/avrotojs.py +0 -254
  28. {structurize-3.7.4 → structurize-3.9.0}/.gitignore +0 -0
  29. {structurize-3.7.4 → structurize-3.9.0}/LICENSE +0 -0
  30. {structurize-3.7.4 → structurize-3.9.0}/MANIFEST.in +0 -0
  31. {structurize-3.7.4 → structurize-3.9.0}/README.md +0 -0
  32. {structurize-3.7.4 → structurize-3.9.0}/avrotize/__main__.py +0 -0
  33. {structurize-3.7.4 → structurize-3.9.0}/avrotize/asn1toavro.py +0 -0
  34. {structurize-3.7.4 → structurize-3.9.0}/avrotize/avrotize.py +0 -0
  35. {structurize-3.7.4 → structurize-3.9.0}/avrotize/avrotocapnproto.py +0 -0
  36. {structurize-3.7.4 → structurize-3.9.0}/avrotize/avrotocpp.py +0 -0
  37. {structurize-3.7.4 → structurize-3.9.0}/avrotize/avrotocsv.py +0 -0
  38. {structurize-3.7.4 → structurize-3.9.0}/avrotize/avrotocue.py +0 -0
  39. {structurize-3.7.4 → structurize-3.9.0}/avrotize/avrotodatapackage.py +0 -0
  40. {structurize-3.7.4 → structurize-3.9.0}/avrotize/avrotodb.py +0 -0
  41. {structurize-3.7.4 → structurize-3.9.0}/avrotize/avrotoflatbuffers.py +0 -0
  42. {structurize-3.7.4 → structurize-3.9.0}/avrotize/avrotographql.py +0 -0
  43. {structurize-3.7.4 → structurize-3.9.0}/avrotize/avrotoiceberg.py +0 -0
  44. {structurize-3.7.4 → structurize-3.9.0}/avrotize/avrotojsons.py +0 -0
  45. {structurize-3.7.4 → structurize-3.9.0}/avrotize/avrotojstruct.py +0 -0
  46. {structurize-3.7.4 → structurize-3.9.0}/avrotize/avrotojtd.py +0 -0
  47. {structurize-3.7.4 → structurize-3.9.0}/avrotize/avrotokusto.py +0 -0
  48. {structurize-3.7.4 → structurize-3.9.0}/avrotize/avrotomd.py +0 -0
  49. {structurize-3.7.4 → structurize-3.9.0}/avrotize/avrotools.py +0 -0
  50. {structurize-3.7.4 → structurize-3.9.0}/avrotize/avrotoparquet.py +0 -0
  51. {structurize-3.7.4 → structurize-3.9.0}/avrotize/avrotoproto.py +0 -0
  52. {structurize-3.7.4 → structurize-3.9.0}/avrotize/avrotoraml.py +0 -0
  53. {structurize-3.7.4 → structurize-3.9.0}/avrotize/avrotosmithy.py +0 -0
  54. {structurize-3.7.4 → structurize-3.9.0}/avrotize/avrotosurreal.py +0 -0
  55. {structurize-3.7.4 → structurize-3.9.0}/avrotize/avrotothrift.py +0 -0
  56. {structurize-3.7.4 → structurize-3.9.0}/avrotize/avrototsml.py +0 -0
  57. {structurize-3.7.4 → structurize-3.9.0}/avrotize/avrotoxsd.py +0 -0
  58. {structurize-3.7.4 → structurize-3.9.0}/avrotize/avrovalidator.py +0 -0
  59. {structurize-3.7.4 → structurize-3.9.0}/avrotize/capnprototoavro.py +0 -0
  60. {structurize-3.7.4 → structurize-3.9.0}/avrotize/cddltostructure.py +0 -0
  61. {structurize-3.7.4 → structurize-3.9.0}/avrotize/choice_inference.py +0 -0
  62. {structurize-3.7.4 → structurize-3.9.0}/avrotize/csvtoavro.py +0 -0
  63. {structurize-3.7.4 → structurize-3.9.0}/avrotize/cuetoavro.py +0 -0
  64. {structurize-3.7.4 → structurize-3.9.0}/avrotize/cuetostructure.py +0 -0
  65. {structurize-3.7.4 → structurize-3.9.0}/avrotize/datapackagetoavro.py +0 -0
  66. {structurize-3.7.4 → structurize-3.9.0}/avrotize/dependencies/cpp/vcpkg/vcpkg.json +0 -0
  67. {structurize-3.7.4 → structurize-3.9.0}/avrotize/dependency_resolver.py +0 -0
  68. {structurize-3.7.4 → structurize-3.9.0}/avrotize/dependency_version.py +0 -0
  69. {structurize-3.7.4 → structurize-3.9.0}/avrotize/flatbufferstoavro.py +0 -0
  70. {structurize-3.7.4 → structurize-3.9.0}/avrotize/flatbufferstojstruct.py +0 -0
  71. {structurize-3.7.4 → structurize-3.9.0}/avrotize/jsonstoavro.py +0 -0
  72. {structurize-3.7.4 → structurize-3.9.0}/avrotize/jsonstostructure.py +0 -0
  73. {structurize-3.7.4 → structurize-3.9.0}/avrotize/jsontoschema.py +0 -0
  74. {structurize-3.7.4 → structurize-3.9.0}/avrotize/jstructtoavro.py +0 -0
  75. {structurize-3.7.4 → structurize-3.9.0}/avrotize/jstructtoflatbuffers.py +0 -0
  76. {structurize-3.7.4 → structurize-3.9.0}/avrotize/jstructtoraml.py +0 -0
  77. {structurize-3.7.4 → structurize-3.9.0}/avrotize/jstructtosmithy.py +0 -0
  78. {structurize-3.7.4 → structurize-3.9.0}/avrotize/jtdtoavro.py +0 -0
  79. {structurize-3.7.4 → structurize-3.9.0}/avrotize/jtdtostructure.py +0 -0
  80. {structurize-3.7.4 → structurize-3.9.0}/avrotize/kstructtoavro.py +0 -0
  81. {structurize-3.7.4 → structurize-3.9.0}/avrotize/kustotoavro.py +0 -0
  82. {structurize-3.7.4 → structurize-3.9.0}/avrotize/kustotojstruct.py +0 -0
  83. {structurize-3.7.4 → structurize-3.9.0}/avrotize/mcp_server.py +0 -0
  84. {structurize-3.7.4 → structurize-3.9.0}/avrotize/openapitostructure.py +0 -0
  85. {structurize-3.7.4 → structurize-3.9.0}/avrotize/parquettoavro.py +0 -0
  86. {structurize-3.7.4 → structurize-3.9.0}/avrotize/proto2parser.py +0 -0
  87. {structurize-3.7.4 → structurize-3.9.0}/avrotize/proto3parser.py +0 -0
  88. {structurize-3.7.4 → structurize-3.9.0}/avrotize/prototoavro.py +0 -0
  89. {structurize-3.7.4 → structurize-3.9.0}/avrotize/ramltoavro.py +0 -0
  90. {structurize-3.7.4 → structurize-3.9.0}/avrotize/ramltojstruct.py +0 -0
  91. {structurize-3.7.4 → structurize-3.9.0}/avrotize/schema_inference.py +0 -0
  92. {structurize-3.7.4 → structurize-3.9.0}/avrotize/smithytoavro.py +0 -0
  93. {structurize-3.7.4 → structurize-3.9.0}/avrotize/smithytojstruct.py +0 -0
  94. {structurize-3.7.4 → structurize-3.9.0}/avrotize/sqltoavro.py +0 -0
  95. {structurize-3.7.4 → structurize-3.9.0}/avrotize/structuretocddl.py +0 -0
  96. {structurize-3.7.4 → structurize-3.9.0}/avrotize/structuretocpp.py +0 -0
  97. {structurize-3.7.4 → structurize-3.9.0}/avrotize/structuretocsv.py +0 -0
  98. {structurize-3.7.4 → structurize-3.9.0}/avrotize/structuretocue.py +0 -0
  99. {structurize-3.7.4 → structurize-3.9.0}/avrotize/structuretodatapackage.py +0 -0
  100. {structurize-3.7.4 → structurize-3.9.0}/avrotize/structuretodb.py +0 -0
  101. {structurize-3.7.4 → structurize-3.9.0}/avrotize/structuretographql.py +0 -0
  102. {structurize-3.7.4 → structurize-3.9.0}/avrotize/structuretoiceberg.py +0 -0
  103. {structurize-3.7.4 → structurize-3.9.0}/avrotize/structuretojsons.py +0 -0
  104. {structurize-3.7.4 → structurize-3.9.0}/avrotize/structuretojtd.py +0 -0
  105. {structurize-3.7.4 → structurize-3.9.0}/avrotize/structuretokusto.py +0 -0
  106. {structurize-3.7.4 → structurize-3.9.0}/avrotize/structuretomd.py +0 -0
  107. {structurize-3.7.4 → structurize-3.9.0}/avrotize/structuretoparquet.py +0 -0
  108. {structurize-3.7.4 → structurize-3.9.0}/avrotize/structuretoproto.py +0 -0
  109. {structurize-3.7.4 → structurize-3.9.0}/avrotize/structuretotsml.py +0 -0
  110. {structurize-3.7.4 → structurize-3.9.0}/avrotize/structuretoxsd.py +0 -0
  111. {structurize-3.7.4 → structurize-3.9.0}/avrotize/surrealtoavro.py +0 -0
  112. {structurize-3.7.4 → structurize-3.9.0}/avrotize/thriftstructure.py +0 -0
  113. {structurize-3.7.4 → structurize-3.9.0}/avrotize/thrifttoavro.py +0 -0
  114. {structurize-3.7.4 → structurize-3.9.0}/avrotize/tmslvalidate.py +0 -0
  115. {structurize-3.7.4 → structurize-3.9.0}/avrotize/validate.py +0 -0
  116. {structurize-3.7.4 → structurize-3.9.0}/avrotize/xmltoschema.py +0 -0
  117. {structurize-3.7.4 → structurize-3.9.0}/avrotize/xsdtoavro.py +0 -0
  118. {structurize-3.7.4 → structurize-3.9.0}/build.ps1 +0 -0
  119. {structurize-3.7.4 → structurize-3.9.0}/build.sh +0 -0
  120. {structurize-3.7.4 → structurize-3.9.0}/pyproject.toml +0 -0
  121. {structurize-3.7.4 → structurize-3.9.0}/setup.cfg +0 -0
  122. {structurize-3.7.4 → structurize-3.9.0}/structurize.egg-info/dependency_links.txt +0 -0
  123. {structurize-3.7.4 → structurize-3.9.0}/structurize.egg-info/entry_points.txt +0 -0
  124. {structurize-3.7.4 → structurize-3.9.0}/structurize.egg-info/requires.txt +0 -0
  125. {structurize-3.7.4 → structurize-3.9.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.4
3
+ Version: 3.9.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.4'
22
- __version_tuple__ = version_tuple = (3, 7, 4)
21
+ __version__ = version = '3.9.0'
22
+ __version_tuple__ = version_tuple = (3, 9, 0)
23
23
 
24
- __commit_id__ = commit_id = 'gd6893c97b'
24
+ __commit_id__ = commit_id = 'g20391dedb'
@@ -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,8 @@
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,
5
+ render_template, format_go_files, xml_wire_name, xml_enum_wire_value)
5
6
 
6
7
  INDENT = ' '
7
8
 
@@ -26,6 +27,7 @@ class AvroToGo:
26
27
  self.referenced_packages_stack: List[Dict[str, Set[str]]] = []
27
28
  self.avro_annotation = False
28
29
  self.json_annotation = False
30
+ self.xml_annotation = False
29
31
  self.longest_common_prefix = ''
30
32
  self.package_site = 'github.com'
31
33
  self.package_username = 'username'
@@ -33,10 +35,19 @@ class AvroToGo:
33
35
  self.enums = []
34
36
 
35
37
  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
38
+ """Converts a name to a safe Go package name (a valid lowercase identifier).
39
+
40
+ Sanitizes any character that is not valid in a Go identifier (dots,
41
+ hyphens from file names, etc.) to underscores so the emitted ``package``
42
+ clause is compilable and gofmt-parseable.
43
+ """
44
+ import re
45
+ safe = re.sub(r'[^a-zA-Z0-9_]+', '_', name).strip('_').lower()
46
+ if not safe or not re.match(r'^[a-z]', safe):
47
+ safe = f"pkg_{safe}" if safe else "pkg"
48
+ if safe in self.GO_RESERVED_WORDS:
49
+ return f"{safe}_"
50
+ return safe
40
51
 
41
52
  def safe_identifier(self, name: str) -> str:
42
53
  """Converts a name to a safe Go identifier"""
@@ -117,9 +128,8 @@ class AvroToGo:
117
128
  return f"[]{item_type}"
118
129
  elif avro_type['type'] == 'map':
119
130
  values_type = self.convert_avro_type_to_go(field_name, avro_type['values'], nullable=True, parent_namespace=parent_namespace)
120
- if values_type.startswith('*'):
121
- return f"map[string]{values_type}"
122
- return f"map[string]{values_type}"
131
+ map_type = f"map[string]{values_type}"
132
+ return f"XMLMap[{values_type}]" if self.xml_annotation else map_type
123
133
  elif 'logicalType' in avro_type:
124
134
  if avro_type['logicalType'] == 'date':
125
135
  return 'time.Time'
@@ -162,11 +172,20 @@ class AvroToGo:
162
172
  self.generated_types_avro_namespace[avro_fullname] = "struct"
163
173
  self.generated_types_go_package[go_struct_name] = "struct"
164
174
 
165
- fields = [{
166
- 'name': pascal(field['name']),
167
- 'type': self.convert_avro_type_to_go(field['name'], field['type'], parent_namespace=namespace),
168
- 'original_name': field['name']
169
- } for field in avro_schema.get('fields', [])]
175
+ fields = []
176
+ for field in avro_schema.get('fields', []):
177
+ field_type = self.convert_avro_type_to_go(field['name'], field['type'], parent_namespace=namespace)
178
+ raw_type = field['type']
179
+ fields.append({
180
+ 'name': pascal(field['name']),
181
+ 'type': field_type,
182
+ 'original_name': field['name'],
183
+ 'xml_name': xml_wire_name(field['name'], field),
184
+ 'xml_kind': field.get('xmlkind', 'element'),
185
+ 'xml_required': not (isinstance(raw_type, list) and 'null' in raw_type) and not (isinstance(raw_type, dict) and raw_type.get('type') in ('array', 'map')),
186
+ 'xml_repeated': isinstance(raw_type, dict) and raw_type.get('type') == 'array',
187
+ 'xml_ambiguous': field_type.lstrip('*') == 'interface{}',
188
+ })
170
189
 
171
190
  # Collect imports from field types
172
191
  go_types = [f['type'] for f in fields]
@@ -178,6 +197,10 @@ class AvroToGo:
178
197
  'fields': fields,
179
198
  'avro_schema': json.dumps(avro_schema),
180
199
  'json_annotation': self.json_annotation,
200
+ 'xml_annotation': self.xml_annotation,
201
+ 'xml_name': xml_wire_name(avro_schema['name'], avro_schema),
202
+ 'xml_namespace': avro_schema.get('xmlns', ''),
203
+ 'needs_xml_name': bool(avro_schema.get('xmlns')) or xml_wire_name(avro_schema['name'], avro_schema) != go_struct_name,
181
204
  'avro_annotation': self.avro_annotation,
182
205
  'json_match_predicates': [self.get_is_json_match_clause(f['name'], f['type']) for f in fields],
183
206
  'base_package': self.base_package,
@@ -214,9 +237,14 @@ class AvroToGo:
214
237
  context = {
215
238
  'doc': avro_schema.get('doc', ''),
216
239
  'struct_name': enum_name,
217
- 'symbols': avro_schema.get('symbols', []),
240
+ 'symbols': [
241
+ {'name': symbol, 'xml_value': xml_enum_wire_value(symbol, avro_schema)}
242
+ for symbol in avro_schema.get('symbols', [])
243
+ ],
218
244
  'imports': imports,
219
245
  'base_package': self.base_package,
246
+ 'xml_annotation': self.xml_annotation,
247
+ 'string_enum': self.avro_annotation,
220
248
  'referenced_packages': self.referenced_packages.keys()
221
249
  }
222
250
 
@@ -229,9 +257,10 @@ class AvroToGo:
229
257
  self.enums.append({
230
258
  'name': enum_name,
231
259
  'symbols': avro_schema.get('symbols', []),
260
+ 'string_enum': self.avro_annotation,
232
261
  })
233
262
 
234
- self.generate_unit_test('enum', enum_name, context['symbols'])
263
+ self.generate_unit_test('enum', enum_name, avro_schema.get('symbols', []))
235
264
 
236
265
  return enum_name
237
266
 
@@ -247,6 +276,7 @@ class AvroToGo:
247
276
  'union_class_name': union_class_name,
248
277
  'union_types': union_types,
249
278
  'json_annotation': self.json_annotation,
279
+ 'xml_annotation': self.xml_annotation,
250
280
  'avro_annotation': self.avro_annotation,
251
281
  'get_is_json_match_clause': self.get_is_json_match_clause,
252
282
  'base_package': self.base_package,
@@ -294,14 +324,14 @@ class AvroToGo:
294
324
  return f"if _, ok := node[\"{field_name}\"].([]byte); !ok {{ return false }}"
295
325
  elif field_type == 'interface{}':
296
326
  return f"if _, ok := node[\"{field_name}\"].(interface{{}}); !ok {{ return false }}"
297
- elif field_type.startswith('map[string]'):
327
+ elif field_type.startswith('map[string]') or field_type.startswith('XMLMap['):
298
328
  return f"if _, ok := node[\"{field_name}\"].(map[string]interface{{}}); !ok {{ return false }}"
299
329
  elif field_type.startswith('[]'):
300
330
  return f"if _, ok := node[\"{field_name}\"].([]interface{{}}); !ok {{ return false }}"
301
331
  elif field_type in self.generated_types_go_package:
302
332
  return f"if _, ok := node[\"{field_name}\"].({field_type}); !ok {{ return false }}"
303
333
  else:
304
- return f"if _, ok := node[\"{field_name}\"].(map[string.interface{{}}); !ok {{ return false }}"
334
+ return f"if _, ok := node[\"{field_name}\"].(map[string]interface{{}}); !ok {{ return false }}"
305
335
 
306
336
  def get_imports_for_definition(self, types: List[str]) -> Set[str]:
307
337
  """Collects necessary imports for the Go definition based on the Go types"""
@@ -393,6 +423,7 @@ class AvroToGo:
393
423
  'package_site': self.package_site,
394
424
  'package_username': self.package_username,
395
425
  'json_annotation': self.json_annotation,
426
+ 'xml_annotation': self.xml_annotation,
396
427
  'avro_annotation': self.avro_annotation
397
428
  }
398
429
 
@@ -418,6 +449,15 @@ class AvroToGo:
418
449
  self.write_go_mod_file()
419
450
  self.write_modname_go_file()
420
451
  self.generate_helpers()
452
+ self.write_xml_helpers()
453
+ format_go_files(self.output_dir)
454
+
455
+ def write_xml_helpers(self) -> None:
456
+ """Write generic XML support for map fields."""
457
+ if not self.xml_annotation:
458
+ return
459
+ helper_path = os.path.join(self.output_dir, 'pkg', self.base_package, 'xml_helpers.go')
460
+ render_template('avrotogo/xml_helpers.jinja', helper_path, base_package=self.base_package)
421
461
 
422
462
  def write_go_mod_file(self):
423
463
  """Writes the go.mod file for the Go project"""
@@ -453,7 +493,7 @@ class AvroToGo:
453
493
  self.convert_schema(schema, output_dir)
454
494
 
455
495
 
456
- def convert_avro_to_go(avro_schema_path, go_file_path, package_name='', avro_annotation=False, json_annotation=False, package_site='github.com', package_username='username'):
496
+ def convert_avro_to_go(avro_schema_path, go_file_path, package_name='', avro_annotation=False, json_annotation=False, xml_annotation=False, package_site='github.com', package_username='username'):
457
497
  """Converts Avro schema to Go structs
458
498
 
459
499
  Args:
@@ -462,6 +502,7 @@ def convert_avro_to_go(avro_schema_path, go_file_path, package_name='', avro_ann
462
502
  package_name (str): Base package name
463
503
  avro_annotation (bool): Include Avro annotations
464
504
  json_annotation (bool): Include JSON annotations
505
+ xml_annotation (bool): Include XML annotations and serialization
465
506
  """
466
507
  if not package_name:
467
508
  package_name = os.path.splitext(os.path.basename(avro_schema_path))[0]
@@ -469,12 +510,13 @@ def convert_avro_to_go(avro_schema_path, go_file_path, package_name='', avro_ann
469
510
  avrotogo = AvroToGo(package_name)
470
511
  avrotogo.avro_annotation = avro_annotation
471
512
  avrotogo.json_annotation = json_annotation
513
+ avrotogo.xml_annotation = xml_annotation
472
514
  avrotogo.package_site = package_site if package_site else 'github.com'
473
515
  avrotogo.package_username = package_username if package_username else 'username'
474
516
  avrotogo.convert(avro_schema_path, go_file_path)
475
517
 
476
518
 
477
- def convert_avro_schema_to_go(avro_schema: JsonNode, output_dir: str, package_name='', avro_annotation=False, json_annotation=False, package_site='github.com', package_username='username'):
519
+ def convert_avro_schema_to_go(avro_schema: JsonNode, output_dir: str, package_name='', avro_annotation=False, json_annotation=False, xml_annotation=False, package_site='github.com', package_username='username'):
478
520
  """Converts Avro schema to Go structs
479
521
 
480
522
  Args:
@@ -483,10 +525,12 @@ def convert_avro_schema_to_go(avro_schema: JsonNode, output_dir: str, package_na
483
525
  package_name (str): Base package name
484
526
  avro_annotation (bool): Include Avro annotations
485
527
  json_annotation (bool): Include JSON annotations
528
+ xml_annotation (bool): Include XML annotations and serialization
486
529
  """
487
530
  avrotogo = AvroToGo(package_name)
488
531
  avrotogo.avro_annotation = avro_annotation
489
532
  avrotogo.json_annotation = json_annotation
533
+ avrotogo.xml_annotation = xml_annotation
490
534
  avrotogo.package_site = package_site if package_site else 'github.com'
491
535
  avrotogo.package_username = package_username if package_username else 'username'
492
536
  avrotogo.convert_schema(avro_schema, output_dir)