structurize 2.16.5__py3-none-any.whl → 2.17.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.
@@ -0,0 +1,679 @@
1
+ # pylint: disable=line-too-long
2
+
3
+ """ StructureToXSD class for converting JSON Structure schema to XML Schema (XSD) """
4
+
5
+ import json
6
+ import os
7
+ from typing import Any, Dict, List, Optional, Union
8
+ import xml.etree.ElementTree as ET
9
+ from xml.etree.ElementTree import Element, SubElement, tostring
10
+ from xml.dom import minidom
11
+ from functools import reduce
12
+
13
+ JsonNode = Dict[str, 'JsonNode'] | List['JsonNode'] | str | None
14
+
15
+
16
+ class StructureToXSD:
17
+ """ Converts JSON Structure schema to XSD """
18
+
19
+ def __init__(self, target_namespace: str = ''):
20
+ self.xmlns = {"xs": "http://www.w3.org/2001/XMLSchema"}
21
+ self.union_types: Dict[str, str] = {}
22
+ self.known_types: List[str] = []
23
+ self.common_namespace = ''
24
+ self.target_namespace = target_namespace
25
+ self.schema_doc: JsonNode = None
26
+ self.definitions: Dict[str, Any] = {}
27
+ self.schema_registry: Dict[str, Dict] = {}
28
+ self.offers: Dict[str, Any] = {}
29
+ self.type_dict: Dict[str, Dict] = {}
30
+
31
+ def find_common_namespace(self, namespaces: List[str]) -> str:
32
+ """Find the common namespace prefix from a list of namespaces."""
33
+ if not namespaces:
34
+ return ''
35
+
36
+ def common_prefix(a, b):
37
+ prefix = ''
38
+ for a_char, b_char in zip(a.split('.'), b.split('.')):
39
+ if a_char == b_char:
40
+ prefix += a_char + '.'
41
+ else:
42
+ break
43
+ return prefix.rstrip('.')
44
+
45
+ return reduce(common_prefix, namespaces)
46
+
47
+ def update_common_namespace(self, namespace: str) -> None:
48
+ """Update the common namespace based on the provided namespace."""
49
+ if not self.common_namespace:
50
+ self.common_namespace = namespace
51
+ else:
52
+ self.common_namespace = self.find_common_namespace([self.common_namespace, namespace])
53
+
54
+ def map_primitive_to_xsd(self, structure_type: str | dict) -> str:
55
+ """Maps JSON Structure primitive types to XSD types."""
56
+
57
+ # Handle type as dict (for annotations)
58
+ if isinstance(structure_type, dict):
59
+ type_name = structure_type.get('type', 'string')
60
+ return self.map_primitive_to_xsd(type_name)
61
+
62
+ mapping = {
63
+ 'null': 'string', # Nullable types handled separately
64
+ 'boolean': 'boolean',
65
+ 'string': 'string',
66
+ 'integer': 'integer',
67
+ 'number': 'double',
68
+ 'int8': 'byte',
69
+ 'uint8': 'unsignedByte',
70
+ 'int16': 'short',
71
+ 'uint16': 'unsignedShort',
72
+ 'int32': 'int',
73
+ 'uint32': 'unsignedInt',
74
+ 'int64': 'long',
75
+ 'uint64': 'unsignedLong',
76
+ 'int128': 'integer', # XSD doesn't have 128-bit, use arbitrary precision
77
+ 'uint128': 'integer',
78
+ 'float8': 'float',
79
+ 'float': 'float',
80
+ 'float32': 'float', # IEEE 754 single precision
81
+ 'float64': 'double', # IEEE 754 double precision
82
+ 'double': 'double',
83
+ 'binary32': 'float',
84
+ 'binary64': 'double',
85
+ 'decimal': 'decimal',
86
+ 'binary': 'base64Binary',
87
+ 'bytes': 'base64Binary',
88
+ 'date': 'date',
89
+ 'time': 'time',
90
+ 'datetime': 'dateTime',
91
+ 'timestamp': 'dateTime',
92
+ 'duration': 'duration',
93
+ 'uuid': 'string', # UUID pattern can be added
94
+ 'uri': 'anyURI',
95
+ 'jsonpointer': 'string',
96
+ 'any': 'anyType'
97
+ }
98
+
99
+ xsd_type = mapping.get(structure_type, 'string')
100
+ return f"xs:{xsd_type}"
101
+
102
+ def is_primitive_type(self, structure_type: str | dict) -> bool:
103
+ """Check if the type is a primitive type."""
104
+ if isinstance(structure_type, dict):
105
+ type_name = structure_type.get('type', '')
106
+ return self.is_primitive_type(type_name)
107
+
108
+ primitives = {
109
+ 'null', 'boolean', 'string', 'integer', 'number',
110
+ 'int8', 'uint8', 'int16', 'uint16', 'int32', 'uint32',
111
+ 'int64', 'uint64', 'int128', 'uint128',
112
+ 'float8', 'float', 'float32', 'float64', 'double', 'binary32', 'binary64',
113
+ 'decimal', 'binary', 'bytes', 'date', 'time', 'datetime',
114
+ 'timestamp', 'duration', 'uuid', 'uri', 'jsonpointer'
115
+ }
116
+ return structure_type in primitives
117
+
118
+ def create_element(self, parent: Element, tag: str, **attributes) -> Element:
119
+ """Create an XML element with the proper namespace."""
120
+ return SubElement(parent, f"{{{self.xmlns['xs']}}}{tag}", **attributes)
121
+
122
+ def create_complex_type(self, parent: Element, **attributes) -> Element:
123
+ """Create an XML complexType element."""
124
+ return self.create_element(parent, "complexType", **attributes)
125
+
126
+ def add_custom_properties_as_appinfo(self, element: Element, type_def: Dict) -> None:
127
+ """Add custom JSON Structure properties as xs:appinfo annotations."""
128
+ custom_props = {}
129
+
130
+ # Collect custom properties that should be preserved
131
+ for key in ['altnames', 'unit', 'currency', 'symbol', 'contentEncoding',
132
+ 'contentMediaType', 'multipleOf']:
133
+ if key in type_def:
134
+ custom_props[key] = type_def[key]
135
+
136
+ if custom_props:
137
+ # Find or create annotation element
138
+ annotation = element.find(f"{{{self.xmlns['xs']}}}annotation")
139
+ if annotation is None:
140
+ # Insert annotation as first child
141
+ annotation = Element(f"{{{self.xmlns['xs']}}}annotation")
142
+ element.insert(0, annotation)
143
+
144
+ # Add appinfo with custom properties
145
+ appinfo = self.create_element(annotation, "appinfo")
146
+ appinfo.set("source", "json-structure-extensions")
147
+
148
+ # Add custom properties as text content (JSON format)
149
+ import json
150
+ appinfo.text = json.dumps(custom_props, indent=2)
151
+
152
+ def create_simple_type_with_restriction(self, parent: Element, name: str, base_type: str, facets: Dict[str, Any]) -> Element:
153
+ """Create a simple type with restrictions (facets)."""
154
+ simple_type = self.create_element(parent, "simpleType", name=name)
155
+ restriction = self.create_element(simple_type, "restriction", base=base_type)
156
+
157
+ # Add facets
158
+ if 'minLength' in facets:
159
+ self.create_element(restriction, "minLength", value=str(facets['minLength']))
160
+ if 'maxLength' in facets:
161
+ self.create_element(restriction, "maxLength", value=str(facets['maxLength']))
162
+ if 'pattern' in facets:
163
+ self.create_element(restriction, "pattern", value=facets['pattern'])
164
+ if 'minimum' in facets:
165
+ self.create_element(restriction, "minInclusive", value=str(facets['minimum']))
166
+ if 'maximum' in facets:
167
+ self.create_element(restriction, "maxInclusive", value=str(facets['maximum']))
168
+ if 'exclusiveMinimum' in facets:
169
+ self.create_element(restriction, "minExclusive", value=str(facets['exclusiveMinimum']))
170
+ if 'exclusiveMaximum' in facets:
171
+ self.create_element(restriction, "maxExclusive", value=str(facets['exclusiveMaximum']))
172
+
173
+ return simple_type
174
+
175
+ def resolve_type_reference(self, ref: str) -> Optional[Dict]:
176
+ """Resolve a $ref to its definition."""
177
+ # Handle local references (#/definitions/TypeName)
178
+ if ref.startswith('#/'):
179
+ parts = ref.split('/')
180
+ if len(parts) >= 3 and parts[1] == 'definitions':
181
+ type_name = parts[2]
182
+ return self.definitions.get(type_name)
183
+
184
+ # Handle URI references
185
+ if ref.startswith('http://') or ref.startswith('https://'):
186
+ return self.schema_registry.get(ref)
187
+
188
+ return None
189
+
190
+ def get_type_name_from_ref(self, ref: str) -> str:
191
+ """Extract type name from a $ref."""
192
+ if ref.startswith('#/definitions/'):
193
+ return ref.split('/')[-1]
194
+ return ref.split('/')[-1]
195
+
196
+ def convert_array_type(self, schema_root: Element, type_name: str, parent: Element, array_def: Dict):
197
+ """Handle array type conversion."""
198
+ complex_type = self.create_element(parent, "complexType")
199
+ sequence = self.create_element(complex_type, "sequence")
200
+
201
+ item_type = array_def.get('items', {'type': 'any'})
202
+ min_items = array_def.get('minItems', 0)
203
+ max_items = array_def.get('maxItems')
204
+
205
+ item_element = self.create_element(
206
+ sequence, "element", name="item",
207
+ minOccurs=str(min_items),
208
+ maxOccurs=str(max_items) if max_items is not None else "unbounded"
209
+ )
210
+
211
+ self.set_element_type(schema_root, type_name, item_element, item_type)
212
+
213
+ def convert_set_type(self, schema_root: Element, type_name: str, parent: Element, set_def: Dict):
214
+ """Handle set type conversion (like array but with unique items)."""
215
+ # XSD doesn't have native set, use array with unique constraint
216
+ complex_type = self.create_element(parent, "complexType")
217
+ sequence = self.create_element(complex_type, "sequence")
218
+
219
+ item_type = set_def.get('items', {'type': 'any'})
220
+ min_items = set_def.get('minItems', 0)
221
+ max_items = set_def.get('maxItems')
222
+
223
+ item_element = self.create_element(
224
+ sequence, "element", name="item",
225
+ minOccurs=str(min_items),
226
+ maxOccurs=str(max_items) if max_items is not None else "unbounded"
227
+ )
228
+
229
+ self.set_element_type(schema_root, type_name, item_element, item_type)
230
+
231
+ # Add unique constraint (though this is more semantic than enforced)
232
+ # Could add xs:unique if there's a key to reference
233
+
234
+ def convert_map_type(self, schema_root: Element, type_name: str, parent: Element, map_def: Dict):
235
+ """Handle map type conversion."""
236
+ complex_type = self.create_element(parent, "complexType")
237
+ sequence = self.create_element(complex_type, "sequence")
238
+
239
+ value_type = map_def.get('values', {'type': 'any'})
240
+
241
+ entry_element = self.create_element(
242
+ sequence, "element", name="entry",
243
+ minOccurs="0", maxOccurs="unbounded"
244
+ )
245
+
246
+ entry_complex_type = self.create_element(entry_element, "complexType")
247
+ entry_sequence = self.create_element(entry_complex_type, "sequence")
248
+
249
+ # Map key is always string
250
+ self.create_element(entry_sequence, "element", name="key", type="xs:string")
251
+
252
+ # Map value
253
+ value_element = self.create_element(entry_sequence, "element", name="value")
254
+ self.set_element_type(schema_root, type_name, value_element, value_type)
255
+
256
+ def convert_tuple_type(self, schema_root: Element, type_name: str, parent: Element, tuple_def: Dict):
257
+ """Handle tuple type conversion."""
258
+ complex_type = self.create_element(parent, "complexType")
259
+ sequence = self.create_element(complex_type, "sequence")
260
+
261
+ items = tuple_def.get('items', [])
262
+ for i, item_type in enumerate(items):
263
+ item_element = self.create_element(
264
+ sequence, "element", name=f"item{i}",
265
+ minOccurs="1", maxOccurs="1"
266
+ )
267
+ self.set_element_type(schema_root, type_name, item_element, item_type)
268
+
269
+ def convert_choice_type(self, schema_root: Element, type_name: str, parent: Element, choice_def: Dict):
270
+ """Handle choice (union) type conversion."""
271
+ choices = choice_def.get('choices', [])
272
+ discriminator = choice_def.get('discriminator') or choice_def.get('selector')
273
+
274
+ # Handle choices as dict (tagged) or list (inline)
275
+ if isinstance(choices, dict):
276
+ choices_list = [(name, typedef) for name, typedef in choices.items()]
277
+ else:
278
+ choices_list = [(f"option{i+1}", choice) for i, choice in enumerate(choices)]
279
+
280
+ if discriminator:
281
+ # Tagged union - use xs:choice with different elements
282
+ complex_type = self.create_element(parent, "complexType")
283
+ choice_element = self.create_element(complex_type, "choice")
284
+
285
+ for choice_name, choice_type in choices_list:
286
+ option_element = self.create_element(choice_element, "element", name=choice_name)
287
+ self.set_element_type(schema_root, type_name, option_element, choice_type)
288
+ else:
289
+ # Inline union - create abstract base type with extensions
290
+ abstract_type_name = type_name if type_name else "Choice"
291
+ if not abstract_type_name in self.known_types:
292
+ self.create_element(schema_root, "complexType", name=abstract_type_name, abstract="true")
293
+ self.known_types.append(abstract_type_name)
294
+
295
+ # Create concrete types for each choice
296
+ for i, (choice_name, choice_type) in enumerate(choices_list):
297
+ option_type_name = f"{abstract_type_name}Option{i+1}"
298
+ if option_type_name not in self.known_types:
299
+ option_complex_type = self.create_element(schema_root, "complexType", name=option_type_name)
300
+ complex_content = self.create_element(option_complex_type, "complexContent")
301
+ extension = self.create_element(complex_content, "extension", base=abstract_type_name)
302
+ sequence = self.create_element(extension, "sequence")
303
+
304
+ value_element = self.create_element(sequence, "element", name="value")
305
+ # Mark this type as known BEFORE processing to avoid recursion issues
306
+ self.known_types.append(option_type_name)
307
+ self.set_element_type(schema_root, option_type_name, value_element, choice_type)
308
+
309
+ def set_element_type(self, schema_root: Element, record_name: str, element: Element, type_def: Any):
310
+ """Set the type of an element based on the type definition."""
311
+
312
+ # Handle type as list (union of types, e.g., ["string", "null"])
313
+ if isinstance(type_def, list):
314
+ # This is a union type
315
+ non_null_types = [t for t in type_def if t != 'null']
316
+ is_nullable = 'null' in type_def
317
+
318
+ if len(non_null_types) == 1:
319
+ # Simple nullable type
320
+ self.set_element_type(schema_root, record_name, element, non_null_types[0])
321
+ if is_nullable and 'minOccurs' not in element.attrib:
322
+ element.set('minOccurs', '0')
323
+ elif len(non_null_types) == 0:
324
+ # Just null - use string
325
+ element.set('type', 'xs:string')
326
+ element.set('minOccurs', '0')
327
+ else:
328
+ # Multiple non-null types
329
+ # Check if all are primitives
330
+ all_primitives = all(self.is_primitive_type(t) if isinstance(t, str) else False for t in non_null_types)
331
+
332
+ if all_primitives:
333
+ # Can create a simple union type - use element name to make it unique
334
+ element_name = element.get('name', 'value')
335
+ unique_type_name = f"{record_name}_{element_name}" if element_name != record_name else record_name
336
+ choice_def = {'type': 'choice', 'choices': non_null_types}
337
+ self.convert_choice_type(schema_root, unique_type_name, element, choice_def)
338
+ else:
339
+ # Has complex types - use xs:anyType for now
340
+ # XSD doesn't have a good way to represent unions with mixed simple/complex types
341
+ element.set('type', 'xs:anyType')
342
+
343
+ if is_nullable and 'minOccurs' not in element.attrib:
344
+ element.set('minOccurs', '0')
345
+ return
346
+
347
+ # Handle $ref
348
+ if isinstance(type_def, dict) and '$ref' in type_def:
349
+ ref_type_name = self.get_type_name_from_ref(type_def['$ref'])
350
+ resolved_type = self.resolve_type_reference(type_def['$ref'])
351
+ if resolved_type:
352
+ # Ensure the referenced type is created
353
+ self.process_type_definition(schema_root, ref_type_name, resolved_type)
354
+ element.set('type', ref_type_name)
355
+ return
356
+
357
+ # Handle object type
358
+ if isinstance(type_def, dict):
359
+ type_name = type_def.get('type', 'object')
360
+
361
+ # Handle case where type is a list within the dict
362
+ if isinstance(type_name, list):
363
+ self.set_element_type(schema_root, record_name, element, type_name)
364
+ return
365
+
366
+ if type_name == 'object':
367
+ # Inline object definition
368
+ self.convert_object_properties(schema_root, record_name, element, type_def)
369
+ elif type_name == 'array':
370
+ self.convert_array_type(schema_root, record_name, element, type_def)
371
+ elif type_name == 'set':
372
+ self.convert_set_type(schema_root, record_name, element, type_def)
373
+ elif type_name == 'map':
374
+ self.convert_map_type(schema_root, record_name, element, type_def)
375
+ elif type_name == 'tuple':
376
+ self.convert_tuple_type(schema_root, record_name, element, type_def)
377
+ elif type_name == 'choice':
378
+ # Check if this choice should be a named type
379
+ choice_name = type_def.get('name')
380
+ if choice_name and choice_name not in self.known_types:
381
+ # Create a named complex type for this choice
382
+ self.process_type_definition(schema_root, choice_name, type_def)
383
+ element.set('type', choice_name)
384
+ else:
385
+ # Inline choice
386
+ self.convert_choice_type(schema_root, record_name, element, type_def)
387
+ elif self.is_primitive_type(type_name):
388
+ xsd_type = self.map_primitive_to_xsd(type_name)
389
+ element.set('type', xsd_type)
390
+
391
+ # Apply constraints as restrictions
392
+ facets = {}
393
+ for key in ['minLength', 'maxLength', 'pattern', 'minimum', 'maximum',
394
+ 'exclusiveMinimum', 'exclusiveMaximum', 'precision', 'scale']:
395
+ if key in type_def:
396
+ facets[key] = type_def[key]
397
+
398
+ # Check for enum or const constraints
399
+ has_enum = 'enum' in type_def
400
+ has_const = 'const' in type_def
401
+
402
+ if facets or has_enum or has_const:
403
+ # Create an anonymous simple type with restriction
404
+ simple_type = self.create_element(element, "simpleType")
405
+ restriction = self.create_element(simple_type, "restriction", base=xsd_type)
406
+ element.attrib.pop('type', None) # Remove type attribute
407
+
408
+ # Handle enum constraint
409
+ if has_enum:
410
+ enum_values = type_def['enum']
411
+ if isinstance(enum_values, list):
412
+ for enum_value in enum_values:
413
+ self.create_element(restriction, "enumeration", value=str(enum_value))
414
+
415
+ # Handle const constraint (single enumeration value)
416
+ elif has_const:
417
+ const_value = type_def['const']
418
+ self.create_element(restriction, "enumeration", value=str(const_value))
419
+
420
+ # Handle other facets
421
+ for facet_name, facet_value in facets.items():
422
+ if facet_name == 'minLength':
423
+ self.create_element(restriction, "minLength", value=str(facet_value))
424
+ elif facet_name == 'maxLength':
425
+ self.create_element(restriction, "maxLength", value=str(facet_value))
426
+ elif facet_name == 'pattern':
427
+ self.create_element(restriction, "pattern", value=facet_value)
428
+ elif facet_name == 'minimum':
429
+ self.create_element(restriction, "minInclusive", value=str(facet_value))
430
+ elif facet_name == 'maximum':
431
+ self.create_element(restriction, "maxInclusive", value=str(facet_value))
432
+ elif facet_name == 'exclusiveMinimum':
433
+ self.create_element(restriction, "minExclusive", value=str(facet_value))
434
+ elif facet_name == 'exclusiveMaximum':
435
+ self.create_element(restriction, "maxExclusive", value=str(facet_value))
436
+ elif facet_name == 'precision' and 'scale' in type_def:
437
+ # For decimal types
438
+ self.create_element(restriction, "totalDigits", value=str(facet_value))
439
+ self.create_element(restriction, "fractionDigits", value=str(type_def['scale']))
440
+ else:
441
+ # Named type reference
442
+ element.set('type', type_name)
443
+ elif isinstance(type_def, str):
444
+ # Simple type reference
445
+ if self.is_primitive_type(type_def):
446
+ element.set('type', self.map_primitive_to_xsd(type_def))
447
+ else:
448
+ element.set('type', type_def)
449
+ else:
450
+ # Default to string
451
+ element.set('type', 'xs:string')
452
+
453
+ def convert_object_properties(self, schema_root: Element, type_name: str, parent: Element, obj_def: Dict):
454
+ """Convert object properties to XSD elements."""
455
+ complex_type = self.create_element(parent, "complexType")
456
+
457
+ # Add documentation if present
458
+ description = obj_def.get('description')
459
+ if description:
460
+ annotation = self.create_element(complex_type, "annotation")
461
+ documentation = self.create_element(annotation, "documentation")
462
+ documentation.text = description
463
+
464
+ sequence = self.create_element(complex_type, "sequence")
465
+
466
+ properties = obj_def.get('properties', {})
467
+ required = obj_def.get('required', [])
468
+
469
+ for prop_name, prop_def in properties.items():
470
+ min_occurs = "1" if prop_name in required else "0"
471
+ prop_element = self.create_element(
472
+ sequence, "element", name=prop_name,
473
+ minOccurs=min_occurs, maxOccurs="1"
474
+ )
475
+
476
+ # Add property description
477
+ if isinstance(prop_def, dict) and 'description' in prop_def:
478
+ annotation = self.create_element(prop_element, "annotation")
479
+ documentation = self.create_element(annotation, "documentation")
480
+ documentation.text = prop_def['description']
481
+
482
+ # Add custom properties as appinfo
483
+ if isinstance(prop_def, dict):
484
+ self.add_custom_properties_as_appinfo(prop_element, prop_def)
485
+
486
+ self.set_element_type(schema_root, type_name, prop_element, prop_def)
487
+
488
+ def process_type_definition(self, schema_root: Element, type_name: str, type_def: Dict):
489
+ """Process a type definition and create corresponding XSD type."""
490
+
491
+ if type_name in self.known_types:
492
+ return
493
+
494
+ type_type = type_def.get('type', 'object')
495
+ namespace = type_def.get('namespace', '')
496
+
497
+ if namespace:
498
+ self.update_common_namespace(namespace)
499
+
500
+ # Handle abstract types
501
+ is_abstract = type_def.get('abstract', False)
502
+
503
+ if type_type == 'object':
504
+ complex_type = self.create_complex_type(schema_root, name=type_name)
505
+ if is_abstract:
506
+ complex_type.set('abstract', 'true')
507
+
508
+ # Add documentation
509
+ description = type_def.get('description')
510
+ if description:
511
+ annotation = self.create_element(complex_type, "annotation")
512
+ documentation = self.create_element(annotation, "documentation")
513
+ documentation.text = description
514
+
515
+ # Handle extensions ($extends)
516
+ extends = type_def.get('$extends')
517
+ if extends:
518
+ complex_content = self.create_element(complex_type, "complexContent")
519
+ base_type = self.get_type_name_from_ref(extends) if isinstance(extends, str) and extends.startswith('#') else extends
520
+ extension = self.create_element(complex_content, "extension", base=base_type)
521
+ sequence = self.create_element(extension, "sequence")
522
+
523
+ # Add properties to the extension
524
+ properties = type_def.get('properties', {})
525
+ required = type_def.get('required', [])
526
+ for prop_name, prop_def in properties.items():
527
+ min_occurs = "1" if prop_name in required else "0"
528
+ prop_element = self.create_element(
529
+ sequence, "element", name=prop_name,
530
+ minOccurs=min_occurs, maxOccurs="1"
531
+ )
532
+ self.set_element_type(schema_root, type_name, prop_element, prop_def)
533
+ else:
534
+ # Regular object
535
+ sequence = self.create_element(complex_type, "sequence")
536
+ properties = type_def.get('properties', {})
537
+ required = type_def.get('required', [])
538
+
539
+ for prop_name, prop_def in properties.items():
540
+ min_occurs = "1" if prop_name in required else "0"
541
+ prop_element = self.create_element(
542
+ sequence, "element", name=prop_name,
543
+ minOccurs=min_occurs, maxOccurs="1"
544
+ )
545
+
546
+ # Add property description
547
+ if isinstance(prop_def, dict) and 'description' in prop_def:
548
+ annotation = self.create_element(prop_element, "annotation")
549
+ documentation = self.create_element(annotation, "documentation")
550
+ documentation.text = prop_def['description']
551
+
552
+ self.set_element_type(schema_root, type_name, prop_element, prop_def)
553
+
554
+ self.known_types.append(type_name)
555
+
556
+ elif type_type == 'array':
557
+ # Create a named complex type for the array
558
+ complex_type = self.create_complex_type(schema_root, name=type_name)
559
+ sequence = self.create_element(complex_type, "sequence")
560
+
561
+ item_type = type_def.get('items', {'type': 'any'})
562
+ min_items = type_def.get('minItems', 0)
563
+ max_items = type_def.get('maxItems')
564
+
565
+ item_element = self.create_element(
566
+ sequence, "element", name="item",
567
+ minOccurs=str(min_items),
568
+ maxOccurs=str(max_items) if max_items is not None else "unbounded"
569
+ )
570
+
571
+ self.set_element_type(schema_root, type_name, item_element, item_type)
572
+ self.known_types.append(type_name)
573
+
574
+ elif type_type == 'choice':
575
+ # Handle choice as complex type with choice element
576
+ choices = type_def.get('choices', [])
577
+ discriminator = type_def.get('discriminator') or type_def.get('selector')
578
+
579
+ # Handle choices as dict (tagged) or list (inline)
580
+ if isinstance(choices, dict):
581
+ choices_list = [(name, typedef) for name, typedef in choices.items()]
582
+ else:
583
+ choices_list = [(f"option{i+1}", choice) for i, choice in enumerate(choices)]
584
+
585
+ complex_type = self.create_complex_type(schema_root, name=type_name)
586
+ choice_element = self.create_element(complex_type, "choice")
587
+
588
+ for choice_name, choice_type in choices_list:
589
+ option_element = self.create_element(choice_element, "element", name=choice_name)
590
+ self.set_element_type(schema_root, type_name, option_element, choice_type)
591
+
592
+ self.known_types.append(type_name)
593
+
594
+ elif self.is_primitive_type(type_type):
595
+ # Create a simple type with restrictions
596
+ xsd_type = self.map_primitive_to_xsd(type_type)
597
+ facets = {}
598
+ for key in ['minLength', 'maxLength', 'pattern', 'minimum', 'maximum']:
599
+ if key in type_def:
600
+ facets[key] = type_def[key]
601
+
602
+ if facets:
603
+ self.create_simple_type_with_restriction(schema_root, type_name, xsd_type, facets)
604
+ else:
605
+ # Simple typedef
606
+ simple_type = self.create_element(schema_root, "simpleType", name=type_name)
607
+ restriction = self.create_element(simple_type, "restriction", base=xsd_type)
608
+
609
+ self.known_types.append(type_name)
610
+
611
+ def structure_schema_to_xsd(self, structure_schema: Dict) -> Element:
612
+ """Convert JSON Structure schema to XSD."""
613
+ ET.register_namespace('xs', self.xmlns['xs'])
614
+ schema = Element(f"{{{self.xmlns['xs']}}}schema")
615
+
616
+ # Extract definitions
617
+ self.definitions = structure_schema.get('definitions', {})
618
+
619
+ # Process top-level type
620
+ if structure_schema.get('type'):
621
+ type_name = structure_schema.get('name', 'Root')
622
+ namespace = structure_schema.get('namespace', '')
623
+
624
+ if namespace:
625
+ self.update_common_namespace(namespace)
626
+
627
+ # Create root element
628
+ root_element = self.create_element(schema, "element", name=type_name)
629
+
630
+ # Add description
631
+ description = structure_schema.get('description')
632
+ if description:
633
+ annotation = self.create_element(root_element, "annotation")
634
+ documentation = self.create_element(annotation, "documentation")
635
+ documentation.text = description
636
+
637
+ # Set the type
638
+ if structure_schema.get('type') == 'object':
639
+ self.convert_object_properties(schema, type_name, root_element, structure_schema)
640
+ else:
641
+ self.set_element_type(schema, type_name, root_element, structure_schema)
642
+
643
+ # Process all definitions
644
+ for def_name, def_value in self.definitions.items():
645
+ self.process_type_definition(schema, def_name, def_value)
646
+
647
+ # Set namespace
648
+ target_ns = self.target_namespace if self.target_namespace else \
649
+ f"urn:{self.common_namespace.replace('.', ':')}" if self.common_namespace else \
650
+ "urn:example:schema"
651
+
652
+ schema.set('targetNamespace', target_ns)
653
+ schema.set('xmlns', target_ns)
654
+ schema.set('elementFormDefault', 'qualified')
655
+ ET.register_namespace('', target_ns)
656
+
657
+ return schema
658
+
659
+ def save_xsd_to_file(self, schema: Element, xml_path: str) -> None:
660
+ """Save the XML schema to a file."""
661
+ os.makedirs(os.path.dirname(xml_path) or '.', exist_ok=True)
662
+ tree_str = tostring(schema, 'utf-8')
663
+ pretty_tree = minidom.parseString(tree_str).toprettyxml(indent=" ")
664
+ with open(xml_path, 'w', encoding='utf-8') as xml_file:
665
+ xml_file.write(pretty_tree)
666
+
667
+ def convert_structure_to_xsd(self, structure_schema_path: str, xml_file_path: str) -> None:
668
+ """Convert JSON Structure schema file to XML schema file."""
669
+ with open(structure_schema_path, 'r', encoding='utf-8') as structure_file:
670
+ structure_schema = json.load(structure_file)
671
+
672
+ xml_schema = self.structure_schema_to_xsd(structure_schema)
673
+ self.save_xsd_to_file(xml_schema, xml_file_path)
674
+
675
+
676
+ def convert_structure_to_xsd(structure_schema_path: str, xml_file_path: str, target_namespace: str = '') -> None:
677
+ """Convert JSON Structure schema to XSD."""
678
+ converter = StructureToXSD(target_namespace)
679
+ converter.convert_structure_to_xsd(structure_schema_path, xml_file_path)