avrotize 2.20.3__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.
@@ -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)