structurize 2.19.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. avrotize/__init__.py +64 -0
  2. avrotize/__main__.py +6 -0
  3. avrotize/_version.py +34 -0
  4. avrotize/asn1toavro.py +160 -0
  5. avrotize/avrotize.py +152 -0
  6. avrotize/avrotocpp.py +483 -0
  7. avrotize/avrotocsharp.py +1075 -0
  8. avrotize/avrotocsv.py +121 -0
  9. avrotize/avrotodatapackage.py +173 -0
  10. avrotize/avrotodb.py +1383 -0
  11. avrotize/avrotogo.py +476 -0
  12. avrotize/avrotographql.py +197 -0
  13. avrotize/avrotoiceberg.py +210 -0
  14. avrotize/avrotojava.py +2156 -0
  15. avrotize/avrotojs.py +250 -0
  16. avrotize/avrotojsons.py +481 -0
  17. avrotize/avrotojstruct.py +345 -0
  18. avrotize/avrotokusto.py +364 -0
  19. avrotize/avrotomd.py +137 -0
  20. avrotize/avrotools.py +168 -0
  21. avrotize/avrotoparquet.py +208 -0
  22. avrotize/avrotoproto.py +359 -0
  23. avrotize/avrotopython.py +624 -0
  24. avrotize/avrotorust.py +435 -0
  25. avrotize/avrotots.py +598 -0
  26. avrotize/avrotoxsd.py +344 -0
  27. avrotize/cddltostructure.py +1841 -0
  28. avrotize/commands.json +3337 -0
  29. avrotize/common.py +834 -0
  30. avrotize/constants.py +72 -0
  31. avrotize/csvtoavro.py +132 -0
  32. avrotize/datapackagetoavro.py +76 -0
  33. avrotize/dependencies/cpp/vcpkg/vcpkg.json +19 -0
  34. avrotize/dependencies/typescript/node22/package.json +16 -0
  35. avrotize/dependency_resolver.py +348 -0
  36. avrotize/dependency_version.py +432 -0
  37. avrotize/jsonstoavro.py +2167 -0
  38. avrotize/jsonstostructure.py +2642 -0
  39. avrotize/jstructtoavro.py +878 -0
  40. avrotize/kstructtoavro.py +93 -0
  41. avrotize/kustotoavro.py +455 -0
  42. avrotize/parquettoavro.py +157 -0
  43. avrotize/proto2parser.py +498 -0
  44. avrotize/proto3parser.py +403 -0
  45. avrotize/prototoavro.py +382 -0
  46. avrotize/structuretocddl.py +597 -0
  47. avrotize/structuretocpp.py +697 -0
  48. avrotize/structuretocsharp.py +2295 -0
  49. avrotize/structuretocsv.py +365 -0
  50. avrotize/structuretodatapackage.py +659 -0
  51. avrotize/structuretodb.py +1125 -0
  52. avrotize/structuretogo.py +720 -0
  53. avrotize/structuretographql.py +502 -0
  54. avrotize/structuretoiceberg.py +355 -0
  55. avrotize/structuretojava.py +853 -0
  56. avrotize/structuretojsons.py +498 -0
  57. avrotize/structuretokusto.py +639 -0
  58. avrotize/structuretomd.py +322 -0
  59. avrotize/structuretoproto.py +764 -0
  60. avrotize/structuretopython.py +772 -0
  61. avrotize/structuretorust.py +714 -0
  62. avrotize/structuretots.py +653 -0
  63. avrotize/structuretoxsd.py +679 -0
  64. avrotize/xsdtoavro.py +413 -0
  65. structurize-2.19.0.dist-info/METADATA +107 -0
  66. structurize-2.19.0.dist-info/RECORD +70 -0
  67. structurize-2.19.0.dist-info/WHEEL +5 -0
  68. structurize-2.19.0.dist-info/entry_points.txt +2 -0
  69. structurize-2.19.0.dist-info/licenses/LICENSE +201 -0
  70. structurize-2.19.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,2295 @@
1
+ # pylint: disable=line-too-long
2
+
3
+ """ StructureToCSharp class for converting JSON Structure schema to C# classes """
4
+
5
+ import json
6
+ import os
7
+ import re
8
+ from typing import Any, Dict, List, Tuple, Union, cast, Optional
9
+ import uuid
10
+
11
+ from avrotize.common import pascal, process_template
12
+ from avrotize.constants import (
13
+ NEWTONSOFT_JSON_VERSION,
14
+ SYSTEM_TEXT_JSON_VERSION,
15
+ SYSTEM_MEMORY_DATA_VERSION,
16
+ NUNIT_VERSION,
17
+ NUNIT_ADAPTER_VERSION,
18
+ MSTEST_SDK_VERSION,
19
+ )
20
+ import glob
21
+
22
+ JsonNode = Dict[str, 'JsonNode'] | List['JsonNode'] | str | None
23
+
24
+
25
+ INDENT = ' '
26
+
27
+
28
+ class StructureToCSharp:
29
+ """ Converts JSON Structure schema to C# classes """
30
+
31
+ def __init__(self, base_namespace: str = '') -> None:
32
+ self.base_namespace = base_namespace
33
+ self.project_name: str = '' # Optional explicit project name, separate from namespace
34
+ self.schema_doc: JsonNode = None
35
+ self.output_dir = os.getcwd()
36
+ self.pascal_properties = False
37
+ self.system_text_json_annotation = False
38
+ self.newtonsoft_json_annotation = False
39
+ self.system_xml_annotation = False
40
+ self.generated_types: Dict[str,str] = {}
41
+ self.generated_structure_types: Dict[str, Dict[str, Union[str, Dict, List]]] = {}
42
+ self.type_dict: Dict[str, Dict] = {}
43
+ self.definitions: Dict[str, Any] = {}
44
+ self.schema_registry: Dict[str, Dict] = {} # Maps $id URIs to schemas
45
+ self.offers: Dict[str, Any] = {} # Maps add-in names to property definitions from $offers
46
+ self.needs_json_structure_converters = False # Track if any types need JSON Structure converters
47
+
48
+ def get_qualified_name(self, namespace: str, name: str) -> str:
49
+ """ Concatenates namespace and name with a dot separator """
50
+ return f"{namespace}.{name}" if namespace != '' else name
51
+
52
+ def concat_namespace(self, namespace: str, name: str) -> str:
53
+ """ Concatenates namespace and name with a dot separator """
54
+ if namespace and name:
55
+ return f"{namespace}.{name}"
56
+ elif namespace:
57
+ return namespace
58
+ else:
59
+ return name
60
+
61
+ def map_primitive_to_csharp(self, structure_type: str) -> str:
62
+ """ Maps JSON Structure primitive types to C# types """
63
+ mapping = {
64
+ 'null': 'void', # Placeholder, actual handling for nullable types is in the union logic
65
+ 'boolean': 'bool',
66
+ 'string': 'string',
67
+ 'integer': 'int', # Generic integer type without format
68
+ 'number': 'double', # Generic number type without format
69
+ 'int8': 'sbyte',
70
+ 'uint8': 'byte',
71
+ 'int16': 'short',
72
+ 'uint16': 'ushort',
73
+ 'int32': 'int',
74
+ 'uint32': 'uint',
75
+ 'int64': 'long',
76
+ 'uint64': 'ulong',
77
+ 'int128': 'System.Int128',
78
+ 'uint128': 'System.UInt128',
79
+ 'float8': 'float', # Approximation - C# doesn't have native 8-bit float
80
+ 'float': 'float',
81
+ 'double': 'double',
82
+ 'binary32': 'float', # IEEE 754 binary32
83
+ 'binary64': 'double', # IEEE 754 binary64
84
+ 'decimal': 'decimal',
85
+ 'binary': 'byte[]',
86
+ 'date': 'DateOnly',
87
+ 'time': 'TimeOnly',
88
+ 'datetime': 'DateTimeOffset',
89
+ 'timestamp': 'DateTimeOffset',
90
+ 'duration': 'TimeSpan',
91
+ 'uuid': 'Guid',
92
+ 'uri': 'Uri',
93
+ 'jsonpointer': 'string',
94
+ 'any': 'object'
95
+ }
96
+ qualified_class_name = 'global::'+self.get_qualified_name(pascal(self.base_namespace), pascal(structure_type))
97
+ if qualified_class_name in self.generated_structure_types:
98
+ result = qualified_class_name
99
+ else:
100
+ result = mapping.get(structure_type, 'object')
101
+ return result
102
+
103
+ def get_json_structure_converter(self, schema_type: str, is_required: bool) -> str | None:
104
+ """ Returns the appropriate JSON Structure converter type for types requiring string serialization.
105
+
106
+ Per JSON Structure Core spec, int64, uint64, int128, uint128, and decimal types
107
+ use string representation in JSON to preserve precision. Duration (TimeSpan)
108
+ uses ISO 8601 format.
109
+
110
+ Args:
111
+ schema_type: The JSON Structure type name
112
+ is_required: Whether the property is required (affects nullable converter selection)
113
+
114
+ Returns:
115
+ The converter class name if needed, or None if no special converter is required
116
+ """
117
+ # Map JSON Structure types to their converter class names
118
+ converter_map = {
119
+ 'int64': ('Int64StringConverter', 'NullableInt64StringConverter'),
120
+ 'uint64': ('UInt64StringConverter', 'NullableUInt64StringConverter'),
121
+ 'int128': ('Int128StringConverter', 'NullableInt128StringConverter'),
122
+ 'uint128': ('UInt128StringConverter', 'NullableUInt128StringConverter'),
123
+ 'decimal': ('DecimalStringConverter', 'NullableDecimalStringConverter'),
124
+ 'duration': ('TimeSpanIso8601Converter', 'NullableTimeSpanIso8601Converter'),
125
+ }
126
+
127
+ if schema_type in converter_map:
128
+ required_converter, nullable_converter = converter_map[schema_type]
129
+ return required_converter if is_required else nullable_converter
130
+
131
+ return None
132
+
133
+ def is_csharp_reserved_word(self, word: str) -> bool:
134
+ """ Checks if a word is a reserved C# keyword """
135
+ reserved_words = [
136
+ 'abstract', 'as', 'base', 'bool', 'break', 'byte', 'case', 'catch', 'char', 'checked', 'class', 'const',
137
+ 'continue', 'decimal', 'default', 'delegate', 'do', 'double', 'else', 'enum', 'event', 'explicit', 'extern',
138
+ 'false', 'finally', 'fixed', 'float', 'for', 'foreach', 'goto', 'if', 'implicit', 'in', 'int', 'interface',
139
+ 'internal', 'is', 'lock', 'long', 'namespace', 'new', 'null', 'object', 'operator', 'out', 'override',
140
+ 'params', 'private', 'protected', 'public', 'readonly', 'ref', 'return', 'sbyte', 'sealed', 'short', 'sizeof',
141
+ 'stackalloc', 'static', 'string', 'struct', 'switch', 'this', 'throw', 'true', 'try', 'typeof', 'uint', 'ulong',
142
+ 'unchecked', 'unsafe', 'ushort', 'using', 'virtual', 'void', 'volatile', 'while'
143
+ ]
144
+ return word in reserved_words
145
+
146
+ def is_csharp_primitive_type(self, csharp_type: str) -> bool:
147
+ """ Checks if a type is a C# primitive type """
148
+ if csharp_type.endswith('?'):
149
+ csharp_type = csharp_type[:-1]
150
+ return csharp_type in ['void', 'bool', 'sbyte', 'byte', 'short', 'ushort', 'int', 'uint', 'long', 'ulong',
151
+ 'float', 'double', 'decimal', 'string', 'DateTime', 'DateTimeOffset', 'DateOnly',
152
+ 'TimeOnly', 'TimeSpan', 'Guid', 'byte[]', 'object', 'System.Int128', 'System.UInt128', 'Uri']
153
+
154
+ def map_csharp_primitive_to_clr_type(self, cs_type: str) -> str:
155
+ """ Maps C# primitive types to CLR types"""
156
+ map = {
157
+ "int": "Int32",
158
+ "long": "Int64",
159
+ "float": "Single",
160
+ "double": "Double",
161
+ "decimal": "Decimal",
162
+ "short": "Int16",
163
+ "sbyte": "SByte",
164
+ "byte": "Byte",
165
+ "ushort": "UInt16",
166
+ "uint": "UInt32",
167
+ "ulong": "UInt64",
168
+ "bool": "Boolean",
169
+ "string": "String",
170
+ "Guid": "Guid"
171
+ }
172
+ return map.get(cs_type, cs_type)
173
+
174
+ def resolve_ref(self, ref: str, context_schema: Optional[Dict] = None) -> Optional[Dict]:
175
+ """ Resolves a $ref to the actual schema definition """
176
+ # Check if it's an absolute URI reference (schema with $id)
177
+ if not ref.startswith('#/'):
178
+ # Try to resolve from schema registry
179
+ if ref in self.schema_registry:
180
+ return self.schema_registry[ref]
181
+ return None
182
+
183
+ # Handle fragment-only references (internal to document)
184
+ path = ref[2:].split('/')
185
+ schema = context_schema if context_schema else self.schema_doc
186
+
187
+ for part in path:
188
+ if not isinstance(schema, dict) or part not in schema:
189
+ return None
190
+ schema = schema[part]
191
+
192
+ return schema
193
+
194
+ def validate_abstract_ref(self, ref_schema: Dict, ref: str, is_extends_context: bool = False) -> None:
195
+ """
196
+ Validates that abstract types are only referenced in $extends context.
197
+ Per JSON Structure Core Spec Section 3.10.1, abstract types cannot be
198
+ directly instantiated and should only be referenced via $extends.
199
+
200
+ Args:
201
+ ref_schema: The resolved schema being referenced
202
+ ref: The $ref string for error reporting
203
+ is_extends_context: True if this reference is in a $extends context
204
+ """
205
+ import sys
206
+ is_abstract = ref_schema.get('abstract', False)
207
+ if is_abstract and not is_extends_context:
208
+ print(f"WARNING: Abstract type referenced outside $extends context: {ref}", file=sys.stderr)
209
+ print(f" Abstract types cannot be directly instantiated. Use $extends to inherit from them.", file=sys.stderr)
210
+
211
+ def register_schema_ids(self, schema: Dict, base_uri: str = '') -> None:
212
+ """ Recursively registers schemas with $id keywords """
213
+ if not isinstance(schema, dict):
214
+ return
215
+
216
+ # Register this schema if it has an $id
217
+ if '$id' in schema:
218
+ schema_id = schema['$id']
219
+ # Handle relative URIs
220
+ if base_uri and not schema_id.startswith(('http://', 'https://', 'urn:')):
221
+ from urllib.parse import urljoin
222
+ schema_id = urljoin(base_uri, schema_id)
223
+ self.schema_registry[schema_id] = schema
224
+ base_uri = schema_id # Update base URI for nested schemas
225
+
226
+ # Recursively process definitions
227
+ if 'definitions' in schema:
228
+ for def_name, def_schema in schema['definitions'].items():
229
+ if isinstance(def_schema, dict):
230
+ self.register_schema_ids(def_schema, base_uri)
231
+
232
+ # Recursively process properties
233
+ if 'properties' in schema:
234
+ for prop_name, prop_schema in schema['properties'].items():
235
+ if isinstance(prop_schema, dict):
236
+ self.register_schema_ids(prop_schema, base_uri)
237
+
238
+ # Recursively process items, values, etc.
239
+ for key in ['items', 'values', 'additionalProperties']:
240
+ if key in schema and isinstance(schema[key], dict):
241
+ self.register_schema_ids(schema[key], base_uri)
242
+
243
+ def convert_structure_type_to_csharp(self, class_name: str, field_name: str, structure_type: JsonNode, parent_namespace: str) -> str:
244
+ """ Converts JSON Structure type to C# type """
245
+ if isinstance(structure_type, str):
246
+ return self.map_primitive_to_csharp(structure_type)
247
+ elif isinstance(structure_type, list):
248
+ # Handle type unions
249
+ non_null_types = [t for t in structure_type if t != 'null']
250
+ if len(non_null_types) == 1:
251
+ # Nullable type
252
+ return f"{self.convert_structure_type_to_csharp(class_name, field_name, non_null_types[0], parent_namespace)}?"
253
+ else:
254
+ return self.generate_embedded_union(class_name, field_name, non_null_types, parent_namespace, write_file=True)
255
+ elif isinstance(structure_type, dict):
256
+ # Handle $ref
257
+ if '$ref' in structure_type:
258
+ ref_schema = self.resolve_ref(structure_type['$ref'], self.schema_doc)
259
+ if ref_schema:
260
+ # Validate abstract type usage (Section 3.10.1)
261
+ # Abstract types should only be referenced via $extends
262
+ self.validate_abstract_ref(ref_schema, structure_type['$ref'], is_extends_context=False)
263
+
264
+ # Extract type name from the ref
265
+ ref_path = structure_type['$ref'].split('/')
266
+ type_name = ref_path[-1]
267
+ ref_namespace = '.'.join(ref_path[2:-1]) if len(ref_path) > 3 else parent_namespace
268
+ return self.generate_class_or_choice(ref_schema, ref_namespace, write_file=True, explicit_name=type_name)
269
+ return 'object'
270
+
271
+ # Handle enum keyword - must be checked before 'type'
272
+ if 'enum' in structure_type:
273
+ return self.generate_enum(structure_type, field_name, parent_namespace, write_file=True)
274
+
275
+ # Handle type keyword
276
+ if 'type' not in structure_type:
277
+ return 'object'
278
+
279
+ struct_type = structure_type['type']
280
+
281
+ # Handle complex types
282
+ if struct_type == 'object':
283
+ return self.generate_class(structure_type, parent_namespace, write_file=True)
284
+ elif struct_type == 'array':
285
+ items_type = self.convert_structure_type_to_csharp(class_name, field_name+'List', structure_type.get('items', {'type': 'any'}), parent_namespace)
286
+ return f"List<{items_type}>"
287
+ elif struct_type == 'set':
288
+ items_type = self.convert_structure_type_to_csharp(class_name, field_name+'Set', structure_type.get('items', {'type': 'any'}), parent_namespace)
289
+ return f"HashSet<{items_type}>"
290
+ elif struct_type == 'map':
291
+ values_type = self.convert_structure_type_to_csharp(class_name, field_name+'Map', structure_type.get('values', {'type': 'any'}), parent_namespace)
292
+ return f"Dictionary<string, {values_type}>"
293
+ elif struct_type == 'choice':
294
+ return self.generate_choice(structure_type, parent_namespace, write_file=True)
295
+ elif struct_type == 'tuple':
296
+ return self.generate_tuple(structure_type, parent_namespace, write_file=True)
297
+ else:
298
+ return self.convert_structure_type_to_csharp(class_name, field_name, struct_type, parent_namespace)
299
+ return 'object'
300
+
301
+ def generate_class_or_choice(self, structure_schema: Dict, parent_namespace: str, write_file: bool = True, explicit_name: str = '') -> str:
302
+ """ Generates a Class or Choice """
303
+ struct_type = structure_schema.get('type', 'object')
304
+ if struct_type == 'object':
305
+ return self.generate_class(structure_schema, parent_namespace, write_file, explicit_name=explicit_name)
306
+ elif struct_type == 'choice':
307
+ return self.generate_choice(structure_schema, parent_namespace, write_file, explicit_name=explicit_name)
308
+ elif struct_type == 'tuple':
309
+ return self.generate_tuple(structure_schema, parent_namespace, write_file, explicit_name=explicit_name)
310
+ elif struct_type in ('map', 'array', 'set'):
311
+ # Root-level container types: generate wrapper class with implicit conversions
312
+ return self.generate_container_wrapper(structure_schema, parent_namespace, write_file, explicit_name=explicit_name)
313
+ return 'object'
314
+
315
+ def generate_class(self, structure_schema: Dict, parent_namespace: str, write_file: bool, explicit_name: str = '') -> str:
316
+ """ Generates a Class from JSON Structure object type """
317
+ class_definition = ''
318
+
319
+ # Get name and namespace
320
+ class_name = pascal(explicit_name if explicit_name else structure_schema.get('name', 'UnnamedClass'))
321
+ schema_namespace = structure_schema.get('namespace', parent_namespace)
322
+ namespace = pascal(self.concat_namespace(self.base_namespace, schema_namespace))
323
+ xml_namespace = structure_schema.get('xmlns', None)
324
+
325
+ ref = 'global::'+self.get_qualified_name(namespace, class_name)
326
+ if ref in self.generated_types:
327
+ return ref
328
+
329
+ # Check if this is an abstract type (Section 3.10.1)
330
+ is_abstract = structure_schema.get('abstract', False)
331
+
332
+ # Generate class documentation
333
+ doc = structure_schema.get('description', structure_schema.get('doc', class_name))
334
+ class_definition += f"/// <summary>\n/// {doc}\n/// </summary>\n"
335
+
336
+ if is_abstract:
337
+ class_definition += f"/// <remarks>\n/// This is an abstract type and cannot be instantiated directly.\n/// </remarks>\n"
338
+
339
+ # Add Obsolete attribute if deprecated
340
+ if structure_schema.get('deprecated', False):
341
+ deprecated_msg = structure_schema.get('description', f'{class_name} is deprecated')
342
+ class_definition += f"[System.Obsolete(\"{deprecated_msg}\")]\n"
343
+
344
+ # Add XML serialization attribute for the class if enabled
345
+ if self.system_xml_annotation:
346
+ if xml_namespace:
347
+ class_definition += f"[XmlRoot(\"{class_name}\", Namespace=\"{xml_namespace}\")]\n"
348
+ else:
349
+ class_definition += f"[XmlRoot(\"{class_name}\")]\n"
350
+
351
+ # Generate properties
352
+ properties = structure_schema.get('properties', {})
353
+ required_props = structure_schema.get('required', [])
354
+
355
+ # Handle alternative required sets
356
+ is_alternative_required = isinstance(required_props, list) and len(required_props) > 0 and isinstance(required_props[0], list)
357
+
358
+ # Check additionalProperties setting (Section 3.7.8)
359
+ additional_props = structure_schema.get('additionalProperties', True if is_abstract else None)
360
+
361
+ fields_str = []
362
+ for prop_name, prop_schema in properties.items():
363
+ field_def = self.generate_property(prop_name, prop_schema, class_name, schema_namespace, required_props)
364
+ fields_str.append(field_def)
365
+
366
+ # Add dictionary for additional properties if needed
367
+ if additional_props is not False and additional_props is not None:
368
+ fields_str.append(f"{INDENT}/// <summary>\n{INDENT}/// Additional properties not defined in schema\n{INDENT}/// </summary>\n")
369
+ # Use JsonExtensionData for automatic capture of unknown properties during deserialization
370
+ fields_str.append(f"{INDENT}[System.Text.Json.Serialization.JsonExtensionData]\n")
371
+ if isinstance(additional_props, dict):
372
+ # additionalProperties is a schema - use the typed value
373
+ value_type = self.convert_structure_type_to_csharp(class_name, 'additionalValue', additional_props, schema_namespace)
374
+ fields_str.append(f"{INDENT}public Dictionary<string, {value_type}>? AdditionalProperties {{ get; set; }}\n")
375
+ else:
376
+ # additionalProperties: true - allow any additional properties with boxed values
377
+ fields_str.append(f"{INDENT}public Dictionary<string, object>? AdditionalProperties {{ get; set; }}\n")
378
+
379
+ class_body = "\n".join(fields_str)
380
+
381
+ # Generate class declaration
382
+ abstract_modifier = "abstract " if is_abstract else ""
383
+ sealed_modifier = "sealed " if additional_props is False and not is_abstract else ""
384
+
385
+ class_definition += f"public {abstract_modifier}{sealed_modifier}partial class {class_name}\n{{\n{class_body}"
386
+
387
+ # Add default constructor (not for abstract classes with no concrete constructors)
388
+ if not is_abstract or properties:
389
+ class_definition += f"\n{INDENT}/// <summary>\n{INDENT}/// Default constructor\n{INDENT}/// </summary>\n"
390
+ constructor_modifier = "protected" if is_abstract else "public"
391
+ class_definition += f"{INDENT}{constructor_modifier} {class_name}()\n{INDENT}{{\n{INDENT}}}"
392
+
393
+ # Add helper methods from template if any annotations are enabled
394
+ if self.system_text_json_annotation or self.newtonsoft_json_annotation or self.system_xml_annotation:
395
+ class_definition += process_template(
396
+ "structuretocsharp/dataclass_core.jinja",
397
+ class_name=class_name,
398
+ system_text_json_annotation=self.system_text_json_annotation,
399
+ newtonsoft_json_annotation=self.newtonsoft_json_annotation,
400
+ system_xml_annotation=self.system_xml_annotation
401
+ )
402
+
403
+ # Generate Equals and GetHashCode
404
+ class_definition += self.generate_equals_and_gethashcode(structure_schema, class_name, schema_namespace)
405
+
406
+ class_definition += "\n"+"}"
407
+
408
+ if write_file:
409
+ self.write_to_file(namespace, class_name, class_definition)
410
+
411
+ self.generated_types[ref] = "class"
412
+ self.generated_structure_types[ref] = structure_schema
413
+ return ref
414
+
415
+ def generate_property(self, prop_name: str, prop_schema: Dict, class_name: str, parent_namespace: str, required_props: List) -> str:
416
+ """ Generates a property for a class """
417
+ property_definition = ''
418
+
419
+ # Resolve property name
420
+ field_name = prop_name
421
+ if self.is_csharp_reserved_word(field_name):
422
+ field_name = f"@{field_name}"
423
+ if self.pascal_properties:
424
+ field_name_cs = pascal(field_name)
425
+ else:
426
+ field_name_cs = field_name
427
+ if field_name_cs == class_name:
428
+ field_name_cs += "_"
429
+
430
+ # Check if this is a const field
431
+ if 'const' in prop_schema:
432
+ const_value = prop_schema['const']
433
+ prop_type = self.convert_structure_type_to_csharp(class_name, field_name, prop_schema, parent_namespace)
434
+
435
+ # Remove nullable marker for const
436
+ if prop_type.endswith('?'):
437
+ prop_type = prop_type[:-1]
438
+
439
+ # Generate documentation
440
+ doc = prop_schema.get('description', prop_schema.get('doc', field_name_cs))
441
+ property_definition += f"{INDENT}/// <summary>\n{INDENT}/// {doc}\n{INDENT}/// </summary>\n"
442
+
443
+ # Add JSON property name annotation when property name differs from schema name
444
+ # This is needed for proper JSON serialization/deserialization, especially with pascal_properties
445
+ if field_name != field_name_cs:
446
+ property_definition += f'{INDENT}[System.Text.Json.Serialization.JsonPropertyName("{prop_name}")]\n'
447
+ if self.newtonsoft_json_annotation and field_name != field_name_cs:
448
+ property_definition += f'{INDENT}[Newtonsoft.Json.JsonProperty("{prop_name}")]\n'
449
+
450
+ # Add XML element annotation if enabled
451
+ if self.system_xml_annotation:
452
+ property_definition += f'{INDENT}[System.Xml.Serialization.XmlElement("{prop_name}")]\n'
453
+
454
+ # Generate const field
455
+ const_val = self.format_default_value(const_value, prop_type)
456
+ property_definition += f"{INDENT}public const {prop_type} {field_name_cs} = {const_val};\n"
457
+
458
+ return property_definition
459
+
460
+ # Determine if required
461
+ is_required = prop_name in required_props if not isinstance(required_props, list) or len(required_props) == 0 or not isinstance(required_props[0], list) else any(prop_name in req_set for req_set in required_props)
462
+
463
+ # Get property type
464
+ prop_type = self.convert_structure_type_to_csharp(class_name, field_name, prop_schema, parent_namespace)
465
+
466
+ # Add nullable marker if not required and not already nullable
467
+ if not is_required and not prop_type.endswith('?') and not prop_type.startswith('List<') and not prop_type.startswith('HashSet<') and not prop_type.startswith('Dictionary<'):
468
+ prop_type += '?'
469
+
470
+ # Generate documentation
471
+ doc = prop_schema.get('description', prop_schema.get('doc', field_name_cs))
472
+ property_definition += f"{INDENT}/// <summary>\n{INDENT}/// {doc}\n{INDENT}/// </summary>\n"
473
+
474
+ # Add JSON property name annotation when property name differs from schema name
475
+ # This is needed for proper JSON serialization/deserialization, especially with pascal_properties
476
+ if field_name != field_name_cs:
477
+ property_definition += f'{INDENT}[System.Text.Json.Serialization.JsonPropertyName("{prop_name}")]\n'
478
+ if self.newtonsoft_json_annotation and field_name != field_name_cs:
479
+ property_definition += f'{INDENT}[Newtonsoft.Json.JsonProperty("{prop_name}")]\n'
480
+
481
+ # Add XML element annotation if enabled
482
+ if self.system_xml_annotation:
483
+ property_definition += f'{INDENT}[System.Xml.Serialization.XmlElement("{prop_name}")]\n'
484
+
485
+ # Add JSON Structure converters for types requiring string serialization
486
+ if self.system_text_json_annotation:
487
+ schema_type = prop_schema.get('type', '')
488
+ converter_type = self.get_json_structure_converter(schema_type, is_required)
489
+ if converter_type:
490
+ property_definition += f'{INDENT}[System.Text.Json.Serialization.JsonConverter(typeof({converter_type}))]\n'
491
+ self.needs_json_structure_converters = True
492
+
493
+ # Add validation attributes based on schema constraints
494
+ # StringLength attribute for maxLength
495
+ if 'maxLength' in prop_schema:
496
+ max_length = prop_schema['maxLength']
497
+ if 'minLength' in prop_schema:
498
+ min_length = prop_schema['minLength']
499
+ property_definition += f'{INDENT}[System.ComponentModel.DataAnnotations.StringLength({max_length}, MinimumLength = {min_length})]\n'
500
+ else:
501
+ property_definition += f'{INDENT}[System.ComponentModel.DataAnnotations.StringLength({max_length})]\n'
502
+ elif 'minLength' in prop_schema:
503
+ # MinLength only (no max)
504
+ min_length = prop_schema['minLength']
505
+ property_definition += f'{INDENT}[System.ComponentModel.DataAnnotations.MinLength({min_length})]\n'
506
+
507
+ # RegularExpression attribute for pattern
508
+ if 'pattern' in prop_schema:
509
+ pattern = prop_schema['pattern'].replace('\\', '\\\\').replace('"', '\\"')
510
+ property_definition += f'{INDENT}[System.ComponentModel.DataAnnotations.RegularExpression(@"{pattern}")]\n'
511
+
512
+ # Range attribute for minimum/maximum on numeric types
513
+ if 'minimum' in prop_schema or 'maximum' in prop_schema:
514
+ min_val = prop_schema.get('minimum', prop_schema.get('exclusiveMinimum', 'double.MinValue'))
515
+ max_val = prop_schema.get('maximum', prop_schema.get('exclusiveMaximum', 'double.MaxValue'))
516
+
517
+ # Convert to appropriate format
518
+ if isinstance(min_val, (int, float)):
519
+ min_str = str(min_val)
520
+ else:
521
+ min_str = str(min_val)
522
+
523
+ if isinstance(max_val, (int, float)):
524
+ max_str = str(max_val)
525
+ else:
526
+ max_str = str(max_val)
527
+
528
+ property_definition += f'{INDENT}[System.ComponentModel.DataAnnotations.Range({min_str}, {max_str})]\n'
529
+
530
+ # Add Obsolete attribute if deprecated
531
+ if prop_schema.get('deprecated', False):
532
+ deprecated_msg = prop_schema.get('description', f'{prop_name} is deprecated')
533
+ property_definition += f'{INDENT}[System.Obsolete("{deprecated_msg}")]\n'
534
+
535
+ # Generate property with required modifier if needed
536
+ required_modifier = "required " if is_required and not prop_type.endswith('?') else ""
537
+
538
+ # Handle readOnly and writeOnly
539
+ is_read_only = prop_schema.get('readOnly', False)
540
+ is_write_only = prop_schema.get('writeOnly', False)
541
+
542
+ if is_read_only:
543
+ # readOnly: private or init-only setter
544
+ property_definition += f"{INDENT}public {required_modifier}{prop_type} {field_name_cs} {{ get; init; }}"
545
+ elif is_write_only:
546
+ # writeOnly: private getter
547
+ property_definition += f"{INDENT}public {required_modifier}{prop_type} {field_name_cs} {{ private get; set; }}"
548
+ else:
549
+ # Normal property
550
+ property_definition += f"{INDENT}public {required_modifier}{prop_type} {field_name_cs} {{ get; set; }}"
551
+
552
+ # Add default value if present
553
+ if 'default' in prop_schema:
554
+ default_val = self.format_default_value(prop_schema['default'], prop_type)
555
+ property_definition += f" = {default_val};\n"
556
+ else:
557
+ property_definition += "\n"
558
+
559
+ return property_definition
560
+
561
+ def format_default_value(self, value: Any, csharp_type: str) -> str:
562
+ """ Formats a default value for C# """
563
+ if value is None:
564
+ return "null"
565
+ elif isinstance(value, bool):
566
+ return "true" if value else "false"
567
+ elif isinstance(value, str):
568
+ return f'"{value}"'
569
+ elif isinstance(value, (int, float)):
570
+ return str(value)
571
+ elif isinstance(value, list):
572
+ return f"new {csharp_type}()"
573
+ elif isinstance(value, dict):
574
+ return f"new {csharp_type}()"
575
+ return f"default({csharp_type})"
576
+
577
+ def generate_enum(self, structure_schema: Dict, field_name: str, parent_namespace: str, write_file: bool) -> str:
578
+ """ Generates a C# enum from JSON Structure enum keyword """
579
+ enum_values = structure_schema.get('enum', [])
580
+ if not enum_values:
581
+ return 'object'
582
+
583
+ # Determine enum name from field name
584
+ enum_name = pascal(field_name) + 'Enum' if field_name else 'UnnamedEnum'
585
+ schema_namespace = structure_schema.get('namespace', parent_namespace)
586
+ namespace = pascal(self.concat_namespace(self.base_namespace, schema_namespace))
587
+
588
+ ref = 'global::'+self.get_qualified_name(namespace, enum_name)
589
+ if ref in self.generated_types:
590
+ return ref
591
+
592
+ # Determine underlying type
593
+ base_type = structure_schema.get('type', 'string')
594
+
595
+ # For string enums, we don't specify an underlying type
596
+ # For numeric enums, we map the type
597
+ numeric_types = ['int8', 'uint8', 'int16', 'uint16', 'int32', 'uint32', 'int64', 'uint64']
598
+ is_numeric = base_type in numeric_types
599
+
600
+ enum_definition = ''
601
+ doc = structure_schema.get('description', structure_schema.get('doc', enum_name))
602
+ enum_definition += f"/// <summary>\n/// {doc}\n/// </summary>\n"
603
+
604
+ # Add Obsolete attribute if deprecated
605
+ if structure_schema.get('deprecated', False):
606
+ deprecated_msg = structure_schema.get('description', f'{enum_name} is deprecated')
607
+ enum_definition += f"[System.Obsolete(\"{deprecated_msg}\")]\n"
608
+
609
+ # Add converter attributes - always include System.Text.Json since it's the default .NET serializer
610
+ # This ensures enums serialize correctly with proper value mapping even if system_text_json_annotation is False
611
+ enum_definition += f"[System.Text.Json.Serialization.JsonConverter(typeof({enum_name}Converter))]\n"
612
+ if self.newtonsoft_json_annotation:
613
+ enum_definition += f"[Newtonsoft.Json.JsonConverter(typeof({enum_name}NewtonsoftConverter))]\n"
614
+
615
+ if is_numeric:
616
+ cs_base_type = self.map_primitive_to_csharp(base_type)
617
+ enum_definition += f"public enum {enum_name} : {cs_base_type}\n{{\n"
618
+ else:
619
+ enum_definition += f"public enum {enum_name}\n{{\n"
620
+
621
+ # Generate enum members
622
+ for i, value in enumerate(enum_values):
623
+ if is_numeric:
624
+ # Numeric enum - use the value directly
625
+ member_name = f"Value{value}" # Prefix with "Value" since enum members can't start with numbers
626
+ enum_definition += f"{INDENT}{member_name} = {value}"
627
+ else:
628
+ # String enum - create member from the string
629
+ member_name = pascal(str(value).replace('-', '_').replace(' ', '_'))
630
+ enum_definition += f"{INDENT}{member_name}"
631
+
632
+ if i < len(enum_values) - 1:
633
+ enum_definition += ",\n"
634
+ else:
635
+ enum_definition += "\n"
636
+
637
+ enum_definition += "}\n\n"
638
+
639
+ # Always generate System.Text.Json converter since it's the default .NET serializer
640
+ # This ensures enums serialize correctly even when system_text_json_annotation is False
641
+ enum_definition += f"/// <summary>\n/// System.Text.Json converter for {enum_name} that maps to schema values\n/// </summary>\n"
642
+ enum_definition += f"public class {enum_name}Converter : System.Text.Json.Serialization.JsonConverter<{enum_name}>\n{{\n"
643
+
644
+ # Read method
645
+ enum_definition += f"{INDENT}/// <inheritdoc/>\n"
646
+ enum_definition += f"{INDENT}public override {enum_name} Read(ref System.Text.Json.Utf8JsonReader reader, Type typeToConvert, System.Text.Json.JsonSerializerOptions options)\n"
647
+ enum_definition += f"{INDENT}{{\n"
648
+
649
+ if is_numeric:
650
+ enum_definition += f"{INDENT*2}if (reader.TokenType == System.Text.Json.JsonTokenType.Number)\n"
651
+ enum_definition += f"{INDENT*2}{{\n"
652
+ enum_definition += f"{INDENT*3}return ({enum_name})reader.GetInt32();\n"
653
+ enum_definition += f"{INDENT*2}}}\n"
654
+ enum_definition += f"{INDENT*2}throw new System.Text.Json.JsonException($\"Expected number for {enum_name}\");\n"
655
+ else:
656
+ enum_definition += f"{INDENT*2}var stringValue = reader.GetString();\n"
657
+ enum_definition += f"{INDENT*2}return stringValue switch\n"
658
+ enum_definition += f"{INDENT*2}{{\n"
659
+ for value in enum_values:
660
+ member_name = pascal(str(value).replace('-', '_').replace(' ', '_'))
661
+ enum_definition += f'{INDENT*3}"{value}" => {enum_name}.{member_name},\n'
662
+ enum_definition += f'{INDENT*3}_ => throw new System.Text.Json.JsonException($"Unknown value \'{{stringValue}}\' for {enum_name}")\n'
663
+ enum_definition += f"{INDENT*2}}};\n"
664
+
665
+ enum_definition += f"{INDENT}}}\n\n"
666
+
667
+ # Write method
668
+ enum_definition += f"{INDENT}/// <inheritdoc/>\n"
669
+ enum_definition += f"{INDENT}public override void Write(System.Text.Json.Utf8JsonWriter writer, {enum_name} value, System.Text.Json.JsonSerializerOptions options)\n"
670
+ enum_definition += f"{INDENT}{{\n"
671
+
672
+ if is_numeric:
673
+ enum_definition += f"{INDENT*2}writer.WriteNumberValue((int)value);\n"
674
+ else:
675
+ enum_definition += f"{INDENT*2}var stringValue = value switch\n"
676
+ enum_definition += f"{INDENT*2}{{\n"
677
+ for value in enum_values:
678
+ member_name = pascal(str(value).replace('-', '_').replace(' ', '_'))
679
+ enum_definition += f'{INDENT*3}{enum_name}.{member_name} => "{value}",\n'
680
+ enum_definition += f'{INDENT*3}_ => throw new System.ArgumentOutOfRangeException(nameof(value))\n'
681
+ enum_definition += f"{INDENT*2}}};\n"
682
+ enum_definition += f"{INDENT*2}writer.WriteStringValue(stringValue);\n"
683
+
684
+ enum_definition += f"{INDENT}}}\n"
685
+ enum_definition += "}\n\n"
686
+
687
+ # Generate Newtonsoft.Json converter when enabled
688
+ if self.newtonsoft_json_annotation:
689
+ enum_definition += f"/// <summary>\n/// Newtonsoft.Json converter for {enum_name} that maps to schema values\n/// </summary>\n"
690
+ enum_definition += f"public class {enum_name}NewtonsoftConverter : Newtonsoft.Json.JsonConverter<{enum_name}>\n{{\n"
691
+
692
+ # ReadJson method
693
+ enum_definition += f"{INDENT}/// <inheritdoc/>\n"
694
+ enum_definition += f"{INDENT}public override {enum_name} ReadJson(Newtonsoft.Json.JsonReader reader, Type objectType, {enum_name} existingValue, bool hasExistingValue, Newtonsoft.Json.JsonSerializer serializer)\n"
695
+ enum_definition += f"{INDENT}{{\n"
696
+
697
+ if is_numeric:
698
+ enum_definition += f"{INDENT*2}if (reader.TokenType == Newtonsoft.Json.JsonToken.Integer)\n"
699
+ enum_definition += f"{INDENT*2}{{\n"
700
+ enum_definition += f"{INDENT*3}return ({enum_name})Convert.ToInt32(reader.Value);\n"
701
+ enum_definition += f"{INDENT*2}}}\n"
702
+ enum_definition += f"{INDENT*2}throw new Newtonsoft.Json.JsonException($\"Expected number for {enum_name}\");\n"
703
+ else:
704
+ enum_definition += f"{INDENT*2}var stringValue = reader.Value?.ToString();\n"
705
+ enum_definition += f"{INDENT*2}return stringValue switch\n"
706
+ enum_definition += f"{INDENT*2}{{\n"
707
+ for value in enum_values:
708
+ member_name = pascal(str(value).replace('-', '_').replace(' ', '_'))
709
+ enum_definition += f'{INDENT*3}"{value}" => {enum_name}.{member_name},\n'
710
+ enum_definition += f'{INDENT*3}_ => throw new Newtonsoft.Json.JsonException($"Unknown value \'{{stringValue}}\' for {enum_name}")\n'
711
+ enum_definition += f"{INDENT*2}}};\n"
712
+
713
+ enum_definition += f"{INDENT}}}\n\n"
714
+
715
+ # WriteJson method
716
+ enum_definition += f"{INDENT}/// <inheritdoc/>\n"
717
+ enum_definition += f"{INDENT}public override void WriteJson(Newtonsoft.Json.JsonWriter writer, {enum_name} value, Newtonsoft.Json.JsonSerializer serializer)\n"
718
+ enum_definition += f"{INDENT}{{\n"
719
+
720
+ if is_numeric:
721
+ enum_definition += f"{INDENT*2}writer.WriteValue((int)value);\n"
722
+ else:
723
+ enum_definition += f"{INDENT*2}var stringValue = value switch\n"
724
+ enum_definition += f"{INDENT*2}{{\n"
725
+ for value in enum_values:
726
+ member_name = pascal(str(value).replace('-', '_').replace(' ', '_'))
727
+ enum_definition += f'{INDENT*3}{enum_name}.{member_name} => "{value}",\n'
728
+ enum_definition += f'{INDENT*3}_ => throw new System.ArgumentOutOfRangeException(nameof(value))\n'
729
+ enum_definition += f"{INDENT*2}}};\n"
730
+ enum_definition += f"{INDENT*2}writer.WriteValue(stringValue);\n"
731
+
732
+ enum_definition += f"{INDENT}}}\n"
733
+ enum_definition += "}\n"
734
+
735
+ if write_file:
736
+ self.write_to_file(namespace, enum_name, enum_definition)
737
+
738
+ self.generated_types[ref] = "enum"
739
+ self.generated_structure_types[ref] = structure_schema
740
+ return ref
741
+
742
+ def generate_choice(self, structure_schema: Dict, parent_namespace: str, write_file: bool, explicit_name: str = '') -> str:
743
+ """ Generates a discriminated union (choice) type """
744
+ # Choice types in JSON Structure can be:
745
+ # 1. Tagged unions - single property with the choice type as key
746
+ # 2. Inline unions - with $extends and selector
747
+
748
+ class_name = pascal(explicit_name if explicit_name else structure_schema.get('name', 'UnnamedChoice'))
749
+ schema_namespace = structure_schema.get('namespace', parent_namespace)
750
+ namespace = pascal(self.concat_namespace(self.base_namespace, schema_namespace))
751
+
752
+ ref = 'global::'+self.get_qualified_name(namespace, class_name)
753
+ if ref in self.generated_types:
754
+ return ref
755
+
756
+ choices = structure_schema.get('choices', {})
757
+ selector = structure_schema.get('selector')
758
+ extends = structure_schema.get('$extends')
759
+
760
+ if extends and selector:
761
+ # Inline union - generate as inheritance hierarchy
762
+ return self.generate_inline_union(structure_schema, parent_namespace, write_file, explicit_name)
763
+ else:
764
+ # Tagged union - generate as a union class similar to Avro
765
+ return self.generate_tagged_union(structure_schema, parent_namespace, write_file, explicit_name)
766
+
767
+ def generate_tagged_union(self, structure_schema: Dict, parent_namespace: str, write_file: bool, explicit_name: str = '') -> str:
768
+ """ Generates a tagged union type """
769
+ class_name = pascal(explicit_name if explicit_name else structure_schema.get('name', 'UnnamedChoice'))
770
+ schema_namespace = structure_schema.get('namespace', parent_namespace)
771
+ namespace = pascal(self.concat_namespace(self.base_namespace, schema_namespace))
772
+
773
+ ref = 'global::'+self.get_qualified_name(namespace, class_name)
774
+ if ref in self.generated_types:
775
+ return ref
776
+
777
+ choices = structure_schema.get('choices', {})
778
+ choice_types = []
779
+
780
+ for choice_name, choice_schema in choices.items():
781
+ choice_type = self.convert_structure_type_to_csharp(class_name, choice_name, choice_schema, schema_namespace)
782
+ choice_types.append((choice_name, choice_type))
783
+
784
+ # Generate the union class similar to Avro unions
785
+ class_definition = f"/// <summary>\n/// {structure_schema.get('description', class_name)}\n/// </summary>\n"
786
+ # Add JsonConverter attribute for proper tagged union serialization
787
+ class_definition += f"[System.Text.Json.Serialization.JsonConverter(typeof({class_name}JsonConverter))]\n"
788
+ class_definition += f"public partial class {class_name}\n{{\n"
789
+
790
+ # Generate properties for each choice
791
+ for choice_name, choice_type in choice_types:
792
+ prop_name = pascal(choice_name)
793
+ class_definition += f"{INDENT}/// <summary>\n{INDENT}/// Gets or sets the {prop_name} value\n{INDENT}/// </summary>\n"
794
+ class_definition += f"{INDENT}public {choice_type}? {prop_name} {{ get; set; }} = null;\n"
795
+
796
+ # Add constructor
797
+ class_definition += f"\n{INDENT}/// <summary>\n{INDENT}/// Default constructor\n{INDENT}/// </summary>\n"
798
+ class_definition += f"{INDENT}public {class_name}()\n{INDENT}{{\n{INDENT}}}\n"
799
+
800
+ # Add constructors for each choice
801
+ for choice_name, choice_type in choice_types:
802
+ prop_name = pascal(choice_name)
803
+ class_definition += f"\n{INDENT}/// <summary>\n{INDENT}/// Constructor for {prop_name} values\n{INDENT}/// </summary>\n"
804
+ class_definition += f"{INDENT}public {class_name}({choice_type} {prop_name.lower()})\n{INDENT}{{\n"
805
+ class_definition += f"{INDENT*2}this.{prop_name} = {prop_name.lower()};\n{INDENT}}}\n"
806
+
807
+ # Generate Equals and GetHashCode for choice types
808
+ class_definition += f"\n{INDENT}/// <summary>\n{INDENT}/// Determines whether the specified object is equal to the current object.\n{INDENT}/// </summary>\n"
809
+ class_definition += f"{INDENT}public override bool Equals(object? obj)\n{INDENT}{{\n"
810
+ class_definition += f"{INDENT*2}if (obj is not {class_name} other) return false;\n"
811
+
812
+ # Compare each choice property
813
+ equality_checks = []
814
+ for choice_name, choice_type in choice_types:
815
+ prop_name = pascal(choice_name)
816
+ equality_checks.append(f"Equals(this.{prop_name}, other.{prop_name})")
817
+
818
+ if len(equality_checks) == 1:
819
+ class_definition += f"{INDENT*2}return {equality_checks[0]};\n"
820
+ else:
821
+ class_definition += f"{INDENT*2}return " + f"\n{INDENT*3}&& ".join(equality_checks) + ";\n"
822
+
823
+ class_definition += f"{INDENT}}}\n\n"
824
+
825
+ # Generate GetHashCode
826
+ class_definition += f"{INDENT}/// <summary>\n{INDENT}/// Serves as the default hash function.\n{INDENT}/// </summary>\n"
827
+ class_definition += f"{INDENT}public override int GetHashCode()\n{INDENT}{{\n"
828
+
829
+ if len(choice_types) <= 8:
830
+ hash_fields = [f"this.{pascal(choice_name)}" for choice_name, _ in choice_types]
831
+ class_definition += f"{INDENT*2}return HashCode.Combine({', '.join(hash_fields)});\n"
832
+ else:
833
+ class_definition += f"{INDENT*2}var hash = new HashCode();\n"
834
+ for choice_name, _ in choice_types:
835
+ prop_name = pascal(choice_name)
836
+ class_definition += f"{INDENT*2}hash.Add(this.{prop_name});\n"
837
+ class_definition += f"{INDENT*2}return hash.ToHashCode();\n"
838
+
839
+ class_definition += f"{INDENT}}}\n"
840
+
841
+ class_definition += "}\n\n"
842
+
843
+ # Generate JSON converter for tagged union serialization
844
+ class_definition += f"/// <summary>\n/// JSON converter for {class_name} tagged union - serializes only the non-null choice\n/// </summary>\n"
845
+ class_definition += f"public class {class_name}JsonConverter : System.Text.Json.Serialization.JsonConverter<{class_name}>\n{{\n"
846
+
847
+ # Read method
848
+ class_definition += f"{INDENT}/// <inheritdoc/>\n"
849
+ class_definition += f"{INDENT}public override {class_name}? Read(ref System.Text.Json.Utf8JsonReader reader, Type typeToConvert, System.Text.Json.JsonSerializerOptions options)\n"
850
+ class_definition += f"{INDENT}{{\n"
851
+ class_definition += f"{INDENT*2}if (reader.TokenType == System.Text.Json.JsonTokenType.Null) return null;\n"
852
+ class_definition += f"{INDENT*2}if (reader.TokenType != System.Text.Json.JsonTokenType.StartObject)\n"
853
+ class_definition += f"{INDENT*3}throw new System.Text.Json.JsonException(\"Expected object for tagged union\");\n"
854
+ class_definition += f"{INDENT*2}var result = new {class_name}();\n"
855
+ class_definition += f"{INDENT*2}while (reader.Read())\n"
856
+ class_definition += f"{INDENT*2}{{\n"
857
+ class_definition += f"{INDENT*3}if (reader.TokenType == System.Text.Json.JsonTokenType.EndObject) break;\n"
858
+ class_definition += f"{INDENT*3}if (reader.TokenType != System.Text.Json.JsonTokenType.PropertyName)\n"
859
+ class_definition += f"{INDENT*4}throw new System.Text.Json.JsonException(\"Expected property name\");\n"
860
+ class_definition += f"{INDENT*3}var propName = reader.GetString();\n"
861
+ class_definition += f"{INDENT*3}reader.Read();\n"
862
+ class_definition += f"{INDENT*3}switch (propName)\n"
863
+ class_definition += f"{INDENT*3}{{\n"
864
+ for choice_name, choice_type in choice_types:
865
+ # Use original schema property name for matching
866
+ class_definition += f'{INDENT*4}case "{choice_name}":\n'
867
+ class_definition += f"{INDENT*5}result.{pascal(choice_name)} = System.Text.Json.JsonSerializer.Deserialize<{choice_type}>(ref reader, options);\n"
868
+ class_definition += f"{INDENT*5}break;\n"
869
+ class_definition += f"{INDENT*4}default:\n"
870
+ class_definition += f"{INDENT*5}reader.Skip();\n"
871
+ class_definition += f"{INDENT*5}break;\n"
872
+ class_definition += f"{INDENT*3}}}\n"
873
+ class_definition += f"{INDENT*2}}}\n"
874
+ class_definition += f"{INDENT*2}return result;\n"
875
+ class_definition += f"{INDENT}}}\n\n"
876
+
877
+ # Write method - only write the non-null choice
878
+ class_definition += f"{INDENT}/// <inheritdoc/>\n"
879
+ class_definition += f"{INDENT}public override void Write(System.Text.Json.Utf8JsonWriter writer, {class_name} value, System.Text.Json.JsonSerializerOptions options)\n"
880
+ class_definition += f"{INDENT}{{\n"
881
+ class_definition += f"{INDENT*2}writer.WriteStartObject();\n"
882
+ for i, (choice_name, choice_type) in enumerate(choice_types):
883
+ prop_name = pascal(choice_name)
884
+ condition = "if" if i == 0 else "else if"
885
+ class_definition += f"{INDENT*2}{condition} (value.{prop_name} != null)\n"
886
+ class_definition += f"{INDENT*2}{{\n"
887
+ class_definition += f'{INDENT*3}writer.WritePropertyName("{choice_name}");\n'
888
+ class_definition += f"{INDENT*3}System.Text.Json.JsonSerializer.Serialize(writer, value.{prop_name}, options);\n"
889
+ class_definition += f"{INDENT*2}}}\n"
890
+ class_definition += f"{INDENT*2}writer.WriteEndObject();\n"
891
+ class_definition += f"{INDENT}}}\n"
892
+ class_definition += "}\n"
893
+
894
+ if write_file:
895
+ self.write_to_file(namespace, class_name, class_definition)
896
+
897
+ self.generated_types[ref] = "choice"
898
+ self.generated_structure_types[ref] = structure_schema
899
+ return ref
900
+
901
+ def generate_inline_union(self, structure_schema: Dict, parent_namespace: str, write_file: bool, explicit_name: str = '') -> str:
902
+ """ Generates an inline union type with inheritance """
903
+ # For inline unions, we generate an abstract base class and derived classes
904
+ # The selector property indicates which derived class is being used
905
+
906
+ class_name = pascal(explicit_name if explicit_name else structure_schema.get('name', 'UnnamedChoice'))
907
+ schema_namespace = structure_schema.get('namespace', parent_namespace)
908
+ namespace = pascal(self.concat_namespace(self.base_namespace, schema_namespace))
909
+
910
+ ref = 'global::'+self.get_qualified_name(namespace, class_name)
911
+ if ref in self.generated_types:
912
+ return ref
913
+
914
+ # Get base class from $extends
915
+ extends_ref = structure_schema.get('$extends', '')
916
+ if extends_ref and isinstance(extends_ref, str):
917
+ base_schema = self.resolve_ref(extends_ref, self.schema_doc)
918
+ if not base_schema:
919
+ # Try resolving relative to the structure_schema itself
920
+ base_schema = self.resolve_ref(extends_ref, structure_schema)
921
+
922
+ # Validate abstract type usage - $extends is ALLOWED to reference abstract types
923
+ if base_schema:
924
+ self.validate_abstract_ref(base_schema, extends_ref, is_extends_context=True)
925
+ else:
926
+ base_schema = None
927
+
928
+ if not base_schema:
929
+ # Fallback to tagged union if no base
930
+ return self.generate_tagged_union(structure_schema, parent_namespace, write_file, explicit_name)
931
+
932
+ # First, ensure base class is generated (if it's abstract, it won't be referenced directly)
933
+ base_schema_copy = base_schema.copy()
934
+ if 'name' not in base_schema_copy:
935
+ # Extract name from $extends ref
936
+ base_name = extends_ref.split('/')[-1]
937
+ base_schema_copy['name'] = base_name
938
+ base_class_ref = self.generate_class(base_schema_copy, schema_namespace, write_file)
939
+ base_class_name = base_class_ref.split('::')[-1].split('.')[-1]
940
+
941
+ choices = structure_schema.get('choices', {})
942
+ selector = structure_schema.get('selector', 'type')
943
+
944
+ # Generate abstract base class with selector property
945
+ class_definition = f"/// <summary>\n/// {structure_schema.get('description', class_name + ' (inline union base)')}\n/// </summary>\n"
946
+
947
+ if self.system_text_json_annotation:
948
+ class_definition += f'[System.Text.Json.Serialization.JsonPolymorphic(TypeDiscriminatorPropertyName = "{selector}")]\n'
949
+ for choice_name in choices.keys():
950
+ derived_class_name = pascal(choice_name)
951
+ class_definition += f'[System.Text.Json.Serialization.JsonDerivedType(typeof({derived_class_name}), "{choice_name}")]\n'
952
+
953
+ class_definition += f"public abstract partial class {class_name}"
954
+
955
+ # Inherit from base class if it exists
956
+ if base_class_name and base_class_name != class_name:
957
+ class_definition += f" : {base_class_name}"
958
+
959
+ class_definition += "\n{\n"
960
+
961
+ # Add selector property (not required since derived classes set it in constructor)
962
+ class_definition += f"{INDENT}/// <summary>\n{INDENT}/// Type discriminator\n{INDENT}/// </summary>\n"
963
+ if self.system_text_json_annotation:
964
+ class_definition += f'{INDENT}[System.Text.Json.Serialization.JsonPropertyName("{selector}")]\n'
965
+
966
+ # Check if selector is already in base properties
967
+ base_has_selector = selector in base_schema.get('properties', {})
968
+ if base_has_selector:
969
+ class_definition += f"{INDENT}public new string {pascal(selector)} {{ get; set; }} = \"\";\n"
970
+ else:
971
+ class_definition += f"{INDENT}public string {pascal(selector)} {{ get; set; }} = \"\";\n"
972
+
973
+ class_definition += "}"
974
+
975
+ if write_file:
976
+ self.write_to_file(namespace, class_name, class_definition)
977
+
978
+ # Generate derived classes for each choice with property merging
979
+ for choice_name, choice_schema_ref in choices.items():
980
+ # Resolve the choice schema
981
+ if isinstance(choice_schema_ref, dict) and '$ref' in choice_schema_ref:
982
+ choice_schema = self.resolve_ref(choice_schema_ref['$ref'], self.schema_doc)
983
+ if not choice_schema:
984
+ # Try resolving relative to the structure_schema itself
985
+ choice_schema = self.resolve_ref(choice_schema_ref['$ref'], structure_schema)
986
+ else:
987
+ choice_schema = choice_schema_ref
988
+
989
+ if not choice_schema or not isinstance(choice_schema, dict):
990
+ continue
991
+
992
+ # Mark this choice as generated to prevent duplicate generation in process_definitions
993
+ derived_class_name = pascal(choice_name)
994
+ derived_namespace = pascal(self.concat_namespace(self.base_namespace, schema_namespace))
995
+ derived_ref = 'global::'+self.get_qualified_name(derived_namespace, derived_class_name)
996
+
997
+ # Only generate if not already generated
998
+ if derived_ref in self.generated_types:
999
+ continue
1000
+
1001
+ # Merge properties from base schema into choice schema
1002
+ merged_schema = self.merge_inherited_properties(choice_schema, base_schema, class_name)
1003
+ merged_schema['name'] = choice_name
1004
+ merged_schema['namespace'] = schema_namespace
1005
+
1006
+ # Mark that this extends the union base
1007
+ merged_schema['$extends_inline_union'] = class_name
1008
+
1009
+ # Generate the derived class
1010
+ self.generate_derived_class(merged_schema, class_name, choice_name, selector, schema_namespace, write_file)
1011
+
1012
+ self.generated_types[ref] = "choice"
1013
+ self.generated_structure_types[ref] = structure_schema
1014
+ return ref
1015
+
1016
+ def merge_inherited_properties(self, derived_schema: Dict, base_schema: Dict, union_class_name: str) -> Dict:
1017
+ """ Merges properties from base schema into derived schema """
1018
+ merged = derived_schema.copy()
1019
+
1020
+ # Get properties from both schemas
1021
+ base_props = base_schema.get('properties', {})
1022
+ derived_props = merged.get('properties', {})
1023
+
1024
+ # Track which properties come from base (for filtering during generation)
1025
+ base_property_names = list(base_props.keys())
1026
+
1027
+ # Merge properties (derived overrides base)
1028
+ merged_props = {}
1029
+ merged_props.update(base_props)
1030
+ merged_props.update(derived_props)
1031
+ merged['properties'] = merged_props
1032
+
1033
+ # Store base property names so we can skip them during code generation
1034
+ merged['$base_properties'] = base_property_names
1035
+
1036
+ # Merge required fields
1037
+ base_required = base_schema.get('required', [])
1038
+ derived_required = merged.get('required', [])
1039
+ if isinstance(base_required, list) and isinstance(derived_required, list):
1040
+ # Combine and deduplicate
1041
+ merged['required'] = list(set(base_required + derived_required))
1042
+
1043
+ return merged
1044
+
1045
+ def generate_derived_class(self, schema: Dict, base_class_name: str, choice_name: str, selector: str, parent_namespace: str, write_file: bool) -> str:
1046
+ """ Generates a derived class for inline union """
1047
+ class_name = pascal(choice_name)
1048
+ namespace = pascal(self.concat_namespace(self.base_namespace, schema.get('namespace', parent_namespace)))
1049
+
1050
+ ref = 'global::'+self.get_qualified_name(namespace, class_name)
1051
+ if ref in self.generated_types:
1052
+ return ref
1053
+
1054
+ # Generate class with inheritance
1055
+ doc = schema.get('description', f'{class_name} - {choice_name} variant')
1056
+ class_definition = f"/// <summary>\n/// {doc}\n/// </summary>\n"
1057
+
1058
+ class_definition += f"public partial class {class_name} : {base_class_name}\n{{\n"
1059
+
1060
+ # Generate properties (only the derived-specific ones, base properties are inherited)
1061
+ properties = schema.get('properties', {})
1062
+ required_props = schema.get('required', [])
1063
+
1064
+ # Get base class properties to filter them out
1065
+ # We need to find the base schema to know what properties to exclude
1066
+ # For now, we'll generate all properties from merged schema
1067
+ # NOTE: BaseAddress properties come through the merged schema but shouldn't be redeclared
1068
+ # We need to identify which properties are from the BASE vs which are NEW
1069
+
1070
+ # Get the original (non-merged) choice schema to determine NEW properties
1071
+ # The merged schema has all properties; we want only the ones NOT in base
1072
+ # Since we don't have access to the original choice schema here, we'll use a heuristic:
1073
+ # Properties marked with a special flag during merging
1074
+
1075
+ # Alternative approach: Only generate properties that are NOT in the InlineChoice base
1076
+ # But InlineChoice only has the selector, not the BaseAddress properties
1077
+ # So we need to look further up the chain
1078
+
1079
+ # SIMPLEST SOLUTION: Filter out properties that come from the extended base
1080
+ # We can detect this by checking if the property exists in the base_schema context
1081
+ # But we don't have base_schema in this method
1082
+
1083
+ # FOR NOW: Generate all properties but mark inherited ones with 'new'
1084
+ # Actually, C# doesn't allow 'new required' - that's the error
1085
+ # So we MUST skip inherited properties entirely
1086
+
1087
+ # The merged schema has ALL properties. We need to skip base properties.
1088
+ # The base properties are those NOT in the original choice schema
1089
+ # We need to pass the original choice schema properties to know what to generate
1090
+
1091
+ # CORRECT FIX: Only generate properties from the ORIGINAL choice schema, not merged
1092
+ # But wait - we're passing merged_schema which has all properties
1093
+ # We need to differentiate
1094
+
1095
+ # Let's add a marker during merge to track which properties are from base
1096
+ # Or better: pass BOTH original and merged schemas
1097
+
1098
+ # QUICK FIX: Check if selector property to skip, and skip properties that were
1099
+ # in the base by checking schema metadata
1100
+
1101
+ # Since schema has '$extends_inline_union', we can use that
1102
+ # But we need the ORIGINAL choice properties, not merged
1103
+
1104
+ # The issue is we're generating from merged_schema which has ALL properties
1105
+ # We need to know which properties are NEW (from choice) vs inherited (from base)
1106
+
1107
+ # SOLUTION: Don't generate inherited properties - but how to identify them?
1108
+ # We could store in merged_schema a list of base property names
1109
+
1110
+ # Let me fix this by adding a key to mark base properties
1111
+ base_properties = schema.get('$base_properties', [])
1112
+
1113
+ for prop_name, prop_schema in properties.items():
1114
+ # Skip selector - it's defined in base as required
1115
+ if prop_name == selector:
1116
+ continue
1117
+ # Skip properties inherited from base schema
1118
+ if prop_name in base_properties:
1119
+ continue
1120
+ field_def = self.generate_property(prop_name, prop_schema, class_name, parent_namespace, required_props)
1121
+ class_definition += field_def
1122
+
1123
+ # Add constructor that sets the discriminator
1124
+ class_definition += f"\n{INDENT}/// <summary>\n{INDENT}/// Constructor that sets the discriminator value\n{INDENT}/// </summary>\n"
1125
+ class_definition += f"{INDENT}public {class_name}()\n{INDENT}{{\n"
1126
+ class_definition += f"{INDENT*2}this.{pascal(selector)} = \"{choice_name}\";\n"
1127
+ class_definition += f"{INDENT}}}\n"
1128
+
1129
+ # Generate Equals and GetHashCode
1130
+ class_definition += self.generate_equals_and_gethashcode(schema, class_name, parent_namespace)
1131
+
1132
+ class_definition += "}"
1133
+
1134
+ if write_file:
1135
+ self.write_to_file(namespace, class_name, class_definition)
1136
+
1137
+ self.generated_types[ref] = "class"
1138
+ self.generated_structure_types[ref] = schema
1139
+ return ref
1140
+
1141
+ def generate_tuple(self, structure_schema: Dict, parent_namespace: str, write_file: bool, explicit_name: str = '') -> str:
1142
+ """ Generates a tuple type - Per JSON Structure spec, tuples serialize as JSON arrays, not objects """
1143
+ class_name = pascal(explicit_name if explicit_name else structure_schema.get('name', 'UnnamedTuple'))
1144
+ schema_namespace = structure_schema.get('namespace', parent_namespace)
1145
+ namespace = pascal(self.concat_namespace(self.base_namespace, schema_namespace))
1146
+
1147
+ ref = 'global::'+self.get_qualified_name(namespace, class_name)
1148
+ if ref in self.generated_types:
1149
+ return ref
1150
+
1151
+ properties = structure_schema.get('properties', {})
1152
+ tuple_order = structure_schema.get('tuple', [])
1153
+
1154
+ # Build list of tuple element types and names in correct order
1155
+ tuple_elements = []
1156
+ for prop_name in tuple_order:
1157
+ if prop_name in properties:
1158
+ prop_schema = properties[prop_name]
1159
+ prop_type = self.convert_structure_type_to_csharp(class_name, prop_name, prop_schema, schema_namespace)
1160
+ field_name = pascal(prop_name) if self.pascal_properties else prop_name
1161
+ tuple_elements.append((prop_type, field_name))
1162
+
1163
+ # Generate as a C# record struct with positional parameters
1164
+ # Per JSON Structure spec: tuples serialize as JSON arrays like ["Alice", 42]
1165
+ tuple_signature = ', '.join([f"{elem_type} {elem_name}" for elem_type, elem_name in tuple_elements])
1166
+
1167
+ # Create the tuple record struct
1168
+ class_definition = f"/// <summary>\n/// {structure_schema.get('description', class_name)}\n/// </summary>\n"
1169
+ class_definition += f"/// <remarks>\n/// JSON Structure tuple type - serializes as JSON array: [{', '.join(['...' for _ in tuple_elements])}]\n/// </remarks>\n"
1170
+
1171
+ # Add JsonConverter attribute if System.Text.Json annotations are enabled
1172
+ if self.system_text_json_annotation:
1173
+ class_definition += f"[System.Text.Json.Serialization.JsonConverter(typeof(TupleJsonConverter<{class_name}>))]\n"
1174
+
1175
+ class_definition += f"public record struct {class_name}({tuple_signature});\n"
1176
+
1177
+ if write_file:
1178
+ self.write_to_file(namespace, class_name, class_definition)
1179
+
1180
+ self.generated_types[ref] = "tuple"
1181
+ self.generated_structure_types[ref] = structure_schema
1182
+ return ref
1183
+
1184
+ def generate_container_wrapper(self, structure_schema: Dict, parent_namespace: str, write_file: bool, explicit_name: str = '') -> str:
1185
+ """ Generates a wrapper class for root-level container types (map, array, set) """
1186
+ struct_type = structure_schema.get('type', 'map')
1187
+ class_name = pascal(explicit_name if explicit_name else structure_schema.get('name', f'Root{struct_type.capitalize()}'))
1188
+ schema_namespace = structure_schema.get('namespace', parent_namespace)
1189
+ namespace = pascal(self.concat_namespace(self.base_namespace, schema_namespace))
1190
+
1191
+ ref = 'global::'+self.get_qualified_name(namespace, class_name)
1192
+ if ref in self.generated_types:
1193
+ return ref
1194
+
1195
+ # Determine the underlying collection type
1196
+ value_type = "string" # Default
1197
+ item_type = "string" # Default
1198
+ underlying_type = "object"
1199
+
1200
+ if struct_type == 'map':
1201
+ values_schema = structure_schema.get('values', {'type': 'string'})
1202
+ value_type = self.convert_structure_type_to_csharp(class_name, 'value', values_schema, schema_namespace)
1203
+ underlying_type = f"Dictionary<string, {value_type}>"
1204
+ elif struct_type == 'array':
1205
+ items_schema = structure_schema.get('items', {'type': 'string'})
1206
+ item_type = self.convert_structure_type_to_csharp(class_name, 'item', items_schema, schema_namespace)
1207
+ underlying_type = f"List<{item_type}>"
1208
+ elif struct_type == 'set':
1209
+ items_schema = structure_schema.get('items', {'type': 'string'})
1210
+ item_type = self.convert_structure_type_to_csharp(class_name, 'item', items_schema, schema_namespace)
1211
+ underlying_type = f"HashSet<{item_type}>"
1212
+
1213
+ # Generate wrapper class with implicit conversions
1214
+ class_definition = f"/// <summary>\n/// {structure_schema.get('description', class_name)}\n/// </summary>\n"
1215
+ class_definition += f"/// <remarks>\n/// Wrapper for root-level {struct_type} type\n/// </remarks>\n"
1216
+ # Add JsonConverter attribute to serialize as the underlying collection
1217
+ class_definition += f"[System.Text.Json.Serialization.JsonConverter(typeof({class_name}JsonConverter))]\n"
1218
+ class_definition += f"public class {class_name}\n{{\n"
1219
+ class_definition += f"{INDENT}internal {underlying_type} _value = new();\n\n"
1220
+
1221
+ # Add indexer or collection access
1222
+ if struct_type == 'map':
1223
+ class_definition += f"{INDENT}public {value_type} this[string key]\n"
1224
+ class_definition += f"{INDENT}{{\n"
1225
+ class_definition += f"{INDENT*2}get => _value[key];\n"
1226
+ class_definition += f"{INDENT*2}set => _value[key] = value;\n"
1227
+ class_definition += f"{INDENT}}}\n\n"
1228
+ elif struct_type in ('array', 'set'):
1229
+ class_definition += f"{INDENT}public {item_type} this[int index]\n"
1230
+ class_definition += f"{INDENT}{{\n"
1231
+ if struct_type == 'array':
1232
+ class_definition += f"{INDENT*2}get => _value[index];\n"
1233
+ class_definition += f"{INDENT*2}set => _value[index] = value;\n"
1234
+ else: # set
1235
+ class_definition += f"{INDENT*2}get => _value.ElementAt(index);\n"
1236
+ class_definition += f"{INDENT*2}set => throw new NotSupportedException(\"Cannot set items by index in a HashSet\");\n"
1237
+ class_definition += f"{INDENT}}}\n\n"
1238
+
1239
+ # Add Count property
1240
+ class_definition += f"{INDENT}public int Count => _value.Count;\n\n"
1241
+
1242
+ # Add Add method for collections
1243
+ if struct_type == 'map':
1244
+ class_definition += f"{INDENT}public void Add(string key, {value_type} value) => _value.Add(key, value);\n\n"
1245
+ elif struct_type in ('array', 'set'):
1246
+ class_definition += f"{INDENT}public void Add({item_type} item) => _value.Add(item);\n\n"
1247
+
1248
+ # Override Equals and GetHashCode for proper value equality
1249
+ class_definition += f"{INDENT}public override bool Equals(object? obj)\n"
1250
+ class_definition += f"{INDENT}{{\n"
1251
+ class_definition += f"{INDENT*2}if (obj is not {class_name} other) return false;\n"
1252
+ if struct_type == 'map':
1253
+ class_definition += f"{INDENT*2}if (_value.Count != other._value.Count) return false;\n"
1254
+ class_definition += f"{INDENT*2}foreach (var kvp in _value)\n"
1255
+ class_definition += f"{INDENT*2}{{\n"
1256
+ class_definition += f"{INDENT*3}if (!other._value.TryGetValue(kvp.Key, out var otherValue) || !Equals(kvp.Value, otherValue))\n"
1257
+ class_definition += f"{INDENT*4}return false;\n"
1258
+ class_definition += f"{INDENT*2}}}\n"
1259
+ class_definition += f"{INDENT*2}return true;\n"
1260
+ elif struct_type == 'array':
1261
+ class_definition += f"{INDENT*2}return _value.SequenceEqual(other._value);\n"
1262
+ else: # set
1263
+ class_definition += f"{INDENT*2}return _value.SetEquals(other._value);\n"
1264
+ class_definition += f"{INDENT}}}\n\n"
1265
+
1266
+ class_definition += f"{INDENT}public override int GetHashCode()\n"
1267
+ class_definition += f"{INDENT}{{\n"
1268
+ class_definition += f"{INDENT*2}var hash = new HashCode();\n"
1269
+ if struct_type == 'map':
1270
+ class_definition += f"{INDENT*2}foreach (var kvp in _value)\n"
1271
+ class_definition += f"{INDENT*2}{{\n"
1272
+ class_definition += f"{INDENT*3}hash.Add(kvp.Key);\n"
1273
+ class_definition += f"{INDENT*3}hash.Add(kvp.Value);\n"
1274
+ class_definition += f"{INDENT*2}}}\n"
1275
+ else: # array or set
1276
+ class_definition += f"{INDENT*2}foreach (var item in _value)\n"
1277
+ class_definition += f"{INDENT*2}{{\n"
1278
+ class_definition += f"{INDENT*3}hash.Add(item);\n"
1279
+ class_definition += f"{INDENT*2}}}\n"
1280
+ class_definition += f"{INDENT*2}return hash.ToHashCode();\n"
1281
+ class_definition += f"{INDENT}}}\n\n"
1282
+
1283
+ # Implicit conversion to underlying type
1284
+ class_definition += f"{INDENT}public static implicit operator {underlying_type}({class_name} wrapper) => wrapper._value;\n\n"
1285
+
1286
+ # Implicit conversion from underlying type
1287
+ class_definition += f"{INDENT}public static implicit operator {class_name}({underlying_type} value) => new() {{ _value = value }};\n"
1288
+
1289
+ class_definition += "}\n\n"
1290
+
1291
+ # Generate custom JsonConverter for the wrapper class to serialize as the underlying collection
1292
+ class_definition += f"/// <summary>\n/// JSON converter for {class_name} to serialize as the underlying collection\n/// </summary>\n"
1293
+ class_definition += f"public class {class_name}JsonConverter : System.Text.Json.Serialization.JsonConverter<{class_name}>\n{{\n"
1294
+ class_definition += f"{INDENT}/// <inheritdoc/>\n"
1295
+ class_definition += f"{INDENT}public override {class_name}? Read(ref System.Text.Json.Utf8JsonReader reader, Type typeToConvert, System.Text.Json.JsonSerializerOptions options)\n"
1296
+ class_definition += f"{INDENT}{{\n"
1297
+ class_definition += f"{INDENT*2}var value = System.Text.Json.JsonSerializer.Deserialize<{underlying_type}>(ref reader, options);\n"
1298
+ class_definition += f"{INDENT*2}return value == null ? null : new {class_name}() {{ _value = value }};\n"
1299
+ class_definition += f"{INDENT}}}\n\n"
1300
+ class_definition += f"{INDENT}/// <inheritdoc/>\n"
1301
+ class_definition += f"{INDENT}public override void Write(System.Text.Json.Utf8JsonWriter writer, {class_name} value, System.Text.Json.JsonSerializerOptions options)\n"
1302
+ class_definition += f"{INDENT}{{\n"
1303
+ class_definition += f"{INDENT*2}System.Text.Json.JsonSerializer.Serialize(writer, value._value, options);\n"
1304
+ class_definition += f"{INDENT}}}\n"
1305
+ class_definition += "}\n"
1306
+
1307
+ if write_file:
1308
+ self.write_to_file(namespace, class_name, class_definition)
1309
+
1310
+ self.generated_types[ref] = "class"
1311
+ self.generated_structure_types[ref] = structure_schema
1312
+ return ref
1313
+
1314
+
1315
+ def generate_embedded_union(self, class_name: str, field_name: str, structure_types: List, parent_namespace: str, write_file: bool) -> str:
1316
+ """ Generates an embedded Union Class """
1317
+ # Similar to Avro's union handling, but for JSON Structure types
1318
+ union_class_name = pascal(field_name)+'Union'
1319
+ ref = class_name+'.'+union_class_name
1320
+
1321
+ # For simplicity, generate as object type
1322
+ # A complete implementation would generate a proper union class
1323
+ return 'object'
1324
+
1325
+ def generate_equals_and_gethashcode(self, structure_schema: Dict, class_name: str, parent_namespace: str) -> str:
1326
+ """ Generates Equals and GetHashCode methods for value equality """
1327
+ code = "\n"
1328
+ properties = structure_schema.get('properties', {})
1329
+
1330
+ # Filter out const properties since they're static and same for all instances
1331
+ non_const_properties = {k: v for k, v in properties.items() if 'const' not in v}
1332
+
1333
+ if not non_const_properties:
1334
+ # Empty class or only const fields - simple implementation
1335
+ code += f"{INDENT}/// <summary>\n{INDENT}/// Determines whether the specified object is equal to the current object.\n{INDENT}/// </summary>\n"
1336
+ code += f"{INDENT}public override bool Equals(object? obj)\n{INDENT}{{\n"
1337
+ code += f"{INDENT*2}return obj is {class_name};\n"
1338
+ code += f"{INDENT}}}\n\n"
1339
+ code += f"{INDENT}/// <summary>\n{INDENT}/// Serves as the default hash function.\n{INDENT}/// </summary>\n"
1340
+ code += f"{INDENT}public override int GetHashCode()\n{INDENT}{{\n"
1341
+ code += f"{INDENT*2}return 0;\n"
1342
+ code += f"{INDENT}}}\n"
1343
+ return code
1344
+
1345
+ # Generate Equals method
1346
+ code += f"{INDENT}/// <summary>\n{INDENT}/// Determines whether the specified object is equal to the current object.\n{INDENT}/// </summary>\n"
1347
+ code += f"{INDENT}public override bool Equals(object? obj)\n{INDENT}{{\n"
1348
+ code += f"{INDENT*2}if (obj is not {class_name} other) return false;\n"
1349
+
1350
+ # Build equality comparisons for each non-const property
1351
+ equality_checks = []
1352
+ for prop_name, prop_schema in non_const_properties.items():
1353
+ field_name = prop_name
1354
+ if self.is_csharp_reserved_word(field_name):
1355
+ field_name = f"@{field_name}"
1356
+ if self.pascal_properties:
1357
+ field_name = pascal(field_name)
1358
+ if field_name == class_name:
1359
+ field_name += "_"
1360
+
1361
+ field_type = self.convert_structure_type_to_csharp(class_name, field_name, prop_schema, parent_namespace)
1362
+
1363
+ # Handle different types of comparisons
1364
+ if field_type == 'byte[]' or field_type == 'byte[]?':
1365
+ # Byte arrays need special handling
1366
+ equality_checks.append(f"System.Linq.Enumerable.SequenceEqual(this.{field_name} ?? Array.Empty<byte>(), other.{field_name} ?? Array.Empty<byte>())")
1367
+ elif field_type.startswith('Dictionary<'):
1368
+ # Dictionaries need special comparison - compare keys and values
1369
+ if field_type.endswith('?'):
1370
+ dict_compare = f"((this.{field_name} == null && other.{field_name} == null) || (this.{field_name} != null && other.{field_name} != null && this.{field_name}.Count == other.{field_name}.Count && this.{field_name}.All(kvp => other.{field_name}.TryGetValue(kvp.Key, out var val) && Equals(kvp.Value, val))))"
1371
+ equality_checks.append(dict_compare)
1372
+ else:
1373
+ dict_compare = f"(this.{field_name}.Count == other.{field_name}.Count && this.{field_name}.All(kvp => other.{field_name}.TryGetValue(kvp.Key, out var val) && Equals(kvp.Value, val)))"
1374
+ equality_checks.append(dict_compare)
1375
+ elif field_type.startswith('List<') or field_type.startswith('HashSet<'):
1376
+ # Lists and HashSets need sequence comparison
1377
+ if field_type.endswith('?'):
1378
+ equality_checks.append(f"((this.{field_name} == null && other.{field_name} == null) || (this.{field_name} != null && other.{field_name} != null && this.{field_name}.SequenceEqual(other.{field_name})))")
1379
+ else:
1380
+ equality_checks.append(f"this.{field_name}.SequenceEqual(other.{field_name})")
1381
+ else:
1382
+ # Use Equals for reference types, == for value types
1383
+ if field_type.endswith('?') or not self.is_csharp_primitive_type(field_type):
1384
+ equality_checks.append(f"Equals(this.{field_name}, other.{field_name})")
1385
+ else:
1386
+ equality_checks.append(f"this.{field_name} == other.{field_name}")
1387
+
1388
+ # Join all checks with &&
1389
+ if len(equality_checks) == 1:
1390
+ code += f"{INDENT*2}return {equality_checks[0]};\n"
1391
+ else:
1392
+ code += f"{INDENT*2}return " + f"\n{INDENT*3}&& ".join(equality_checks) + ";\n"
1393
+
1394
+ code += f"{INDENT}}}\n\n"
1395
+
1396
+ # Generate GetHashCode method
1397
+ code += f"{INDENT}/// <summary>\n{INDENT}/// Serves as the default hash function.\n{INDENT}/// </summary>\n"
1398
+ code += f"{INDENT}public override int GetHashCode()\n{INDENT}{{\n"
1399
+
1400
+ # Collect field names for HashCode.Combine (skip const fields)
1401
+ hash_fields = []
1402
+ for prop_name, prop_schema in non_const_properties.items():
1403
+ field_name = prop_name
1404
+ if self.is_csharp_reserved_word(field_name):
1405
+ field_name = f"@{field_name}"
1406
+ if self.pascal_properties:
1407
+ field_name = pascal(field_name)
1408
+ if field_name == class_name:
1409
+ field_name += "_"
1410
+
1411
+ field_type = self.convert_structure_type_to_csharp(class_name, field_name, prop_schema, parent_namespace)
1412
+
1413
+ # Handle special types that need custom hash code computation
1414
+ if field_type == 'byte[]' or field_type == 'byte[]?':
1415
+ hash_fields.append(f"({field_name} != null ? System.Convert.ToBase64String({field_name}).GetHashCode() : 0)")
1416
+ elif field_type.startswith('List<') or field_type.startswith('HashSet<') or field_type.startswith('Dictionary<'):
1417
+ # For collections, compute hash from elements
1418
+ if field_type.endswith('?'):
1419
+ hash_fields.append(f"({field_name} != null ? {field_name}.Aggregate(0, (acc, item) => HashCode.Combine(acc, item)) : 0)")
1420
+ else:
1421
+ hash_fields.append(f"{field_name}.Aggregate(0, (acc, item) => HashCode.Combine(acc, item))")
1422
+ else:
1423
+ hash_fields.append(field_name)
1424
+
1425
+ # HashCode.Combine supports up to 8 parameters
1426
+ if len(hash_fields) <= 8:
1427
+ code += f"{INDENT*2}return HashCode.Combine({', '.join(hash_fields)});\n"
1428
+ else:
1429
+ # For more than 8 fields, use HashCode.Add
1430
+ code += f"{INDENT*2}var hash = new HashCode();\n"
1431
+ for field in hash_fields:
1432
+ code += f"{INDENT*2}hash.Add({field});\n"
1433
+ code += f"{INDENT*2}return hash.ToHashCode();\n"
1434
+
1435
+ code += f"{INDENT}}}\n"
1436
+
1437
+ return code
1438
+
1439
+ def write_to_file(self, namespace: str, name: str, definition: str) -> None:
1440
+ """ Writes the class or enum to a file """
1441
+ directory_path = os.path.join(
1442
+ self.output_dir, os.path.join('src', namespace.replace('.', os.sep)))
1443
+ if not os.path.exists(directory_path):
1444
+ os.makedirs(directory_path, exist_ok=True)
1445
+ file_path = os.path.join(directory_path, f"{name}.cs")
1446
+
1447
+ with open(file_path, 'w', encoding='utf-8') as file:
1448
+ # Common using statements (add more as needed)
1449
+ file_content = "using System;\nusing System.Collections.Generic;\n"
1450
+ file_content += "using System.Linq;\n"
1451
+ if self.system_text_json_annotation:
1452
+ file_content += "using System.Text.Json;\n"
1453
+ file_content += "using System.Text.Json.Serialization;\n"
1454
+ if self.newtonsoft_json_annotation:
1455
+ file_content += "using Newtonsoft.Json;\n"
1456
+ if self.system_xml_annotation: # Add XML serialization using directive
1457
+ file_content += "using System.Xml.Serialization;\n"
1458
+
1459
+ if namespace:
1460
+ # Namespace declaration with correct indentation for the definition
1461
+ file_content += f"\nnamespace {namespace}\n{{\n"
1462
+ indented_definition = '\n'.join(
1463
+ [f"{INDENT}{line}" for line in definition.split('\n')])
1464
+ file_content += f"{indented_definition}\n}}"
1465
+ else:
1466
+ file_content += definition
1467
+ file.write(file_content)
1468
+
1469
+ def convert(self, structure_schema_path: str, output_dir: str) -> None:
1470
+ """ Converts a JSON Structure schema file to C# classes """
1471
+ self.output_dir = output_dir
1472
+
1473
+ with open(structure_schema_path, 'r', encoding='utf-8') as file:
1474
+ schema = json.load(file)
1475
+
1476
+ self.convert_schema(schema, output_dir)
1477
+
1478
+ def convert_schema(self, schema: JsonNode, output_dir: str) -> None:
1479
+ """ Converts a JSON Structure schema to C# classes """
1480
+ if not isinstance(schema, list):
1481
+ schema = [schema]
1482
+
1483
+ # Determine project name: use explicit project_name if set, otherwise derive from base_namespace
1484
+ if self.project_name and self.project_name.strip():
1485
+ # Use explicitly set project name
1486
+ project_name = self.project_name
1487
+ else:
1488
+ # Fall back to using base_namespace as project name
1489
+ project_name = self.base_namespace
1490
+ if not project_name or project_name.strip() == '':
1491
+ # Derive from output directory name as fallback
1492
+ project_name = os.path.basename(os.path.abspath(output_dir))
1493
+ if not project_name or project_name.strip() == '':
1494
+ project_name = 'Generated'
1495
+ # Clean up the project name
1496
+ project_name = project_name.replace('-', '_').replace(' ', '_')
1497
+ # Update base_namespace to match (only if it was empty)
1498
+ self.base_namespace = project_name
1499
+ import warnings
1500
+ warnings.warn(f"No namespace provided, using '{project_name}' derived from output directory", UserWarning)
1501
+
1502
+ self.schema_doc = schema
1503
+ if not os.path.exists(output_dir):
1504
+ os.makedirs(output_dir, exist_ok=True)
1505
+
1506
+ # Create solution file if it doesn't exist
1507
+ if not glob.glob(os.path.join(output_dir, "src", "*.sln")):
1508
+ sln_file = os.path.join(output_dir, f"{project_name}.sln")
1509
+ if not os.path.exists(sln_file):
1510
+ if not os.path.exists(os.path.dirname(sln_file)) and os.path.dirname(sln_file):
1511
+ os.makedirs(os.path.dirname(sln_file))
1512
+ with open(sln_file, 'w', encoding='utf-8') as file:
1513
+ file.write(process_template(
1514
+ "structuretocsharp/project.sln.jinja",
1515
+ project_name=project_name,
1516
+ uuid=lambda:str(uuid.uuid4()),
1517
+ system_xml_annotation=self.system_xml_annotation,
1518
+ system_text_json_annotation=self.system_text_json_annotation,
1519
+ newtonsoft_json_annotation=self.newtonsoft_json_annotation))
1520
+
1521
+ # Create main project file if it doesn't exist
1522
+ if not glob.glob(os.path.join(output_dir, "src", "*.csproj")):
1523
+ csproj_file = os.path.join(output_dir, "src", f"{pascal(project_name)}.csproj")
1524
+ if not os.path.exists(csproj_file):
1525
+ if not os.path.exists(os.path.dirname(csproj_file)):
1526
+ os.makedirs(os.path.dirname(csproj_file))
1527
+ with open(csproj_file, 'w', encoding='utf-8') as file:
1528
+ file.write(process_template(
1529
+ "structuretocsharp/project.csproj.jinja",
1530
+ project_name=project_name,
1531
+ system_xml_annotation=self.system_xml_annotation,
1532
+ system_text_json_annotation=self.system_text_json_annotation,
1533
+ newtonsoft_json_annotation=self.newtonsoft_json_annotation,
1534
+ NEWTONSOFT_JSON_VERSION=NEWTONSOFT_JSON_VERSION,
1535
+ SYSTEM_TEXT_JSON_VERSION=SYSTEM_TEXT_JSON_VERSION,
1536
+ SYSTEM_MEMORY_DATA_VERSION=SYSTEM_MEMORY_DATA_VERSION,
1537
+ NUNIT_VERSION=NUNIT_VERSION,
1538
+ NUNIT_ADAPTER_VERSION=NUNIT_ADAPTER_VERSION,
1539
+ MSTEST_SDK_VERSION=MSTEST_SDK_VERSION))
1540
+
1541
+ # Create test project file if it doesn't exist
1542
+ if not glob.glob(os.path.join(output_dir, "test", "*.csproj")):
1543
+ csproj_test_file = os.path.join(output_dir, "test", f"{pascal(project_name)}.Test.csproj")
1544
+ if not os.path.exists(csproj_test_file):
1545
+ if not os.path.exists(os.path.dirname(csproj_test_file)):
1546
+ os.makedirs(os.path.dirname(csproj_test_file))
1547
+ with open(csproj_test_file, 'w', encoding='utf-8') as file:
1548
+ file.write(process_template(
1549
+ "structuretocsharp/testproject.csproj.jinja",
1550
+ project_name=project_name,
1551
+ system_xml_annotation=self.system_xml_annotation,
1552
+ system_text_json_annotation=self.system_text_json_annotation,
1553
+ newtonsoft_json_annotation=self.newtonsoft_json_annotation,
1554
+ NUNIT_VERSION=NUNIT_VERSION,
1555
+ NUNIT_ADAPTER_VERSION=NUNIT_ADAPTER_VERSION,
1556
+ MSTEST_SDK_VERSION=MSTEST_SDK_VERSION))
1557
+
1558
+ self.output_dir = output_dir
1559
+
1560
+ # Register all schemas with $id keywords for cross-references
1561
+ for structure_schema in (s for s in schema if isinstance(s, dict)):
1562
+ self.register_schema_ids(structure_schema)
1563
+
1564
+ # Process each schema
1565
+ for structure_schema in (s for s in schema if isinstance(s, dict)):
1566
+ # Store definitions for later use
1567
+ if 'definitions' in structure_schema:
1568
+ self.definitions = structure_schema['definitions']
1569
+
1570
+ # Store $offers for add-in system
1571
+ if '$offers' in structure_schema:
1572
+ self.offers = structure_schema['$offers']
1573
+
1574
+ # Process root type FIRST so inline unions can generate derived classes
1575
+ if 'type' in structure_schema:
1576
+ self.generate_class_or_choice(structure_schema, '', write_file=True)
1577
+ elif '$root' in structure_schema:
1578
+ root_ref = structure_schema['$root']
1579
+ root_schema = self.resolve_ref(root_ref, structure_schema)
1580
+ if root_schema:
1581
+ ref_path = root_ref.split('/')
1582
+ type_name = ref_path[-1]
1583
+ ref_namespace = '.'.join(ref_path[2:-1]) if len(ref_path) > 3 else ''
1584
+ self.generate_class_or_choice(root_schema, ref_namespace, write_file=True, explicit_name=type_name)
1585
+
1586
+ # Now process remaining definitions that weren't generated as part of inline unions
1587
+ if 'definitions' in structure_schema:
1588
+ self.process_definitions(self.definitions, '')
1589
+
1590
+ # Generate add-in interfaces and extensible wrapper classes
1591
+ if self.offers:
1592
+ self.generate_addins(structure_schema)
1593
+
1594
+ # Generate tuple converter utility class if needed (after all types processed)
1595
+ if self.system_text_json_annotation:
1596
+ self.generate_tuple_converter(output_dir)
1597
+ self.generate_json_structure_converters(output_dir)
1598
+
1599
+ # Generate tests
1600
+ self.generate_tests(output_dir)
1601
+
1602
+ # Generate instance serializer program
1603
+ self.generate_instance_serializer(output_dir)
1604
+
1605
+ def generate_addins(self, structure_schema: Dict) -> None:
1606
+ """
1607
+ Generates add-in interfaces and view classes for types that have $offers.
1608
+
1609
+ For each add-in in $offers, creates:
1610
+ 1. An interface I{AddinName} with the add-in properties
1611
+ 2. An internal view class that wraps the Extensions dictionary
1612
+ 3. Implicit operators on the base class that convert to the interface
1613
+ """
1614
+ if not self.offers or not isinstance(self.offers, dict):
1615
+ return
1616
+
1617
+ root_type_name = structure_schema.get('name', 'Document')
1618
+ namespace_pascal = pascal(self.base_namespace)
1619
+
1620
+ # Generate interface and view class for each add-in
1621
+ view_classes = []
1622
+ for addin_name, addin_def in self.offers.items():
1623
+ self.generate_addin_interface(addin_name, addin_def, namespace_pascal)
1624
+ view_class_name = self.generate_addin_view_class(addin_name, addin_def, namespace_pascal)
1625
+ view_classes.append((addin_name, view_class_name))
1626
+
1627
+ # Add Extensions dictionary and implicit operators to the base class
1628
+ if 'type' in structure_schema and structure_schema['type'] == 'object':
1629
+ self.add_extensions_to_base_class(root_type_name, view_classes, namespace_pascal)
1630
+
1631
+ def generate_addin_interface(self, addin_name: str, addin_def: Any, namespace: str) -> None:
1632
+ """
1633
+ Generates an interface for an add-in from $offers.
1634
+
1635
+ Args:
1636
+ addin_name: Name of the add-in (e.g., "AuditInfo")
1637
+ addin_def: Definition of the add-in (either inline properties or a $ref)
1638
+ namespace: Target namespace for the interface
1639
+ """
1640
+ interface_name = f"I{pascal(addin_name)}"
1641
+
1642
+ # Resolve the add-in definition if it's a reference
1643
+ if isinstance(addin_def, str):
1644
+ # It's a JSON pointer reference
1645
+ addin_def = self.resolve_ref(addin_def, self.schema_doc)
1646
+ elif isinstance(addin_def, dict) and '$ref' in addin_def:
1647
+ addin_def = self.resolve_ref(addin_def['$ref'], self.schema_doc)
1648
+
1649
+ if not addin_def or not isinstance(addin_def, dict):
1650
+ return
1651
+
1652
+ properties = addin_def.get('properties', {})
1653
+ if not properties:
1654
+ return
1655
+
1656
+ # Generate interface definition
1657
+ interface_code = f"{INDENT}/// <summary>\n"
1658
+ interface_code += f"{INDENT}/// Add-in interface: {addin_name}\n"
1659
+ if 'description' in addin_def:
1660
+ interface_code += f"{INDENT}/// {addin_def['description']}\n"
1661
+ interface_code += f"{INDENT}/// </summary>\n"
1662
+ interface_code += f"{INDENT}public interface {interface_name}\n"
1663
+ interface_code += f"{INDENT}{{\n"
1664
+
1665
+ # Generate properties
1666
+ for prop_name, prop_schema in properties.items():
1667
+ if not isinstance(prop_schema, dict):
1668
+ continue
1669
+
1670
+ csharp_prop_name = pascal(prop_name) if self.pascal_properties else prop_name
1671
+ csharp_type = self.convert_structure_type_to_csharp(interface_name, prop_name, prop_schema, namespace)
1672
+
1673
+ # Add XML doc comment
1674
+ if 'description' in prop_schema:
1675
+ interface_code += f"{INDENT}{INDENT}/// <summary>\n"
1676
+ interface_code += f"{INDENT}{INDENT}/// {prop_schema['description']}\n"
1677
+ interface_code += f"{INDENT}{INDENT}/// </summary>\n"
1678
+
1679
+ # Interface properties are always nullable for add-ins (both value types and reference types)
1680
+ if not csharp_type.endswith('?'):
1681
+ csharp_type += '?'
1682
+
1683
+ interface_code += f"{INDENT}{INDENT}{csharp_type} {csharp_prop_name} {{ get; set; }}\n"
1684
+
1685
+ interface_code += f"{INDENT}}}\n"
1686
+
1687
+ # Write interface to file
1688
+ self.write_to_file(namespace, interface_name, interface_code)
1689
+
1690
+ # Track as generated
1691
+ qualified_name = 'global::' + self.get_qualified_name(namespace, interface_name)
1692
+ self.generated_types[qualified_name] = "interface"
1693
+
1694
+ def generate_extensible_class(self, base_type_name: str, addin_names: List[str], namespace: str) -> None:
1695
+ """
1696
+ DEPRECATED: Replaced by generate_addin_view_class and add_extensions_to_base_class.
1697
+ This method is kept for backward compatibility but does nothing.
1698
+ """
1699
+ pass
1700
+
1701
+ def generate_addin_view_class(self, addin_name: str, addin_def: Any, namespace: str) -> str:
1702
+ """
1703
+ Generates an internal view class that wraps the Extensions dictionary.
1704
+
1705
+ Example output:
1706
+ internal sealed class AuditInfoView : IAuditInfo
1707
+ {
1708
+ private readonly Dictionary<string, object?> _extensions;
1709
+
1710
+ public AuditInfoView(Dictionary<string, object?> extensions)
1711
+ {
1712
+ _extensions = extensions;
1713
+ }
1714
+
1715
+ public string? CreatedBy
1716
+ {
1717
+ get => _extensions.TryGetValue("createdBy", out var val) ? val as string : null;
1718
+ set { if (value != null) _extensions["createdBy"] = value; else _extensions.Remove("createdBy"); }
1719
+ }
1720
+ }
1721
+
1722
+ Args:
1723
+ addin_name: Name of the add-in (e.g., "AuditInfo")
1724
+ addin_def: Definition of the add-in
1725
+ namespace: Target namespace
1726
+
1727
+ Returns:
1728
+ The name of the generated view class
1729
+ """
1730
+ view_class_name = f"{pascal(addin_name)}View"
1731
+ interface_name = f"I{pascal(addin_name)}"
1732
+
1733
+ # Resolve the add-in definition if it's a reference
1734
+ if isinstance(addin_def, str):
1735
+ addin_def = self.resolve_ref(addin_def, self.schema_doc)
1736
+ elif isinstance(addin_def, dict) and '$ref' in addin_def:
1737
+ addin_def = self.resolve_ref(addin_def['$ref'], self.schema_doc)
1738
+
1739
+ if not addin_def or not isinstance(addin_def, dict):
1740
+ return view_class_name
1741
+
1742
+ properties = addin_def.get('properties', {})
1743
+ if not properties:
1744
+ return view_class_name
1745
+
1746
+ # Generate class definition
1747
+ class_code = f"{INDENT}/// <summary>\n"
1748
+ class_code += f"{INDENT}/// View class wrapping Extensions dictionary for {addin_name} add-in\n"
1749
+ if 'description' in addin_def:
1750
+ class_code += f"{INDENT}/// {addin_def['description']}\n"
1751
+ class_code += f"{INDENT}/// </summary>\n"
1752
+ class_code += f"{INDENT}public sealed class {view_class_name} : {interface_name}\n"
1753
+ class_code += f"{INDENT}{{\n"
1754
+
1755
+ # Add private field
1756
+ class_code += f"{INDENT}{INDENT}private readonly Dictionary<string, object?> _extensions;\n\n"
1757
+
1758
+ # Add constructor
1759
+ class_code += f"{INDENT}{INDENT}public {view_class_name}(Dictionary<string, object?> extensions)\n"
1760
+ class_code += f"{INDENT}{INDENT}{{\n"
1761
+ class_code += f"{INDENT}{INDENT}{INDENT}_extensions = extensions;\n"
1762
+ class_code += f"{INDENT}{INDENT}}}\n\n"
1763
+
1764
+ # Generate properties
1765
+ for prop_name, prop_schema in properties.items():
1766
+ if not isinstance(prop_schema, dict):
1767
+ continue
1768
+
1769
+ csharp_prop_name = pascal(prop_name) if self.pascal_properties else prop_name
1770
+ csharp_type = self.convert_structure_type_to_csharp(view_class_name, prop_name, prop_schema, namespace)
1771
+
1772
+ # Remove nullable marker for determining base type
1773
+ base_csharp_type = csharp_type.rstrip('?')
1774
+ is_nullable = csharp_type.endswith('?')
1775
+
1776
+ # Ensure nullable for add-ins
1777
+ if not is_nullable:
1778
+ csharp_type += '?'
1779
+
1780
+ # Add XML doc comment
1781
+ if 'description' in prop_schema:
1782
+ class_code += f"{INDENT}{INDENT}/// <summary>\n"
1783
+ class_code += f"{INDENT}{INDENT}/// {prop_schema['description']}\n"
1784
+ class_code += f"{INDENT}{INDENT}/// </summary>\n"
1785
+
1786
+ # Generate getter that reads from dictionary
1787
+ class_code += f"{INDENT}{INDENT}public {csharp_type} {csharp_prop_name}\n"
1788
+ class_code += f"{INDENT}{INDENT}{{\n"
1789
+
1790
+ # Getter - use TryGetValue with type-specific conversion
1791
+ class_code += f'{INDENT}{INDENT}{INDENT}get => _extensions.TryGetValue("{prop_name}", out var val) && val != null ? '
1792
+
1793
+ # Add appropriate conversion based on type
1794
+ if base_csharp_type in ['string', 'bool', 'int', 'long', 'float', 'double', 'decimal']:
1795
+ if base_csharp_type == 'string':
1796
+ class_code += 'val as string : null;\n'
1797
+ elif base_csharp_type == 'bool':
1798
+ class_code += 'Convert.ToBoolean(val) : null;\n'
1799
+ elif base_csharp_type in ['int', 'long', 'float', 'double', 'decimal']:
1800
+ class_code += f'Convert.To{base_csharp_type.capitalize()}(val) : null;\n'
1801
+ else:
1802
+ class_code += 'val : null;\n'
1803
+ else:
1804
+ # For complex types, try direct cast
1805
+ class_code += f'({base_csharp_type})val : null;\n'
1806
+
1807
+ # Setter - write to dictionary or remove if null
1808
+ class_code += f'{INDENT}{INDENT}{INDENT}set {{ if (value != null) _extensions["{prop_name}"] = value; else _extensions.Remove("{prop_name}"); }}\n'
1809
+
1810
+ class_code += f"{INDENT}{INDENT}}}\n\n"
1811
+
1812
+ class_code += f"{INDENT}}}\n"
1813
+
1814
+ # Write class to file
1815
+ self.write_to_file(namespace, view_class_name, class_code)
1816
+
1817
+ # Track as generated (internal, not exported)
1818
+ qualified_name = 'global::' + self.get_qualified_name(namespace, view_class_name)
1819
+ self.generated_types[qualified_name] = "view_class"
1820
+
1821
+ return view_class_name
1822
+
1823
+ def add_extensions_to_base_class(self, base_type_name: str, view_classes: List[tuple], namespace: str) -> None:
1824
+ """
1825
+ Adds Extensions dictionary property and implicit operators to the base class.
1826
+
1827
+ Appends to the existing base class file:
1828
+ - Extensions property (Dictionary<string, object?>)
1829
+ - Implicit operators for each add-in interface
1830
+
1831
+ Args:
1832
+ base_type_name: Name of the base type
1833
+ view_classes: List of (addin_name, view_class_name) tuples
1834
+ namespace: Target namespace
1835
+ """
1836
+ base_class_name = pascal(base_type_name)
1837
+
1838
+ # Generate the partial class extension code
1839
+ extension_code = f"{INDENT}/// <summary>\n"
1840
+ extension_code += f"{INDENT}/// Partial class extension for {base_class_name} with add-in support\n"
1841
+ extension_code += f"{INDENT}/// </summary>\n"
1842
+ extension_code += f"{INDENT}public partial class {base_class_name}\n"
1843
+ extension_code += f"{INDENT}{{\n"
1844
+
1845
+ # Add Extensions property
1846
+ extension_code += f"{INDENT}{INDENT}/// <summary>\n"
1847
+ extension_code += f"{INDENT}{INDENT}/// Extension properties storage for add-ins.\n"
1848
+ extension_code += f"{INDENT}{INDENT}/// Unknown JSON properties are automatically captured here during deserialization.\n"
1849
+ extension_code += f"{INDENT}{INDENT}/// </summary>\n"
1850
+
1851
+ if self.system_text_json_annotation:
1852
+ extension_code += f'{INDENT}{INDENT}[System.Text.Json.Serialization.JsonExtensionData]\n'
1853
+ if self.newtonsoft_json_annotation:
1854
+ extension_code += f'{INDENT}{INDENT}[Newtonsoft.Json.JsonExtensionData]\n'
1855
+
1856
+ extension_code += f"{INDENT}{INDENT}public Dictionary<string, object?> Extensions {{ get; set; }} = new();\n\n"
1857
+
1858
+ # Add implicit operators for each add-in
1859
+ for addin_name, view_class_name in view_classes:
1860
+ interface_name = f"I{pascal(addin_name)}"
1861
+
1862
+ extension_code += f"{INDENT}{INDENT}/// <summary>\n"
1863
+ extension_code += f"{INDENT}{INDENT}/// Implicit conversion to {interface_name} view\n"
1864
+ extension_code += f"{INDENT}{INDENT}/// </summary>\n"
1865
+ extension_code += f"{INDENT}{INDENT}public static implicit operator {view_class_name}({base_class_name} obj)\n"
1866
+ extension_code += f"{INDENT}{INDENT}{INDENT}=> new {view_class_name}(obj.Extensions);\n\n"
1867
+
1868
+ extension_code += f"{INDENT}}}\n"
1869
+
1870
+ # Write to a separate file (e.g., ProductExtensions.cs)
1871
+ extension_file_name = f"{base_class_name}Extensions"
1872
+ self.write_to_file(namespace, extension_file_name, extension_code)
1873
+
1874
+ def process_definitions(self, definitions: Dict, namespace_path: str) -> None:
1875
+ """ Processes the definitions section recursively """
1876
+ for name, definition in definitions.items():
1877
+ if isinstance(definition, dict):
1878
+ if 'type' in definition:
1879
+ # This is a type definition
1880
+ current_namespace = self.concat_namespace(namespace_path, '')
1881
+ # Check if this type was already generated (e.g., as part of inline union)
1882
+ check_namespace = pascal(self.concat_namespace(self.base_namespace, current_namespace))
1883
+ check_name = pascal(name)
1884
+ check_ref = 'global::'+self.get_qualified_name(check_namespace, check_name)
1885
+ if check_ref not in self.generated_types:
1886
+ self.generate_class_or_choice(definition, current_namespace, write_file=True, explicit_name=name)
1887
+ else:
1888
+ # This is a namespace
1889
+ new_namespace = self.concat_namespace(namespace_path, name)
1890
+ self.process_definitions(definition, new_namespace)
1891
+
1892
+ def generate_tests(self, output_dir: str) -> None:
1893
+ """ Generates unit tests for all the generated C# classes and enums """
1894
+ test_directory_path = os.path.join(output_dir, "test")
1895
+ if not os.path.exists(test_directory_path):
1896
+ os.makedirs(test_directory_path, exist_ok=True)
1897
+
1898
+ for class_name, type_kind in self.generated_types.items():
1899
+ # Skip test generation for:
1900
+ # 1. View classes (internal wrappers for Extensions dictionary)
1901
+ # 2. Extension partial classes (add implicit operators to base classes)
1902
+ base_name = class_name.split('.')[-1]
1903
+
1904
+ # Skip view classes (e.g., AuditInfoView)
1905
+ if type_kind == "view_class" or base_name.endswith('View'):
1906
+ continue
1907
+
1908
+ # Skip extension partial classes (e.g., ProductExtensions)
1909
+ if base_name.endswith('Extensions'):
1910
+ continue
1911
+
1912
+ if type_kind in ["class", "enum"]:
1913
+ self.generate_test_class(class_name, type_kind, test_directory_path)
1914
+
1915
+ def generate_tuple_converter(self, output_dir: str) -> None:
1916
+ """ Generates the TupleJsonConverter utility class for JSON array serialization """
1917
+ # Check if any tuples were generated
1918
+ has_tuples = any(type_kind == "tuple" for type_kind in self.generated_types.values())
1919
+ if not has_tuples:
1920
+ return # No tuples, no need for converter
1921
+
1922
+ # Convert base namespace to PascalCase for consistency with other generated classes
1923
+ namespace_pascal = pascal(self.base_namespace)
1924
+
1925
+ # Generate the converter class
1926
+ converter_definition = process_template(
1927
+ "structuretocsharp/tuple_converter.cs.jinja",
1928
+ namespace=namespace_pascal
1929
+ )
1930
+
1931
+ # Write to the same directory structure as other classes (using PascalCase path)
1932
+ directory_path = os.path.join(
1933
+ output_dir, os.path.join('src', namespace_pascal.replace('.', os.sep)))
1934
+ if not os.path.exists(directory_path):
1935
+ os.makedirs(directory_path, exist_ok=True)
1936
+ converter_file_path = os.path.join(directory_path, "TupleJsonConverter.cs")
1937
+
1938
+ # Add using statements
1939
+ file_content = "using System;\n"
1940
+ file_content += "using System.Linq;\n"
1941
+ file_content += "using System.Reflection;\n"
1942
+ file_content += "using System.Text.Json;\n"
1943
+ file_content += "using System.Text.Json.Serialization;\n\n"
1944
+ file_content += converter_definition
1945
+
1946
+ with open(converter_file_path, 'w', encoding='utf-8') as converter_file:
1947
+ converter_file.write(file_content)
1948
+
1949
+ def generate_json_structure_converters(self, output_dir: str) -> None:
1950
+ """ Generates JSON Structure converters for types requiring string serialization.
1951
+
1952
+ Per JSON Structure Core spec, int64, uint64, int128, uint128, decimal types
1953
+ use string representation in JSON to preserve precision. Duration (TimeSpan)
1954
+ uses ISO 8601 format.
1955
+ """
1956
+ # Check if any types need converters
1957
+ if not self.needs_json_structure_converters:
1958
+ return # No types need special converters
1959
+
1960
+ # Convert base namespace to PascalCase for consistency with other generated classes
1961
+ namespace_pascal = pascal(self.base_namespace)
1962
+
1963
+ # Generate the converter class
1964
+ converter_definition = process_template(
1965
+ "structuretocsharp/json_structure_converters.cs.jinja",
1966
+ namespace=namespace_pascal
1967
+ )
1968
+
1969
+ # Write to the same directory structure as other classes (using PascalCase path)
1970
+ directory_path = os.path.join(
1971
+ output_dir, os.path.join('src', namespace_pascal.replace('.', os.sep)))
1972
+ if not os.path.exists(directory_path):
1973
+ os.makedirs(directory_path, exist_ok=True)
1974
+ converter_file_path = os.path.join(directory_path, "JsonStructureConverters.cs")
1975
+
1976
+ with open(converter_file_path, 'w', encoding='utf-8') as converter_file:
1977
+ converter_file.write(converter_definition)
1978
+
1979
+ def generate_instance_serializer(self, output_dir: str) -> None:
1980
+ """ Generates InstanceSerializer.cs that creates instances and serializes them to JSON """
1981
+ test_directory_path = os.path.join(output_dir, "test")
1982
+ if not os.path.exists(test_directory_path):
1983
+ os.makedirs(test_directory_path, exist_ok=True)
1984
+
1985
+ # Collect all classes (not enums, tuples, or other types) that have test classes
1986
+ # Skip abstract classes since they cannot be instantiated
1987
+ # Skip view classes and extension partial classes
1988
+ classes = []
1989
+ for class_name, type_kind in self.generated_types.items():
1990
+ if type_kind == "class":
1991
+ base_name = class_name.split('.')[-1]
1992
+
1993
+ # Skip view classes (internal wrappers for Extensions dictionary)
1994
+ if base_name.endswith('View'):
1995
+ continue
1996
+
1997
+ # Skip extension partial classes
1998
+ if base_name.endswith('Extensions'):
1999
+ continue
2000
+
2001
+ # Skip abstract classes
2002
+ structure_schema = cast(Dict[str, JsonNode], self.generated_structure_types.get(class_name, {}))
2003
+ if structure_schema.get('abstract', False):
2004
+ continue
2005
+
2006
+ if class_name.startswith("global::"):
2007
+ class_name = class_name[8:]
2008
+ test_class_name = f"{class_name.split('.')[-1]}Tests"
2009
+ class_base_name = class_name.split('.')[-1]
2010
+
2011
+ # Get proper namespace from class_name
2012
+ if '.' in class_name:
2013
+ namespace = ".".join(class_name.split('.')[:-1])
2014
+ else:
2015
+ namespace = self.base_namespace if self.base_namespace else ''
2016
+
2017
+ # Build fully qualified test name
2018
+ full_qualified_test_name = f"{namespace}.{test_class_name}" if namespace else test_class_name
2019
+
2020
+ classes.append({
2021
+ 'class_name': class_base_name,
2022
+ 'test_class_name': test_class_name,
2023
+ 'full_name': class_name,
2024
+ 'full_qualified_test_name': full_qualified_test_name
2025
+ })
2026
+
2027
+ if not classes:
2028
+ return # No classes to serialize
2029
+
2030
+ # Determine if ToByteArray method is available (requires any serialization annotation)
2031
+ has_to_byte_array = self.system_text_json_annotation or self.newtonsoft_json_annotation or self.system_xml_annotation
2032
+
2033
+ program_definition = process_template(
2034
+ "structuretocsharp/program.cs.jinja",
2035
+ classes=classes,
2036
+ has_to_byte_array=has_to_byte_array
2037
+ )
2038
+
2039
+ program_file_path = os.path.join(test_directory_path, "InstanceSerializer.cs")
2040
+ with open(program_file_path, 'w', encoding='utf-8') as program_file:
2041
+ program_file.write(program_definition)
2042
+
2043
+ def generate_test_class(self, class_name: str, type_kind: str, test_directory_path: str) -> None:
2044
+ """ Generates a unit test class for a given C# class or enum """
2045
+ structure_schema: Dict[str, JsonNode] = cast(Dict[str, JsonNode], self.generated_structure_types.get(class_name, {}))
2046
+ if class_name.startswith("global::"):
2047
+ class_name = class_name[8:]
2048
+ test_class_name = f"{class_name.split('.')[-1]}Tests"
2049
+ namespace = ".".join(class_name.split('.')[:-1])
2050
+ class_base_name = class_name.split('.')[-1]
2051
+
2052
+ # Skip test generation for abstract classes (cannot be instantiated)
2053
+ if type_kind == "class" and structure_schema.get('abstract', False):
2054
+ return
2055
+
2056
+ if type_kind == "class":
2057
+ fields = self.get_class_test_fields(structure_schema, class_base_name)
2058
+ test_class_definition = process_template(
2059
+ "structuretocsharp/class_test.cs.jinja",
2060
+ namespace=namespace,
2061
+ test_class_name=test_class_name,
2062
+ class_base_name=class_base_name,
2063
+ fields=fields,
2064
+ system_xml_annotation=self.system_xml_annotation,
2065
+ system_text_json_annotation=self.system_text_json_annotation,
2066
+ newtonsoft_json_annotation=self.newtonsoft_json_annotation
2067
+ )
2068
+ elif type_kind == "enum":
2069
+ # For enums, extract symbols from the enum schema
2070
+ enum_values = structure_schema.get('enum', [])
2071
+ symbols = []
2072
+ if enum_values:
2073
+ for value in enum_values:
2074
+ if isinstance(value, str):
2075
+ # Convert to PascalCase enum member name
2076
+ symbol_name = ''.join(word.capitalize() for word in re.split(r'[_\-\s]+', value))
2077
+ symbols.append(symbol_name)
2078
+ else:
2079
+ # For numeric enums, use Value1, Value2, etc.
2080
+ symbols.append(f"Value{value}")
2081
+
2082
+ test_class_definition = process_template(
2083
+ "structuretocsharp/enum_test.cs.jinja",
2084
+ namespace=namespace,
2085
+ test_class_name=test_class_name,
2086
+ enum_base_name=class_base_name,
2087
+ symbols=symbols,
2088
+ system_xml_annotation=self.system_xml_annotation,
2089
+ system_text_json_annotation=self.system_text_json_annotation,
2090
+ newtonsoft_json_annotation=self.newtonsoft_json_annotation
2091
+ )
2092
+ else:
2093
+ return
2094
+
2095
+ test_file_path = os.path.join(test_directory_path, f"{test_class_name}.cs")
2096
+ with open(test_file_path, 'w', encoding='utf-8') as test_file:
2097
+ test_file.write(test_class_definition)
2098
+
2099
+ def get_class_test_fields(self, structure_schema: Dict[str, JsonNode], class_name: str) -> List[Any]:
2100
+ """ Retrieves fields for a given class name """
2101
+
2102
+ class Field:
2103
+ def __init__(self, fn: str, ft: str, tv: Any, ct: bool, pm: bool):
2104
+ self.field_name = fn
2105
+ self.field_type = ft
2106
+ self.test_value = tv
2107
+ self.is_const = ct
2108
+ self.is_primitive = pm
2109
+
2110
+ fields: List[Field] = []
2111
+ if structure_schema and 'properties' in structure_schema:
2112
+ for prop_name, prop_schema in cast(Dict[str, Dict], structure_schema['properties']).items():
2113
+ field_name = prop_name
2114
+ if self.pascal_properties:
2115
+ field_name = pascal(field_name)
2116
+ if field_name == class_name:
2117
+ field_name += "_"
2118
+ if self.is_csharp_reserved_word(field_name):
2119
+ field_name = f"@{field_name}"
2120
+
2121
+ field_type = self.convert_structure_type_to_csharp(
2122
+ class_name, field_name, prop_schema, str(structure_schema.get('namespace', '')))
2123
+ is_class = field_type in self.generated_types and self.generated_types[field_type] == "class"
2124
+
2125
+ # Check if this is a const field
2126
+ is_const = 'const' in prop_schema
2127
+ schema_type = prop_schema.get('type', '') if isinstance(prop_schema, dict) else ''
2128
+ test_value = self.get_test_value(field_type, schema_type) if not is_const else self.format_default_value(prop_schema['const'], field_type)
2129
+
2130
+ f = Field(field_name, field_type, test_value, is_const, not is_class)
2131
+ fields.append(f)
2132
+ return cast(List[Any], fields)
2133
+
2134
+ def get_test_value(self, csharp_type: str, schema_type: str = '') -> str:
2135
+ """Returns a default test value based on the C# type and schema type"""
2136
+ # For nullable object types, return typed null to avoid var issues
2137
+ if csharp_type == "object?" or csharp_type == "object":
2138
+ return "null" # Use null for object types (typically unions) to avoid reference inequality
2139
+
2140
+ # Special test values for JSON Structure types that map to string in C#
2141
+ # but have specific format requirements
2142
+ if schema_type == 'jsonpointer':
2143
+ return '"/example/path"' # Valid JSON Pointer format
2144
+
2145
+ test_values = {
2146
+ 'string': '"test_string"',
2147
+ 'bool': 'true',
2148
+ 'sbyte': '(sbyte)42',
2149
+ 'byte': '(byte)42',
2150
+ 'short': '(short)42',
2151
+ 'ushort': '(ushort)42',
2152
+ 'int': '42',
2153
+ 'uint': '42U',
2154
+ 'long': '42L',
2155
+ 'ulong': '42UL',
2156
+ 'System.Int128': 'new System.Int128(0, 42)',
2157
+ 'System.UInt128': 'new System.UInt128(0, 42)',
2158
+ 'float': '3.14f',
2159
+ 'double': '3.14',
2160
+ 'decimal': '3.14m',
2161
+ 'byte[]': 'new byte[] { 0x01, 0x02, 0x03 }',
2162
+ 'DateOnly': 'new DateOnly(2024, 1, 1)',
2163
+ 'TimeOnly': 'new TimeOnly(12, 0, 0)',
2164
+ 'DateTimeOffset': 'new DateTimeOffset(2024, 1, 1, 12, 0, 0, TimeSpan.Zero)',
2165
+ 'TimeSpan': 'TimeSpan.FromHours(1)',
2166
+ 'Guid': 'new Guid("12345678-1234-1234-1234-123456789012")',
2167
+ 'Uri': 'new Uri("https://example.com")',
2168
+ 'null': 'null'
2169
+ }
2170
+ if csharp_type.endswith('?'):
2171
+ csharp_type = csharp_type[:-1]
2172
+
2173
+ # Normalize to use qualified reference (strip global:: prefix if present, then add it)
2174
+ base_type = csharp_type.replace('global::', '')
2175
+ qualified_ref = f'global::{base_type}'
2176
+
2177
+ # Check if this is a tuple type (generated_types tracks what we've created)
2178
+ if qualified_ref in self.generated_types and self.generated_types[qualified_ref] == "tuple":
2179
+ # For tuple types, we need to construct with test values based on the schema
2180
+ schema = self.generated_structure_types.get(qualified_ref)
2181
+ if schema:
2182
+ tuple_order = schema.get('tuple', [])
2183
+ properties = schema.get('properties', {})
2184
+ test_params = []
2185
+ for prop_name in tuple_order:
2186
+ if prop_name in properties:
2187
+ prop_schema = properties[prop_name]
2188
+ prop_type = self.convert_structure_type_to_csharp(base_type, prop_name, prop_schema, str(schema.get('namespace', '')))
2189
+ test_params.append(self.get_test_value(prop_type))
2190
+ if test_params:
2191
+ return f'new {base_type}({", ".join(test_params)})'
2192
+
2193
+ # Check if this is a choice type (discriminated union)
2194
+ if qualified_ref in self.generated_types and self.generated_types[qualified_ref] == "choice":
2195
+ # For choice types, create an instance with the first choice property set
2196
+ schema = self.generated_structure_types.get(qualified_ref)
2197
+ if schema:
2198
+ choices = schema.get('choices', {})
2199
+ if choices:
2200
+ # Get the first choice property
2201
+ first_choice_name, first_choice_schema = next(iter(choices.items()))
2202
+ choice_type = self.convert_structure_type_to_csharp(base_type, first_choice_name, first_choice_schema, str(schema.get('namespace', '')))
2203
+ choice_test_value = self.get_test_value(choice_type)
2204
+ # Use the constructor that takes the first choice
2205
+ return f'new {base_type}({choice_test_value})'
2206
+
2207
+ # Check if this is an enum type
2208
+ if qualified_ref in self.generated_types and self.generated_types[qualified_ref] == "enum":
2209
+ schema = self.generated_structure_types.get(qualified_ref)
2210
+ if schema:
2211
+ enum_values = schema.get('enum', [])
2212
+ if enum_values:
2213
+ first_value = enum_values[0]
2214
+ enum_base_type = schema.get('type', 'string')
2215
+ numeric_types = ['int8', 'uint8', 'int16', 'uint16', 'int32', 'uint32', 'int64', 'uint64']
2216
+ is_numeric = enum_base_type in numeric_types
2217
+
2218
+ if is_numeric:
2219
+ # Numeric enum - use the member name (Value1, Value2, etc.)
2220
+ member_name = f"Value{first_value}"
2221
+ else:
2222
+ # String enum - convert to PascalCase member name
2223
+ member_name = pascal(str(first_value).replace('-', '_').replace(' ', '_'))
2224
+
2225
+ return f'{base_type}.{member_name}'
2226
+
2227
+ return test_values.get(base_type, test_values.get(csharp_type, f'new {csharp_type}()'))
2228
+
2229
+
2230
+
2231
+
2232
+ def convert_structure_to_csharp(
2233
+ structure_schema_path: str,
2234
+ cs_file_path: str,
2235
+ base_namespace: str = '',
2236
+ project_name: str = '',
2237
+ pascal_properties: bool = False,
2238
+ system_text_json_annotation: bool = False,
2239
+ newtonsoft_json_annotation: bool = False,
2240
+ system_xml_annotation: bool = False
2241
+ ):
2242
+ """Converts JSON Structure schema to C# classes
2243
+
2244
+ Args:
2245
+ structure_schema_path (str): JSON Structure input schema path
2246
+ cs_file_path (str): Output C# file path
2247
+ base_namespace (str, optional): Base namespace. Defaults to ''.
2248
+ project_name (str, optional): Explicit project name for .csproj files (separate from namespace). Defaults to ''.
2249
+ pascal_properties (bool, optional): Pascal case properties. Defaults to False.
2250
+ system_text_json_annotation (bool, optional): Use System.Text.Json annotations. Defaults to False.
2251
+ newtonsoft_json_annotation (bool, optional): Use Newtonsoft.Json annotations. Defaults to False.
2252
+ system_xml_annotation (bool, optional): Use System.Xml.Serialization annotations. Defaults to False.
2253
+ """
2254
+
2255
+ if not base_namespace:
2256
+ base_namespace = os.path.splitext(os.path.basename(cs_file_path))[0].replace('-', '_')
2257
+
2258
+ structtocs = StructureToCSharp(base_namespace)
2259
+ structtocs.project_name = project_name
2260
+ structtocs.pascal_properties = pascal_properties
2261
+ structtocs.system_text_json_annotation = system_text_json_annotation
2262
+ structtocs.newtonsoft_json_annotation = newtonsoft_json_annotation
2263
+ structtocs.system_xml_annotation = system_xml_annotation
2264
+ structtocs.convert(structure_schema_path, cs_file_path)
2265
+
2266
+
2267
+ def convert_structure_schema_to_csharp(
2268
+ structure_schema: JsonNode,
2269
+ output_dir: str,
2270
+ base_namespace: str = '',
2271
+ project_name: str = '',
2272
+ pascal_properties: bool = False,
2273
+ system_text_json_annotation: bool = False,
2274
+ newtonsoft_json_annotation: bool = False,
2275
+ system_xml_annotation: bool = False
2276
+ ):
2277
+ """Converts JSON Structure schema to C# classes
2278
+
2279
+ Args:
2280
+ structure_schema (JsonNode): JSON Structure schema to convert
2281
+ output_dir (str): Output directory
2282
+ base_namespace (str, optional): Base namespace for the generated classes. Defaults to ''.
2283
+ project_name (str, optional): Explicit project name for .csproj files (separate from namespace). Defaults to ''.
2284
+ pascal_properties (bool, optional): Pascal case properties. Defaults to False.
2285
+ system_text_json_annotation (bool, optional): Use System.Text.Json annotations. Defaults to False.
2286
+ newtonsoft_json_annotation (bool, optional): Use Newtonsoft.Json annotations. Defaults to False.
2287
+ system_xml_annotation (bool, optional): Use System.Xml.Serialization annotations. Defaults to False.
2288
+ """
2289
+ structtocs = StructureToCSharp(base_namespace)
2290
+ structtocs.project_name = project_name
2291
+ structtocs.pascal_properties = pascal_properties
2292
+ structtocs.system_text_json_annotation = system_text_json_annotation
2293
+ structtocs.newtonsoft_json_annotation = newtonsoft_json_annotation
2294
+ structtocs.system_xml_annotation = system_xml_annotation
2295
+ structtocs.convert_schema(structure_schema, output_dir)