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