diff --git a/avrotize/avrotize.py b/avrotize/avrotize.py index 1921d55..22e75d5 100644 --- a/avrotize/avrotize.py +++ b/avrotize/avrotize.py @@ -37,9 +37,18 @@ def create_subparsers(subparsers, commands): if arg['type'] == 'bool': kwargs['action'] = 'store_true' del kwargs['type'] - carg = cmd_parser.add_argument(arg['name'], **kwargs) + argname = arg['name'] + if '_' in argname: + argname2 = argname.replace('_', '-') + carg = cmd_parser.add_argument(argname, argname2, **kwargs) + else: + carg = cmd_parser.add_argument(arg['name'], **kwargs) else: - carg = cmd_parser.add_argument(arg['name'], **kwargs) + if '_' in arg['name']: + argname2 = arg['name'].replace('_', '-') + carg = cmd_parser.add_argument(arg['name'], argname2, **kwargs) + else: + carg = cmd_parser.add_argument(arg['name'], **kwargs) carg.required = arg.get('required', True) def dynamic_import(module, func): diff --git a/avrotize/avrotocsharp.py b/avrotize/avrotocsharp.py index ed0fe8d..6cf5ae3 100644 --- a/avrotize/avrotocsharp.py +++ b/avrotize/avrotocsharp.py @@ -8,7 +8,7 @@ from typing import Any, Dict, List, Tuple, Union, cast import uuid -from avrotize.common import is_generic_avro_type, pascal, process_template +from avrotize.common import build_flat_type_dict, inline_avro_references, is_generic_avro_type, pascal, process_template import glob JsonNode = Dict[str, 'JsonNode'] | List['JsonNode'] | str | None @@ -38,9 +38,11 @@ def __init__(self, base_namespace: str = '') -> None: self.pascal_properties = False self.system_text_json_annotation = False self.newtonsoft_json_annotation = False + self.system_xml_annotation = False self.avro_annotation = False self.generated_types: Dict[str,str] = {} self.generated_avro_types: Dict[str, Dict[str, Union[str, Dict, List]]] = {} + self.type_dict = {} def get_qualified_name(self, namespace: str, name: str) -> str: """ Concatenates namespace and name with a dot separator """ @@ -67,7 +69,12 @@ def map_primitive_to_csharp(self, avro_type: str) -> str: 'bytes': 'byte[]', 'string': 'string', } - return mapping.get(avro_type, 'object') + qualified_class_name = 'global::'+self.get_qualified_name(pascal(self.base_namespace), pascal(avro_type)) + if qualified_class_name in self.generated_avro_types: + result = qualified_class_name + else: + result = mapping.get(avro_type, 'object') + return result def is_csharp_reserved_word(self, word: str) -> bool: """ Checks if a word is a reserved C# keyword """ @@ -144,6 +151,7 @@ def generate_class(self, avro_schema: Dict, parent_namespace: str, write_file: b avro_namespace = avro_schema.get('namespace', parent_namespace) if not 'namespace' in avro_schema: avro_schema['namespace'] = parent_namespace + xml_namespace = avro_schema.get('xmlns', None) namespace = pascal(self.concat_namespace(self.base_namespace, avro_namespace)) class_name = pascal(avro_schema['name']) ref = 'global::'+self.get_qualified_name(namespace, class_name) @@ -151,6 +159,14 @@ def generate_class(self, avro_schema: Dict, parent_namespace: str, write_file: b return ref class_definition += f"/// \n/// { avro_schema.get('doc', class_name ) }\n/// \n" + + # Add XML serialization attribute for the class if enabled + if self.system_xml_annotation: + if xml_namespace: + class_definition += f"[XmlRoot(\"{class_name}\", Namespace=\"{xml_namespace}\")]\n" + else: + class_definition += f"[XmlRoot(\"{class_name}\")]\n" + fields_str = [self.generate_property(field, class_name, avro_namespace) for field in avro_schema.get('fields', [])] class_body = "\n".join(fields_str) class_definition += f"public partial class {class_name}" @@ -166,7 +182,9 @@ def generate_class(self, avro_schema: Dict, parent_namespace: str, write_file: b class_definition += f"{INDENT*2}for (int i = 0; obj.Schema.Fields.Count > i; ++i)\n{INDENT*2}{{\n" class_definition += f"{INDENT*3}self.Put(i, obj.GetValue(i));\n{INDENT*2}}}\n{INDENT}}}\n" if self.avro_annotation: - avro_schema_json = json.dumps(avro_schema) + + local_avro_schema = inline_avro_references(avro_schema.copy(), self.type_dict, '') + avro_schema_json = json.dumps(local_avro_schema) # wrap schema at 80 characters avro_schema_json = avro_schema_json.replace('"', 'ยง') avro_schema_json = f"\"+\n{INDENT}\"".join( @@ -224,7 +242,9 @@ def generate_class(self, avro_schema: Dict, parent_namespace: str, write_file: b avro_annotation=self.avro_annotation, system_text_json_annotation=self.system_text_json_annotation, newtonsoft_json_annotation=self.newtonsoft_json_annotation, - json_match_clauses=self.create_is_json_match_clauses(avro_schema, avro_namespace, class_name)) + system_xml_annotation=self.system_xml_annotation, + json_match_clauses=self.create_is_json_match_clauses(avro_schema, avro_namespace, class_name) + ) class_definition += "\n"+"}" @@ -303,7 +323,7 @@ def get_is_json_match_clause_type(self, element_name, class_name, field_type) -> if type_kind == "class": class_definition += f"({f'{element_name}.ValueKind == System.Text.Json.JsonValueKind.Null || ' if is_optional else ''}{field_type}.IsJsonMatch({element_name}))" elif type_kind == "enum": - 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 _ ))))" + 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 _ )))" else: is_union = False field_union = pascal(element_name)+'Union' @@ -322,6 +342,7 @@ def generate_enum(self, avro_schema: Dict, parent_namespace: str, write_file: bo enum_definition = '' namespace = pascal(self.concat_namespace( self.base_namespace, avro_schema.get('namespace', parent_namespace))) + xml_namespace = avro_schema.get('xmlns', None) enum_name = pascal(avro_schema['name']) ref = 'global::'+self.get_qualified_name(namespace, enum_name) if ref in self.generated_types: @@ -329,8 +350,18 @@ def generate_enum(self, avro_schema: Dict, parent_namespace: str, write_file: bo enum_definition += "#pragma warning disable 1591\n\n" enum_definition += f"/// \n/// {avro_schema.get('doc', enum_name )}\n/// \n" - symbols_str = [ - f"{INDENT}{symbol}" for symbol in avro_schema['symbols']] + + # Add XML serialization attribute for the enum if enabled + if self.system_xml_annotation: + if xml_namespace: + enum_definition += f"[XmlType(\"{enum_name}\", Namespace=\"{xml_namespace}\")]\n" + else: + enum_definition += f"[XmlType(\"{enum_name}\")]\n" + + if self.system_xml_annotation: + symbols_str = [f"{INDENT}[XmlEnum(Name=\"{symbol}\")]\n{INDENT}{symbol}" for symbol in avro_schema['symbols']] + else: + symbols_str = [f"{INDENT}{symbol}" for symbol in avro_schema['symbols']] enum_body = ",\n".join(symbols_str) enum_definition += f"public enum {enum_name}\n{{\n{enum_body}\n}}" @@ -379,43 +410,52 @@ def generate_embedded_union(self, class_name: str, field_name: str, avro_type: L 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" class_definition_decls += \ f"{INDENT*2}/// \n{INDENT*2}/// Gets the {union_type_name} value\n{INDENT*2}/// \n" + \ - f"{INDENT*2}public {union_type}? {union_type_name} {{ get; private set; }} = null;\n" + f"{INDENT*2}public {union_type}? {union_type_name} {{ get; set; }} = null;\n" class_definition_toobject += f"{INDENT*3}if ({union_type_name} != null) {{\n{INDENT*4}return {union_type_name};\n{INDENT*3}}}\n" - if is_dict: - class_definition_read += f"{INDENT*3}if (element.ValueKind == JsonValueKind.Object)\n{INDENT*3}{{\n" + \ - f"{INDENT*4}var map = System.Text.Json.JsonSerializer.Deserialize<{union_type}>(element, options);\n" + \ - f"{INDENT*4}if (map != null) {{ return new {union_class_name}(map); }} else {{ throw new NotSupportedException(); }};\n" + \ - f"{INDENT*3}}}\n" - elif is_list: - class_definition_read += f"{INDENT*3}if (element.ValueKind == JsonValueKind.Array)\n{INDENT*3}{{\n" + \ - f"{INDENT*4}var map = System.Text.Json.JsonSerializer.Deserialize<{union_type}>(element, options);\n" + \ - f"{INDENT*4}if (map != null) {{ return new {union_class_name}(map); }} else {{ throw new NotSupportedException(); }};\n" + \ - f"{INDENT*3}}}\n" - elif self.is_csharp_primitive_type(union_type): - if union_type == "byte[]": - 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" - if union_type == "string": - 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" - elif union_type in ['int', 'long', 'float', 'double', 'decimal', 'short', 'sbyte', 'ushort', 'uint', 'ulong']: - 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" - elif union_type == "bool": - class_definition_read += f"{INDENT*3}if (element.ValueKind == JsonValueKind.True || element.ValueKind == JsonValueKind.False)\n{INDENT*2}{{\n{INDENT*3}return new {union_class_name}(element.GetBoolean());\n{INDENT*3}}}\n" - elif union_type == "DateTime": - 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" - elif union_type == "DateTimeOffset": - 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" - else: - 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" - 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" - gij = self.get_is_json_match_clause_type("element", class_name, union_type) - if gij: - list_is_json_match.append(gij) + if self.system_text_json_annotation: + if is_dict: + class_definition_read += f"{INDENT*3}if (element.ValueKind == JsonValueKind.Object)\n{INDENT*3}{{\n" + \ + f"{INDENT*4}var map = System.Text.Json.JsonSerializer.Deserialize<{union_type}>(element, options);\n" + \ + f"{INDENT*4}if (map != null) {{ return new {union_class_name}(map); }} else {{ throw new NotSupportedException(); }};\n" + \ + f"{INDENT*3}}}\n" + elif is_list: + class_definition_read += f"{INDENT*3}if (element.ValueKind == JsonValueKind.Array)\n{INDENT*3}{{\n" + \ + f"{INDENT*4}var map = System.Text.Json.JsonSerializer.Deserialize<{union_type}>(element, options);\n" + \ + f"{INDENT*4}if (map != null) {{ return new {union_class_name}(map); }} else {{ throw new NotSupportedException(); }};\n" + \ + f"{INDENT*3}}}\n" + elif self.is_csharp_primitive_type(union_type): + if union_type == "byte[]": + 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" + if union_type == "string": + 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" + elif union_type in ['int', 'long', 'float', 'double', 'decimal', 'short', 'sbyte', 'ushort', 'uint', 'ulong']: + 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" + elif union_type == "bool": + 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" + elif union_type == "DateTime": + 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" + elif union_type == "DateTimeOffset": + 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" + else: + if union_type.startswith("global::"): + type_kind = self.generated_types[union_type] if union_type in self.generated_types else "class" + if type_kind == "class": + 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" + elif type_kind == "enum": + 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" + 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" + gij = self.get_is_json_match_clause_type("element", class_name, union_type) + if gij: + list_is_json_match.append(gij) class_definition = \ f"/// \n/// {class_name}. Type union resolver. \n/// \n" + \ f"public partial class {class_name}\n{{\n" + \ f"{INDENT}/// \n{INDENT}/// Union class for {field_name}\n{INDENT}/// \n" + if self.system_xml_annotation: + class_definition += \ + f"{INDENT}[XmlRoot(\"{union_class_name}\")]\n" if self.system_text_json_annotation: class_definition += \ f"{INDENT}[System.Text.Json.Serialization.JsonConverter(typeof({union_class_name}))]\n" @@ -519,6 +559,15 @@ def generate_property(self, field: Dict, class_name: str, parent_namespace: str) field_name += "_" prop = '' prop += f"{INDENT}/// \n{INDENT}/// { field.get('doc', field_name) }\n{INDENT}/// \n" + + # Add XML serialization attribute if enabled + if self.system_xml_annotation: + xmlkind = field.get('xmlkind', 'element') + if xmlkind == 'element': + prop += f"{INDENT}[XmlElement(\"{annotation_name}\")]\n" + elif xmlkind == 'attribute': + prop += f"{INDENT}[XmlAttribute(\"{annotation_name}\")]\n" + if self.system_text_json_annotation: prop += f"{INDENT}[System.Text.Json.Serialization.JsonPropertyName(\"{annotation_name}\")]\n" if is_enum_type: @@ -527,7 +576,7 @@ def generate_property(self, field: Dict, class_name: str, parent_namespace: str) prop += f"{INDENT}[System.Text.Json.Serialization.JsonConverter(typeof({field_type}))]\n" if self.newtonsoft_json_annotation: prop += f"{INDENT}[Newtonsoft.Json.JsonProperty(\"{annotation_name}\")]\n" - prop += f"{INDENT}public {field_type} {field_name} {{ get; {'private ' if 'const' in field else ''}set; }}" + ((" = "+(f"\"{field_default}\"" if isinstance(field_default,str) else field_default) + ";") if field_default else "") + prop += f"{INDENT}public {field_type} {field_name} {{ get; set; }}" + ((" = "+(f"\"{field_default}\"" if isinstance(field_default,str) else field_default) + ";") if field_default else "") return prop def write_to_file(self, namespace: str, name: str, definition: str): @@ -547,6 +596,8 @@ def write_to_file(self, namespace: str, name: str, definition: str): file_content += "using System.Text.Json.Serialization;\n" if self.newtonsoft_json_annotation: file_content += "using Newtonsoft.Json;\n" + if self.system_xml_annotation: # Add XML serialization using directive + file_content += "using System.Xml.Serialization;\n" if namespace: # Namespace declaration with correct indentation for the definition @@ -585,7 +636,10 @@ def generate_test_class(self, class_name: str, type_kind: str, test_directory_pa test_class_name=test_class_name, class_base_name=class_base_name, fields=fields, - avro_annotation=self.avro_annotation + avro_annotation=self.avro_annotation, + system_xml_annotation=self.system_xml_annotation, + system_text_json_annotation=self.system_text_json_annotation, + newtonsoft_json_annotation=self.newtonsoft_json_annotation ) elif type_kind == "enum": test_class_definition = process_template( @@ -594,6 +648,10 @@ def generate_test_class(self, class_name: str, type_kind: str, test_directory_pa test_class_name=test_class_name, enum_base_name=class_base_name, symbols=avro_schema.get('symbols', []), + avro_annotation=self.avro_annotation, + system_xml_annotation=self.system_xml_annotation, + system_text_json_annotation=self.system_text_json_annotation, + newtonsoft_json_annotation=self.newtonsoft_json_annotation ) test_file_path = os.path.join(test_directory_path, f"{test_class_name}.cs") @@ -658,6 +716,7 @@ def convert_schema(self, schema: JsonNode, output_dir: str): project_name = self.base_namespace self.schema_doc = schema + self.type_dict = build_flat_type_dict(self.schema_doc) if not os.path.exists(output_dir): os.makedirs(output_dir, exist_ok=True) if not glob.glob(os.path.join(output_dir, "src", "*.sln")): @@ -667,7 +726,14 @@ def convert_schema(self, schema: JsonNode, output_dir: str): if not os.path.exists(os.path.dirname(sln_file)): os.makedirs(os.path.dirname(sln_file)) with open(sln_file, 'w', encoding='utf-8') as file: - file.write(process_template("avrotocsharp/project.sln.jinja", project_name=project_name, uuid=lambda:str(uuid.uuid4()))) + file.write(process_template( + "avrotocsharp/project.sln.jinja", + project_name=project_name, + uuid=lambda:str(uuid.uuid4()), + avro_annotation=self.avro_annotation, + system_xml_annotation=self.system_xml_annotation, + system_text_json_annotation=self.system_text_json_annotation, + newtonsoft_json_annotation=self.newtonsoft_json_annotation)) if not glob.glob(os.path.join(output_dir, "src", "*.csproj")): csproj_file = os.path.join( output_dir, "src", f"{pascal(project_name)}.csproj") @@ -675,7 +741,13 @@ def convert_schema(self, schema: JsonNode, output_dir: str): if not os.path.exists(os.path.dirname(csproj_file)): os.makedirs(os.path.dirname(csproj_file)) with open(csproj_file, 'w', encoding='utf-8') as file: - file.write(process_template("avrotocsharp/project.csproj.jinja")) + file.write(process_template( + "avrotocsharp/project.csproj.jinja", + project_name=project_name, + avro_annotation=self.avro_annotation, + system_xml_annotation=self.system_xml_annotation, + system_text_json_annotation=self.system_text_json_annotation, + newtonsoft_json_annotation=self.newtonsoft_json_annotation)) if not glob.glob(os.path.join(output_dir, "test", "*.csproj")): csproj_test_file = os.path.join( output_dir, "test", f"{pascal(project_name)}.Test.csproj") @@ -683,7 +755,13 @@ def convert_schema(self, schema: JsonNode, output_dir: str): if not os.path.exists(os.path.dirname(csproj_test_file)): os.makedirs(os.path.dirname(csproj_test_file)) with open(csproj_test_file, 'w', encoding='utf-8') as file: - file.write(process_template("avrotocsharp/testproject.csproj.jinja", project_name=project_name)) + file.write(process_template( + "avrotocsharp/testproject.csproj.jinja", + project_name=project_name, + avro_annotation=self.avro_annotation, + system_xml_annotation=self.system_xml_annotation, + system_text_json_annotation=self.system_text_json_annotation, + newtonsoft_json_annotation=self.newtonsoft_json_annotation)) self.output_dir = output_dir for avro_schema in (avs for avs in schema if isinstance(avs, dict)): @@ -697,14 +775,27 @@ def convert(self, avro_schema_path: str, output_dir: str): self.convert_schema(schema, output_dir) -def convert_avro_to_csharp(avro_schema_path, cs_file_path, base_namespace='', pascal_properties=False, system_text_json_annotation=False, newtonsoft_json_annotation=False, avro_annotation=False): - """_summary_ - - Converts Avro schema to C# classes +def convert_avro_to_csharp( + avro_schema_path, + cs_file_path, + base_namespace='', + pascal_properties=False, + system_text_json_annotation=False, + newtonsoft_json_annotation=False, + system_xml_annotation=False, # New parameter + avro_annotation=False +): + """Converts Avro schema to C# classes Args: - avro_schema_path (_type_): Avro input schema path - cs_file_path (_type_): Output C# file path + avro_schema_path (str): Avro input schema path + cs_file_path (str): Output C# file path + base_namespace (str, optional): Base namespace. Defaults to ''. + pascal_properties (bool, optional): Pascal case properties. Defaults to False. + system_text_json_annotation (bool, optional): Use System.Text.Json annotations. Defaults to False. + newtonsoft_json_annotation (bool, optional): Use Newtonsoft.Json annotations. Defaults to False. + system_xml_annotation (bool, optional): Use System.Xml.Serialization annotations. Defaults to False. + avro_annotation (bool, optional): Use Avro annotations. Defaults to False. """ if not base_namespace: @@ -713,27 +804,37 @@ def convert_avro_to_csharp(avro_schema_path, cs_file_path, base_namespace='', pa avrotocs.pascal_properties = pascal_properties avrotocs.system_text_json_annotation = system_text_json_annotation avrotocs.newtonsoft_json_annotation = newtonsoft_json_annotation + avrotocs.system_xml_annotation = system_xml_annotation # Set the flag avrotocs.avro_annotation = avro_annotation avrotocs.convert(avro_schema_path, cs_file_path) -def convert_avro_schema_to_csharp(avro_schema: JsonNode, output_dir: str, base_namespace: str = '', pascal_properties: bool = False, system_text_json_annotation: bool = False, newtonsoft_json_annotation: bool = False, avro_annotation: bool = False): - """_summary_ - - Converts Avro schema to C# classes +def convert_avro_schema_to_csharp( + avro_schema: JsonNode, + output_dir: str, + base_namespace: str = '', + pascal_properties: bool = False, + system_text_json_annotation: bool = False, + newtonsoft_json_annotation: bool = False, + system_xml_annotation: bool = False, # New parameter + avro_annotation: bool = False +): + """Converts Avro schema to C# classes Args: - avro_schema (_type_): Avro schema to convert - output_dir (_type_): Output directory - base_namespace (_type_): Base namespace for the generated classes - pascal_properties (_type_): Pascal case properties - system_text_json_annotation (_type_): Use System.Text.Json annotations - newtonsoft_json_annotation (_type_): Use Newtonsoft.Json annotations - avro_annotation (_type_): Use Avro annotations + avro_schema (JsonNode): Avro schema to convert + output_dir (str): Output directory + base_namespace (str, optional): Base namespace for the generated classes. Defaults to ''. + pascal_properties (bool, optional): Pascal case properties. Defaults to False. + system_text_json_annotation (bool, optional): Use System.Text.Json annotations. Defaults to False. + newtonsoft_json_annotation (bool, optional): Use Newtonsoft.Json annotations. Defaults to False. + system_xml_annotation (bool, optional): Use System.Xml.Serialization annotations. Defaults to False. + avro_annotation (bool, optional): Use Avro annotations. Defaults to False. """ avrotocs = AvroToCSharp(base_namespace) avrotocs.pascal_properties = pascal_properties avrotocs.system_text_json_annotation = system_text_json_annotation avrotocs.newtonsoft_json_annotation = newtonsoft_json_annotation + avrotocs.system_xml_annotation = system_xml_annotation # Set the flag avrotocs.avro_annotation = avro_annotation avrotocs.convert_schema(avro_schema, output_dir) diff --git a/avrotize/avrotocsharp/class_test.cs.jinja b/avrotize/avrotocsharp/class_test.cs.jinja index 3e72e46..ffd3b09 100644 --- a/avrotize/avrotocsharp/class_test.cs.jinja +++ b/avrotize/avrotocsharp/class_test.cs.jinja @@ -56,14 +56,24 @@ public class {{ test_class_name }} {%- if avro_annotation %} /// Testing Avro serializer [Test] - public void Test_ToByteArray_FromData() + public void Test_ToByteArray_FromData_Avro() { var mediaType = "application/vnd.apache.avro+avro"; var bytes = _instance.ToByteArray(mediaType); var newInstance = {{ class_base_name }}.FromData(bytes, mediaType); _instance.Should().BeEquivalentTo(newInstance); } - + {%- endif %} + {%- if system_xml_annotation %} + /// Testing XML serializer + [Test] + public void Test_ToByteArray_FromData_Xml() + { + var mediaType = "application/xml"; + var bytes = _instance.ToByteArray(mediaType); + var newInstance = {{ class_base_name }}.FromData(bytes, mediaType); + _instance.Should().BeEquivalentTo(newInstance); + } {%- endif %} } {% endfilter %} diff --git a/avrotize/avrotocsharp/dataclass_core.jinja b/avrotize/avrotocsharp/dataclass_core.jinja index e753b3d..e7c32ba 100644 --- a/avrotize/avrotocsharp/dataclass_core.jinja +++ b/avrotize/avrotocsharp/dataclass_core.jinja @@ -1,4 +1,4 @@ - {%- if avro_annotation or system_text_json_annotation or newtonsoft_json_annotation %} + {%- if avro_annotation or system_text_json_annotation or newtonsoft_json_annotation or system_xml_annotation %} /// /// Creates an object from the data /// @@ -11,7 +11,7 @@ if ( data is {{ class_name }}) return ({{ class_name }})data; if ( contentTypeString == null ) contentTypeString = System.Net.Mime.MediaTypeNames.Application.Octet; var contentType = new System.Net.Mime.ContentType(contentTypeString); - {%- if avro_annotation or system_text_json_annotation or newtonsoft_json_annotation %} + {%- if avro_annotation or system_text_json_annotation or newtonsoft_json_annotation or system_xml_annotation %} if ( contentType.MediaType.EndsWith("+gzip")) { var stream = data switch @@ -87,10 +87,37 @@ return ((System.BinaryData)data).ToObjectFromJson<{{ class_name }}>(); } } + {%- endif %} + {%- if system_xml_annotation %} + if ( contentType.MediaType.StartsWith(System.Net.Mime.MediaTypeNames.Text.Xml) || contentType.MediaType.StartsWith(System.Net.Mime.MediaTypeNames.Application.Xml) || contentType.MediaType.EndsWith("+xml")) + { + var serializer = new System.Xml.Serialization.XmlSerializer(typeof({{ class_name }})); + if (data is string) + { + using (var reader = new System.IO.StringReader((string)data)) + { + return ({{ class_name }}?)serializer.Deserialize(reader); + } + } + else if (data is System.IO.Stream) + { + return ({{ class_name }}?)serializer.Deserialize((System.IO.Stream)data); + } + else if (data is System.BinaryData) + { + var memoryStream = new System.IO.MemoryStream(((System.BinaryData)data).ToArray()); + return ({{ class_name }}?)serializer.Deserialize(memoryStream); + } + else if (data is byte[]) + { + var memoryStream = new System.IO.MemoryStream((byte[])data); + return ({{ class_name }}?)serializer.Deserialize(memoryStream); + } + } {%- endif %} throw new System.NotSupportedException($"Unsupported media type {contentType.MediaType}"); } - {%-endif %} + {%- endif %} {%- if avro_annotation %} private class SpecificDatumWriter : global::Avro.Specific.SpecificDatumWriter<{{ class_name }}> @@ -101,7 +128,15 @@ protected override WriteItem ResolveEnum(global::Avro.EnumSchema es) { - return base.ResolveEnum(global::Avro.EnumSchema.Create(es.Name, es.Symbols, GetType().Assembly.GetName().Name+"."+es.Namespace, null, null, es.Documentation, es.Default)); + var enumType = GetType().Assembly.GetType(GetType().Assembly.GetName().Name+"."+es.Namespace + "." + es.Name, false, true); + if (enumType != null) + { + return base.ResolveEnum(global::Avro.EnumSchema.Create(enumType.Name, es.Symbols, enumType.Namespace, null, null, es.Documentation, es.Default)); + } + else + { + return base.ResolveEnum(es); + } } } {%- endif %} @@ -109,7 +144,7 @@ {%- if avro_annotation %} {%- endif%} - {%- if avro_annotation or system_text_json_annotation or newtonsoft_json_annotation %} + {%- if avro_annotation or system_text_json_annotation or newtonsoft_json_annotation or system_xml_annotation %} /// /// Converts the object to a byte array /// @@ -151,7 +186,22 @@ result = System.Text.Encoding.GetEncoding(contentType.CharSet??"utf-8").GetBytes(Newtonsoft.Json.JsonConvert.SerializeObject(this)); } {%- endif %} - {%- if avro_annotation or system_text_json_annotation or newtonsoft_json_annotation %} + {%- if system_xml_annotation %} + if (contentType.MediaType.StartsWith(System.Net.Mime.MediaTypeNames.Text.Xml) || contentType.MediaType.StartsWith(System.Net.Mime.MediaTypeNames.Application.Xml) || contentType.MediaType.EndsWith("+xml")) + { + var serializer = new System.Xml.Serialization.XmlSerializer(typeof({{ class_name }})); + using (var stream = new System.IO.MemoryStream()) + { + using (var writer = new System.IO.StreamWriter(stream)) + { + serializer.Serialize(writer, this); + writer.Flush(); + result = stream.ToArray(); + } + } + } + {%- endif %} + {%- if avro_annotation or system_text_json_annotation or newtonsoft_json_annotation or system_xml_annotation %} if (result != null && contentType.MediaType.EndsWith("+gzip")) { var stream = new System.IO.MemoryStream(); @@ -166,7 +216,7 @@ } {%- endif %} - {%- if system_text_json_annotation %} + {%- if system_text_json_annotation or newtonsoft_json_annotation %} /// /// Checks if the JSON element matches the schema /// diff --git a/avrotize/avrotocsharp/project.csproj.jinja b/avrotize/avrotocsharp/project.csproj.jinja index 5d4e039..df5b609 100644 --- a/avrotize/avrotocsharp/project.csproj.jinja +++ b/avrotize/avrotocsharp/project.csproj.jinja @@ -5,9 +5,15 @@ true + {%- if avro_annotation %} + {%- endif %} + {%- if newtonsoft_json_annotation %} + {%- endif %} + {%- if system_text_json_annotation %} + {%- endif %} diff --git a/avrotize/avrotots/class_core.ts.jinja b/avrotize/avrotots/class_core.ts.jinja index 774ad7b..a3d8b3d 100644 --- a/avrotize/avrotots/class_core.ts.jinja +++ b/avrotize/avrotots/class_core.ts.jinja @@ -107,6 +107,8 @@ export class {{ class_name }} { throw new Error(`Unsupported media type: ${contentTypeString}`); } + + {%- if typed_json_annotation %} public static isJsonMatch(element: any): boolean { {%- if fields|length == 0 %} return true; @@ -120,4 +122,5 @@ export class {{ class_name }} { {%- endif %} } {%- endif %} + {%- endif %} } diff --git a/avrotize/avrotots/enum_core.ts.jinja b/avrotize/avrotots/enum_core.ts.jinja index f094ba5..fa272a2 100644 --- a/avrotize/avrotots/enum_core.ts.jinja +++ b/avrotize/avrotots/enum_core.ts.jinja @@ -38,7 +38,9 @@ export class {{ enum_name }}Utils { } } + {%- if typed_json_annotation %} static isJsonMatch(value: string): boolean { return Object.values({{ enum_name }}).includes(value as {{ enum_name }}); } + {%- endif %} } \ No newline at end of file diff --git a/avrotize/commands.json b/avrotize/commands.json index 7df8861..6117dd8 100644 --- a/avrotize/commands.json +++ b/avrotize/commands.json @@ -899,6 +899,7 @@ "avro_annotation": "args.avro_annotation", "system_text_json_annotation": "args.system_text_json_annotation", "newtonsoft_json_annotation": "args.newtonsoft_json_annotation", + "system_xml_annotation": "args.system_xml_annotation", "pascal_properties": "args.pascal_properties", "base_namespace": "args.namespace" } @@ -944,6 +945,13 @@ "default": false, "required": false }, + { + "name": "--system-xml-annotation", + "type": "bool", + "help": "Use System.Xml annotations", + "default": false, + "required": false + }, { "name": "--newtonsoft-json-annotation", "type": "bool", diff --git a/avrotize/xsdtoavro.py b/avrotize/xsdtoavro.py index 408195a..6b36cbd 100644 --- a/avrotize/xsdtoavro.py +++ b/avrotize/xsdtoavro.py @@ -22,6 +22,7 @@ def __init__(self) -> None: """ Initialize the class. """ self.simple_type_map: Dict[str, str | dict] = {} self.avro_namespace = '' + self.xml_namespace = '' def xsd_targetnamespace_to_avro_namespace(self, targetnamespace: str) -> str: """Convert a XSD namespace to Avro Namespace.""" @@ -303,6 +304,7 @@ def process_top_level_element(self, element: ET.Element, namespaces: dict): 'type': 'record', 'name': 'Root', 'namespace': self.avro_namespace, + 'xmlns': self.xml_namespace, 'fields': [] } annotation = element.find(f'{{{XSD_NAMESPACE}}}annotation', namespaces) @@ -349,6 +351,7 @@ def xsd_to_avro(self, xsd_path: str, code_namespace: str | None = None): target_namespace = root.get('targetNamespace') if target_namespace is None: raise ValueError('targetNamespace not found') + self.xml_namespace = target_namespace if not code_namespace: self.avro_namespace = self.xsd_targetnamespace_to_avro_namespace(target_namespace) else: diff --git a/csharpcodegen.md b/csharpcodegen.md index 8ff0932..7b7b237 100644 --- a/csharpcodegen.md +++ b/csharpcodegen.md @@ -2,10 +2,10 @@ Avrotize is can generates C# classes from Avro schema files with the "a2csharp" command. The generated classes reflect the type model described by the Avro schema. -With the `avro_annotation` option, the code generator is an alternative to the `avrogen` tool provided by the Avro project. Unlike `avrogen`, Avrotize generates classes that directly support type unions and it allows combining Avro annotations +With the `--avro-annotation` option, the code generator is an alternative to the `avrogen` tool provided by the Avro project. Unlike `avrogen`, Avrotize generates classes that directly support type unions and it allows combining Avro annotations with annotations for `System.Text.Json` serialization. -With the `system_text_json_annotation` option, the code generator emits annotations for `System.Text.Json` serialization. This option can be used standalone and is not dependent on the `avro_annotation` option, which means that Avro Schema +With the `--system-text-json-annotation` option, the code generator emits annotations for `System.Text.Json` serialization. This option can be used standalone and is not dependent on the `--avro-annotation` option, which means that Avro Schema can be used to generate classes with `System.Text.Json` serialization annotations as an alternative to JSON Schema, without the Avro serialization framework being required. The generated classes fully support type unions (equivalent to JSN Schema's `oneOf`) without requiring a "discriminator" field, but rather deduce the type from the serialized data's structure. @@ -46,7 +46,7 @@ the Avro serialization framework being required. The generated classes fully sup ``` The following is an example of the generated code for the schema above, with the -`avro_annotation` option and the `system_text_json_annotation` option turned on. +`--avro-annotation` option and the `--system-text-json-annotation` option turned on. We will discuss the generated code in detail below and which parts are generated by which option. @@ -94,7 +94,7 @@ The generated code includes a class declaration for the record type defined in the Avro schema. The class name is derived from the name of the record type in the Avro schema. -If the `avro_annotation` option is provided, the class implements the +If the `--avro-annotation` option is provided, the class implements the `ISpecificRecord` interface from the Avro library. The interface provides methods to access the fields of the record and to convert the record to and from byte arrays. The interface is used by the Avro serialization framework to @@ -145,7 +145,7 @@ The mapping for Avro types to C# types is as follows: | timestamp-micros | DateTime | | duration | TimeSpan | -If the `system_text_json_annotation` option is used, fields are annotated with +If the `--system-text-json-annotation` option is used, fields are annotated with the `JsonPropertyName` attribute from the `System.Text.Json.Serialization` namespace. The attribute specifies the name of the field in the JSON representation of the record. @@ -200,7 +200,7 @@ constructor initializes the fields of the record to their default values. ### Constructor from Avro GenericRecord -If the `avro_annotation` option is used, the generated code includes a +If the `--avro-annotation` option is used, the generated code includes a constructor that takes an Avro `GenericRecord` object as a parameter. The constructor initializes the fields of the record from the values in the `GenericRecord`. @@ -222,7 +222,7 @@ constructor initializes the fields of the record from the values in the ### Avro Schema -If the `avro_annotation` option is used, the generated code includes a static +If the `--avro-annotation` option is used, the generated code includes a static field that contains the Avro schema for the record type. The schema is parsed from a JSON string that represents the schema. @@ -240,7 +240,7 @@ from a JSON string that represents the schema. ### ISpecificRecord Interface Implementation -If the `avro_annotation` option is used, the generated code includes +If the `--avro-annotation` option is used, the generated code includes implementations of the `Get` and `Put` methods from the Avro framework's `ISpecificRecord` interface. The methods provide access to the fields of the record and allow the record to be serialized and deserialized by the Avro @@ -277,7 +277,7 @@ serialization framework. ### ToByteArray Method -If either or both the `avro_annotation` and `system_text_json_annotation` +If either or both the `--avro-annotation` and `--system-text-json-annotation` options are used, the generated code includes a `ToByteArray` method that converts the record to a byte array. The method takes a content type string as a parameter that specifies the encoding of the data. The method encodes the record @@ -287,14 +287,14 @@ The following encodings are supported: | Enabled Option | Content Type String | Encoding | |----------------|---------------------|----------| -| avro_annotation | `avro/binary` | Avro binary encoding | -| avro_annotation | `avro/vnd.apache.avro+avro` | Avro binary encoding | -| avro_annotation | `avro/vnd.apache.avro+avro+gzip` | Avro binary encoding with GZIP compression | -| avro_annotation | `avro/json` | Avro JSON encoding | -| avro_annotation | `application/vnd.apache.avro+json` | Avro JSON encoding | -| avro_annotation | `avro/vnd.apache.avro+json+gzip` | Avro JSON encoding with GZIP compression | -| system_text_json_annotation | `application/json` | JSON encoding | -| system_text_json_annotation | `application/json+gzip` | JSON encoding with GZIP compression | +| --avro-annotation | `avro/binary` | Avro binary encoding | +| --avro-annotation | `avro/vnd.apache.avro+avro` | Avro binary encoding | +| --avro-annotation | `avro/vnd.apache.avro+avro+gzip` | Avro binary encoding with GZIP compression | +| --avro-annotation | `avro/json` | Avro JSON encoding | +| --avro-annotation | `application/vnd.apache.avro+json` | Avro JSON encoding | +| --avro-annotation | `avro/vnd.apache.avro+json+gzip` | Avro JSON encoding with GZIP compression | +| --system-text-json-annotation | `application/json` | JSON encoding | +| --system-text-json-annotation | `application/json+gzip` | JSON encoding with GZIP compression | ```csharp @@ -347,7 +347,7 @@ The following encodings are supported: ### FromData Method -If either or both the `avro_annotation` and `system_text_json_annotation` +If either or both the `--avro-annotation` and `--system-text-json-annotation` options are used, the generated code includes a `FromData` method that converts a byte array to a record object. The method takes the encoded data and a content type string as parameters and returns the decoded record object. @@ -444,7 +444,7 @@ the decoded record object. ### IsJsonMatch Method -If the `system_text_json_annotation` option is used, the generated code includes +If the `--system-text-json-annotation` option is used, the generated code includes an `IsJsonMatch` method that checks if a JSON element matches the schema. The method takes a `JsonElement` object as a parameter and returns a boolean value that indicates whether the JSON element matches the schema. @@ -548,7 +548,7 @@ In this case: The following generated code refers to the `DocumentUnion` class that is produced for the `document` field shown above. If the -`system_text_json_annotation` option is used, the union class is annotated with +`--system-text-json-annotation` option is used, the union class is annotated with the `JsonConverter` attribute from the `System.Text.Json.Serialization` namespace. The attribute specifies the converter class that is used to serialize and deserialize the union. @@ -565,7 +565,7 @@ and deserialize the union. The `DocumentUnion` class is embedded in the generated record class, into a separate file, but into the same partial class. -If the `system_text_json_annotation` option is used, the `DocumentUnion` class +If the `--system-text-json-annotation` option is used, the `DocumentUnion` class is derived from the `JsonConverter` class from the `System.Text.Json.Serialization` namespace. The class provides methods to serialize and deserialize the union. @@ -613,7 +613,7 @@ union value that corresponds to the type of the object. This is a factory method instead of a constructor to disambiguate from generated constructors for maps, which are also represented as `Object` in some cases. -If the `avro_annotation` option is used, the method checks whether the object is +If the `--avro-annotation` option is used, the method checks whether the object is a GenericRecord and creates a union value from the GenericRecord with the respective constructor. @@ -644,7 +644,7 @@ constructor. ### Constructor from Avro GenericRecord -If the `avro_annotation` option is used, the generated code includes a +If the `--avro-annotation` option is used, the generated code includes a constructor that takes an Avro `GenericRecord` object as a parameter. The constructor initializes the fields of the union from the values in the `GenericRecord`. @@ -737,7 +737,7 @@ void global::Avro.Specific.ISpecificRecord.Put(int fieldPos, object fieldValue) ### Read Method -If the `system_text_json_annotation` option is used, the generated code includes a +If the `--system-text-json-annotation` option is used, the generated code includes a `Read` method that reads the JSON representation of the object. The method takes a `Utf8JsonReader` object as a parameter and returns the union value that corresponds to the JSON data. @@ -768,7 +768,7 @@ constructor. ### Write Method -If the `system_text_json_annotation` option is used, the generated code includes a +If the `--system-text-json-annotation` option is used, the generated code includes a `Write` method that writes the JSON representation of the object. The method takes a `Utf8JsonWriter` object as a parameter and writes the JSON representation of the union value. @@ -798,7 +798,7 @@ that corresponds to the type. ### IsJsonMatch Method -If the `system_text_json_annotation` option is used, the generated code includes an +If the `--system-text-json-annotation` option is used, the generated code includes an `IsJsonMatch` method that checks if a JSON element matches the schema. The method takes a `JsonElement` object as a parameter and returns a boolean value that indicates whether the JSON element matches the schema. @@ -938,7 +938,7 @@ a nullable field. The `Test1Union` class is generated for the `test1` field. As you may expect from the previous example, the union class has constructors for each type in the union, a `ToObject` method, and the `Read`, `Write`, and `IsJsonMatch` -methods generated if 'system_text_json_annotation' is used. +methods generated if '--system-text-json-annotation' is used. ```csharp #pragma warning disable CS8618 diff --git a/test/test_avrotocsharp.py b/test/test_avrotocsharp.py index 14bfd37..ca1b09f 100644 --- a/test/test_avrotocsharp.py +++ b/test/test_avrotocsharp.py @@ -20,7 +20,7 @@ class TestAvroToCSharp(unittest.TestCase): - def run_convert_avsc_to_csharp(self, avsc_name, system_text_json_annotation=False, newtonsoft_json_annotation=False, avro_annotation=False, pascal_properties=False): + def run_convert_avsc_to_csharp(self, avsc_name, system_text_json_annotation=False, newtonsoft_json_annotation=False, avro_annotation=False, system_xml_annotation=False, pascal_properties=False): """ Test converting an avsc file to C# """ cwd = os.getcwd() avro_path = os.path.join(cwd, "test", "avsc", avsc_name + ".avsc") @@ -29,33 +29,41 @@ def run_convert_avsc_to_csharp(self, avsc_name, system_text_json_annotation=Fals shutil.rmtree(cs_path, ignore_errors=True) os.makedirs(cs_path, exist_ok=True) - convert_avro_to_csharp(avro_path, cs_path, pascal_properties=pascal_properties, system_text_json_annotation=system_text_json_annotation, newtonsoft_json_annotation=newtonsoft_json_annotation, avro_annotation=avro_annotation) + convert_avro_to_csharp(avro_path, cs_path, pascal_properties=pascal_properties, system_text_json_annotation=system_text_json_annotation, newtonsoft_json_annotation=newtonsoft_json_annotation, avro_annotation=avro_annotation, system_xml_annotation=system_xml_annotation) assert subprocess.check_call( - ['dotnet', 'build'], cwd=cs_path, stdout=sys.stdout, stderr=sys.stderr) == 0 + ['dotnet', 'test'], cwd=cs_path, stdout=sys.stdout, stderr=sys.stderr) == 0 def test_convert_address_avsc_to_csharp(self): """ Test converting an address.avsc file to C# """ self.run_convert_avsc_to_csharp("address") - def test_convert_address_avsc_to_csharp_avro_annotation(self): - """ Test converting an address.avsc file to C# """ - self.run_convert_avsc_to_csharp("address", avro_annotation=True) + def test_convert_address_avsc_to_csharp_annotated(self): + for system_text_json_annotation in [True, False]: + for newtonsoft_json_annotation in [True, False]: + for avro_annotation in [True, False]: + for pascal_properties in [True, False]: + for system_xml_annotation in [True, False]: + self.run_convert_avsc_to_csharp("address", system_text_json_annotation=system_text_json_annotation, newtonsoft_json_annotation=newtonsoft_json_annotation, avro_annotation=avro_annotation, pascal_properties=pascal_properties, system_xml_annotation=system_xml_annotation) - def test_convert_address_avsc_to_csharp_system_text_json_annotation(self): - """ Test converting an address.avsc file to C# """ - self.run_convert_avsc_to_csharp("address", system_text_json_annotation=True) - def test_convert_enumfield_avsc_to_csharp_system_text_json_annotation(self): - """ Test converting an address.avsc file to C# """ - self.run_convert_avsc_to_csharp("enumfield", system_text_json_annotation=True) - - def test_convert_address_avsc_to_csharp_newtonsoft_json_annotation(self): - """ Test converting an address.avsc file to C# """ - self.run_convert_avsc_to_csharp("address", newtonsoft_json_annotation=True) - - def test_convert_telemetry_avsc_to_csharp(self): - """ Test converting a telemetry.avsc file to C# """ - self.run_convert_avsc_to_csharp("telemetry") + def test_convert_enumfield_avsc_to_csharp_annotated(self): + """ Test converting an enumfield.avsc file to C# """ + for system_text_json_annotation in [True, False]: + for newtonsoft_json_annotation in [True, False]: + for avro_annotation in [True, False]: + for pascal_properties in [True, False]: + for system_xml_annotation in [True, False]: + self.run_convert_avsc_to_csharp("enumfield", system_text_json_annotation=system_text_json_annotation, newtonsoft_json_annotation=newtonsoft_json_annotation, avro_annotation=avro_annotation, pascal_properties=pascal_properties, system_xml_annotation=system_xml_annotation) + + + def test_convert_telemetry_avsc_to_csharp_annotated(self): + """ Test converting an telemetry.avsc file to C# """ + for system_text_json_annotation in [True, False]: + for newtonsoft_json_annotation in [True, False]: + for avro_annotation in [True, False]: + for pascal_properties in [True, False]: + for system_xml_annotation in [True, False]: + self.run_convert_avsc_to_csharp("telemetry", system_text_json_annotation=system_text_json_annotation, newtonsoft_json_annotation=newtonsoft_json_annotation, avro_annotation=avro_annotation, pascal_properties=pascal_properties, system_xml_annotation=system_xml_annotation) def test_convert_address_nn_avsc_to_csharp(self): """ Test converting an address.nn.avsc file to C# """ @@ -81,7 +89,7 @@ def test_convert_twotypeunion_ann_avsc_to_csharp(self): assert subprocess.check_call( ['dotnet', 'run', '--force'], cwd=cs_test_path, stdout=sys.stdout, stderr=sys.stderr) == 0 - def run_test_convert_twotypeunion_avsc_to_csharp(self, system_text_json_annotation=True, newtonsoft_json_annotation=True, avro_annotation=True, pascal_properties=True): + def run_test_convert_twotypeunion_avsc_to_csharp(self, system_text_json_annotation=True, newtonsoft_json_annotation=True, avro_annotation=True, pascal_properties=True, system_xml_annotation=True): """ Test converting a twotypeunion.avsc file to C# """ cwd = os.getcwd() avro_path = os.path.join(cwd, "test", "avsc", "twotypeunion.avsc") @@ -90,20 +98,19 @@ def run_test_convert_twotypeunion_avsc_to_csharp(self, system_text_json_annotati shutil.rmtree(cs_path, ignore_errors=True) os.makedirs(cs_path, exist_ok=True) - convert_avro_to_csharp(avro_path, cs_path, base_namespace="TwoTypeUnion", system_text_json_annotation=system_text_json_annotation, newtonsoft_json_annotation=newtonsoft_json_annotation, avro_annotation=avro_annotation, pascal_properties=pascal_properties) + convert_avro_to_csharp(avro_path, cs_path, base_namespace="TwoTypeUnion", system_text_json_annotation=system_text_json_annotation, newtonsoft_json_annotation=newtonsoft_json_annotation, avro_annotation=avro_annotation, pascal_properties=pascal_properties, system_xml_annotation=system_xml_annotation) assert subprocess.check_call( ['dotnet', 'build'], cwd=cs_path, stdout=sys.stdout, stderr=sys.stderr) == 0 - def test_convert_twotypeunion_avsc_to_csharp_system_text_json_annotation(self): + def test_convert_twotypeunion_avsc_to_csharp_annotated(self): """ Test converting a twotypeunion.avsc file to C# """ - self.run_test_convert_twotypeunion_avsc_to_csharp(system_text_json_annotation=True, newtonsoft_json_annotation=False, avro_annotation=False) - self.run_test_convert_twotypeunion_avsc_to_csharp(system_text_json_annotation=True, newtonsoft_json_annotation=True, avro_annotation=False) - self.run_test_convert_twotypeunion_avsc_to_csharp(system_text_json_annotation=True, newtonsoft_json_annotation=False, avro_annotation=True) - self.run_test_convert_twotypeunion_avsc_to_csharp(system_text_json_annotation=True, newtonsoft_json_annotation=True, avro_annotation=True) - self.run_test_convert_twotypeunion_avsc_to_csharp(system_text_json_annotation=False, newtonsoft_json_annotation=False, avro_annotation=False) - self.run_test_convert_twotypeunion_avsc_to_csharp(system_text_json_annotation=False, newtonsoft_json_annotation=True, avro_annotation=False) - self.run_test_convert_twotypeunion_avsc_to_csharp(system_text_json_annotation=False, newtonsoft_json_annotation=False, avro_annotation=True) - self.run_test_convert_twotypeunion_avsc_to_csharp(system_text_json_annotation=False, newtonsoft_json_annotation=True, avro_annotation=True) + for system_text_json_annotation in [True, False]: + for newtonsoft_json_annotation in [True, False]: + for avro_annotation in [True, False]: + for pascal_properties in [True, False]: + for system_xml_annotation in [True, False]: + self.run_test_convert_twotypeunion_avsc_to_csharp(system_text_json_annotation=system_text_json_annotation, newtonsoft_json_annotation=newtonsoft_json_annotation, avro_annotation=avro_annotation, pascal_properties=pascal_properties, system_xml_annotation=system_xml_annotation) + def test_convert_typemapunion_avsc_to_csharp(self): """ Test converting an address.avsc file to C# """ @@ -179,5 +186,5 @@ def test_convert_jfrog_pipelines_jsons_to_avro_to_csharp_annotated(self): convert_jsons_to_avro(jsons_path, avro_path) convert_avro_to_csharp(avro_path, cs_path, pascal_properties=True, avro_annotation=True, - system_text_json_annotation=True, newtonsoft_json_annotation=True) + system_text_json_annotation=True, newtonsoft_json_annotation=True, system_xml_annotation=True) assert subprocess.check_call(['dotnet', 'build'], cwd=cs_path, stdout=sys.stdout, stderr=sys.stderr) == 0 diff --git a/vscode/avrotize/src/extension.ts b/vscode/avrotize/src/extension.ts index ec6c505..2140729 100644 --- a/vscode/avrotize/src/extension.ts +++ b/vscode/avrotize/src/extension.ts @@ -312,11 +312,13 @@ export function activate(context: vscode.ExtensionContext) { const avro_annotation_value_arg = avro_annotation_value ? '--avro-annotation' : ''; const system_text_json_annotation_value = await vscode.window.showQuickPick(['Yes', 'No'], { title: 'Use System.Text.Json annotations?' }) === 'Yes'; const system_text_json_annotation_value_arg = system_text_json_annotation_value ? '--system_text_json_annotation' : ''; + const system_xml_annotation_value = await vscode.window.showQuickPick(['Yes', 'No'], { title: 'Use System.Xml annotations?' }) === 'Yes'; + const system_xml_annotation_value_arg = system_xml_annotation_value ? '--system_xml_annotation' : ''; const pascal_properties_value = await vscode.window.showQuickPick(['Yes', 'No'], { title: 'Use PascalCase properties?' }) === 'Yes'; const pascal_properties_value_arg = pascal_properties_value ? '--pascal-properties' : ''; const outputPath = await vscode.window.showSaveDialog({ defaultUri: vscode.Uri.file(outputPathSuggestion), saveLabel: 'Save Output', filters : { 'All Files': ['*'] } }); if (!outputPath) { return; } - const command = `avrotize a2cs ${filePath} --out ${outputPath.fsPath} ${namespace_value_arg} ${avro_annotation_value_arg} ${system_text_json_annotation_value_arg} ${pascal_properties_value_arg}`; + const command = `avrotize a2cs ${filePath} --out ${outputPath.fsPath} ${namespace_value_arg} ${avro_annotation_value_arg} ${system_text_json_annotation_value_arg} ${system_xml_annotation_value_arg} ${pascal_properties_value_arg}`; executeCommand(command, outputPath, outputChannel); }));