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