structurize 2.20.2__py3-none-any.whl → 2.20.4__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 +2 -0
- avrotize/_version.py +3 -3
- avrotize/avrotocsharp.py +121 -16
- avrotize/avrotots.py +2 -2
- avrotize/commands.json +168 -9
- avrotize/constants.py +15 -0
- avrotize/jsonstostructure.py +234 -12
- avrotize/openapitostructure.py +717 -0
- avrotize/structuretojs.py +657 -0
- avrotize/structuretots.py +26 -2
- {structurize-2.20.2.dist-info → structurize-2.20.4.dist-info}/METADATA +1 -1
- {structurize-2.20.2.dist-info → structurize-2.20.4.dist-info}/RECORD +16 -14
- {structurize-2.20.2.dist-info → structurize-2.20.4.dist-info}/WHEEL +0 -0
- {structurize-2.20.2.dist-info → structurize-2.20.4.dist-info}/entry_points.txt +0 -0
- {structurize-2.20.2.dist-info → structurize-2.20.4.dist-info}/licenses/LICENSE +0 -0
- {structurize-2.20.2.dist-info → structurize-2.20.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenAPI/Swagger to JSON Structure converter.
|
|
3
|
+
|
|
4
|
+
This module converts OpenAPI 3.x and Swagger 2.0 documents to JSON Structure format
|
|
5
|
+
by extracting schema definitions and delegating to the existing JSON Schema to
|
|
6
|
+
JSON Structure converter.
|
|
7
|
+
|
|
8
|
+
Supported versions:
|
|
9
|
+
- Swagger 2.0
|
|
10
|
+
- OpenAPI 3.0.x
|
|
11
|
+
- OpenAPI 3.1.x
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
# pylint: disable=line-too-long
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
19
|
+
from urllib.parse import urlparse
|
|
20
|
+
|
|
21
|
+
import requests
|
|
22
|
+
|
|
23
|
+
from avrotize.jsonstostructure import JsonToStructureConverter
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class OpenApiToStructureConverter:
|
|
27
|
+
"""
|
|
28
|
+
Converts OpenAPI 3.x and Swagger 2.0 documents to JSON Structure format.
|
|
29
|
+
|
|
30
|
+
This converter extracts schema definitions from API documents and converts
|
|
31
|
+
them to JSON Structure format using the existing JSON Schema to JSON Structure
|
|
32
|
+
conversion machinery.
|
|
33
|
+
|
|
34
|
+
Supported document types:
|
|
35
|
+
- Swagger 2.0: Extracts from `definitions`
|
|
36
|
+
- OpenAPI 3.0.x: Extracts from `components.schemas`
|
|
37
|
+
- OpenAPI 3.1.x: Extracts from `components.schemas` (JSON Schema compatible)
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
root_namespace: The namespace for the root schema.
|
|
41
|
+
root_class_name: The name of the root class.
|
|
42
|
+
preserve_composition: Flag to preserve composition keywords.
|
|
43
|
+
detect_inheritance: Flag to detect inheritance patterns.
|
|
44
|
+
detect_discriminators: Flag to detect OpenAPI discriminator patterns.
|
|
45
|
+
convert_empty_objects_to_maps: Flag to convert objects with only additionalProperties to maps.
|
|
46
|
+
lift_inline_schemas: Flag to lift inline schemas from paths to definitions.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
# Supported specification versions
|
|
50
|
+
SWAGGER_2 = 'swagger_2'
|
|
51
|
+
OPENAPI_3_0 = 'openapi_3_0'
|
|
52
|
+
OPENAPI_3_1 = 'openapi_3_1'
|
|
53
|
+
|
|
54
|
+
def __init__(self) -> None:
|
|
55
|
+
"""Initialize the OpenAPI to JSON Structure converter."""
|
|
56
|
+
self.root_namespace = 'example.com'
|
|
57
|
+
self.root_class_name = 'document'
|
|
58
|
+
self.preserve_composition = False # Resolve composition for JSON Structure Core compliance
|
|
59
|
+
self.detect_inheritance = True
|
|
60
|
+
self.detect_discriminators = True
|
|
61
|
+
self.convert_empty_objects_to_maps = True
|
|
62
|
+
self.lift_inline_schemas = False # Optional: lift inline schemas from paths
|
|
63
|
+
self.content_cache: Dict[str, str] = {}
|
|
64
|
+
|
|
65
|
+
def detect_spec_version(self, doc: dict) -> Tuple[str, str]:
|
|
66
|
+
"""
|
|
67
|
+
Detect the specification version of an API document.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
doc: The API document as a dictionary.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Tuple of (spec_type, version_string) where spec_type is one of:
|
|
74
|
+
SWAGGER_2, OPENAPI_3_0, OPENAPI_3_1
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
ValueError: If the document is not a valid Swagger/OpenAPI document.
|
|
78
|
+
"""
|
|
79
|
+
# Check for Swagger 2.0
|
|
80
|
+
if 'swagger' in doc:
|
|
81
|
+
swagger_version = str(doc['swagger'])
|
|
82
|
+
if swagger_version.startswith('2.'):
|
|
83
|
+
return (self.SWAGGER_2, swagger_version)
|
|
84
|
+
raise ValueError(f"Unsupported Swagger version: {swagger_version}. Only Swagger 2.0 is supported.")
|
|
85
|
+
|
|
86
|
+
# Check for OpenAPI 3.x
|
|
87
|
+
if 'openapi' in doc:
|
|
88
|
+
openapi_version = str(doc['openapi'])
|
|
89
|
+
if openapi_version.startswith('3.0'):
|
|
90
|
+
return (self.OPENAPI_3_0, openapi_version)
|
|
91
|
+
if openapi_version.startswith('3.1'):
|
|
92
|
+
return (self.OPENAPI_3_1, openapi_version)
|
|
93
|
+
if openapi_version.startswith('3.'):
|
|
94
|
+
# Future 3.x versions - treat as 3.1 compatible
|
|
95
|
+
return (self.OPENAPI_3_1, openapi_version)
|
|
96
|
+
raise ValueError(f"Unsupported OpenAPI version: {openapi_version}. Supported versions: 3.0.x, 3.1.x")
|
|
97
|
+
|
|
98
|
+
raise ValueError("Not a valid Swagger/OpenAPI document: missing 'swagger' or 'openapi' version field")
|
|
99
|
+
|
|
100
|
+
def fetch_content(self, url: str) -> str:
|
|
101
|
+
"""
|
|
102
|
+
Fetch content from a URL or file path.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
url: The URL or file path to fetch content from.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
The content as a string.
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
requests.RequestException: If there is an error fetching from HTTP/HTTPS.
|
|
112
|
+
FileNotFoundError: If the file does not exist.
|
|
113
|
+
"""
|
|
114
|
+
if url in self.content_cache:
|
|
115
|
+
return self.content_cache[url]
|
|
116
|
+
|
|
117
|
+
parsed_url = urlparse(url)
|
|
118
|
+
|
|
119
|
+
if parsed_url.scheme in ['http', 'https']:
|
|
120
|
+
response = requests.get(url, timeout=30)
|
|
121
|
+
response.raise_for_status()
|
|
122
|
+
content = response.text
|
|
123
|
+
self.content_cache[url] = content
|
|
124
|
+
return content
|
|
125
|
+
elif parsed_url.scheme == 'file' or not parsed_url.scheme:
|
|
126
|
+
# Handle file URLs or local paths
|
|
127
|
+
file_path = parsed_url.path if parsed_url.scheme == 'file' else url
|
|
128
|
+
if os.name == 'nt' and file_path.startswith('/'):
|
|
129
|
+
file_path = file_path[1:]
|
|
130
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
131
|
+
content = f.read()
|
|
132
|
+
self.content_cache[url] = content
|
|
133
|
+
return content
|
|
134
|
+
else:
|
|
135
|
+
raise ValueError(f"Unsupported URL scheme: {parsed_url.scheme}")
|
|
136
|
+
|
|
137
|
+
def extract_schemas_from_swagger2(self, swagger_doc: dict) -> dict:
|
|
138
|
+
"""
|
|
139
|
+
Extract schemas from a Swagger 2.0 document and convert to JSON Schema format.
|
|
140
|
+
|
|
141
|
+
Swagger 2.0 stores definitions at the root level under `definitions`.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
swagger_doc: The Swagger 2.0 document as a dictionary.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
A JSON Schema document with definitions from the Swagger document.
|
|
148
|
+
"""
|
|
149
|
+
json_schema: Dict[str, Any] = {
|
|
150
|
+
"$schema": "http://json-schema.org/draft-04/schema#"
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
# Copy Swagger info to JSON Schema metadata
|
|
154
|
+
if 'info' in swagger_doc:
|
|
155
|
+
info = swagger_doc['info']
|
|
156
|
+
if 'title' in info:
|
|
157
|
+
json_schema['title'] = info['title']
|
|
158
|
+
if 'description' in info:
|
|
159
|
+
json_schema['description'] = info['description']
|
|
160
|
+
if 'version' in info:
|
|
161
|
+
title_slug = info.get('title', 'swagger').lower().replace(' ', '-')
|
|
162
|
+
json_schema['$id'] = f"https://{self.root_namespace}/schemas/{title_slug}/{info['version']}"
|
|
163
|
+
|
|
164
|
+
# Swagger 2.0 uses top-level 'definitions'
|
|
165
|
+
definitions = swagger_doc.get('definitions', {})
|
|
166
|
+
|
|
167
|
+
if definitions:
|
|
168
|
+
json_schema['definitions'] = {}
|
|
169
|
+
for schema_name, schema_def in definitions.items():
|
|
170
|
+
# Process the schema to handle Swagger-specific keywords
|
|
171
|
+
processed_schema = self._process_swagger2_schema(schema_def)
|
|
172
|
+
json_schema['definitions'][schema_name] = processed_schema
|
|
173
|
+
|
|
174
|
+
# Optionally lift inline schemas from paths
|
|
175
|
+
if self.lift_inline_schemas:
|
|
176
|
+
self._lift_inline_schemas_from_swagger2_paths(swagger_doc, json_schema)
|
|
177
|
+
|
|
178
|
+
return json_schema
|
|
179
|
+
|
|
180
|
+
def _process_swagger2_schema(self, schema: Union[dict, Any]) -> Union[dict, Any]:
|
|
181
|
+
"""
|
|
182
|
+
Process a Swagger 2.0 schema to handle Swagger-specific keywords.
|
|
183
|
+
|
|
184
|
+
Swagger 2.0 uses slightly different keywords than OpenAPI 3.x.
|
|
185
|
+
Notable differences:
|
|
186
|
+
- No 'nullable' keyword (use x-nullable extension)
|
|
187
|
+
- Uses 'type: file' for file uploads
|
|
188
|
+
- discriminator is a string (property name), not an object
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
schema: The Swagger 2.0 schema object.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
The processed schema compatible with JSON Schema.
|
|
195
|
+
"""
|
|
196
|
+
if not isinstance(schema, dict):
|
|
197
|
+
return schema
|
|
198
|
+
|
|
199
|
+
processed: Dict[str, Any] = {}
|
|
200
|
+
|
|
201
|
+
for key, value in schema.items():
|
|
202
|
+
if key == 'x-nullable':
|
|
203
|
+
# Swagger extension for nullable
|
|
204
|
+
if value is True:
|
|
205
|
+
processed['nullable'] = True
|
|
206
|
+
elif key == 'discriminator':
|
|
207
|
+
# Swagger 2.0 discriminator is just a property name string
|
|
208
|
+
if isinstance(value, str):
|
|
209
|
+
processed['discriminator'] = {
|
|
210
|
+
'propertyName': value
|
|
211
|
+
}
|
|
212
|
+
else:
|
|
213
|
+
processed['discriminator'] = value
|
|
214
|
+
elif key == 'readOnly':
|
|
215
|
+
if 'x-metadata' not in processed:
|
|
216
|
+
processed['x-metadata'] = {}
|
|
217
|
+
processed['x-metadata']['readOnly'] = value
|
|
218
|
+
elif key == 'xml':
|
|
219
|
+
if 'x-metadata' not in processed:
|
|
220
|
+
processed['x-metadata'] = {}
|
|
221
|
+
processed['x-metadata']['xml'] = value
|
|
222
|
+
elif key == 'externalDocs':
|
|
223
|
+
if 'x-metadata' not in processed:
|
|
224
|
+
processed['x-metadata'] = {}
|
|
225
|
+
processed['x-metadata']['externalDocs'] = value
|
|
226
|
+
elif key == 'example':
|
|
227
|
+
if 'examples' not in processed:
|
|
228
|
+
processed['examples'] = []
|
|
229
|
+
processed['examples'].append(value)
|
|
230
|
+
elif key == '$ref':
|
|
231
|
+
# Swagger 2.0 refs are already in #/definitions/ format
|
|
232
|
+
processed['$ref'] = value
|
|
233
|
+
elif key in ('properties', 'additionalProperties', 'patternProperties'):
|
|
234
|
+
if key == 'properties' and isinstance(value, dict):
|
|
235
|
+
processed[key] = {
|
|
236
|
+
prop_name: self._process_swagger2_schema(prop_schema)
|
|
237
|
+
for prop_name, prop_schema in value.items()
|
|
238
|
+
}
|
|
239
|
+
elif key == 'additionalProperties' and isinstance(value, dict):
|
|
240
|
+
processed[key] = self._process_swagger2_schema(value)
|
|
241
|
+
elif key == 'patternProperties' and isinstance(value, dict):
|
|
242
|
+
processed[key] = {
|
|
243
|
+
pattern: self._process_swagger2_schema(prop_schema)
|
|
244
|
+
for pattern, prop_schema in value.items()
|
|
245
|
+
}
|
|
246
|
+
else:
|
|
247
|
+
processed[key] = value
|
|
248
|
+
elif key == 'items':
|
|
249
|
+
if isinstance(value, dict):
|
|
250
|
+
processed[key] = self._process_swagger2_schema(value)
|
|
251
|
+
elif isinstance(value, list):
|
|
252
|
+
processed[key] = [self._process_swagger2_schema(item) for item in value]
|
|
253
|
+
else:
|
|
254
|
+
processed[key] = value
|
|
255
|
+
elif key == 'allOf':
|
|
256
|
+
if isinstance(value, list):
|
|
257
|
+
processed[key] = [self._process_swagger2_schema(item) for item in value]
|
|
258
|
+
else:
|
|
259
|
+
processed[key] = value
|
|
260
|
+
elif key == 'type' and value == 'file':
|
|
261
|
+
# Swagger 2.0 file type - convert to string with format
|
|
262
|
+
processed['type'] = 'string'
|
|
263
|
+
processed['format'] = 'binary'
|
|
264
|
+
else:
|
|
265
|
+
processed[key] = value
|
|
266
|
+
|
|
267
|
+
# Handle nullable conversion
|
|
268
|
+
if processed.get('nullable') is True:
|
|
269
|
+
if 'type' in processed:
|
|
270
|
+
current_type = processed['type']
|
|
271
|
+
if isinstance(current_type, list):
|
|
272
|
+
if 'null' not in current_type:
|
|
273
|
+
processed['type'] = current_type + ['null']
|
|
274
|
+
else:
|
|
275
|
+
processed['type'] = [current_type, 'null']
|
|
276
|
+
del processed['nullable']
|
|
277
|
+
|
|
278
|
+
return processed
|
|
279
|
+
|
|
280
|
+
def _lift_inline_schemas_from_swagger2_paths(self, swagger_doc: dict, json_schema: dict) -> None:
|
|
281
|
+
"""
|
|
282
|
+
Lift inline schemas from Swagger 2.0 paths into named definitions.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
swagger_doc: The Swagger 2.0 document.
|
|
286
|
+
json_schema: The JSON Schema being built (modified in place).
|
|
287
|
+
"""
|
|
288
|
+
if 'definitions' not in json_schema:
|
|
289
|
+
json_schema['definitions'] = {}
|
|
290
|
+
|
|
291
|
+
paths = swagger_doc.get('paths', {})
|
|
292
|
+
|
|
293
|
+
for path, path_item in paths.items():
|
|
294
|
+
if not isinstance(path_item, dict):
|
|
295
|
+
continue
|
|
296
|
+
|
|
297
|
+
for method in ['get', 'put', 'post', 'delete', 'options', 'head', 'patch']:
|
|
298
|
+
operation = path_item.get(method)
|
|
299
|
+
if not isinstance(operation, dict):
|
|
300
|
+
continue
|
|
301
|
+
|
|
302
|
+
operation_id = operation.get('operationId', f"{method}_{path.replace('/', '_')}")
|
|
303
|
+
|
|
304
|
+
# Extract parameter schemas (body parameters in Swagger 2.0)
|
|
305
|
+
parameters = operation.get('parameters', [])
|
|
306
|
+
for param in parameters:
|
|
307
|
+
if isinstance(param, dict) and param.get('in') == 'body':
|
|
308
|
+
schema = param.get('schema', {})
|
|
309
|
+
if isinstance(schema, dict) and '$ref' not in schema:
|
|
310
|
+
def_name = f"{operation_id}_Request"
|
|
311
|
+
processed = self._process_swagger2_schema(schema)
|
|
312
|
+
json_schema['definitions'][def_name] = processed
|
|
313
|
+
|
|
314
|
+
# Extract response schemas
|
|
315
|
+
responses = operation.get('responses', {})
|
|
316
|
+
for status_code, response in responses.items():
|
|
317
|
+
if isinstance(response, dict) and 'schema' in response:
|
|
318
|
+
schema = response['schema']
|
|
319
|
+
if isinstance(schema, dict) and '$ref' not in schema:
|
|
320
|
+
def_name = f"{operation_id}_{status_code}_Response"
|
|
321
|
+
processed = self._process_swagger2_schema(schema)
|
|
322
|
+
json_schema['definitions'][def_name] = processed
|
|
323
|
+
|
|
324
|
+
def extract_schemas_from_openapi(self, openapi_doc: dict, spec_type: str = None) -> dict:
|
|
325
|
+
"""
|
|
326
|
+
Extract schemas from an OpenAPI 3.x document and convert to JSON Schema format.
|
|
327
|
+
|
|
328
|
+
This method extracts all schema definitions from `components.schemas` and
|
|
329
|
+
creates a consolidated JSON Schema document that can be processed by the
|
|
330
|
+
existing JSON Schema to JSON Structure converter.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
openapi_doc: The OpenAPI document as a dictionary.
|
|
334
|
+
spec_type: The spec type (OPENAPI_3_0 or OPENAPI_3_1).
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
A JSON Schema document with definitions from the OpenAPI document.
|
|
338
|
+
"""
|
|
339
|
+
# Use appropriate JSON Schema version based on OpenAPI version
|
|
340
|
+
# OpenAPI 3.1 uses JSON Schema draft 2020-12
|
|
341
|
+
if spec_type == self.OPENAPI_3_1:
|
|
342
|
+
schema_version = "https://json-schema.org/draft/2020-12/schema"
|
|
343
|
+
else:
|
|
344
|
+
schema_version = "http://json-schema.org/draft-07/schema#"
|
|
345
|
+
|
|
346
|
+
json_schema: Dict[str, Any] = {
|
|
347
|
+
"$schema": schema_version
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
# Copy OpenAPI info to JSON Schema metadata
|
|
351
|
+
if 'info' in openapi_doc:
|
|
352
|
+
info = openapi_doc['info']
|
|
353
|
+
if 'title' in info:
|
|
354
|
+
json_schema['title'] = info['title']
|
|
355
|
+
if 'description' in info:
|
|
356
|
+
json_schema['description'] = info['description']
|
|
357
|
+
if 'version' in info:
|
|
358
|
+
title_slug = info.get('title', 'openapi').lower().replace(' ', '-')
|
|
359
|
+
json_schema['$id'] = f"https://{self.root_namespace}/schemas/{title_slug}/{info['version']}"
|
|
360
|
+
|
|
361
|
+
# Extract components.schemas
|
|
362
|
+
components = openapi_doc.get('components', {})
|
|
363
|
+
schemas = components.get('schemas', {})
|
|
364
|
+
|
|
365
|
+
if schemas:
|
|
366
|
+
# Convert OpenAPI schemas to JSON Schema definitions
|
|
367
|
+
json_schema['definitions'] = {}
|
|
368
|
+
for schema_name, schema_def in schemas.items():
|
|
369
|
+
# Process the schema to handle OpenAPI-specific keywords
|
|
370
|
+
processed_schema = self._process_openapi_schema(schema_def)
|
|
371
|
+
json_schema['definitions'][schema_name] = processed_schema
|
|
372
|
+
|
|
373
|
+
# Optionally lift inline schemas from paths
|
|
374
|
+
if self.lift_inline_schemas:
|
|
375
|
+
self._lift_inline_schemas_from_paths(openapi_doc, json_schema)
|
|
376
|
+
|
|
377
|
+
return json_schema
|
|
378
|
+
|
|
379
|
+
def _process_openapi_schema(self, schema: Union[dict, Any]) -> Union[dict, Any]:
|
|
380
|
+
"""
|
|
381
|
+
Process an OpenAPI schema to handle OpenAPI-specific keywords.
|
|
382
|
+
|
|
383
|
+
This method converts OpenAPI-specific keywords to JSON Structure metadata
|
|
384
|
+
annotations or handles them appropriately.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
schema: The OpenAPI schema object.
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
The processed schema with OpenAPI keywords handled.
|
|
391
|
+
"""
|
|
392
|
+
if not isinstance(schema, dict):
|
|
393
|
+
return schema
|
|
394
|
+
|
|
395
|
+
processed: Dict[str, Any] = {}
|
|
396
|
+
|
|
397
|
+
for key, value in schema.items():
|
|
398
|
+
if key == 'nullable':
|
|
399
|
+
# OpenAPI nullable is handled by making type a union with null
|
|
400
|
+
# This is handled during conversion, store it for later processing
|
|
401
|
+
processed['nullable'] = value
|
|
402
|
+
elif key == 'readOnly':
|
|
403
|
+
# Map to JSON Structure metadata annotation
|
|
404
|
+
if 'x-metadata' not in processed:
|
|
405
|
+
processed['x-metadata'] = {}
|
|
406
|
+
processed['x-metadata']['readOnly'] = value
|
|
407
|
+
elif key == 'writeOnly':
|
|
408
|
+
# Map to JSON Structure metadata annotation
|
|
409
|
+
if 'x-metadata' not in processed:
|
|
410
|
+
processed['x-metadata'] = {}
|
|
411
|
+
processed['x-metadata']['writeOnly'] = value
|
|
412
|
+
elif key == 'deprecated':
|
|
413
|
+
# Map to JSON Structure metadata annotation
|
|
414
|
+
if 'x-metadata' not in processed:
|
|
415
|
+
processed['x-metadata'] = {}
|
|
416
|
+
processed['x-metadata']['deprecated'] = value
|
|
417
|
+
elif key == 'discriminator':
|
|
418
|
+
# OpenAPI discriminator - process and convert mapping references
|
|
419
|
+
if isinstance(value, dict):
|
|
420
|
+
processed_discriminator = dict(value)
|
|
421
|
+
if 'mapping' in processed_discriminator:
|
|
422
|
+
# Convert mapping references from OpenAPI to JSON Schema format
|
|
423
|
+
converted_mapping = {}
|
|
424
|
+
for key_name, ref_value in processed_discriminator['mapping'].items():
|
|
425
|
+
if isinstance(ref_value, str) and ref_value.startswith('#/components/schemas/'):
|
|
426
|
+
converted_mapping[key_name] = ref_value.replace('#/components/schemas/', '#/definitions/')
|
|
427
|
+
else:
|
|
428
|
+
converted_mapping[key_name] = ref_value
|
|
429
|
+
processed_discriminator['mapping'] = converted_mapping
|
|
430
|
+
processed['discriminator'] = processed_discriminator
|
|
431
|
+
else:
|
|
432
|
+
processed['discriminator'] = value
|
|
433
|
+
elif key == 'xml':
|
|
434
|
+
# OpenAPI XML object - map to metadata
|
|
435
|
+
if 'x-metadata' not in processed:
|
|
436
|
+
processed['x-metadata'] = {}
|
|
437
|
+
processed['x-metadata']['xml'] = value
|
|
438
|
+
elif key == 'externalDocs':
|
|
439
|
+
# Map to JSON Structure metadata
|
|
440
|
+
if 'x-metadata' not in processed:
|
|
441
|
+
processed['x-metadata'] = {}
|
|
442
|
+
processed['x-metadata']['externalDocs'] = value
|
|
443
|
+
elif key == 'example':
|
|
444
|
+
# Map example to JSON Schema examples array
|
|
445
|
+
if 'examples' not in processed:
|
|
446
|
+
processed['examples'] = []
|
|
447
|
+
processed['examples'].append(value)
|
|
448
|
+
elif key == 'examples':
|
|
449
|
+
# OpenAPI examples object
|
|
450
|
+
processed['examples'] = list(value.values()) if isinstance(value, dict) else value
|
|
451
|
+
elif key == '$ref':
|
|
452
|
+
# Handle OpenAPI references - convert to JSON Schema format
|
|
453
|
+
ref = value
|
|
454
|
+
if ref.startswith('#/components/schemas/'):
|
|
455
|
+
# Convert OpenAPI ref to JSON Schema ref
|
|
456
|
+
ref = ref.replace('#/components/schemas/', '#/definitions/')
|
|
457
|
+
processed['$ref'] = ref
|
|
458
|
+
elif key in ('properties', 'additionalProperties', 'patternProperties'):
|
|
459
|
+
# Recursively process nested schemas
|
|
460
|
+
if key == 'properties' and isinstance(value, dict):
|
|
461
|
+
processed[key] = {
|
|
462
|
+
prop_name: self._process_openapi_schema(prop_schema)
|
|
463
|
+
for prop_name, prop_schema in value.items()
|
|
464
|
+
}
|
|
465
|
+
elif key == 'additionalProperties' and isinstance(value, dict):
|
|
466
|
+
processed[key] = self._process_openapi_schema(value)
|
|
467
|
+
elif key == 'patternProperties' and isinstance(value, dict):
|
|
468
|
+
processed[key] = {
|
|
469
|
+
pattern: self._process_openapi_schema(prop_schema)
|
|
470
|
+
for pattern, prop_schema in value.items()
|
|
471
|
+
}
|
|
472
|
+
else:
|
|
473
|
+
processed[key] = value
|
|
474
|
+
elif key == 'items':
|
|
475
|
+
# Recursively process array items
|
|
476
|
+
if isinstance(value, dict):
|
|
477
|
+
processed[key] = self._process_openapi_schema(value)
|
|
478
|
+
elif isinstance(value, list):
|
|
479
|
+
processed[key] = [self._process_openapi_schema(item) for item in value]
|
|
480
|
+
else:
|
|
481
|
+
processed[key] = value
|
|
482
|
+
elif key in ('allOf', 'anyOf', 'oneOf'):
|
|
483
|
+
# Recursively process composition schemas
|
|
484
|
+
if isinstance(value, list):
|
|
485
|
+
processed[key] = [self._process_openapi_schema(item) for item in value]
|
|
486
|
+
else:
|
|
487
|
+
processed[key] = value
|
|
488
|
+
elif key == 'not':
|
|
489
|
+
# Recursively process negation schema
|
|
490
|
+
processed[key] = self._process_openapi_schema(value)
|
|
491
|
+
else:
|
|
492
|
+
# Pass through all other keywords
|
|
493
|
+
processed[key] = value
|
|
494
|
+
|
|
495
|
+
# Handle nullable by converting to union with null
|
|
496
|
+
if processed.get('nullable') is True:
|
|
497
|
+
if 'type' in processed:
|
|
498
|
+
current_type = processed['type']
|
|
499
|
+
if isinstance(current_type, list):
|
|
500
|
+
if 'null' not in current_type:
|
|
501
|
+
processed['type'] = current_type + ['null']
|
|
502
|
+
else:
|
|
503
|
+
processed['type'] = [current_type, 'null']
|
|
504
|
+
# Remove the nullable keyword after processing
|
|
505
|
+
del processed['nullable']
|
|
506
|
+
elif 'nullable' in processed:
|
|
507
|
+
del processed['nullable']
|
|
508
|
+
|
|
509
|
+
return processed
|
|
510
|
+
|
|
511
|
+
def _lift_inline_schemas_from_paths(self, openapi_doc: dict, json_schema: dict) -> None:
|
|
512
|
+
"""
|
|
513
|
+
Optionally lift inline schemas from paths/operations into named definitions.
|
|
514
|
+
|
|
515
|
+
This method extracts inline schemas from request bodies, responses, and parameters
|
|
516
|
+
in the OpenAPI paths section and adds them to the definitions.
|
|
517
|
+
|
|
518
|
+
Args:
|
|
519
|
+
openapi_doc: The OpenAPI document.
|
|
520
|
+
json_schema: The JSON Schema being built (modified in place).
|
|
521
|
+
"""
|
|
522
|
+
if 'definitions' not in json_schema:
|
|
523
|
+
json_schema['definitions'] = {}
|
|
524
|
+
|
|
525
|
+
paths = openapi_doc.get('paths', {})
|
|
526
|
+
|
|
527
|
+
for path, path_item in paths.items():
|
|
528
|
+
if not isinstance(path_item, dict):
|
|
529
|
+
continue
|
|
530
|
+
|
|
531
|
+
for method in ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace']:
|
|
532
|
+
operation = path_item.get(method)
|
|
533
|
+
if not isinstance(operation, dict):
|
|
534
|
+
continue
|
|
535
|
+
|
|
536
|
+
operation_id = operation.get('operationId', f"{method}_{path.replace('/', '_')}")
|
|
537
|
+
|
|
538
|
+
# Extract request body schemas
|
|
539
|
+
request_body = operation.get('requestBody', {})
|
|
540
|
+
if isinstance(request_body, dict):
|
|
541
|
+
content = request_body.get('content', {})
|
|
542
|
+
for media_type, media_type_obj in content.items():
|
|
543
|
+
if isinstance(media_type_obj, dict) and 'schema' in media_type_obj:
|
|
544
|
+
schema = media_type_obj['schema']
|
|
545
|
+
if isinstance(schema, dict) and '$ref' not in schema:
|
|
546
|
+
# Inline schema - lift to definitions
|
|
547
|
+
def_name = f"{operation_id}_Request"
|
|
548
|
+
processed = self._process_openapi_schema(schema)
|
|
549
|
+
json_schema['definitions'][def_name] = processed
|
|
550
|
+
|
|
551
|
+
# Extract response schemas
|
|
552
|
+
responses = operation.get('responses', {})
|
|
553
|
+
for status_code, response in responses.items():
|
|
554
|
+
if isinstance(response, dict):
|
|
555
|
+
content = response.get('content', {})
|
|
556
|
+
for media_type, media_type_obj in content.items():
|
|
557
|
+
if isinstance(media_type_obj, dict) and 'schema' in media_type_obj:
|
|
558
|
+
schema = media_type_obj['schema']
|
|
559
|
+
if isinstance(schema, dict) and '$ref' not in schema:
|
|
560
|
+
# Inline schema - lift to definitions
|
|
561
|
+
def_name = f"{operation_id}_{status_code}_Response"
|
|
562
|
+
processed = self._process_openapi_schema(schema)
|
|
563
|
+
json_schema['definitions'][def_name] = processed
|
|
564
|
+
|
|
565
|
+
def convert_openapi_to_structure(
|
|
566
|
+
self,
|
|
567
|
+
openapi_doc: Union[dict, str],
|
|
568
|
+
base_uri: str = ''
|
|
569
|
+
) -> dict:
|
|
570
|
+
"""
|
|
571
|
+
Convert an OpenAPI/Swagger document to JSON Structure format.
|
|
572
|
+
|
|
573
|
+
Supports:
|
|
574
|
+
- Swagger 2.0
|
|
575
|
+
- OpenAPI 3.0.x
|
|
576
|
+
- OpenAPI 3.1.x
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
openapi_doc: The API document as a dictionary or JSON string.
|
|
580
|
+
base_uri: The base URI for resolving references.
|
|
581
|
+
|
|
582
|
+
Returns:
|
|
583
|
+
The JSON Structure document.
|
|
584
|
+
|
|
585
|
+
Raises:
|
|
586
|
+
ValueError: If the input is not a valid Swagger/OpenAPI document.
|
|
587
|
+
TypeError: If the input type is not supported.
|
|
588
|
+
"""
|
|
589
|
+
# Parse JSON string if needed
|
|
590
|
+
if isinstance(openapi_doc, str):
|
|
591
|
+
openapi_doc = json.loads(openapi_doc)
|
|
592
|
+
|
|
593
|
+
if not isinstance(openapi_doc, dict):
|
|
594
|
+
raise TypeError(f"Expected dict or str, got {type(openapi_doc)}")
|
|
595
|
+
|
|
596
|
+
# Detect and validate specification version
|
|
597
|
+
spec_type, version = self.detect_spec_version(openapi_doc)
|
|
598
|
+
|
|
599
|
+
# Extract schemas based on specification type
|
|
600
|
+
if spec_type == self.SWAGGER_2:
|
|
601
|
+
json_schema = self.extract_schemas_from_swagger2(openapi_doc)
|
|
602
|
+
else:
|
|
603
|
+
json_schema = self.extract_schemas_from_openapi(openapi_doc, spec_type)
|
|
604
|
+
|
|
605
|
+
# Use the JSON Schema to JSON Structure converter
|
|
606
|
+
json_converter = JsonToStructureConverter()
|
|
607
|
+
json_converter.root_namespace = self.root_namespace
|
|
608
|
+
json_converter.root_class_name = self.root_class_name
|
|
609
|
+
json_converter.preserve_composition = self.preserve_composition
|
|
610
|
+
json_converter.detect_inheritance = self.detect_inheritance
|
|
611
|
+
json_converter.detect_discriminators = self.detect_discriminators
|
|
612
|
+
json_converter.convert_empty_objects_to_maps = self.convert_empty_objects_to_maps
|
|
613
|
+
|
|
614
|
+
# Convert to JSON Structure
|
|
615
|
+
structure_schema = json_converter.convert_json_schema_to_structure(json_schema, base_uri)
|
|
616
|
+
|
|
617
|
+
return structure_schema
|
|
618
|
+
|
|
619
|
+
def convert_openapi_file_to_structure(
|
|
620
|
+
self,
|
|
621
|
+
openapi_file_path: str,
|
|
622
|
+
structure_file_path: Optional[str] = None
|
|
623
|
+
) -> dict:
|
|
624
|
+
"""
|
|
625
|
+
Convert an OpenAPI file to JSON Structure format.
|
|
626
|
+
|
|
627
|
+
Args:
|
|
628
|
+
openapi_file_path: Path to the input OpenAPI file.
|
|
629
|
+
structure_file_path: Optional path for the output JSON Structure file.
|
|
630
|
+
|
|
631
|
+
Returns:
|
|
632
|
+
The JSON Structure document.
|
|
633
|
+
"""
|
|
634
|
+
# Read the OpenAPI file
|
|
635
|
+
with open(openapi_file_path, 'r', encoding='utf-8') as f:
|
|
636
|
+
content = f.read()
|
|
637
|
+
|
|
638
|
+
# Detect format (JSON or YAML)
|
|
639
|
+
try:
|
|
640
|
+
openapi_doc = json.loads(content)
|
|
641
|
+
except json.JSONDecodeError:
|
|
642
|
+
# Try YAML
|
|
643
|
+
try:
|
|
644
|
+
import yaml
|
|
645
|
+
openapi_doc = yaml.safe_load(content)
|
|
646
|
+
except ImportError:
|
|
647
|
+
raise ValueError("YAML support requires PyYAML. Install with: pip install pyyaml")
|
|
648
|
+
except Exception as e:
|
|
649
|
+
raise ValueError(f"Failed to parse OpenAPI document as JSON or YAML: {e}")
|
|
650
|
+
|
|
651
|
+
# Convert to JSON Structure
|
|
652
|
+
base_uri = f"file://{os.path.abspath(openapi_file_path)}"
|
|
653
|
+
structure_schema = self.convert_openapi_to_structure(openapi_doc, base_uri)
|
|
654
|
+
|
|
655
|
+
# Write output if path specified
|
|
656
|
+
if structure_file_path:
|
|
657
|
+
with open(structure_file_path, 'w', encoding='utf-8') as f:
|
|
658
|
+
json.dump(structure_schema, f, indent=2)
|
|
659
|
+
|
|
660
|
+
return structure_schema
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def convert_openapi_to_structure(
|
|
664
|
+
input_data: str,
|
|
665
|
+
root_namespace: str = 'example.com'
|
|
666
|
+
) -> str:
|
|
667
|
+
"""
|
|
668
|
+
Convert an OpenAPI/Swagger document to JSON Structure format.
|
|
669
|
+
|
|
670
|
+
Supports Swagger 2.0, OpenAPI 3.0.x, and OpenAPI 3.1.x.
|
|
671
|
+
|
|
672
|
+
Args:
|
|
673
|
+
input_data: The API document as a JSON string.
|
|
674
|
+
root_namespace: The namespace for the root schema.
|
|
675
|
+
|
|
676
|
+
Returns:
|
|
677
|
+
The JSON Structure document as a JSON string.
|
|
678
|
+
"""
|
|
679
|
+
converter = OpenApiToStructureConverter()
|
|
680
|
+
converter.root_namespace = root_namespace
|
|
681
|
+
|
|
682
|
+
result = converter.convert_openapi_to_structure(input_data)
|
|
683
|
+
|
|
684
|
+
return json.dumps(result, indent=2)
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def convert_openapi_to_structure_files(
|
|
688
|
+
openapi_file_path: str,
|
|
689
|
+
structure_schema_path: str,
|
|
690
|
+
root_namespace: Optional[str] = None,
|
|
691
|
+
preserve_composition: bool = False,
|
|
692
|
+
detect_discriminators: bool = True,
|
|
693
|
+
lift_inline_schemas: bool = False
|
|
694
|
+
) -> None:
|
|
695
|
+
"""
|
|
696
|
+
Convert an OpenAPI/Swagger file to JSON Structure format.
|
|
697
|
+
|
|
698
|
+
Supports Swagger 2.0, OpenAPI 3.0.x, and OpenAPI 3.1.x.
|
|
699
|
+
|
|
700
|
+
Args:
|
|
701
|
+
openapi_file_path: Path to the input API file (JSON or YAML).
|
|
702
|
+
structure_schema_path: Path to the output JSON Structure file.
|
|
703
|
+
root_namespace: The namespace for the root schema.
|
|
704
|
+
preserve_composition: Flag to preserve composition keywords.
|
|
705
|
+
detect_discriminators: Flag to detect discriminator patterns.
|
|
706
|
+
lift_inline_schemas: Flag to lift inline schemas from paths to definitions.
|
|
707
|
+
"""
|
|
708
|
+
if root_namespace is None:
|
|
709
|
+
root_namespace = 'example.com'
|
|
710
|
+
|
|
711
|
+
converter = OpenApiToStructureConverter()
|
|
712
|
+
converter.root_namespace = root_namespace
|
|
713
|
+
converter.preserve_composition = preserve_composition
|
|
714
|
+
converter.detect_discriminators = detect_discriminators
|
|
715
|
+
converter.lift_inline_schemas = lift_inline_schemas
|
|
716
|
+
|
|
717
|
+
converter.convert_openapi_file_to_structure(openapi_file_path, structure_schema_path)
|