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