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