avrotize 2.21.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. avrotize/__init__.py +66 -0
  2. avrotize/__main__.py +6 -0
  3. avrotize/_version.py +34 -0
  4. avrotize/asn1toavro.py +160 -0
  5. avrotize/avrotize.py +152 -0
  6. avrotize/avrotocpp/CMakeLists.txt.jinja +77 -0
  7. avrotize/avrotocpp/build.bat.jinja +7 -0
  8. avrotize/avrotocpp/build.sh.jinja +7 -0
  9. avrotize/avrotocpp/dataclass_body.jinja +108 -0
  10. avrotize/avrotocpp/vcpkg.json.jinja +21 -0
  11. avrotize/avrotocpp.py +483 -0
  12. avrotize/avrotocsharp/README.md.jinja +166 -0
  13. avrotize/avrotocsharp/class_test.cs.jinja +266 -0
  14. avrotize/avrotocsharp/dataclass_core.jinja +293 -0
  15. avrotize/avrotocsharp/enum_test.cs.jinja +20 -0
  16. avrotize/avrotocsharp/project.csproj.jinja +30 -0
  17. avrotize/avrotocsharp/project.sln.jinja +34 -0
  18. avrotize/avrotocsharp/run_coverage.ps1.jinja +98 -0
  19. avrotize/avrotocsharp/run_coverage.sh.jinja +149 -0
  20. avrotize/avrotocsharp/testproject.csproj.jinja +19 -0
  21. avrotize/avrotocsharp.py +1180 -0
  22. avrotize/avrotocsv.py +121 -0
  23. avrotize/avrotodatapackage.py +173 -0
  24. avrotize/avrotodb.py +1383 -0
  25. avrotize/avrotogo/go_enum.jinja +12 -0
  26. avrotize/avrotogo/go_helpers.jinja +31 -0
  27. avrotize/avrotogo/go_struct.jinja +151 -0
  28. avrotize/avrotogo/go_test.jinja +47 -0
  29. avrotize/avrotogo/go_union.jinja +38 -0
  30. avrotize/avrotogo.py +476 -0
  31. avrotize/avrotographql.py +197 -0
  32. avrotize/avrotoiceberg.py +210 -0
  33. avrotize/avrotojava/class_test.java.jinja +212 -0
  34. avrotize/avrotojava/enum_test.java.jinja +21 -0
  35. avrotize/avrotojava/testproject.pom.jinja +54 -0
  36. avrotize/avrotojava.py +2156 -0
  37. avrotize/avrotojs.py +250 -0
  38. avrotize/avrotojsons.py +481 -0
  39. avrotize/avrotojstruct.py +345 -0
  40. avrotize/avrotokusto.py +364 -0
  41. avrotize/avrotomd/README.md.jinja +49 -0
  42. avrotize/avrotomd.py +137 -0
  43. avrotize/avrotools.py +168 -0
  44. avrotize/avrotoparquet.py +208 -0
  45. avrotize/avrotoproto.py +359 -0
  46. avrotize/avrotopython/dataclass_core.jinja +241 -0
  47. avrotize/avrotopython/enum_core.jinja +87 -0
  48. avrotize/avrotopython/pyproject_toml.jinja +18 -0
  49. avrotize/avrotopython/test_class.jinja +97 -0
  50. avrotize/avrotopython/test_enum.jinja +23 -0
  51. avrotize/avrotopython.py +626 -0
  52. avrotize/avrotorust/dataclass_enum.rs.jinja +74 -0
  53. avrotize/avrotorust/dataclass_struct.rs.jinja +204 -0
  54. avrotize/avrotorust/dataclass_union.rs.jinja +105 -0
  55. avrotize/avrotorust.py +435 -0
  56. avrotize/avrotots/class_core.ts.jinja +140 -0
  57. avrotize/avrotots/class_test.ts.jinja +77 -0
  58. avrotize/avrotots/enum_core.ts.jinja +46 -0
  59. avrotize/avrotots/gitignore.jinja +34 -0
  60. avrotize/avrotots/index.ts.jinja +0 -0
  61. avrotize/avrotots/package.json.jinja +23 -0
  62. avrotize/avrotots/tsconfig.json.jinja +21 -0
  63. avrotize/avrotots.py +687 -0
  64. avrotize/avrotoxsd.py +344 -0
  65. avrotize/cddltostructure.py +1841 -0
  66. avrotize/commands.json +3496 -0
  67. avrotize/common.py +834 -0
  68. avrotize/constants.py +87 -0
  69. avrotize/csvtoavro.py +132 -0
  70. avrotize/datapackagetoavro.py +76 -0
  71. avrotize/dependencies/cpp/vcpkg/vcpkg.json +19 -0
  72. avrotize/dependencies/cs/net90/dependencies.csproj +29 -0
  73. avrotize/dependencies/go/go121/go.mod +6 -0
  74. avrotize/dependencies/java/jdk21/pom.xml +91 -0
  75. avrotize/dependencies/python/py312/requirements.txt +13 -0
  76. avrotize/dependencies/rust/stable/Cargo.toml +17 -0
  77. avrotize/dependencies/typescript/node22/package.json +16 -0
  78. avrotize/dependency_resolver.py +348 -0
  79. avrotize/dependency_version.py +432 -0
  80. avrotize/generic/generic.avsc +57 -0
  81. avrotize/jsonstoavro.py +2167 -0
  82. avrotize/jsonstostructure.py +2864 -0
  83. avrotize/jstructtoavro.py +878 -0
  84. avrotize/kstructtoavro.py +93 -0
  85. avrotize/kustotoavro.py +455 -0
  86. avrotize/openapitostructure.py +717 -0
  87. avrotize/parquettoavro.py +157 -0
  88. avrotize/proto2parser.py +498 -0
  89. avrotize/proto3parser.py +403 -0
  90. avrotize/prototoavro.py +382 -0
  91. avrotize/prototypes/any.avsc +19 -0
  92. avrotize/prototypes/api.avsc +106 -0
  93. avrotize/prototypes/duration.avsc +20 -0
  94. avrotize/prototypes/field_mask.avsc +18 -0
  95. avrotize/prototypes/struct.avsc +60 -0
  96. avrotize/prototypes/timestamp.avsc +20 -0
  97. avrotize/prototypes/type.avsc +253 -0
  98. avrotize/prototypes/wrappers.avsc +117 -0
  99. avrotize/structuretocddl.py +597 -0
  100. avrotize/structuretocpp/CMakeLists.txt.jinja +76 -0
  101. avrotize/structuretocpp/build.bat.jinja +3 -0
  102. avrotize/structuretocpp/build.sh.jinja +3 -0
  103. avrotize/structuretocpp/dataclass_body.jinja +50 -0
  104. avrotize/structuretocpp/vcpkg.json.jinja +11 -0
  105. avrotize/structuretocpp.py +697 -0
  106. avrotize/structuretocsharp/class_test.cs.jinja +180 -0
  107. avrotize/structuretocsharp/dataclass_core.jinja +156 -0
  108. avrotize/structuretocsharp/enum_test.cs.jinja +36 -0
  109. avrotize/structuretocsharp/json_structure_converters.cs.jinja +399 -0
  110. avrotize/structuretocsharp/program.cs.jinja +49 -0
  111. avrotize/structuretocsharp/project.csproj.jinja +17 -0
  112. avrotize/structuretocsharp/project.sln.jinja +34 -0
  113. avrotize/structuretocsharp/testproject.csproj.jinja +18 -0
  114. avrotize/structuretocsharp/tuple_converter.cs.jinja +121 -0
  115. avrotize/structuretocsharp.py +2295 -0
  116. avrotize/structuretocsv.py +365 -0
  117. avrotize/structuretodatapackage.py +659 -0
  118. avrotize/structuretodb.py +1125 -0
  119. avrotize/structuretogo/go_enum.jinja +12 -0
  120. avrotize/structuretogo/go_helpers.jinja +26 -0
  121. avrotize/structuretogo/go_interface.jinja +18 -0
  122. avrotize/structuretogo/go_struct.jinja +187 -0
  123. avrotize/structuretogo/go_test.jinja +70 -0
  124. avrotize/structuretogo.py +729 -0
  125. avrotize/structuretographql.py +502 -0
  126. avrotize/structuretoiceberg.py +355 -0
  127. avrotize/structuretojava/choice_core.jinja +34 -0
  128. avrotize/structuretojava/class_core.jinja +23 -0
  129. avrotize/structuretojava/enum_core.jinja +18 -0
  130. avrotize/structuretojava/equals_hashcode.jinja +30 -0
  131. avrotize/structuretojava/pom.xml.jinja +26 -0
  132. avrotize/structuretojava/tuple_core.jinja +49 -0
  133. avrotize/structuretojava.py +938 -0
  134. avrotize/structuretojs/class_core.js.jinja +33 -0
  135. avrotize/structuretojs/enum_core.js.jinja +10 -0
  136. avrotize/structuretojs/package.json.jinja +12 -0
  137. avrotize/structuretojs/test_class.js.jinja +84 -0
  138. avrotize/structuretojs/test_enum.js.jinja +58 -0
  139. avrotize/structuretojs/test_runner.js.jinja +45 -0
  140. avrotize/structuretojs.py +657 -0
  141. avrotize/structuretojsons.py +498 -0
  142. avrotize/structuretokusto.py +639 -0
  143. avrotize/structuretomd/README.md.jinja +204 -0
  144. avrotize/structuretomd.py +322 -0
  145. avrotize/structuretoproto.py +764 -0
  146. avrotize/structuretopython/dataclass_core.jinja +363 -0
  147. avrotize/structuretopython/enum_core.jinja +45 -0
  148. avrotize/structuretopython/map_alias.jinja +21 -0
  149. avrotize/structuretopython/pyproject_toml.jinja +23 -0
  150. avrotize/structuretopython/test_class.jinja +103 -0
  151. avrotize/structuretopython/test_enum.jinja +34 -0
  152. avrotize/structuretopython.py +799 -0
  153. avrotize/structuretorust/dataclass_enum.rs.jinja +63 -0
  154. avrotize/structuretorust/dataclass_struct.rs.jinja +121 -0
  155. avrotize/structuretorust/dataclass_union.rs.jinja +81 -0
  156. avrotize/structuretorust.py +714 -0
  157. avrotize/structuretots/class_core.ts.jinja +78 -0
  158. avrotize/structuretots/enum_core.ts.jinja +6 -0
  159. avrotize/structuretots/gitignore.jinja +8 -0
  160. avrotize/structuretots/index.ts.jinja +1 -0
  161. avrotize/structuretots/package.json.jinja +39 -0
  162. avrotize/structuretots/test_class.ts.jinja +35 -0
  163. avrotize/structuretots/tsconfig.json.jinja +21 -0
  164. avrotize/structuretots.py +740 -0
  165. avrotize/structuretoxsd.py +679 -0
  166. avrotize/xsdtoavro.py +413 -0
  167. avrotize-2.21.1.dist-info/METADATA +1319 -0
  168. avrotize-2.21.1.dist-info/RECORD +171 -0
  169. avrotize-2.21.1.dist-info/WHEEL +4 -0
  170. avrotize-2.21.1.dist-info/entry_points.txt +3 -0
  171. avrotize-2.21.1.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,1180 @@
1
+ # pylint: disable=line-too-long
2
+
3
+ """ AvroToCSharp class for converting Avro schema to C# classes """
4
+
5
+ import json
6
+ import os
7
+ import re
8
+ from typing import Any, Dict, List, Tuple, Union, cast
9
+ import uuid
10
+
11
+ from avrotize.common import build_flat_type_dict, inline_avro_references, is_generic_avro_type, pascal, process_template
12
+ from avrotize.constants import (
13
+ CSHARP_AVRO_VERSION,
14
+ NEWTONSOFT_JSON_VERSION,
15
+ SYSTEM_TEXT_JSON_VERSION,
16
+ SYSTEM_MEMORY_DATA_VERSION,
17
+ PROTOBUF_NET_VERSION,
18
+ MSGPACK_VERSION,
19
+ NUNIT_VERSION,
20
+ NUNIT_ADAPTER_VERSION,
21
+ MSTEST_SDK_VERSION,
22
+ COVERLET_VERSION,
23
+ )
24
+ import glob
25
+
26
+ JsonNode = Dict[str, 'JsonNode'] | List['JsonNode'] | str | None
27
+
28
+
29
+ INDENT = ' '
30
+
31
+ AVRO_CLASS_PREAMBLE = \
32
+ """
33
+ public {type_name}(global::Avro.Generic.GenericRecord obj)
34
+ {
35
+ global::Avro.Specific.ISpecificRecord self = this;
36
+ for (int i = 0; obj.Schema.Fields.Count > i; ++i)
37
+ {
38
+ self.Put(i, obj.GetValue(i));
39
+ }
40
+ }
41
+ """
42
+
43
+ class AvroToCSharp:
44
+ """ Converts Avro schema to C# classes """
45
+
46
+ def __init__(self, base_namespace: str = '') -> None:
47
+ self.base_namespace = base_namespace
48
+ self.project_name: str = '' # Optional explicit project name, separate from namespace
49
+ self.schema_doc: JsonNode = None
50
+ self.output_dir = os.getcwd()
51
+ self.pascal_properties = False
52
+ self.system_text_json_annotation = False
53
+ self.newtonsoft_json_annotation = False
54
+ self.system_xml_annotation = False
55
+ self.msgpack_annotation = False
56
+ self.avro_annotation = False
57
+ self.protobuf_net_annotation = False
58
+ self.generated_types: Dict[str,str] = {}
59
+ self.generated_avro_types: Dict[str, Dict[str, Union[str, Dict, List]]] = {}
60
+ self.type_dict: Dict[str, Dict] = {}
61
+
62
+ def get_qualified_name(self, namespace: str, name: str) -> str:
63
+ """ Concatenates namespace and name with a dot separator """
64
+ return f"{namespace}.{name}" if namespace != '' else name
65
+
66
+ def concat_namespace(self, namespace: str, name: str) -> str:
67
+ """ Concatenates namespace and name with a dot separator """
68
+ if namespace and name:
69
+ return f"{namespace}.{name}"
70
+ elif namespace:
71
+ return namespace
72
+ else:
73
+ return name
74
+
75
+ def map_primitive_to_csharp(self, avro_type: str) -> str:
76
+ """ Maps Avro primitive types to C# types """
77
+ mapping = {
78
+ 'null': 'void', # Placeholder, actual handling for nullable types is in the union logic
79
+ 'boolean': 'bool',
80
+ 'int': 'int',
81
+ 'long': 'long',
82
+ 'float': 'float',
83
+ 'double': 'double',
84
+ 'bytes': 'byte[]',
85
+ 'string': 'string',
86
+ }
87
+ qualified_class_name = 'global::'+self.get_qualified_name(pascal(self.base_namespace), pascal(avro_type))
88
+ if qualified_class_name in self.generated_avro_types:
89
+ result = qualified_class_name
90
+ else:
91
+ result = mapping.get(avro_type, 'object')
92
+ return result
93
+
94
+ def is_csharp_reserved_word(self, word: str) -> bool:
95
+ """ Checks if a word is a reserved C# keyword """
96
+ reserved_words = [
97
+ 'abstract', 'as', 'base', 'bool', 'break', 'byte', 'case', 'catch', 'char', 'checked', 'class', 'const',
98
+ 'continue', 'decimal', 'default', 'delegate', 'do', 'double', 'else', 'enum', 'event', 'explicit', 'extern',
99
+ 'false', 'finally', 'fixed', 'float', 'for', 'foreach', 'goto', 'if', 'implicit', 'in', 'int', 'interface',
100
+ 'internal', 'is', 'lock', 'long', 'namespace', 'new', 'null', 'object', 'operator', 'out', 'override',
101
+ 'params', 'private', 'protected', 'public', 'readonly', 'ref', 'return', 'sbyte', 'sealed', 'short', 'sizeof',
102
+ 'stackalloc', 'static', 'string', 'struct', 'switch', 'this', 'throw', 'true', 'try', 'typeof', 'uint', 'ulong',
103
+ 'unchecked', 'unsafe', 'ushort', 'using', 'virtual', 'void', 'volatile', 'while'
104
+ ]
105
+ return word in reserved_words
106
+
107
+ def is_csharp_primitive_type(self, csharp_type: str) -> bool:
108
+ """ Checks if an Avro type is a C# primitive type """
109
+ if csharp_type.endswith('?'):
110
+ csharp_type = csharp_type[:-1]
111
+ return csharp_type in ['null', 'bool', 'int', 'long', 'float', 'double', 'bytes', 'string', 'DateTime', 'decimal', 'short', 'sbyte', 'ushort', 'uint', 'ulong', 'byte[]', 'object']
112
+
113
+ def map_csharp_primitive_to_clr_type(self, cs_type: str) -> str:
114
+ """ Maps C# primitive types to CLR types"""
115
+ map = {
116
+ "int": "Int32",
117
+ "long": "Int64",
118
+ "float": "Single",
119
+ "double": "Double",
120
+ "decimal": "Decimal",
121
+ "short": "Int16",
122
+ "sbyte": "SByte",
123
+ "ushort": "UInt16",
124
+ "uint": "UInt32",
125
+ "ulong": "UInt64"
126
+ }
127
+ return map.get(cs_type, cs_type)
128
+
129
+ def convert_avro_type_to_csharp(self, class_name: str, field_name: str, avro_type: JsonNode, parent_namespace: str) -> str:
130
+ """ Converts Avro type to C# type """
131
+ if isinstance(avro_type, str):
132
+ return self.map_primitive_to_csharp(avro_type)
133
+ elif isinstance(avro_type, list):
134
+ # Handle nullable types and unions
135
+ if is_generic_avro_type(avro_type):
136
+ return 'Dictionary<string, object>'
137
+ else:
138
+ non_null_types = [t for t in avro_type if t != 'null']
139
+ if len(non_null_types) == 1:
140
+ # Nullable type
141
+ return f"{self.convert_avro_type_to_csharp(class_name, field_name, non_null_types[0], parent_namespace)}?"
142
+ else:
143
+ return self.generate_embedded_union(class_name, field_name, non_null_types, parent_namespace, write_file=True)
144
+ elif isinstance(avro_type, dict):
145
+ # Handle complex types: records, enums, arrays, and maps
146
+ if avro_type['type'] in ['record', 'enum']:
147
+ return self.generate_class_or_enum(avro_type, parent_namespace, write_file=True)
148
+ elif avro_type['type'] == 'array':
149
+ return f"List<{self.convert_avro_type_to_csharp(class_name, field_name+'List', avro_type['items'], parent_namespace)}>"
150
+ elif avro_type['type'] == 'map':
151
+ return f"Dictionary<string, {self.convert_avro_type_to_csharp(class_name, field_name, avro_type['values'], parent_namespace)}>"
152
+ return self.convert_avro_type_to_csharp(class_name, field_name, avro_type['type'], parent_namespace)
153
+ return 'object'
154
+
155
+ def generate_class_or_enum(self, avro_schema: Dict, parent_namespace: str, write_file: bool = True) -> str:
156
+ """ Generates a Class or Enum """
157
+ if avro_schema['type'] == 'record':
158
+ return self.generate_class(avro_schema, parent_namespace, write_file)
159
+ elif avro_schema['type'] == 'enum':
160
+ return self.generate_enum(avro_schema, parent_namespace, write_file)
161
+ return ''
162
+
163
+ def generate_class(self, avro_schema: Dict, parent_namespace: str, write_file: bool) -> str:
164
+ """ Generates a Class """
165
+ class_definition = ''
166
+ avro_namespace = avro_schema.get('namespace', parent_namespace)
167
+ if not 'namespace' in avro_schema:
168
+ avro_schema['namespace'] = parent_namespace
169
+ xml_namespace = avro_schema.get('xmlns', None)
170
+ namespace = pascal(self.concat_namespace(self.base_namespace, avro_namespace))
171
+ class_name = pascal(avro_schema['name'])
172
+ ref = 'global::'+self.get_qualified_name(namespace, class_name)
173
+ if ref in self.generated_types:
174
+ return ref
175
+
176
+ class_definition += f"/// <summary>\n/// { avro_schema.get('doc', class_name ) }\n/// </summary>\n"
177
+
178
+ # Add XML serialization attribute for the class if enabled
179
+ if self.system_xml_annotation:
180
+ if xml_namespace:
181
+ class_definition += f"[XmlRoot(\"{class_name}\", Namespace=\"{xml_namespace}\")]\n"
182
+ else:
183
+ class_definition += f"[XmlRoot(\"{class_name}\")]\n"
184
+
185
+ if self.protobuf_net_annotation:
186
+ class_definition += "[ProtoContract]\n"
187
+
188
+ # Add MessagePack serialization attribute for the class if enabled
189
+ if self.msgpack_annotation:
190
+ class_definition += "[MessagePackObject]\n"
191
+
192
+ fields_str = [self.generate_property(index + 1, field, class_name, avro_namespace) for index, field in enumerate(avro_schema.get('fields', []))]
193
+ class_body = "\n".join(fields_str)
194
+ class_definition += f"public partial class {class_name}"
195
+ if self.avro_annotation:
196
+ class_definition += " : global::Avro.Specific.ISpecificRecord"
197
+ class_definition += "\n{\n"+class_body
198
+ class_definition += f"\n{INDENT}/// <summary>\n{INDENT}/// Default constructor\n{INDENT}///</summary>\n"
199
+ class_definition += f"{INDENT}public {class_name}()\n{INDENT}{{\n{INDENT}}}"
200
+ if self.avro_annotation:
201
+ class_definition += f"\n\n{INDENT}/// <summary>\n{INDENT}/// Constructor from Avro GenericRecord\n{INDENT}///</summary>\n"
202
+ class_definition += f"{INDENT}public {class_name}(global::Avro.Generic.GenericRecord obj)\n{INDENT}{{\n"
203
+ class_definition += f"{INDENT*2}global::Avro.Specific.ISpecificRecord self = this;\n"
204
+ class_definition += f"{INDENT*2}for (int i = 0; obj.Schema.Fields.Count > i; ++i)\n{INDENT*2}{{\n"
205
+ class_definition += f"{INDENT*3}self.Put(i, obj.GetValue(i));\n{INDENT*2}}}\n{INDENT}}}\n"
206
+ if self.avro_annotation:
207
+
208
+ local_avro_schema = inline_avro_references(avro_schema.copy(), self.type_dict, '')
209
+ avro_schema_json = json.dumps(local_avro_schema)
210
+ # wrap schema at 80 characters
211
+ avro_schema_json = avro_schema_json.replace('"', '§')
212
+ avro_schema_json = f"\"+\n{INDENT}\"".join(
213
+ [avro_schema_json[i:i+80] for i in range(0, len(avro_schema_json), 80)])
214
+ avro_schema_json = avro_schema_json.replace('§', '\\"')
215
+ class_definition += f"\n\n{INDENT}/// <summary>\n{INDENT}/// Avro schema for this class\n{INDENT}/// </summary>"
216
+ class_definition += f"\n{INDENT}public static global::Avro.Schema AvroSchema = global::Avro.Schema.Parse(\n{INDENT}\"{avro_schema_json}\");\n"
217
+ class_definition += f"\n{INDENT}global::Avro.Schema global::Avro.Specific.ISpecificRecord.Schema => AvroSchema;\n"
218
+ get_method = f"{INDENT}object global::Avro.Specific.ISpecificRecord.Get(int fieldPos)\n" + \
219
+ INDENT+"{"+f"\n{INDENT*2}switch (fieldPos)\n{INDENT*2}" + "{"
220
+ put_method = f"{INDENT}void global::Avro.Specific.ISpecificRecord.Put(int fieldPos, object fieldValue)\n" + \
221
+ INDENT+"{"+f"\n{INDENT*2}switch (fieldPos)\n{INDENT*2}"+"{"
222
+ for pos, field in enumerate(avro_schema.get('fields', [])):
223
+ field_name = field['name']
224
+ if self.is_csharp_reserved_word(field_name):
225
+ field_name = f"@{field_name}"
226
+ field_type = self.convert_avro_type_to_csharp(class_name, field_name, field['type'], avro_namespace)
227
+ if self.pascal_properties:
228
+ field_name = pascal(field_name)
229
+ if field_name == class_name:
230
+ field_name += "_"
231
+ if field_type in self.generated_types:
232
+ if self.generated_types[field_type] == "union":
233
+ get_method += f"\n{INDENT*3}case {pos}: return this.{field_name}?.ToObject();"
234
+ put_method += f"\n{INDENT*3}case {pos}: this.{field_name} = {field_type}.FromObject(fieldValue); break;"
235
+ elif self.generated_types[field_type] == "enum":
236
+ get_method += f"\n{INDENT*3}case {pos}: return ({field_type})this.{field_name};"
237
+ put_method += f"\n{INDENT*3}case {pos}: this.{field_name} = fieldValue is global::Avro.Generic.GenericEnum?Enum.Parse<{field_type}>(((global::Avro.Generic.GenericEnum)fieldValue).Value):({field_type})fieldValue; break;"
238
+ elif self.generated_types[field_type] == "class":
239
+ get_method += f"\n{INDENT*3}case {pos}: return this.{field_name};"
240
+ put_method += f"\n{INDENT*3}case {pos}: this.{field_name} = fieldValue is global::Avro.Generic.GenericRecord?new {field_type}((global::Avro.Generic.GenericRecord)fieldValue):({field_type})fieldValue; break;"
241
+ else:
242
+ get_method += f"\n{INDENT*3}case {pos}: return this.{field_name};"
243
+ if field_type.startswith("List<"):
244
+ inner_type = field_type.strip()[5:-2] if field_type[-1] == '?' else field_type[5:-1]
245
+ if inner_type in self.generated_types:
246
+ if self.generated_types[inner_type] == "class":
247
+ put_method += f"\n{INDENT*3}case {pos}: this.{field_name} = fieldValue is Object[]?((Object[])fieldValue).Select(x => new {inner_type}((global::Avro.Generic.GenericRecord)x)).ToList():({field_type})fieldValue; break;"
248
+ else:
249
+ put_method += f"\n{INDENT*3}case {pos}: this.{field_name} = fieldValue is Object[]?((Object[])fieldValue).Select(x => ({inner_type})x).ToList():({field_type})fieldValue; break;"
250
+ else:
251
+ put_method += f"\n{INDENT*3}case {pos}: this.{field_name} = fieldValue is Object[]?((Object[])fieldValue).Select(x => ({inner_type})x).ToList():({field_type})fieldValue; break;"
252
+ else:
253
+ put_method += f"\n{INDENT*3}case {pos}: this.{field_name} = ({field_type})fieldValue; break;"
254
+ get_method += f"\n{INDENT*3}default: throw new global::Avro.AvroRuntimeException($\"Bad index {{fieldPos}} in Get()\");"
255
+ put_method += f"\n{INDENT*3}default: throw new global::Avro.AvroRuntimeException($\"Bad index {{fieldPos}} in Put()\");"
256
+ get_method += "\n"+INDENT+INDENT+"}\n"+INDENT+"}"
257
+ put_method += "\n"+INDENT+INDENT+"}\n"+INDENT+"}"
258
+ class_definition += f"\n{get_method}\n{put_method}\n"
259
+
260
+ # emit helper methods
261
+ class_definition += process_template(
262
+ "avrotocsharp/dataclass_core.jinja",
263
+ class_name=class_name,
264
+ avro_annotation=self.avro_annotation,
265
+ protobuf_net_annotation=self.protobuf_net_annotation,
266
+ system_text_json_annotation=self.system_text_json_annotation,
267
+ newtonsoft_json_annotation=self.newtonsoft_json_annotation,
268
+ system_xml_annotation=self.system_xml_annotation,
269
+ msgpack_annotation=self.msgpack_annotation,
270
+ json_match_clauses=self.create_is_json_match_clauses(avro_schema, avro_namespace, class_name)
271
+ )
272
+
273
+ # emit Equals and GetHashCode for value equality
274
+ class_definition += self.generate_equals_and_gethashcode(avro_schema, class_name, avro_namespace)
275
+
276
+ class_definition += "\n"+"}"
277
+
278
+ if write_file:
279
+ self.write_to_file(namespace, class_name, class_definition)
280
+
281
+ self.generated_types[ref] = "class"
282
+ self.generated_avro_types[ref] = avro_schema
283
+ return ref
284
+
285
+ def create_is_json_match_clauses(self, avro_schema, parent_namespace, class_name) -> List[str]:
286
+ """ Generates the IsJsonMatch method for System.Text.Json """
287
+ clauses: List[str] = []
288
+ for field in avro_schema.get('fields', []):
289
+ field_name = field['name']
290
+ if self.is_csharp_reserved_word(field_name):
291
+ field_name = f"@{field_name}"
292
+ if field_name == class_name:
293
+ field_name += "_"
294
+ field_type = self.convert_avro_type_to_csharp(
295
+ class_name, field_name, field['type'], parent_namespace)
296
+ clauses.append(self.get_is_json_match_clause(class_name, field_name, field_type))
297
+ if len(clauses) == 0:
298
+ clauses.append("true")
299
+ return clauses
300
+
301
+ def get_is_json_match_clause(self, class_name, field_name, field_type) -> str:
302
+ """ Generates the IsJsonMatch clause for a field """
303
+ class_definition = ''
304
+ field_name_js = field_name[1:] if field_name[0] == '@' else field_name
305
+ is_optional = field_type[-1] == '?'
306
+ field_type = field_type[:-1] if is_optional else field_type
307
+ if field_type == 'byte[]':
308
+ class_definition += f"({'!' if is_optional else ''}element.TryGetProperty(\"{field_name_js}\", out System.Text.Json.JsonElement {field_name}) {'||' if is_optional else '&&'} ({field_name}.ValueKind == System.Text.Json.JsonValueKind.String){f' || {field_name}.ValueKind == System.Text.Json.JsonValueKind.Null' if is_optional else ''})"
309
+ elif field_type == 'string':
310
+ class_definition += f"({'!' if is_optional else ''}element.TryGetProperty(\"{field_name_js}\", out System.Text.Json.JsonElement {field_name}) {'||' if is_optional else '&&'} ({field_name}.ValueKind == System.Text.Json.JsonValueKind.String){f' || {field_name}.ValueKind == System.Text.Json.JsonValueKind.Null' if is_optional else ''})"
311
+ elif field_type in ['int', 'long', 'float', 'double', 'decimal', 'short', 'sbyte', 'ushort', 'uint', 'ulong']:
312
+ class_definition += f"({'!' if is_optional else ''}element.TryGetProperty(\"{field_name_js}\", out System.Text.Json.JsonElement {field_name}) {'||' if is_optional else '&&'} ({field_name}.ValueKind == System.Text.Json.JsonValueKind.Number){f' || {field_name}.ValueKind == System.Text.Json.JsonValueKind.Null' if is_optional else ''})"
313
+ elif field_type == 'bool':
314
+ class_definition += f"({'!' if is_optional else ''}element.TryGetProperty(\"{field_name_js}\", out System.Text.Json.JsonElement {field_name}) {'||' if is_optional else '&&'} ({field_name}.ValueKind == System.Text.Json.JsonValueKind.True || {field_name}.ValueKind == System.Text.Json.JsonValueKind.False){f' || {field_name}.ValueKind == System.Text.Json.JsonValueKind.Null' if is_optional else ''})"
315
+ elif field_type.startswith("global::"):
316
+ type_kind = self.generated_types[field_type] if field_type in self.generated_types else "class"
317
+ if type_kind == "class":
318
+ class_definition += f"({'!' if is_optional else ''}element.TryGetProperty(\"{field_name_js}\", out System.Text.Json.JsonElement {field_name}) {'||' if is_optional else '&&'} ({f'{field_name}.ValueKind == System.Text.Json.JsonValueKind.Null || ' if is_optional else ''}{field_type}.IsJsonMatch({field_name})))"
319
+ elif type_kind == "enum":
320
+ class_definition += f"({'!' if is_optional else ''}element.TryGetProperty(\"{field_name_js}\", out System.Text.Json.JsonElement {field_name}) {'||' if is_optional else '&&'} ({f'{field_name}.ValueKind == System.Text.Json.JsonValueKind.Null ||' if is_optional else ''}({field_name}.ValueKind == System.Text.Json.JsonValueKind.String && Enum.TryParse<{field_type}>({field_name}.GetString(), true, out _ ))))"
321
+ else:
322
+ is_union = False
323
+ field_union = pascal(field_name)+'Union'
324
+ if field_type == field_union:
325
+ field_union = class_name+"."+pascal(field_name)+'Union'
326
+ type_kind = self.generated_types[field_union] if field_union in self.generated_types else "class"
327
+ if type_kind == "union":
328
+ is_union = True
329
+ class_definition += f"({'!' if is_optional else ''}element.TryGetProperty(\"{field_name_js}\", out System.Text.Json.JsonElement {field_name}) {'||' if is_optional else '&&'} ({f'{field_name}.ValueKind == System.Text.Json.JsonValueKind.Null || ' if is_optional else ''}{field_type}.IsJsonMatch({field_name})))"
330
+ if not is_union:
331
+ class_definition += f"({'!' if is_optional else ''}element.TryGetProperty(\"{field_name_js}\", out System.Text.Json.JsonElement {field_name}) {'||' if is_optional else '&&'} true )"
332
+ return class_definition
333
+
334
+ def get_is_json_match_clause_type(self, element_name, class_name, field_type) -> str:
335
+ """ Generates the IsJsonMatch clause for a field """
336
+ class_definition = ''
337
+ is_optional = field_type[-1] == '?'
338
+ field_type = field_type[:-1] if is_optional else field_type
339
+ if field_type == 'byte[]':
340
+ class_definition += f"({element_name}.ValueKind == System.Text.Json.JsonValueKind.String{f' || {element_name}.ValueKind == System.Text.Json.JsonValueKind.Null' if is_optional else ''})"
341
+ elif field_type == 'string':
342
+ class_definition += f"({element_name}.ValueKind == System.Text.Json.JsonValueKind.String{f' || {element_name}.ValueKind == System.Text.Json.JsonValueKind.Null' if is_optional else ''})"
343
+ elif field_type in ['int', 'long', 'float', 'double', 'decimal', 'short', 'sbyte', 'ushort', 'uint', 'ulong']:
344
+ class_definition += f"({element_name}.ValueKind == System.Text.Json.JsonValueKind.Number{f' || {element_name}.ValueKind == System.Text.Json.JsonValueKind.Null' if is_optional else ''})"
345
+ elif field_type == 'bool':
346
+ class_definition += f"({element_name}.ValueKind == System.Text.Json.JsonValueKind.True || {element_name}.ValueKind == System.Text.Json.JsonValueKind.False{f' || {element_name}.ValueKind == System.Text.Json.JsonValueKind.Null' if is_optional else ''})"
347
+ elif field_type.startswith("global::"):
348
+ type_kind = self.generated_types[field_type] if field_type in self.generated_types else "class"
349
+ if type_kind == "class":
350
+ class_definition += f"({f'{element_name}.ValueKind == System.Text.Json.JsonValueKind.Null || ' if is_optional else ''}{field_type}.IsJsonMatch({element_name}))"
351
+ elif type_kind == "enum":
352
+ class_definition += f"({f'{element_name}.ValueKind == System.Text.Json.JsonValueKind.Null ||' if is_optional else ''}({element_name}.ValueKind == System.Text.Json.JsonValueKind.String && Enum.TryParse<{field_type}>({element_name}.GetString(), true, out _ )))"
353
+ else:
354
+ is_union = False
355
+ field_union = pascal(element_name)+'Union'
356
+ if field_type == field_union:
357
+ field_union = class_name+"."+pascal(element_name)+'Union'
358
+ type_kind = self.generated_types[field_union] if field_union in self.generated_types else "class"
359
+ if type_kind == "union":
360
+ is_union = True
361
+ class_definition += f"({f'{element_name}.ValueKind == System.Text.Json.JsonValueKind.Null || ' if is_optional else ''}{field_type}.IsJsonMatch({element_name})))"
362
+ if not is_union:
363
+ class_definition += f"({element_name}.ValueKind == System.Text.Json.JsonValueKind.Object{f' || {element_name}.ValueKind == System.Text.Json.JsonValueKind.Null' if is_optional else ''})"
364
+ return class_definition
365
+
366
+ def generate_equals_and_gethashcode(self, avro_schema: Dict, class_name: str, parent_namespace: str) -> str:
367
+ """ Generates Equals and GetHashCode methods for value equality """
368
+ code = "\n"
369
+ fields = avro_schema.get('fields', [])
370
+
371
+ if not fields:
372
+ # Empty class - simple implementation
373
+ code += f"{INDENT}/// <summary>\n{INDENT}/// Determines whether the specified object is equal to the current object.\n{INDENT}/// </summary>\n"
374
+ code += f"{INDENT}public override bool Equals(object? obj)\n{INDENT}{{\n"
375
+ code += f"{INDENT*2}return obj is {class_name};\n"
376
+ code += f"{INDENT}}}\n\n"
377
+ code += f"{INDENT}/// <summary>\n{INDENT}/// Serves as the default hash function.\n{INDENT}/// </summary>\n"
378
+ code += f"{INDENT}public override int GetHashCode()\n{INDENT}{{\n"
379
+ code += f"{INDENT*2}return 0;\n"
380
+ code += f"{INDENT}}}\n"
381
+ return code
382
+
383
+ # Generate Equals method
384
+ code += f"{INDENT}/// <summary>\n{INDENT}/// Determines whether the specified object is equal to the current object.\n{INDENT}/// </summary>\n"
385
+ code += f"{INDENT}public override bool Equals(object? obj)\n{INDENT}{{\n"
386
+ code += f"{INDENT*2}if (obj is not {class_name} other) return false;\n"
387
+
388
+ # Build equality comparisons for each field
389
+ equality_checks = []
390
+ for field in fields:
391
+ field_name = field['name']
392
+ if self.is_csharp_reserved_word(field_name):
393
+ field_name = f"@{field_name}"
394
+ if self.pascal_properties:
395
+ field_name = pascal(field_name)
396
+ if field_name == class_name:
397
+ field_name += "_"
398
+
399
+ field_type = self.convert_avro_type_to_csharp(class_name, field_name, field['type'], parent_namespace)
400
+
401
+ # Handle different types of comparisons
402
+ if field_type == 'byte[]' or field_type == 'byte[]?':
403
+ # Byte arrays need special handling
404
+ equality_checks.append(f"System.Linq.Enumerable.SequenceEqual({field_name} ?? Array.Empty<byte>(), other.{field_name} ?? Array.Empty<byte>())")
405
+ elif field_type.startswith('List<') or field_type.startswith('Dictionary<'):
406
+ # Collections need sequence comparison
407
+ if field_type.endswith('?'):
408
+ equality_checks.append(f"(({field_name} == null && other.{field_name} == null) || ({field_name} != null && other.{field_name} != null && {field_name}.SequenceEqual(other.{field_name})))")
409
+ else:
410
+ equality_checks.append(f"{field_name}.SequenceEqual(other.{field_name})")
411
+ else:
412
+ # Use Equals for reference types, == for value types
413
+ if field_type.endswith('?') or not self.is_csharp_primitive_type(field_type):
414
+ equality_checks.append(f"Equals({field_name}, other.{field_name})")
415
+ else:
416
+ equality_checks.append(f"{field_name} == other.{field_name}")
417
+
418
+ # Join all checks with &&
419
+ if len(equality_checks) == 1:
420
+ code += f"{INDENT*2}return {equality_checks[0]};\n"
421
+ else:
422
+ code += f"{INDENT*2}return " + f"\n{INDENT*3}&& ".join(equality_checks) + ";\n"
423
+
424
+ code += f"{INDENT}}}\n\n"
425
+
426
+ # Generate GetHashCode method
427
+ code += f"{INDENT}/// <summary>\n{INDENT}/// Serves as the default hash function.\n{INDENT}/// </summary>\n"
428
+ code += f"{INDENT}public override int GetHashCode()\n{INDENT}{{\n"
429
+
430
+ # Collect field names for HashCode.Combine
431
+ hash_fields = []
432
+ for field in fields:
433
+ field_name = field['name']
434
+ if self.is_csharp_reserved_word(field_name):
435
+ field_name = f"@{field_name}"
436
+ if self.pascal_properties:
437
+ field_name = pascal(field_name)
438
+ if field_name == class_name:
439
+ field_name += "_"
440
+
441
+ field_type = self.convert_avro_type_to_csharp(class_name, field_name, field['type'], parent_namespace)
442
+
443
+ # Handle special types that need custom hash code computation
444
+ if field_type == 'byte[]' or field_type == 'byte[]?':
445
+ hash_fields.append(f"({field_name} != null ? System.Convert.ToBase64String({field_name}).GetHashCode() : 0)")
446
+ elif field_type.startswith('List<') or field_type.startswith('Dictionary<'):
447
+ # For collections, compute hash from elements
448
+ if field_type.endswith('?'):
449
+ hash_fields.append(f"({field_name} != null ? {field_name}.Aggregate(0, (acc, item) => HashCode.Combine(acc, item)) : 0)")
450
+ else:
451
+ hash_fields.append(f"{field_name}.Aggregate(0, (acc, item) => HashCode.Combine(acc, item))")
452
+ else:
453
+ hash_fields.append(field_name)
454
+
455
+ # HashCode.Combine supports up to 8 parameters
456
+ if len(hash_fields) <= 8:
457
+ code += f"{INDENT*2}return HashCode.Combine({', '.join(hash_fields)});\n"
458
+ else:
459
+ # For more than 8 fields, use HashCode.Add
460
+ code += f"{INDENT*2}var hash = new HashCode();\n"
461
+ for field in hash_fields:
462
+ code += f"{INDENT*2}hash.Add({field});\n"
463
+ code += f"{INDENT*2}return hash.ToHashCode();\n"
464
+
465
+ code += f"{INDENT}}}\n"
466
+
467
+ return code
468
+
469
+ def generate_enum(self, avro_schema: Dict, parent_namespace: str, write_file: bool) -> str:
470
+ """ Generates an Enum """
471
+ enum_definition = ''
472
+ namespace = pascal(self.concat_namespace(
473
+ self.base_namespace, avro_schema.get('namespace', parent_namespace)))
474
+ xml_namespace = avro_schema.get('xmlns', None)
475
+ enum_name = pascal(avro_schema['name'])
476
+ ref = 'global::'+self.get_qualified_name(namespace, enum_name)
477
+ if ref in self.generated_types:
478
+ return ref
479
+
480
+ enum_definition += f"/// <summary>\n/// {avro_schema.get('doc', enum_name )}\n/// </summary>\n"
481
+
482
+ # Add XML serialization attribute for the enum if enabled
483
+ if self.system_xml_annotation:
484
+ if xml_namespace:
485
+ enum_definition += f"[XmlType(\"{enum_name}\", Namespace=\"{xml_namespace}\")]\n"
486
+ else:
487
+ enum_definition += f"[XmlType(\"{enum_name}\")]\n"
488
+
489
+ if self.system_xml_annotation:
490
+ symbols_str = [f"{INDENT}/// <summary>\n{INDENT}/// {symbol}\n{INDENT}/// </summary>\n{INDENT}[XmlEnum(Name=\"{symbol}\")]\n{INDENT}{symbol}" for symbol in avro_schema['symbols']]
491
+ else:
492
+ symbols_str = [f"{INDENT}/// <summary>\n{INDENT}/// {symbol}\n{INDENT}/// </summary>\n{INDENT}{symbol}" for symbol in avro_schema['symbols']]
493
+ enum_body = ",\n".join(symbols_str)
494
+ enum_definition += f"public enum {enum_name}\n{{\n{enum_body}\n}}"
495
+
496
+ if write_file:
497
+ self.write_to_file(namespace, enum_name, enum_definition)
498
+ ref = 'global::'+self.get_qualified_name(namespace, enum_name)
499
+ self.generated_types[ref] = "enum"
500
+ self.generated_avro_types[ref] = avro_schema
501
+ return ref
502
+
503
+ def generate_embedded_union(self, class_name: str, field_name: str, avro_type: List, parent_namespace: str, write_file: bool) -> str:
504
+ """ Generates an embedded Union Class """
505
+
506
+ class_definition_ctors = class_definition_decls = class_definition_read = ''
507
+ class_definition_write = class_definition = class_definition_toobject = ''
508
+ class_definition_objctr = class_definition_genericrecordctor = ''
509
+ namespace = pascal(self.concat_namespace(self.base_namespace, parent_namespace))
510
+ list_is_json_match: List [str] = []
511
+ union_class_name = pascal(field_name)+'Union'
512
+ ref = class_name+'.'+union_class_name
513
+
514
+ union_types = [self.convert_avro_type_to_csharp(class_name, field_name+"Option"+str(i), t, parent_namespace) for i,t in enumerate(avro_type)]
515
+ for i, union_type in enumerate(union_types):
516
+ is_dict = is_list = False
517
+ if union_type.startswith("Dictionary<"):
518
+ # get the type information from the dictionary
519
+ is_dict = True
520
+ match = re.findall(r"Dictionary<(.+)\s*,\s*(.+)>", union_type)
521
+ union_type_name = "Map" + pascal(match[0][1].rsplit('.', 1)[-1])
522
+ elif union_type.startswith("List<"):
523
+ # get the type information from the list
524
+ is_list = True
525
+ match = re.findall(r"List<(.+)>", union_type)
526
+ union_type_name = "Array" + pascal(match[0].rsplit('.', 1)[-1])
527
+ elif union_type == "byte[]":
528
+ union_type_name = "bytes"
529
+ else:
530
+ union_type_name = union_type.rsplit('.', 1)[-1]
531
+ if self.is_csharp_reserved_word(union_type_name):
532
+ union_type_name = f"@{union_type_name}"
533
+ proto_member_name = union_type_name[1:] if union_type_name.startswith("@") else union_type_name
534
+ class_definition_objctr += f"{INDENT*3}if (obj is {union_type})\n{INDENT*3}{{\n{INDENT*4}self.{union_type_name} = ({union_type})obj;\n{INDENT*4}return self;\n{INDENT*3}}}\n"
535
+ if union_type in self.generated_types and self.generated_types[union_type] == "class":
536
+ class_definition_genericrecordctor += f"{INDENT*3}if (obj.Schema.Fullname == {union_type}.AvroSchema.Fullname)\n{INDENT*3}{{\n{INDENT*4}this.{union_type_name} = new {union_type}(obj);\n{INDENT*4}return;\n{INDENT*3}}}\n"
537
+ class_definition_ctors += \
538
+ f"{INDENT*2}/// <summary>\n{INDENT*2}/// Constructor for {union_type_name} values\n{INDENT*2}/// </summary>\n" + \
539
+ f"{INDENT*2}public {union_class_name}({union_type}? {union_type_name})\n{INDENT*2}{{\n{INDENT*3}this.{union_type_name} = {union_type_name};\n{INDENT*2}}}\n"
540
+ # Add Key attribute for MessagePack serialization if enabled
541
+ msgpack_key_attr = f"{INDENT*2}[Key({i})]\n" if self.msgpack_annotation else ""
542
+ class_definition_decls += \
543
+ f"{INDENT*2}/// <summary>\n{INDENT*2}/// Gets the {union_type_name} value\n{INDENT*2}/// </summary>\n" + \
544
+ (f"{INDENT*2}[ProtoMember({i+1}, Name=\"{proto_member_name}\")]\n" if self.protobuf_net_annotation else "") + \
545
+ msgpack_key_attr + \
546
+ f"{INDENT*2}public {union_type}? {union_type_name} {{ get; set; }} = null;\n"
547
+ class_definition_toobject += f"{INDENT*3}if ({union_type_name} != null) {{\n{INDENT*4}return {union_type_name};\n{INDENT*3}}}\n"
548
+
549
+ if self.system_text_json_annotation:
550
+ if is_dict:
551
+ class_definition_read += f"{INDENT*3}if (element.ValueKind == JsonValueKind.Object)\n{INDENT*3}{{\n" + \
552
+ f"{INDENT*4}var map = System.Text.Json.JsonSerializer.Deserialize<{union_type}>(element, options);\n" + \
553
+ f"{INDENT*4}if (map != null) {{ return new {union_class_name}(map); }} else {{ throw new NotSupportedException(); }};\n" + \
554
+ f"{INDENT*3}}}\n"
555
+ elif is_list:
556
+ class_definition_read += f"{INDENT*3}if (element.ValueKind == JsonValueKind.Array)\n{INDENT*3}{{\n" + \
557
+ f"{INDENT*4}var map = System.Text.Json.JsonSerializer.Deserialize<{union_type}>(element, options);\n" + \
558
+ f"{INDENT*4}if (map != null) {{ return new {union_class_name}(map); }} else {{ throw new NotSupportedException(); }};\n" + \
559
+ f"{INDENT*3}}}\n"
560
+ elif self.is_csharp_primitive_type(union_type):
561
+ if union_type == "byte[]":
562
+ class_definition_read += f"{INDENT*3}if (element.ValueKind == JsonValueKind.String)\n{INDENT*3}{{\n{INDENT*4}return new {union_class_name}(element.GetBytesFromBase64());\n{INDENT*3}}}\n"
563
+ if union_type == "string":
564
+ class_definition_read += f"{INDENT*3}if (element.ValueKind == JsonValueKind.String)\n{INDENT*3}{{\n{INDENT*4}return new {union_class_name}(element.GetString());\n{INDENT*3}}}\n"
565
+ elif union_type in ['int', 'long', 'float', 'double', 'decimal', 'short', 'sbyte', 'ushort', 'uint', 'ulong']:
566
+ class_definition_read += f"{INDENT*3}if (element.ValueKind == JsonValueKind.Number)\n{INDENT*3}{{\n{INDENT*4}return new {union_class_name}(element.Get{self.map_csharp_primitive_to_clr_type(union_type)}());\n{INDENT*3}}}\n"
567
+ elif union_type == "bool":
568
+ class_definition_read += f"{INDENT*3}if (element.ValueKind == JsonValueKind.True || element.ValueKind == System.Text.Json.JsonValueKind.False)\n{INDENT*2}{{\n{INDENT*3}return new {union_class_name}(element.GetBoolean());\n{INDENT*3}}}\n"
569
+ elif union_type == "DateTime":
570
+ class_definition_read += f"{INDENT*3}if (element.ValueKind == JsonValueKind.String)\n{INDENT*3}{{\n{INDENT*4}return new {union_class_name}(System.DateTime.Parse(element.GetString()));\n{INDENT*3}}}\n"
571
+ elif union_type == "DateTimeOffset":
572
+ class_definition_read += f"{INDENT*3}if (element.ValueKind == JsonValueKind.String)\n{INDENT*3}{{\n{INDENT*4}return new {union_class_name}(System.DateTimeOffset.Parse(element.GetString()));\n{INDENT*3}}}\n"
573
+ else:
574
+ if union_type.startswith("global::"):
575
+ type_kind = self.generated_types[union_type] if union_type in self.generated_types else "class"
576
+ if type_kind == "class":
577
+ class_definition_read += f"{INDENT*3}if ({union_type}.IsJsonMatch(element))\n{INDENT*3}{{\n{INDENT*4}return new {union_class_name}({union_type}.FromData(element, System.Net.Mime.MediaTypeNames.Application.Json));\n{INDENT*3}}}\n"
578
+ elif type_kind == "enum":
579
+ class_definition_read += f"{INDENT*3}if (element.ValueKind == JsonValueKind.String && Enum.TryParse<{union_type}>(element.GetString(), true, out _ ))\n{INDENT*3}{{\n{INDENT*4}return new {union_class_name}(Enum.Parse<{union_type}>(element.GetString()));\n{INDENT*3}}}\n"
580
+ class_definition_write += f"{INDENT*3}{'else ' if i>0 else ''}if (value.{union_type_name} != null)\n{INDENT*3}{{\n{INDENT*4}System.Text.Json.JsonSerializer.Serialize(writer, value.{union_type_name}, options);\n{INDENT*3}}}\n"
581
+ gij = self.get_is_json_match_clause_type("element", class_name, union_type)
582
+ if gij:
583
+ list_is_json_match.append(gij)
584
+
585
+ class_definition = \
586
+ f"/// <summary>\n/// {class_name}. Type union resolver. \n/// </summary>\n" + \
587
+ f"public partial class {class_name}\n{{\n" + \
588
+ f"{INDENT}/// <summary>\n{INDENT}/// Union class for {field_name}\n{INDENT}/// </summary>\n"
589
+ if self.system_xml_annotation:
590
+ class_definition += \
591
+ f"{INDENT}[XmlRoot(\"{union_class_name}\")]\n"
592
+ if self.msgpack_annotation:
593
+ class_definition += \
594
+ f"{INDENT}[MessagePackObject]\n"
595
+ if self.system_text_json_annotation:
596
+ class_definition += \
597
+ f"{INDENT}[System.Text.Json.Serialization.JsonConverter(typeof({union_class_name}))]\n"
598
+ if self.protobuf_net_annotation:
599
+ class_definition += f"{INDENT}[ProtoContract]\n"
600
+ class_definition += \
601
+ f"{INDENT}public sealed class {union_class_name}"
602
+ if self.system_text_json_annotation:
603
+ class_definition += f": System.Text.Json.Serialization.JsonConverter<global::{namespace}.{class_name}.{union_class_name}>"
604
+ class_definition += f"\n{INDENT}{{\n" + \
605
+ f"{INDENT*2}/// <summary>\n{INDENT*2}/// Default constructor\n{INDENT*2}/// </summary>\n" + \
606
+ f"{INDENT*2}public {union_class_name}() {{ }}\n"
607
+ class_definition += class_definition_ctors
608
+ if self.avro_annotation:
609
+ class_definition += \
610
+ f"{INDENT*2}/// <summary>\n{INDENT*2}/// Constructor for Avro decoder\n{INDENT*2}/// </summary>\n" + \
611
+ f"{INDENT*2}internal static {union_class_name} FromObject(object obj)\n{INDENT*2}{{\n"
612
+ if class_definition_genericrecordctor:
613
+ class_definition += \
614
+ f"{INDENT*3}if (obj is global::Avro.Generic.GenericRecord)\n{INDENT*3}{{\n" + \
615
+ f"{INDENT*4}return new {union_class_name}((global::Avro.Generic.GenericRecord)obj);\n" + \
616
+ f"{INDENT*3}}}\n"
617
+ class_definition += \
618
+ f"{INDENT*3}var self = new {union_class_name}();\n" + \
619
+ class_definition_objctr + \
620
+ f"{INDENT*3}throw new NotSupportedException(\"No record type matched the type\");\n" + \
621
+ f"{INDENT*2}}}\n"
622
+ if class_definition_genericrecordctor:
623
+ class_definition += f"\n{INDENT*2}/// <summary>\n{INDENT*2}/// Constructor from Avro GenericRecord\n{INDENT*2}/// </summary>\n" + \
624
+ f"{INDENT*2}public {union_class_name}(global::Avro.Generic.GenericRecord obj)\n{INDENT*2}{{\n" + \
625
+ class_definition_genericrecordctor + \
626
+ f"{INDENT*3}throw new NotSupportedException(\"No record type matched the type\");\n" + \
627
+ f"{INDENT*2}}}\n"
628
+ class_definition += \
629
+ class_definition_decls + \
630
+ f"\n{INDENT*2}/// <summary>\n{INDENT*2}/// Yields the current value of the union\n{INDENT*2}/// </summary>\n" + \
631
+ f"\n{INDENT*2}public Object ToObject()\n{INDENT*2}{{\n" + \
632
+ class_definition_toobject+ \
633
+ f"{INDENT*3}throw new NotSupportedException(\"No record type is set in the union\");\n" + \
634
+ f"{INDENT*2}}}\n"
635
+ if self.system_text_json_annotation:
636
+ class_definition += \
637
+ f"\n{INDENT*2}/// <summary>\n{INDENT*2}/// Reads the JSON representation of the object.\n{INDENT*2}/// </summary>\n" + \
638
+ f"{INDENT*2}public override {union_class_name}? Read(ref Utf8JsonReader reader, System.Type typeToConvert, JsonSerializerOptions options)\n{INDENT*2}{{\n{INDENT*3}var element = JsonElement.ParseValue(ref reader);\n" + \
639
+ class_definition_read + \
640
+ f"{INDENT*3}throw new NotSupportedException(\"No record type matched the JSON data\");\n{INDENT*2}}}\n" + \
641
+ f"\n{INDENT*2}/// <summary>\n{INDENT*2}/// Writes the JSON representation of the object.\n{INDENT*2}/// </summary>\n" + \
642
+ f"{INDENT*2}public override void Write(Utf8JsonWriter writer, {union_class_name} value, JsonSerializerOptions options)\n{INDENT*2}{{\n" + \
643
+ class_definition_write + \
644
+ f"{INDENT*3}else\n{INDENT*3}{{\n{INDENT*4}throw new NotSupportedException(\"No record type is set in the union\");\n{INDENT*3}}}\n{INDENT*2}}}\n" + \
645
+ f"\n{INDENT*2}/// <summary>\n{INDENT*2}/// Checks if the JSON element matches the schema\n{INDENT*2}/// </summary>\n" + \
646
+ f"{INDENT*2}public static bool IsJsonMatch(System.Text.Json.JsonElement element)\n{INDENT*2}{{" + \
647
+ f"\n{INDENT*3}return "+f"\n{INDENT*3} || ".join(list_is_json_match)+f";\n{INDENT*2}}}\n"
648
+
649
+ # Generate Equals and GetHashCode for the union class
650
+ # Build list of property names for comparison
651
+ union_property_names = []
652
+ for union_type in union_types:
653
+ if union_type.startswith("Dictionary<"):
654
+ match = re.findall(r"Dictionary<(.+)\s*,\s*(.+)>", union_type)
655
+ union_property_names.append("Map" + pascal(match[0][1].rsplit('.', 1)[-1]))
656
+ elif union_type.startswith("List<"):
657
+ match = re.findall(r"List<(.+)>", union_type)
658
+ union_property_names.append("Array" + pascal(match[0].rsplit('.', 1)[-1]))
659
+ elif union_type == "byte[]":
660
+ union_property_names.append("bytes")
661
+ else:
662
+ prop_name = union_type.rsplit('.', 1)[-1]
663
+ if self.is_csharp_reserved_word(prop_name):
664
+ prop_name = f"@{prop_name}"
665
+ union_property_names.append(prop_name)
666
+
667
+ equals_conditions = " && ".join([f"Equals({name}, other.{name})" for name in union_property_names])
668
+
669
+ # HashCode.Combine only accepts up to 8 arguments, so we need to chain calls for larger unions
670
+ def generate_hashcode_expression(props: list) -> str:
671
+ if len(props) <= 8:
672
+ return f"HashCode.Combine({', '.join(props)})"
673
+ else:
674
+ # Chain HashCode.Combine calls: Combine(Combine(first 7...), next 7...)
675
+ first_batch = props[:7]
676
+ remaining = props[7:]
677
+ inner = generate_hashcode_expression(remaining)
678
+ return f"HashCode.Combine({', '.join(first_batch)}, {inner})"
679
+
680
+ hashcode_expression = generate_hashcode_expression(union_property_names)
681
+
682
+ class_definition += \
683
+ f"\n{INDENT*2}/// <summary>\n{INDENT*2}/// Determines whether the specified object is equal to the current object.\n{INDENT*2}/// </summary>\n" + \
684
+ f"{INDENT*2}public override bool Equals(object? obj)\n{INDENT*2}{{\n" + \
685
+ f"{INDENT*3}if (obj is not {union_class_name} other) return false;\n" + \
686
+ f"{INDENT*3}return {equals_conditions};\n" + \
687
+ f"{INDENT*2}}}\n" + \
688
+ f"\n{INDENT*2}/// <summary>\n{INDENT*2}/// Serves as the default hash function.\n{INDENT*2}/// </summary>\n" + \
689
+ f"{INDENT*2}public override int GetHashCode()\n{INDENT*2}{{\n" + \
690
+ f"{INDENT*3}return {hashcode_expression};\n" + \
691
+ f"{INDENT*2}}}\n"
692
+
693
+ class_definition += f"{INDENT}}}\n}}"
694
+
695
+ if write_file:
696
+ self.write_to_file(namespace, class_name +"."+union_class_name, class_definition)
697
+
698
+ self.generated_types[ref] = "union" # it doesn't matter if the names clash, we just need to know whether it's a union
699
+ return ref
700
+
701
+ def find_type(self, kind: str, avro_schema: JsonNode, type_name: str, type_namespace: str, parent_namespace = '') -> JsonNode:
702
+ """ recursively find the type (kind 'record' or 'enum') in the schema """
703
+ if isinstance(avro_schema, list):
704
+ for s in avro_schema:
705
+ found = self.find_type(kind, s, type_name, type_namespace, parent_namespace)
706
+ if found:
707
+ return found
708
+ elif isinstance(avro_schema, dict):
709
+ if avro_schema.get('type') == kind and avro_schema.get('name') == type_name and avro_schema.get('namespace', parent_namespace) == type_namespace:
710
+ return avro_schema
711
+ parent_namespace = avro_schema.get('namespace', parent_namespace)
712
+ if 'fields' in avro_schema and isinstance(avro_schema['fields'], list):
713
+ for field in avro_schema['fields']:
714
+ if isinstance(field, dict) and 'type' in field:
715
+ # Recursively search within field types (including union arrays)
716
+ found = self.find_type(kind, field['type'], type_name, type_namespace, parent_namespace)
717
+ if found:
718
+ return found
719
+ return None
720
+
721
+ def is_enum_type(self, avro_type: Union[str, Dict, List]) -> bool:
722
+ """ Checks if a type is an enum (including nullable enums) """
723
+ if isinstance(avro_type, str):
724
+ if avro_type in ('null', 'boolean', 'int', 'long', 'float', 'double', 'bytes', 'string'):
725
+ return False
726
+ schema = self.schema_doc
727
+ name = avro_type.split('.')[-1]
728
+ namespace = ".".join(avro_type.split('.')[:-1])
729
+ return self.find_type('enum', schema, name, namespace) is not None
730
+ elif isinstance(avro_type, list):
731
+ # Check for nullable enum: ["null", <enum-type>] or [<enum-type>, "null"]
732
+ non_null_types = [t for t in avro_type if t != 'null']
733
+ if len(non_null_types) == 1:
734
+ return self.is_enum_type(non_null_types[0])
735
+ return False
736
+ elif isinstance(avro_type, dict):
737
+ return avro_type.get('type') == 'enum'
738
+ return False
739
+
740
+ def generate_property(self, field_index: int, field: Dict, class_name: str, parent_namespace: str) -> str:
741
+ """ Generates a property """
742
+ is_enum_type = self.is_enum_type(field['type'])
743
+ field_type = self.convert_avro_type_to_csharp(
744
+ class_name, field['name'], field['type'], parent_namespace)
745
+ field_default = field.get('const', field.get('default', None))
746
+ annotation_name = field_name = field['name']
747
+ if self.is_csharp_reserved_word(field_name):
748
+ field_name = f"@{field_name}"
749
+ if self.pascal_properties:
750
+ field_name = pascal(field_name)
751
+ if field_name == class_name:
752
+ field_name += "_"
753
+ prop = ''
754
+ prop += f"{INDENT}/// <summary>\n{INDENT}/// { field.get('doc', field_name) }\n{INDENT}/// </summary>\n"
755
+
756
+ if self.protobuf_net_annotation:
757
+ prop += f"{INDENT}[ProtoMember({field_index}, Name=\"{annotation_name}\")]\n"
758
+
759
+ # Add XML serialization attribute if enabled
760
+ if self.system_xml_annotation:
761
+ xmlkind = field.get('xmlkind', 'element')
762
+ if xmlkind == 'element':
763
+ prop += f"{INDENT}[XmlElement(\"{annotation_name}\")]\n"
764
+ elif xmlkind == 'attribute':
765
+ prop += f"{INDENT}[XmlAttribute(\"{annotation_name}\")]\n"
766
+
767
+ # Add MessagePack serialization attribute if enabled
768
+ if self.msgpack_annotation:
769
+ prop += f"{INDENT}[Key({field_index})]\n"
770
+
771
+ if self.system_text_json_annotation:
772
+ prop += f"{INDENT}[System.Text.Json.Serialization.JsonPropertyName(\"{annotation_name}\")]\n"
773
+ if is_enum_type:
774
+ prop += f"{INDENT}[System.Text.Json.Serialization.JsonConverter(typeof(JsonStringEnumConverter))]\n"
775
+ if field_type.endswith("Union") and not field_type.startswith("global::"):
776
+ prop += f"{INDENT}[System.Text.Json.Serialization.JsonConverter(typeof({field_type}))]\n"
777
+ if self.newtonsoft_json_annotation:
778
+ prop += f"{INDENT}[Newtonsoft.Json.JsonProperty(\"{annotation_name}\")]\n"
779
+ if is_enum_type:
780
+ prop += f"{INDENT}[Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]\n"
781
+
782
+ # Determine initialization value
783
+ initialization = ""
784
+ if field_default is not None:
785
+ # Has explicit default value
786
+ if is_enum_type:
787
+ # For enum types, use qualified enum value (e.g., Type.Circle)
788
+ # Get the base enum type name (strip nullable ? suffix if present)
789
+ enum_type = field_type.rstrip('?')
790
+ initialization = f" = {enum_type}.{field_default};"
791
+ elif isinstance(field_default, str):
792
+ initialization = f" = \"{field_default}\";"
793
+ else:
794
+ initialization = f" = {field_default};"
795
+ elif field_type == "string":
796
+ # Non-nullable string without default should be initialized to empty string
797
+ initialization = " = string.Empty;"
798
+ elif not field_type.endswith("?") and field_type.startswith("List<"):
799
+ # Non-nullable List should be initialized to empty list
800
+ initialization = " = new();"
801
+ elif not field_type.endswith("?") and field_type.startswith("Dictionary<"):
802
+ # Non-nullable Dictionary should be initialized to empty dictionary
803
+ initialization = " = new();"
804
+ elif not field_type.endswith("?") and field_type.startswith("global::") and not self.is_csharp_primitive_type(field_type):
805
+ # Non-nullable custom reference types should be initialized with new instance
806
+ initialization = " = new();"
807
+
808
+ prop += f"{INDENT}public {field_type} {field_name} {{ get; set; }}{initialization}"
809
+ return prop
810
+
811
+ def write_to_file(self, namespace: str, name: str, definition: str):
812
+ """ Writes the class or enum to a file """
813
+ directory_path = os.path.join(
814
+ self.output_dir, os.path.join('src', namespace.replace('.', os.sep)))
815
+ if not os.path.exists(directory_path):
816
+ os.makedirs(directory_path, exist_ok=True)
817
+ file_path = os.path.join(directory_path, f"{name}.cs")
818
+
819
+ with open(file_path, 'w', encoding='utf-8') as file:
820
+ # Common using statements (add more as needed)
821
+ file_content = "using System;\nusing System.Collections.Generic;\n"
822
+ file_content += "using System.Linq;\n"
823
+ if self.protobuf_net_annotation:
824
+ file_content += "using ProtoBuf;\n"
825
+ if self.system_text_json_annotation:
826
+ file_content += "using System.Text.Json;\n"
827
+ file_content += "using System.Text.Json.Serialization;\n"
828
+ if self.newtonsoft_json_annotation:
829
+ file_content += "using Newtonsoft.Json;\n"
830
+ if self.system_xml_annotation: # Add XML serialization using directive
831
+ file_content += "using System.Xml.Serialization;\n"
832
+ if self.msgpack_annotation: # Add MessagePack serialization using directive
833
+ file_content += "using MessagePack;\n"
834
+
835
+ if namespace:
836
+ # Namespace declaration with correct indentation for the definition
837
+ file_content += f"\nnamespace {namespace}\n{{\n"
838
+ indented_definition = '\n'.join(
839
+ [f"{INDENT}{line}" for line in definition.split('\n')])
840
+ file_content += f"{indented_definition}\n}}"
841
+ else:
842
+ file_content += definition
843
+ file.write(file_content)
844
+
845
+ def generate_tests(self, output_dir: str) -> None:
846
+ """ Generates unit tests for all the generated C# classes and enums """
847
+ test_directory_path = os.path.join(output_dir, "test")
848
+ if not os.path.exists(test_directory_path):
849
+ os.makedirs(test_directory_path, exist_ok=True)
850
+
851
+ for class_name, type_kind in self.generated_types.items():
852
+ if type_kind in ["class", "enum"]:
853
+ self.generate_test_class(class_name, type_kind, test_directory_path)
854
+
855
+ def generate_test_class(self, class_name: str, type_kind: str, test_directory_path: str) -> None:
856
+ """ Generates a unit test class for a given C# class or enum """
857
+ avro_schema:Dict[str,JsonNode] = cast(Dict[str,JsonNode], self.generated_avro_types.get(class_name, {}))
858
+ if class_name.startswith("global::"):
859
+ class_name = class_name[8:]
860
+ test_class_name = f"{class_name.split('.')[-1]}Tests"
861
+ namespace = ".".join(class_name.split('.')[:-1])
862
+ class_base_name = class_name.split('.')[-1]
863
+
864
+ if type_kind == "class":
865
+ fields = self.get_class_test_fields(avro_schema, class_base_name)
866
+ test_class_definition = process_template(
867
+ "avrotocsharp/class_test.cs.jinja",
868
+ namespace=namespace,
869
+ test_class_name=test_class_name,
870
+ class_base_name=class_base_name,
871
+ fields=fields,
872
+ avro_annotation=self.avro_annotation,
873
+ system_xml_annotation=self.system_xml_annotation,
874
+ system_text_json_annotation=self.system_text_json_annotation,
875
+ newtonsoft_json_annotation=self.newtonsoft_json_annotation,
876
+ protobuf_net_annotation=self.protobuf_net_annotation,
877
+ msgpack_annotation=self.msgpack_annotation
878
+ )
879
+ elif type_kind == "enum":
880
+ test_class_definition = process_template(
881
+ "avrotocsharp/enum_test.cs.jinja",
882
+ namespace=namespace,
883
+ test_class_name=test_class_name,
884
+ enum_base_name=class_base_name,
885
+ symbols=avro_schema.get('symbols', []),
886
+ avro_annotation=self.avro_annotation,
887
+ system_xml_annotation=self.system_xml_annotation,
888
+ system_text_json_annotation=self.system_text_json_annotation,
889
+ newtonsoft_json_annotation=self.newtonsoft_json_annotation,
890
+ protobuf_net_annotation=self.protobuf_net_annotation
891
+ )
892
+
893
+ test_file_path = os.path.join(test_directory_path, f"{test_class_name}.cs")
894
+ with open(test_file_path, 'w', encoding='utf-8') as test_file:
895
+ test_file.write(test_class_definition)
896
+
897
+ def get_class_test_fields(self, avro_schema: Dict[str,JsonNode], class_name: str) -> List[Any]:
898
+ """ Retrieves fields for a given class name """
899
+
900
+ class Field:
901
+ def __init__(self, fn: str, ft:str, tv:Any, ct: bool, pm: bool, ie: bool, iu: bool):
902
+ self.field_name = fn
903
+ self.field_type = ft
904
+ self.test_value = tv
905
+ self.is_const = ct
906
+ self.is_primitive = pm
907
+ self.is_enum = ie
908
+ self.is_union = iu
909
+
910
+ fields: List[Field] = []
911
+ if avro_schema and 'fields' in avro_schema:
912
+ for field in cast(List[Dict[str,JsonNode]],avro_schema['fields']):
913
+ field_name = str(field['name'])
914
+ if self.pascal_properties:
915
+ field_name = pascal(field_name)
916
+ if field_name == class_name:
917
+ field_name += "_"
918
+ if self.is_csharp_reserved_word(field_name):
919
+ field_name = f"@{field_name}"
920
+ field_type = self.convert_avro_type_to_csharp(class_name, field_name, field['type'], str(avro_schema.get('namespace', '')))
921
+ is_class = field_type in self.generated_types and self.generated_types[field_type] == "class"
922
+ is_enum = self.is_enum_type(field['type'])
923
+ is_union = field_type in self.generated_types and self.generated_types[field_type] == "union"
924
+ test_value = self.get_test_value(field_type, field['type'], str(avro_schema.get('namespace', ''))) if not "const" in field else '\"'+str(field["const"])+'\"'
925
+ f = Field(field_name,
926
+ field_type,
927
+ test_value,
928
+ "const" in field and field["const"] is not None,
929
+ not is_class,
930
+ is_enum,
931
+ is_union)
932
+ fields.append(f)
933
+ return cast(List[Any], fields)
934
+
935
+ def get_test_value(self, csharp_type: str, avro_type: JsonNode = None, parent_namespace: str = '') -> str:
936
+ """Returns a default test value based on the Avro type"""
937
+ # For nullable object types, return typed null to avoid var issues
938
+ if csharp_type == "object?":
939
+ return "(object?)null"
940
+
941
+ test_values = {
942
+ 'string': '"test_string"',
943
+ 'bool': 'true',
944
+ 'int': '42',
945
+ 'long': '42L',
946
+ 'float': '3.14f',
947
+ 'double': '3.14',
948
+ 'decimal': '3.14d',
949
+ 'byte[]': 'new byte[] { 0x01, 0x02, 0x03 }',
950
+ 'null': 'null',
951
+ 'Date': 'new Date()',
952
+ 'DateTime': 'DateTime.UtcNow()',
953
+ 'Guid': 'Guid.NewGuid()'
954
+ }
955
+ if csharp_type.endswith('?'):
956
+ csharp_type = csharp_type[:-1]
957
+
958
+ # Check if this is a union type (either by generated_types lookup or by type structure)
959
+ if csharp_type in self.generated_types and self.generated_types[csharp_type] == "union":
960
+ # For union types, we need to initialize with one of the valid options
961
+ # Find the first non-null type in the union and create a value for it
962
+ if isinstance(avro_type, list):
963
+ non_null_types = [t for t in avro_type if t != 'null']
964
+ if non_null_types:
965
+ first_option = non_null_types[0]
966
+ first_option_csharp = self.convert_avro_type_to_csharp('', 'Option0', first_option, parent_namespace)
967
+ first_option_value = self.get_test_value(first_option_csharp, first_option, parent_namespace)
968
+ return f'new {csharp_type}({first_option_value})'
969
+
970
+ return test_values.get(csharp_type, f'new {csharp_type}()')
971
+
972
+ def convert_schema(self, schema: JsonNode, output_dir: str):
973
+ """ Converts Avro schema to C# """
974
+ if not isinstance(schema, list):
975
+ schema = [schema]
976
+
977
+ # Determine project name: use explicit project_name if set, otherwise derive from base_namespace
978
+ if self.project_name and self.project_name.strip():
979
+ # Use explicitly set project name
980
+ project_name = self.project_name
981
+ else:
982
+ # Fall back to using base_namespace as project name
983
+ project_name = self.base_namespace
984
+ if not project_name or project_name.strip() == '':
985
+ # Derive from output directory name as fallback
986
+ project_name = os.path.basename(os.path.abspath(output_dir))
987
+ if not project_name or project_name.strip() == '':
988
+ project_name = 'Generated'
989
+ # Clean up the project name
990
+ project_name = project_name.replace('-', '_').replace(' ', '_')
991
+ # Update base_namespace to match (only if it was empty)
992
+ self.base_namespace = project_name
993
+ import warnings
994
+ warnings.warn(f"No namespace provided, using '{project_name}' derived from output directory", UserWarning)
995
+
996
+ self.schema_doc = schema
997
+ self.type_dict = build_flat_type_dict(self.schema_doc)
998
+ if not os.path.exists(output_dir):
999
+ os.makedirs(output_dir, exist_ok=True)
1000
+ if not glob.glob(os.path.join(output_dir, "src", "*.sln")):
1001
+ sln_file = os.path.join(
1002
+ output_dir, f"{project_name}.sln")
1003
+ if not os.path.exists(sln_file):
1004
+ if not os.path.exists(os.path.dirname(sln_file)):
1005
+ os.makedirs(os.path.dirname(sln_file))
1006
+ with open(sln_file, 'w', encoding='utf-8') as file:
1007
+ file.write(process_template(
1008
+ "avrotocsharp/project.sln.jinja",
1009
+ project_name=project_name,
1010
+ uuid=lambda:str(uuid.uuid4()),
1011
+ avro_annotation=self.avro_annotation,
1012
+ system_xml_annotation=self.system_xml_annotation,
1013
+ system_text_json_annotation=self.system_text_json_annotation,
1014
+ newtonsoft_json_annotation=self.newtonsoft_json_annotation))
1015
+ if not glob.glob(os.path.join(output_dir, "src", "*.csproj")):
1016
+ csproj_file = os.path.join(
1017
+ output_dir, "src", f"{pascal(project_name)}.csproj")
1018
+ if not os.path.exists(csproj_file):
1019
+ if not os.path.exists(os.path.dirname(csproj_file)):
1020
+ os.makedirs(os.path.dirname(csproj_file))
1021
+ with open(csproj_file, 'w', encoding='utf-8') as file:
1022
+ file.write(process_template(
1023
+ "avrotocsharp/project.csproj.jinja",
1024
+ project_name=project_name,
1025
+ avro_annotation=self.avro_annotation,
1026
+ system_xml_annotation=self.system_xml_annotation,
1027
+ system_text_json_annotation=self.system_text_json_annotation,
1028
+ newtonsoft_json_annotation=self.newtonsoft_json_annotation,
1029
+ protobuf_net_annotation=self.protobuf_net_annotation,
1030
+ msgpack_annotation=self.msgpack_annotation,
1031
+ CSHARP_AVRO_VERSION=CSHARP_AVRO_VERSION,
1032
+ NEWTONSOFT_JSON_VERSION=NEWTONSOFT_JSON_VERSION,
1033
+ SYSTEM_TEXT_JSON_VERSION=SYSTEM_TEXT_JSON_VERSION,
1034
+ SYSTEM_MEMORY_DATA_VERSION=SYSTEM_MEMORY_DATA_VERSION,
1035
+ PROTOBUF_NET_VERSION=PROTOBUF_NET_VERSION,
1036
+ MSGPACK_VERSION=MSGPACK_VERSION,
1037
+ NUNIT_VERSION=NUNIT_VERSION,
1038
+ NUNIT_ADAPTER_VERSION=NUNIT_ADAPTER_VERSION,
1039
+ MSTEST_SDK_VERSION=MSTEST_SDK_VERSION))
1040
+ if not glob.glob(os.path.join(output_dir, "test", "*.csproj")):
1041
+ csproj_test_file = os.path.join(
1042
+ output_dir, "test", f"{pascal(project_name)}.Test.csproj")
1043
+ if not os.path.exists(csproj_test_file):
1044
+ if not os.path.exists(os.path.dirname(csproj_test_file)):
1045
+ os.makedirs(os.path.dirname(csproj_test_file))
1046
+ with open(csproj_test_file, 'w', encoding='utf-8') as file:
1047
+ file.write(process_template(
1048
+ "avrotocsharp/testproject.csproj.jinja",
1049
+ project_name=project_name,
1050
+ avro_annotation=self.avro_annotation,
1051
+ system_xml_annotation=self.system_xml_annotation,
1052
+ system_text_json_annotation=self.system_text_json_annotation,
1053
+ newtonsoft_json_annotation=self.newtonsoft_json_annotation,
1054
+ protobuf_net_annotation=self.protobuf_net_annotation,
1055
+ NUNIT_VERSION=NUNIT_VERSION,
1056
+ NUNIT_ADAPTER_VERSION=NUNIT_ADAPTER_VERSION,
1057
+ MSTEST_SDK_VERSION=MSTEST_SDK_VERSION,
1058
+ COVERLET_VERSION=COVERLET_VERSION))
1059
+
1060
+ # Generate coverage scripts
1061
+ if not os.path.exists(os.path.join(output_dir, "run_coverage.ps1")):
1062
+ coverage_ps1_file = os.path.join(output_dir, "run_coverage.ps1")
1063
+ with open(coverage_ps1_file, 'w', encoding='utf-8') as file:
1064
+ file.write(process_template(
1065
+ "avrotocsharp/run_coverage.ps1.jinja",
1066
+ project_name=project_name))
1067
+
1068
+ if not os.path.exists(os.path.join(output_dir, "run_coverage.sh")):
1069
+ coverage_sh_file = os.path.join(output_dir, "run_coverage.sh")
1070
+ with open(coverage_sh_file, 'w', encoding='utf-8') as file:
1071
+ file.write(process_template(
1072
+ "avrotocsharp/run_coverage.sh.jinja",
1073
+ project_name=project_name))
1074
+ # Make the shell script executable on Unix-like systems
1075
+ try:
1076
+ os.chmod(coverage_sh_file, 0o755)
1077
+ except:
1078
+ pass # Ignore on Windows
1079
+
1080
+ # Generate README with coverage documentation
1081
+ if not os.path.exists(os.path.join(output_dir, "README.md")):
1082
+ readme_file = os.path.join(output_dir, "README.md")
1083
+ with open(readme_file, 'w', encoding='utf-8') as file:
1084
+ file.write(process_template(
1085
+ "avrotocsharp/README.md.jinja",
1086
+ project_name=project_name))
1087
+
1088
+ self.output_dir = output_dir
1089
+ for avro_schema in (avs for avs in schema if isinstance(avs, dict)):
1090
+ self.generate_class_or_enum(avro_schema, '')
1091
+ self.generate_tests(output_dir)
1092
+
1093
+ def convert(self, avro_schema_path: str, output_dir: str):
1094
+ """ Converts Avro schema to C# """
1095
+ with open(avro_schema_path, 'r', encoding='utf-8') as file:
1096
+ schema = json.load(file)
1097
+ self.convert_schema(schema, output_dir)
1098
+
1099
+
1100
+ def convert_avro_to_csharp(
1101
+ avro_schema_path,
1102
+ cs_file_path,
1103
+ base_namespace='',
1104
+ project_name='',
1105
+ pascal_properties=False,
1106
+ system_text_json_annotation=False,
1107
+ newtonsoft_json_annotation=False,
1108
+ system_xml_annotation=False,
1109
+ msgpack_annotation=False,
1110
+ avro_annotation=False,
1111
+ protobuf_net_annotation=False
1112
+ ):
1113
+ """Converts Avro schema to C# classes
1114
+
1115
+ Args:
1116
+ avro_schema_path (str): Avro input schema path
1117
+ cs_file_path (str): Output C# file path
1118
+ base_namespace (str, optional): Base namespace. Defaults to ''.
1119
+ project_name (str, optional): Explicit project name for .csproj files (separate from namespace). Defaults to ''.
1120
+ pascal_properties (bool, optional): Pascal case properties. Defaults to False.
1121
+ system_text_json_annotation (bool, optional): Use System.Text.Json annotations. Defaults to False.
1122
+ newtonsoft_json_annotation (bool, optional): Use Newtonsoft.Json annotations. Defaults to False.
1123
+ system_xml_annotation (bool, optional): Use System.Xml.Serialization annotations. Defaults to False.
1124
+ msgpack_annotation (bool, optional): Use MessagePack annotations. Defaults to False.
1125
+ avro_annotation (bool, optional): Use Avro annotations. Defaults to False.
1126
+ protobuf_net_annotation (bool, optional): Use protobuf-net annotations. Defaults to False.
1127
+ """
1128
+
1129
+ if not base_namespace:
1130
+ base_namespace = os.path.splitext(os.path.basename(cs_file_path))[0].replace('-', '_')
1131
+ avrotocs = AvroToCSharp(base_namespace)
1132
+ avrotocs.project_name = project_name
1133
+ avrotocs.pascal_properties = pascal_properties
1134
+ avrotocs.system_text_json_annotation = system_text_json_annotation
1135
+ avrotocs.newtonsoft_json_annotation = newtonsoft_json_annotation
1136
+ avrotocs.system_xml_annotation = system_xml_annotation
1137
+ avrotocs.msgpack_annotation = msgpack_annotation
1138
+ avrotocs.avro_annotation = avro_annotation
1139
+ avrotocs.protobuf_net_annotation = protobuf_net_annotation
1140
+ avrotocs.convert(avro_schema_path, cs_file_path)
1141
+
1142
+
1143
+ def convert_avro_schema_to_csharp(
1144
+ avro_schema: JsonNode,
1145
+ output_dir: str,
1146
+ base_namespace: str = '',
1147
+ project_name: str = '',
1148
+ pascal_properties: bool = False,
1149
+ system_text_json_annotation: bool = False,
1150
+ newtonsoft_json_annotation: bool = False,
1151
+ system_xml_annotation: bool = False,
1152
+ msgpack_annotation: bool = False,
1153
+ avro_annotation: bool = False,
1154
+ protobuf_net_annotation: bool = False
1155
+ ):
1156
+ """Converts Avro schema to C# classes
1157
+
1158
+ Args:
1159
+ avro_schema (JsonNode): Avro schema to convert
1160
+ output_dir (str): Output directory
1161
+ base_namespace (str, optional): Base namespace for the generated classes. Defaults to ''.
1162
+ project_name (str, optional): Explicit project name for .csproj files (separate from namespace). Defaults to ''.
1163
+ pascal_properties (bool, optional): Pascal case properties. Defaults to False.
1164
+ system_text_json_annotation (bool, optional): Use System.Text.Json annotations. Defaults to False.
1165
+ newtonsoft_json_annotation (bool, optional): Use Newtonsoft.Json annotations. Defaults to False.
1166
+ system_xml_annotation (bool, optional): Use System.Xml.Serialization annotations. Defaults to False.
1167
+ msgpack_annotation (bool, optional): Use MessagePack annotations. Defaults to False.
1168
+ avro_annotation (bool, optional): Use Avro annotations. Defaults to False.
1169
+ protobuf_net_annotation (bool, optional): Use protobuf-net annotations. Defaults to False.
1170
+ """
1171
+ avrotocs = AvroToCSharp(base_namespace)
1172
+ avrotocs.project_name = project_name
1173
+ avrotocs.pascal_properties = pascal_properties
1174
+ avrotocs.system_text_json_annotation = system_text_json_annotation
1175
+ avrotocs.newtonsoft_json_annotation = newtonsoft_json_annotation
1176
+ avrotocs.system_xml_annotation = system_xml_annotation
1177
+ avrotocs.msgpack_annotation = msgpack_annotation
1178
+ avrotocs.avro_annotation = avro_annotation
1179
+ avrotocs.protobuf_net_annotation = protobuf_net_annotation
1180
+ avrotocs.convert_schema(avro_schema, output_dir)