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,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)
|