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