pyopenapi-gen 2.7.2__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.
- pyopenapi_gen/__init__.py +224 -0
- pyopenapi_gen/__main__.py +6 -0
- pyopenapi_gen/cli.py +62 -0
- pyopenapi_gen/context/CLAUDE.md +284 -0
- pyopenapi_gen/context/file_manager.py +52 -0
- pyopenapi_gen/context/import_collector.py +382 -0
- pyopenapi_gen/context/render_context.py +726 -0
- pyopenapi_gen/core/CLAUDE.md +224 -0
- pyopenapi_gen/core/__init__.py +0 -0
- pyopenapi_gen/core/auth/base.py +22 -0
- pyopenapi_gen/core/auth/plugins.py +89 -0
- pyopenapi_gen/core/cattrs_converter.py +810 -0
- pyopenapi_gen/core/exceptions.py +20 -0
- pyopenapi_gen/core/http_status_codes.py +218 -0
- pyopenapi_gen/core/http_transport.py +222 -0
- pyopenapi_gen/core/loader/__init__.py +12 -0
- pyopenapi_gen/core/loader/loader.py +174 -0
- pyopenapi_gen/core/loader/operations/__init__.py +12 -0
- pyopenapi_gen/core/loader/operations/parser.py +161 -0
- pyopenapi_gen/core/loader/operations/post_processor.py +62 -0
- pyopenapi_gen/core/loader/operations/request_body.py +90 -0
- pyopenapi_gen/core/loader/parameters/__init__.py +10 -0
- pyopenapi_gen/core/loader/parameters/parser.py +186 -0
- pyopenapi_gen/core/loader/responses/__init__.py +10 -0
- pyopenapi_gen/core/loader/responses/parser.py +111 -0
- pyopenapi_gen/core/loader/schemas/__init__.py +11 -0
- pyopenapi_gen/core/loader/schemas/extractor.py +275 -0
- pyopenapi_gen/core/pagination.py +64 -0
- pyopenapi_gen/core/parsing/__init__.py +13 -0
- pyopenapi_gen/core/parsing/common/__init__.py +1 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/__init__.py +9 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/__init__.py +0 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/cyclic_properties.py +66 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/direct_cycle.py +33 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/existing_schema.py +22 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/list_response.py +54 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/missing_ref.py +52 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/new_schema.py +50 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/stripped_suffix.py +51 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/resolve_schema_ref.py +86 -0
- pyopenapi_gen/core/parsing/common/type_parser.py +73 -0
- pyopenapi_gen/core/parsing/context.py +187 -0
- pyopenapi_gen/core/parsing/cycle_helpers.py +126 -0
- pyopenapi_gen/core/parsing/keywords/__init__.py +1 -0
- pyopenapi_gen/core/parsing/keywords/all_of_parser.py +81 -0
- pyopenapi_gen/core/parsing/keywords/any_of_parser.py +84 -0
- pyopenapi_gen/core/parsing/keywords/array_items_parser.py +72 -0
- pyopenapi_gen/core/parsing/keywords/one_of_parser.py +77 -0
- pyopenapi_gen/core/parsing/keywords/properties_parser.py +98 -0
- pyopenapi_gen/core/parsing/schema_finalizer.py +169 -0
- pyopenapi_gen/core/parsing/schema_parser.py +804 -0
- pyopenapi_gen/core/parsing/transformers/__init__.py +0 -0
- pyopenapi_gen/core/parsing/transformers/inline_enum_extractor.py +285 -0
- pyopenapi_gen/core/parsing/transformers/inline_object_promoter.py +120 -0
- pyopenapi_gen/core/parsing/unified_cycle_detection.py +293 -0
- pyopenapi_gen/core/postprocess_manager.py +260 -0
- pyopenapi_gen/core/spec_fetcher.py +148 -0
- pyopenapi_gen/core/streaming_helpers.py +84 -0
- pyopenapi_gen/core/telemetry.py +69 -0
- pyopenapi_gen/core/utils.py +456 -0
- pyopenapi_gen/core/warning_collector.py +83 -0
- pyopenapi_gen/core/writers/code_writer.py +135 -0
- pyopenapi_gen/core/writers/documentation_writer.py +222 -0
- pyopenapi_gen/core/writers/line_writer.py +217 -0
- pyopenapi_gen/core/writers/python_construct_renderer.py +321 -0
- pyopenapi_gen/core_package_template/README.md +21 -0
- pyopenapi_gen/emit/models_emitter.py +143 -0
- pyopenapi_gen/emitters/CLAUDE.md +286 -0
- pyopenapi_gen/emitters/client_emitter.py +51 -0
- pyopenapi_gen/emitters/core_emitter.py +181 -0
- pyopenapi_gen/emitters/docs_emitter.py +44 -0
- pyopenapi_gen/emitters/endpoints_emitter.py +247 -0
- pyopenapi_gen/emitters/exceptions_emitter.py +187 -0
- pyopenapi_gen/emitters/mocks_emitter.py +185 -0
- pyopenapi_gen/emitters/models_emitter.py +426 -0
- pyopenapi_gen/generator/CLAUDE.md +352 -0
- pyopenapi_gen/generator/client_generator.py +567 -0
- pyopenapi_gen/generator/exceptions.py +7 -0
- pyopenapi_gen/helpers/CLAUDE.md +325 -0
- pyopenapi_gen/helpers/__init__.py +1 -0
- pyopenapi_gen/helpers/endpoint_utils.py +532 -0
- pyopenapi_gen/helpers/type_cleaner.py +334 -0
- pyopenapi_gen/helpers/type_helper.py +112 -0
- pyopenapi_gen/helpers/type_resolution/__init__.py +1 -0
- pyopenapi_gen/helpers/type_resolution/array_resolver.py +57 -0
- pyopenapi_gen/helpers/type_resolution/composition_resolver.py +79 -0
- pyopenapi_gen/helpers/type_resolution/finalizer.py +105 -0
- pyopenapi_gen/helpers/type_resolution/named_resolver.py +172 -0
- pyopenapi_gen/helpers/type_resolution/object_resolver.py +216 -0
- pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +109 -0
- pyopenapi_gen/helpers/type_resolution/resolver.py +47 -0
- pyopenapi_gen/helpers/url_utils.py +14 -0
- pyopenapi_gen/http_types.py +20 -0
- pyopenapi_gen/ir.py +165 -0
- pyopenapi_gen/py.typed +1 -0
- pyopenapi_gen/types/CLAUDE.md +140 -0
- pyopenapi_gen/types/__init__.py +11 -0
- pyopenapi_gen/types/contracts/__init__.py +13 -0
- pyopenapi_gen/types/contracts/protocols.py +106 -0
- pyopenapi_gen/types/contracts/types.py +28 -0
- pyopenapi_gen/types/resolvers/__init__.py +7 -0
- pyopenapi_gen/types/resolvers/reference_resolver.py +71 -0
- pyopenapi_gen/types/resolvers/response_resolver.py +177 -0
- pyopenapi_gen/types/resolvers/schema_resolver.py +498 -0
- pyopenapi_gen/types/services/__init__.py +5 -0
- pyopenapi_gen/types/services/type_service.py +165 -0
- pyopenapi_gen/types/strategies/__init__.py +5 -0
- pyopenapi_gen/types/strategies/response_strategy.py +310 -0
- pyopenapi_gen/visit/CLAUDE.md +272 -0
- pyopenapi_gen/visit/client_visitor.py +477 -0
- pyopenapi_gen/visit/docs_visitor.py +38 -0
- pyopenapi_gen/visit/endpoint/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/endpoint_visitor.py +292 -0
- pyopenapi_gen/visit/endpoint/generators/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +123 -0
- pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py +222 -0
- pyopenapi_gen/visit/endpoint/generators/mock_generator.py +140 -0
- pyopenapi_gen/visit/endpoint/generators/overload_generator.py +252 -0
- pyopenapi_gen/visit/endpoint/generators/request_generator.py +103 -0
- pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +705 -0
- pyopenapi_gen/visit/endpoint/generators/signature_generator.py +83 -0
- pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +207 -0
- pyopenapi_gen/visit/endpoint/processors/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +78 -0
- pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +171 -0
- pyopenapi_gen/visit/exception_visitor.py +90 -0
- pyopenapi_gen/visit/model/__init__.py +0 -0
- pyopenapi_gen/visit/model/alias_generator.py +93 -0
- pyopenapi_gen/visit/model/dataclass_generator.py +553 -0
- pyopenapi_gen/visit/model/enum_generator.py +212 -0
- pyopenapi_gen/visit/model/model_visitor.py +198 -0
- pyopenapi_gen/visit/visitor.py +97 -0
- pyopenapi_gen-2.7.2.dist-info/METADATA +1169 -0
- pyopenapi_gen-2.7.2.dist-info/RECORD +137 -0
- pyopenapi_gen-2.7.2.dist-info/WHEEL +4 -0
- pyopenapi_gen-2.7.2.dist-info/entry_points.txt +2 -0
- pyopenapi_gen-2.7.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,804 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core schema parsing logic, transforming a schema node into an IRSchema object.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
from typing import Any, Callable, List, Mapping, Set, Tuple
|
|
10
|
+
|
|
11
|
+
from pyopenapi_gen import IRSchema
|
|
12
|
+
from pyopenapi_gen.core.utils import NameSanitizer
|
|
13
|
+
|
|
14
|
+
from .context import ParsingContext
|
|
15
|
+
from .keywords.all_of_parser import _process_all_of
|
|
16
|
+
from .keywords.any_of_parser import _parse_any_of_schemas
|
|
17
|
+
from .keywords.one_of_parser import _parse_one_of_schemas
|
|
18
|
+
from .unified_cycle_detection import CycleAction
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# Environment variables for configurable limits, with defaults
|
|
23
|
+
try:
|
|
24
|
+
MAX_CYCLES = int(os.environ.get("PYOPENAPI_MAX_CYCLES", "0")) # Default 0 means no explicit cycle count limit
|
|
25
|
+
except ValueError:
|
|
26
|
+
MAX_CYCLES = 0
|
|
27
|
+
try:
|
|
28
|
+
ENV_MAX_DEPTH = int(os.environ.get("PYOPENAPI_MAX_DEPTH", "150")) # Default 150
|
|
29
|
+
except ValueError:
|
|
30
|
+
ENV_MAX_DEPTH = 150 # Fallback to 150 if env var is invalid
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _resolve_ref(
|
|
34
|
+
ref_path_str: str,
|
|
35
|
+
parent_schema_name: str | None, # Name of the schema containing this $ref
|
|
36
|
+
context: ParsingContext,
|
|
37
|
+
max_depth_override: int | None, # Propagated from the main _parse_schema call
|
|
38
|
+
allow_self_reference_for_parent: bool,
|
|
39
|
+
) -> IRSchema:
|
|
40
|
+
"""Resolves a $ref string, handling cycles and depth for the referenced schema."""
|
|
41
|
+
ref_name_parts = ref_path_str.split("/")
|
|
42
|
+
if not (ref_name_parts and ref_name_parts[-1]):
|
|
43
|
+
logger.warning(
|
|
44
|
+
f"Malformed $ref path '{ref_path_str}' encountered while parsing "
|
|
45
|
+
f"parent '{parent_schema_name or 'anonymous'}'."
|
|
46
|
+
)
|
|
47
|
+
return IRSchema(
|
|
48
|
+
name=None, # Anonymous placeholder for a bad ref
|
|
49
|
+
description=f"Malformed $ref: {ref_path_str}",
|
|
50
|
+
_from_unresolved_ref=True,
|
|
51
|
+
)
|
|
52
|
+
ref_name = ref_name_parts[-1]
|
|
53
|
+
|
|
54
|
+
# 1. Check if already parsed (fully or as a placeholder)
|
|
55
|
+
if ref_name in context.parsed_schemas and not context.parsed_schemas[ref_name]._max_depth_exceeded_marker:
|
|
56
|
+
# Re-using already parsed schema from context
|
|
57
|
+
return context.parsed_schemas[ref_name]
|
|
58
|
+
|
|
59
|
+
# 2. Get the raw schema node for the reference
|
|
60
|
+
ref_node = context.raw_spec_schemas.get(ref_name)
|
|
61
|
+
if ref_node is None:
|
|
62
|
+
logger.warning(
|
|
63
|
+
f"Cannot resolve $ref '{ref_path_str}' for parent '{parent_schema_name or 'anonymous'}'. "
|
|
64
|
+
f"Target '{ref_name}' not in raw_spec_schemas. Returning placeholder."
|
|
65
|
+
)
|
|
66
|
+
return IRSchema(
|
|
67
|
+
name=NameSanitizer.sanitize_class_name(ref_name),
|
|
68
|
+
_from_unresolved_ref=True,
|
|
69
|
+
description=f"Unresolved $ref: {ref_path_str} (target not found)",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Delegate all cycle detection and context management to _parse_schema
|
|
73
|
+
# The unified system in _parse_schema will handle all cycle detection, depth limits, and context management
|
|
74
|
+
return _parse_schema(
|
|
75
|
+
ref_name,
|
|
76
|
+
ref_node,
|
|
77
|
+
context,
|
|
78
|
+
max_depth_override,
|
|
79
|
+
allow_self_reference=allow_self_reference_for_parent,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _parse_composition_keywords(
|
|
84
|
+
node: Mapping[str, Any],
|
|
85
|
+
name: str | None,
|
|
86
|
+
context: ParsingContext,
|
|
87
|
+
max_depth: int,
|
|
88
|
+
parse_fn: Callable[[str | None, Mapping[str, Any] | None, ParsingContext, int | None], IRSchema],
|
|
89
|
+
) -> Tuple[List[IRSchema] | None, List[IRSchema] | None, List[IRSchema] | None, dict[str, IRSchema], Set[str], bool]:
|
|
90
|
+
"""Parse composition keywords (anyOf, oneOf, allOf) from a schema node.
|
|
91
|
+
|
|
92
|
+
Contracts:
|
|
93
|
+
Pre-conditions:
|
|
94
|
+
- node is a valid Mapping
|
|
95
|
+
- context is a valid ParsingContext instance
|
|
96
|
+
- max_depth >= 0
|
|
97
|
+
- parse_fn is a callable for parsing schemas
|
|
98
|
+
Post-conditions:
|
|
99
|
+
- Returns a tuple of (any_of_schemas, one_of_schemas, all_of_components,
|
|
100
|
+
properties, required_fields, is_nullable)
|
|
101
|
+
"""
|
|
102
|
+
any_of_schemas: List[IRSchema] | None = None
|
|
103
|
+
one_of_schemas: List[IRSchema] | None = None
|
|
104
|
+
parsed_all_of_components: List[IRSchema] | None = None
|
|
105
|
+
merged_properties: dict[str, IRSchema] = {}
|
|
106
|
+
merged_required_set: Set[str] = set()
|
|
107
|
+
is_nullable: bool = False
|
|
108
|
+
|
|
109
|
+
if "anyOf" in node:
|
|
110
|
+
parsed_sub_schemas, nullable_from_sub, _ = _parse_any_of_schemas(node["anyOf"], context, max_depth, parse_fn)
|
|
111
|
+
any_of_schemas = parsed_sub_schemas
|
|
112
|
+
is_nullable = is_nullable or nullable_from_sub
|
|
113
|
+
|
|
114
|
+
if "oneOf" in node:
|
|
115
|
+
parsed_sub_schemas, nullable_from_sub, _ = _parse_one_of_schemas(node["oneOf"], context, max_depth, parse_fn)
|
|
116
|
+
one_of_schemas = parsed_sub_schemas
|
|
117
|
+
is_nullable = is_nullable or nullable_from_sub
|
|
118
|
+
|
|
119
|
+
if "allOf" in node:
|
|
120
|
+
merged_properties, merged_required_set, parsed_all_of_components = _process_all_of(
|
|
121
|
+
node, name, context, parse_fn, max_depth=max_depth
|
|
122
|
+
)
|
|
123
|
+
else:
|
|
124
|
+
merged_required_set = set(node.get("required", []))
|
|
125
|
+
|
|
126
|
+
return any_of_schemas, one_of_schemas, parsed_all_of_components, merged_properties, merged_required_set, is_nullable
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _parse_properties(
|
|
130
|
+
properties_node: Mapping[str, Any],
|
|
131
|
+
parent_schema_name: str | None,
|
|
132
|
+
existing_properties: dict[str, IRSchema], # Properties already merged, e.g., from allOf
|
|
133
|
+
context: ParsingContext,
|
|
134
|
+
max_depth_override: int | None,
|
|
135
|
+
allow_self_reference: bool,
|
|
136
|
+
) -> dict[str, IRSchema]:
|
|
137
|
+
"""Parses the 'properties' block of a schema node."""
|
|
138
|
+
parsed_props: dict[str, IRSchema] = existing_properties.copy()
|
|
139
|
+
|
|
140
|
+
for prop_name, prop_schema_node in properties_node.items():
|
|
141
|
+
if not isinstance(prop_name, str) or not prop_name:
|
|
142
|
+
logger.warning(
|
|
143
|
+
f"Skipping property with invalid name '{prop_name}' in schema '{parent_schema_name or 'anonymous'}'."
|
|
144
|
+
)
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
if prop_name in parsed_props: # Already handled by allOf or a previous definition, skip
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
if isinstance(prop_schema_node, Mapping) and "$ref" in prop_schema_node:
|
|
151
|
+
parsed_props[prop_name] = _resolve_ref(
|
|
152
|
+
prop_schema_node["$ref"], parent_schema_name, context, max_depth_override, allow_self_reference
|
|
153
|
+
)
|
|
154
|
+
else:
|
|
155
|
+
# Inline object promotion or direct parsing of property schema
|
|
156
|
+
is_inline_object_node = (
|
|
157
|
+
isinstance(prop_schema_node, Mapping)
|
|
158
|
+
and prop_schema_node.get("type") == "object"
|
|
159
|
+
and "$ref" not in prop_schema_node
|
|
160
|
+
and (
|
|
161
|
+
"properties" in prop_schema_node or "description" in prop_schema_node
|
|
162
|
+
) # Heuristic for actual object def
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
if is_inline_object_node and parent_schema_name:
|
|
166
|
+
# Promote inline object to its own schema
|
|
167
|
+
promoted_schema_name = f"{parent_schema_name}{NameSanitizer.sanitize_class_name(prop_name)}"
|
|
168
|
+
promoted_ir_schema = _parse_schema(
|
|
169
|
+
promoted_schema_name,
|
|
170
|
+
prop_schema_node,
|
|
171
|
+
context,
|
|
172
|
+
max_depth_override,
|
|
173
|
+
allow_self_reference,
|
|
174
|
+
)
|
|
175
|
+
# The property itself becomes a reference to this new schema
|
|
176
|
+
property_ref_ir = IRSchema(
|
|
177
|
+
name=prop_name, # The actual property name
|
|
178
|
+
type=promoted_ir_schema.name, # Type is the name of the promoted schema
|
|
179
|
+
description=promoted_ir_schema.description or prop_schema_node.get("description"),
|
|
180
|
+
is_nullable=(prop_schema_node.get("nullable", False) or promoted_ir_schema.is_nullable),
|
|
181
|
+
_refers_to_schema=promoted_ir_schema,
|
|
182
|
+
default=prop_schema_node.get("default"),
|
|
183
|
+
example=prop_schema_node.get("example"),
|
|
184
|
+
)
|
|
185
|
+
parsed_props[prop_name] = property_ref_ir
|
|
186
|
+
# Add the newly created promoted schema to parsed_schemas if it's not a placeholder from error/cycle
|
|
187
|
+
if (
|
|
188
|
+
promoted_schema_name
|
|
189
|
+
and not promoted_ir_schema._from_unresolved_ref
|
|
190
|
+
and not promoted_ir_schema._max_depth_exceeded_marker
|
|
191
|
+
and not promoted_ir_schema._is_circular_ref
|
|
192
|
+
):
|
|
193
|
+
context.parsed_schemas[promoted_schema_name] = promoted_ir_schema
|
|
194
|
+
else:
|
|
195
|
+
# Directly parse other inline types (string, number, array of simple types, etc.)
|
|
196
|
+
# or objects that are not being promoted (e.g. if parent_schema_name is None)
|
|
197
|
+
|
|
198
|
+
# Check if this is a simple primitive type that should NOT be promoted to a separate schema
|
|
199
|
+
is_simple_primitive = (
|
|
200
|
+
isinstance(prop_schema_node, Mapping)
|
|
201
|
+
and prop_schema_node.get("type") in ["string", "integer", "number", "boolean"]
|
|
202
|
+
and "$ref" not in prop_schema_node
|
|
203
|
+
and "properties" not in prop_schema_node
|
|
204
|
+
and "allOf" not in prop_schema_node
|
|
205
|
+
and "anyOf" not in prop_schema_node
|
|
206
|
+
and "oneOf" not in prop_schema_node
|
|
207
|
+
and "items" not in prop_schema_node
|
|
208
|
+
and "enum" not in prop_schema_node # Enums should still be promoted
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Check if this is a simple array that should NOT be promoted to a separate schema
|
|
212
|
+
# This includes:
|
|
213
|
+
# 1. Arrays of referenced types (items has $ref)
|
|
214
|
+
# 2. Arrays of primitive types (items has type: string, integer, number, boolean)
|
|
215
|
+
items_node = prop_schema_node.get("items", {}) if isinstance(prop_schema_node, Mapping) else {}
|
|
216
|
+
is_primitive_items = (
|
|
217
|
+
isinstance(items_node, Mapping)
|
|
218
|
+
and items_node.get("type") in ["string", "integer", "number", "boolean"]
|
|
219
|
+
and "$ref" not in items_node
|
|
220
|
+
and "properties" not in items_node
|
|
221
|
+
and "allOf" not in items_node
|
|
222
|
+
and "anyOf" not in items_node
|
|
223
|
+
and "oneOf" not in items_node
|
|
224
|
+
)
|
|
225
|
+
# Items with no type/ref/composition (malformed schema) should resolve to Any
|
|
226
|
+
is_typeless_items = (
|
|
227
|
+
isinstance(items_node, Mapping)
|
|
228
|
+
and not items_node.get("type")
|
|
229
|
+
and "$ref" not in items_node
|
|
230
|
+
and "properties" not in items_node
|
|
231
|
+
and "allOf" not in items_node
|
|
232
|
+
and "anyOf" not in items_node
|
|
233
|
+
and "oneOf" not in items_node
|
|
234
|
+
)
|
|
235
|
+
is_simple_array = (
|
|
236
|
+
isinstance(prop_schema_node, Mapping)
|
|
237
|
+
and prop_schema_node.get("type") == "array"
|
|
238
|
+
and "$ref" not in prop_schema_node
|
|
239
|
+
and "properties" not in prop_schema_node
|
|
240
|
+
and "allOf" not in prop_schema_node
|
|
241
|
+
and "anyOf" not in prop_schema_node
|
|
242
|
+
and "oneOf" not in prop_schema_node
|
|
243
|
+
and isinstance(prop_schema_node.get("items"), Mapping)
|
|
244
|
+
and (
|
|
245
|
+
"$ref" in items_node # Array of referenced types
|
|
246
|
+
or is_primitive_items # Array of primitive types
|
|
247
|
+
or is_typeless_items # Array with malformed items (no type) - resolves to List[Any]
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# Use a sanitized version of prop_name as context name for this sub-parse
|
|
252
|
+
prop_context_name = NameSanitizer.sanitize_class_name(prop_name)
|
|
253
|
+
|
|
254
|
+
# For simple primitives and simple arrays, avoid creating separate schemas
|
|
255
|
+
if (is_simple_primitive or is_simple_array) and prop_context_name in context.parsed_schemas:
|
|
256
|
+
# There's a naming conflict - use a unique name to avoid confusion
|
|
257
|
+
prop_context_name = f"_primitive_{prop_name}_{id(prop_schema_node)}"
|
|
258
|
+
|
|
259
|
+
# For simple primitives and simple arrays, don't assign names to prevent
|
|
260
|
+
# them from being registered as standalone schemas
|
|
261
|
+
# Note: Inline enums DO get names so they're registered properly
|
|
262
|
+
schema_name_for_parsing = None if (is_simple_primitive or is_simple_array) else prop_context_name
|
|
263
|
+
|
|
264
|
+
parsed_prop_schema_ir = _parse_schema(
|
|
265
|
+
schema_name_for_parsing, # Use None for simple types to prevent standalone registration
|
|
266
|
+
prop_schema_node,
|
|
267
|
+
context,
|
|
268
|
+
max_depth_override,
|
|
269
|
+
allow_self_reference,
|
|
270
|
+
)
|
|
271
|
+
# If the parsed schema retained the contextual name and it was registered,
|
|
272
|
+
# it implies it might be a complex anonymous type that got registered.
|
|
273
|
+
# In such cases, the property should *refer* to it.
|
|
274
|
+
# Otherwise, the parsed_prop_schema_ir *is* the property's schema directly.
|
|
275
|
+
#
|
|
276
|
+
# However, we should NOT create references for simple primitives or simple arrays
|
|
277
|
+
# as they should remain inline to avoid unnecessary schema proliferation.
|
|
278
|
+
should_create_reference = (
|
|
279
|
+
schema_name_for_parsing is not None # Only if we assigned a name
|
|
280
|
+
and parsed_prop_schema_ir.name == schema_name_for_parsing
|
|
281
|
+
and context.is_schema_parsed(schema_name_for_parsing)
|
|
282
|
+
and context.get_parsed_schema(schema_name_for_parsing) is parsed_prop_schema_ir
|
|
283
|
+
and (
|
|
284
|
+
parsed_prop_schema_ir.type == "object"
|
|
285
|
+
or (parsed_prop_schema_ir.type == "array" and not is_simple_array)
|
|
286
|
+
)
|
|
287
|
+
and not parsed_prop_schema_ir._from_unresolved_ref
|
|
288
|
+
and not parsed_prop_schema_ir._max_depth_exceeded_marker
|
|
289
|
+
and not parsed_prop_schema_ir._is_circular_ref
|
|
290
|
+
and not is_simple_primitive
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
if should_create_reference:
|
|
294
|
+
prop_is_nullable = False
|
|
295
|
+
if isinstance(prop_schema_node, Mapping):
|
|
296
|
+
if "nullable" in prop_schema_node:
|
|
297
|
+
prop_is_nullable = prop_schema_node["nullable"]
|
|
298
|
+
elif isinstance(prop_schema_node.get("type"), list) and "null" in prop_schema_node["type"]:
|
|
299
|
+
prop_is_nullable = True
|
|
300
|
+
elif parsed_prop_schema_ir.is_nullable:
|
|
301
|
+
prop_is_nullable = True
|
|
302
|
+
|
|
303
|
+
property_holder_ir = IRSchema(
|
|
304
|
+
name=prop_name, # The actual property name
|
|
305
|
+
# Type is the name of the (potentially registered) anonymous schema
|
|
306
|
+
type=parsed_prop_schema_ir.name,
|
|
307
|
+
description=prop_schema_node.get("description", parsed_prop_schema_ir.description),
|
|
308
|
+
is_nullable=prop_is_nullable,
|
|
309
|
+
default=prop_schema_node.get("default"),
|
|
310
|
+
example=prop_schema_node.get("example"),
|
|
311
|
+
enum=prop_schema_node.get("enum") if not parsed_prop_schema_ir.enum else None,
|
|
312
|
+
items=parsed_prop_schema_ir.items if parsed_prop_schema_ir.type == "array" else None,
|
|
313
|
+
format=parsed_prop_schema_ir.format,
|
|
314
|
+
_refers_to_schema=parsed_prop_schema_ir,
|
|
315
|
+
)
|
|
316
|
+
parsed_props[prop_name] = property_holder_ir
|
|
317
|
+
else:
|
|
318
|
+
# Simpler type, or error placeholder. Assign directly but ensure original prop_name is used.
|
|
319
|
+
# Also, try to respect original node's description, default, example, nullable if available.
|
|
320
|
+
final_prop_ir = parsed_prop_schema_ir
|
|
321
|
+
# Always assign the property name - this is the property's name in the parent object
|
|
322
|
+
final_prop_ir.name = prop_name
|
|
323
|
+
if isinstance(prop_schema_node, Mapping):
|
|
324
|
+
final_prop_ir.description = prop_schema_node.get(
|
|
325
|
+
"description", parsed_prop_schema_ir.description
|
|
326
|
+
)
|
|
327
|
+
final_prop_ir.default = prop_schema_node.get("default", parsed_prop_schema_ir.default)
|
|
328
|
+
final_prop_ir.example = prop_schema_node.get("example", parsed_prop_schema_ir.example)
|
|
329
|
+
current_prop_node_nullable = prop_schema_node.get("nullable", False)
|
|
330
|
+
type_list_nullable = (
|
|
331
|
+
isinstance(prop_schema_node.get("type"), list) and "null" in prop_schema_node["type"]
|
|
332
|
+
)
|
|
333
|
+
final_prop_ir.is_nullable = (
|
|
334
|
+
final_prop_ir.is_nullable or current_prop_node_nullable or type_list_nullable
|
|
335
|
+
)
|
|
336
|
+
# If the sub-parse didn't pick up an enum (e.g. for simple types), take it from prop_schema_node
|
|
337
|
+
if not final_prop_ir.enum and "enum" in prop_schema_node:
|
|
338
|
+
final_prop_ir.enum = prop_schema_node["enum"]
|
|
339
|
+
|
|
340
|
+
parsed_props[prop_name] = final_prop_ir
|
|
341
|
+
return parsed_props
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _parse_schema(
|
|
345
|
+
schema_name: str | None,
|
|
346
|
+
schema_node: Mapping[str, Any] | None,
|
|
347
|
+
context: ParsingContext,
|
|
348
|
+
max_depth_override: int | None = None,
|
|
349
|
+
allow_self_reference: bool = False,
|
|
350
|
+
) -> IRSchema:
|
|
351
|
+
"""
|
|
352
|
+
Parse a schema node and return an IRSchema object.
|
|
353
|
+
"""
|
|
354
|
+
# Pre-conditions
|
|
355
|
+
if context is None:
|
|
356
|
+
raise ValueError("Context cannot be None for _parse_schema")
|
|
357
|
+
|
|
358
|
+
# Set allow_self_reference flag on unified context
|
|
359
|
+
context.unified_cycle_context.allow_self_reference = allow_self_reference
|
|
360
|
+
|
|
361
|
+
# Use unified cycle detection system
|
|
362
|
+
detection_result = context.unified_enter_schema(schema_name)
|
|
363
|
+
|
|
364
|
+
# Handle different detection results
|
|
365
|
+
if detection_result.action == CycleAction.RETURN_EXISTING:
|
|
366
|
+
# Schema already completed - return existing
|
|
367
|
+
context.unified_exit_schema(schema_name) # Balance the enter call
|
|
368
|
+
if schema_name:
|
|
369
|
+
existing_schema = context.unified_cycle_context.parsed_schemas.get(schema_name)
|
|
370
|
+
if existing_schema:
|
|
371
|
+
return existing_schema
|
|
372
|
+
# Fallback to legacy parsed_schemas if not in unified context
|
|
373
|
+
existing_schema = context.parsed_schemas.get(schema_name)
|
|
374
|
+
if existing_schema:
|
|
375
|
+
return existing_schema
|
|
376
|
+
# If schema marked as existing but not found anywhere, it might be a state management issue
|
|
377
|
+
# Reset the state and continue with normal parsing
|
|
378
|
+
from .unified_cycle_detection import SchemaState
|
|
379
|
+
|
|
380
|
+
context.unified_cycle_context.schema_states[schema_name] = SchemaState.NOT_STARTED
|
|
381
|
+
# Don't call unified_exit_schema again, continue to normal parsing
|
|
382
|
+
|
|
383
|
+
elif detection_result.action == CycleAction.RETURN_PLACEHOLDER:
|
|
384
|
+
# Schema is already a placeholder - return it
|
|
385
|
+
context.unified_exit_schema(schema_name) # Balance the enter call
|
|
386
|
+
if detection_result.placeholder_schema:
|
|
387
|
+
placeholder: IRSchema = detection_result.placeholder_schema
|
|
388
|
+
return placeholder
|
|
389
|
+
elif schema_name and schema_name in context.parsed_schemas:
|
|
390
|
+
return context.parsed_schemas[schema_name]
|
|
391
|
+
else:
|
|
392
|
+
# Fallback to empty schema
|
|
393
|
+
return IRSchema(name=NameSanitizer.sanitize_class_name(schema_name) if schema_name else None)
|
|
394
|
+
|
|
395
|
+
elif detection_result.action == CycleAction.CREATE_PLACEHOLDER:
|
|
396
|
+
# Cycle or depth limit detected - return the created placeholder
|
|
397
|
+
context.unified_exit_schema(schema_name) # Balance the enter call
|
|
398
|
+
created_placeholder: IRSchema = detection_result.placeholder_schema
|
|
399
|
+
return created_placeholder
|
|
400
|
+
|
|
401
|
+
# If we reach here, detection_result.action == CycleAction.CONTINUE_PARSING
|
|
402
|
+
|
|
403
|
+
try: # Ensure exit_schema is called
|
|
404
|
+
if schema_node is None:
|
|
405
|
+
# Create empty schema for null schema nodes
|
|
406
|
+
# Do NOT set generation_name - null schemas should resolve to Any inline, not generate separate files
|
|
407
|
+
return IRSchema(name=NameSanitizer.sanitize_class_name(schema_name) if schema_name else None)
|
|
408
|
+
|
|
409
|
+
if not isinstance(schema_node, Mapping):
|
|
410
|
+
raise TypeError(
|
|
411
|
+
f"Schema node for '{schema_name or 'anonymous'}' must be a Mapping (e.g., dict), got {type(schema_node)}"
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
# If the current schema_node itself is a $ref, resolve it.
|
|
415
|
+
if "$ref" in schema_node:
|
|
416
|
+
# schema_name is the original name we are trying to parse (e.g., 'Pet')
|
|
417
|
+
# schema_node is {"$ref": "#/components/schemas/ActualPet"}
|
|
418
|
+
# We want to resolve "ActualPet", but the resulting IRSchema should ideally
|
|
419
|
+
# retain the name 'Pet' if appropriate,
|
|
420
|
+
# or _resolve_ref handles naming if ActualPet itself is parsed.
|
|
421
|
+
# The `parent_schema_name` for _resolve_ref here is `schema_name` itself.
|
|
422
|
+
resolved_schema = _resolve_ref(
|
|
423
|
+
schema_node["$ref"], schema_name, context, max_depth_override, allow_self_reference
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
# Store the resolved schema under the current schema_name if it has a name
|
|
427
|
+
# This ensures that synthetic names like "ChildrenItem" are properly stored
|
|
428
|
+
# However, don't create duplicate entries for pure references to existing schemas
|
|
429
|
+
if (
|
|
430
|
+
schema_name
|
|
431
|
+
and resolved_schema
|
|
432
|
+
and resolved_schema.name # Resolved schema has a real name
|
|
433
|
+
and resolved_schema.name != schema_name # Different from synthetic name
|
|
434
|
+
and resolved_schema.name in context.parsed_schemas
|
|
435
|
+
): # Already exists in context
|
|
436
|
+
# This is a pure reference to an existing schema, don't create duplicate
|
|
437
|
+
pass
|
|
438
|
+
elif schema_name and resolved_schema and schema_name not in context.parsed_schemas:
|
|
439
|
+
context.parsed_schemas[schema_name] = resolved_schema
|
|
440
|
+
|
|
441
|
+
return resolved_schema
|
|
442
|
+
|
|
443
|
+
extracted_type: str | None = None
|
|
444
|
+
is_nullable_from_type_field = False
|
|
445
|
+
raw_type_field = schema_node.get("type")
|
|
446
|
+
|
|
447
|
+
if isinstance(raw_type_field, str):
|
|
448
|
+
# Handle non-standard type values that might appear
|
|
449
|
+
if raw_type_field in ["Any", "any"]:
|
|
450
|
+
# Convert 'Any' to None - will be handled as object later
|
|
451
|
+
extracted_type = None
|
|
452
|
+
logger.warning(
|
|
453
|
+
f"Schema{f' {schema_name}' if schema_name else ''} uses non-standard type 'Any'. "
|
|
454
|
+
"Converting to 'object'. Use standard OpenAPI types: string, number, integer, boolean, array, object."
|
|
455
|
+
)
|
|
456
|
+
elif raw_type_field == "None":
|
|
457
|
+
# Convert 'None' string to null handling
|
|
458
|
+
extracted_type = "null"
|
|
459
|
+
logger.warning(
|
|
460
|
+
f"Schema{f' {schema_name}' if schema_name else ''} uses type 'None'. "
|
|
461
|
+
'Converting to nullable object. Use \'type: ["object", "null"]\' for nullable types.'
|
|
462
|
+
)
|
|
463
|
+
else:
|
|
464
|
+
extracted_type = raw_type_field
|
|
465
|
+
elif isinstance(raw_type_field, list):
|
|
466
|
+
if "null" in raw_type_field:
|
|
467
|
+
is_nullable_from_type_field = True
|
|
468
|
+
non_null_types = [t for t in raw_type_field if t != "null"]
|
|
469
|
+
if non_null_types:
|
|
470
|
+
extracted_type = non_null_types[0]
|
|
471
|
+
if len(non_null_types) > 1:
|
|
472
|
+
pass
|
|
473
|
+
elif is_nullable_from_type_field:
|
|
474
|
+
extracted_type = "null"
|
|
475
|
+
|
|
476
|
+
any_of_irs, one_of_irs, all_of_components_irs, props_from_comp, req_from_comp, nullable_from_comp = (
|
|
477
|
+
_parse_composition_keywords(
|
|
478
|
+
schema_node,
|
|
479
|
+
schema_name,
|
|
480
|
+
context,
|
|
481
|
+
ENV_MAX_DEPTH,
|
|
482
|
+
lambda n, sn, c, md: _parse_schema(n, sn, c, md, allow_self_reference),
|
|
483
|
+
)
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
# Check for direct nullable field (OpenAPI 3.0 Swagger extension)
|
|
487
|
+
is_nullable_from_node = schema_node.get("nullable", False)
|
|
488
|
+
is_nullable_overall = is_nullable_from_type_field or nullable_from_comp or is_nullable_from_node
|
|
489
|
+
final_properties_for_ir: dict[str, IRSchema] = {}
|
|
490
|
+
current_final_type = extracted_type
|
|
491
|
+
if not current_final_type:
|
|
492
|
+
if props_from_comp or "allOf" in schema_node or "properties" in schema_node:
|
|
493
|
+
current_final_type = "object"
|
|
494
|
+
elif any_of_irs or one_of_irs:
|
|
495
|
+
# Keep None for composition types - they'll be handled by resolver
|
|
496
|
+
current_final_type = None
|
|
497
|
+
elif "enum" in schema_node:
|
|
498
|
+
# Enum without explicit type - infer from enum values
|
|
499
|
+
enum_values = schema_node.get("enum", [])
|
|
500
|
+
if enum_values:
|
|
501
|
+
first_val = enum_values[0]
|
|
502
|
+
if isinstance(first_val, str):
|
|
503
|
+
current_final_type = "string"
|
|
504
|
+
elif isinstance(first_val, (int, float)):
|
|
505
|
+
current_final_type = "number"
|
|
506
|
+
elif isinstance(first_val, bool):
|
|
507
|
+
current_final_type = "boolean"
|
|
508
|
+
else:
|
|
509
|
+
# Fallback to object for complex enum values
|
|
510
|
+
current_final_type = "object"
|
|
511
|
+
else:
|
|
512
|
+
current_final_type = "string" # Default for empty enums
|
|
513
|
+
else:
|
|
514
|
+
# No type specified and no clear indicators - default to object
|
|
515
|
+
# This is safer than 'Any' and matches OpenAPI spec defaults
|
|
516
|
+
current_final_type = "object"
|
|
517
|
+
|
|
518
|
+
if current_final_type == "null":
|
|
519
|
+
# Explicit null type - mark as nullable but use object type
|
|
520
|
+
is_nullable_overall = True
|
|
521
|
+
current_final_type = "object"
|
|
522
|
+
|
|
523
|
+
if current_final_type == "object":
|
|
524
|
+
# Properties from allOf have already been handled by _parse_composition_keywords
|
|
525
|
+
# and are in props_from_comp. We pass these as existing_properties.
|
|
526
|
+
if "properties" in schema_node:
|
|
527
|
+
final_properties_for_ir = _parse_properties(
|
|
528
|
+
schema_node["properties"],
|
|
529
|
+
schema_name,
|
|
530
|
+
props_from_comp, # these are from allOf merge
|
|
531
|
+
context,
|
|
532
|
+
max_depth_override,
|
|
533
|
+
allow_self_reference,
|
|
534
|
+
)
|
|
535
|
+
else:
|
|
536
|
+
final_properties_for_ir = props_from_comp.copy() # No direct properties, only from allOf
|
|
537
|
+
|
|
538
|
+
# final_required_set: Set[str] = set() # This was old logic, req_from_comp covers allOf
|
|
539
|
+
# if "properties" in schema_node: # This loop is now inside _parse_properties effectively
|
|
540
|
+
# ... existing property parsing loop removed ...
|
|
541
|
+
final_required_fields_set = req_from_comp.copy()
|
|
542
|
+
if "required" in schema_node and isinstance(schema_node["required"], list):
|
|
543
|
+
final_required_fields_set.update(schema_node["required"])
|
|
544
|
+
|
|
545
|
+
items_ir: IRSchema | None = None
|
|
546
|
+
if current_final_type == "array":
|
|
547
|
+
items_node = schema_node.get("items")
|
|
548
|
+
if items_node:
|
|
549
|
+
# Avoid generating synthetic names for $ref items - let the ref resolve naturally
|
|
550
|
+
# This prevents false cycle detection when AgentListResponse -> $ref: AgentListResponseItem
|
|
551
|
+
if isinstance(items_node, Mapping) and "$ref" in items_node:
|
|
552
|
+
# For $ref items, pass None as schema_name to let _resolve_ref handle the naming
|
|
553
|
+
item_schema_name_for_recursive_parse = None
|
|
554
|
+
elif isinstance(items_node, Mapping) and items_node.get("type") in [
|
|
555
|
+
"string",
|
|
556
|
+
"integer",
|
|
557
|
+
"number",
|
|
558
|
+
"boolean",
|
|
559
|
+
]:
|
|
560
|
+
# Primitive items should NOT get names - they should remain inline as List[str] etc.
|
|
561
|
+
item_schema_name_for_recursive_parse = None
|
|
562
|
+
elif (
|
|
563
|
+
isinstance(items_node, Mapping)
|
|
564
|
+
and not items_node.get("type")
|
|
565
|
+
and not items_node.get("$ref")
|
|
566
|
+
and not items_node.get("allOf")
|
|
567
|
+
and not items_node.get("anyOf")
|
|
568
|
+
and not items_node.get("oneOf")
|
|
569
|
+
and not items_node.get("properties")
|
|
570
|
+
):
|
|
571
|
+
# Items with no type, $ref, or composition (e.g., just {nullable: true}) - treat as Any
|
|
572
|
+
# This is a malformed schema pattern, log a warning and use null type (resolves to Any)
|
|
573
|
+
nullable_only = items_node.get("nullable", False) and len(items_node) == 1
|
|
574
|
+
if nullable_only:
|
|
575
|
+
logger.warning(
|
|
576
|
+
f"Array items with only 'nullable: true' and no type "
|
|
577
|
+
f"found{f' in {schema_name}' if schema_name else ''}. "
|
|
578
|
+
"This is likely a malformed OpenAPI schema. Using 'Any' as item type. "
|
|
579
|
+
"Consider adding 'type: string' or appropriate type to your OpenAPI spec."
|
|
580
|
+
)
|
|
581
|
+
else:
|
|
582
|
+
logger.warning(
|
|
583
|
+
f"Array items with no 'type' field found{f' in {schema_name}' if schema_name else ''}. "
|
|
584
|
+
"Using 'Any' as item type. "
|
|
585
|
+
"Consider adding 'type' to your OpenAPI spec for better type safety."
|
|
586
|
+
)
|
|
587
|
+
# Create a null-type schema that will resolve to Any
|
|
588
|
+
items_ir = IRSchema(type="null", is_nullable=items_node.get("nullable", False))
|
|
589
|
+
elif isinstance(items_node, Mapping) and items_node.get("type") == "null":
|
|
590
|
+
# Explicit null type for items - resolve to Any | None
|
|
591
|
+
items_ir = IRSchema(type="null", is_nullable=True)
|
|
592
|
+
else:
|
|
593
|
+
# For inline items, generate a synthetic name
|
|
594
|
+
base_name_for_item = schema_name or "AnonymousArray"
|
|
595
|
+
item_schema_name_for_recursive_parse = NameSanitizer.sanitize_class_name(
|
|
596
|
+
f"{base_name_for_item}Item"
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
# Ensure unique names for anonymous array items to avoid schema overwrites
|
|
600
|
+
# Only check for collision if this specific name already exists
|
|
601
|
+
if not schema_name and item_schema_name_for_recursive_parse in context.parsed_schemas:
|
|
602
|
+
counter = 2 # Start from 2 since original is 1
|
|
603
|
+
original_name = item_schema_name_for_recursive_parse
|
|
604
|
+
while item_schema_name_for_recursive_parse in context.parsed_schemas:
|
|
605
|
+
item_schema_name_for_recursive_parse = f"{original_name}{counter}"
|
|
606
|
+
counter += 1
|
|
607
|
+
|
|
608
|
+
# Only parse items if items_ir wasn't directly set above (for null/Any cases)
|
|
609
|
+
if items_ir is None:
|
|
610
|
+
actual_item_ir = _parse_schema(
|
|
611
|
+
item_schema_name_for_recursive_parse,
|
|
612
|
+
items_node,
|
|
613
|
+
context,
|
|
614
|
+
max_depth_override,
|
|
615
|
+
allow_self_reference,
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
is_promoted_inline_object = (
|
|
619
|
+
isinstance(items_node, Mapping)
|
|
620
|
+
and items_node.get("type") == "object"
|
|
621
|
+
and "$ref" not in items_node
|
|
622
|
+
and actual_item_ir.name == item_schema_name_for_recursive_parse
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
if is_promoted_inline_object:
|
|
626
|
+
ref_holder_ir = IRSchema(
|
|
627
|
+
name=None,
|
|
628
|
+
type=actual_item_ir.name,
|
|
629
|
+
description=actual_item_ir.description or items_node.get("description"),
|
|
630
|
+
)
|
|
631
|
+
ref_holder_ir._refers_to_schema = actual_item_ir
|
|
632
|
+
items_ir = ref_holder_ir
|
|
633
|
+
else:
|
|
634
|
+
items_ir = actual_item_ir
|
|
635
|
+
else:
|
|
636
|
+
# Array without items specification - use object as safer default
|
|
637
|
+
# Log warning to help developers fix their specs
|
|
638
|
+
logger.warning(
|
|
639
|
+
f"Array type without 'items' specification found{f' in {schema_name}' if schema_name else ''}. "
|
|
640
|
+
"Using 'object' as item type. Consider adding 'items' to your OpenAPI spec for better type safety."
|
|
641
|
+
)
|
|
642
|
+
items_ir = IRSchema(type="object")
|
|
643
|
+
|
|
644
|
+
schema_ir_name_attr = NameSanitizer.sanitize_class_name(schema_name) if schema_name else None
|
|
645
|
+
|
|
646
|
+
# Parse additionalProperties field
|
|
647
|
+
additional_properties_value: bool | IRSchema | None = None
|
|
648
|
+
if "additionalProperties" in schema_node:
|
|
649
|
+
additional_props_node = schema_node["additionalProperties"]
|
|
650
|
+
if isinstance(additional_props_node, bool):
|
|
651
|
+
additional_properties_value = additional_props_node
|
|
652
|
+
elif isinstance(additional_props_node, dict):
|
|
653
|
+
# Parse the additionalProperties schema
|
|
654
|
+
additional_properties_value = _parse_schema(
|
|
655
|
+
None, # No name for additional properties schema
|
|
656
|
+
additional_props_node,
|
|
657
|
+
context,
|
|
658
|
+
max_depth_override,
|
|
659
|
+
allow_self_reference,
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
schema_ir = IRSchema(
|
|
663
|
+
name=schema_ir_name_attr,
|
|
664
|
+
type=current_final_type,
|
|
665
|
+
properties=final_properties_for_ir,
|
|
666
|
+
any_of=any_of_irs,
|
|
667
|
+
one_of=one_of_irs,
|
|
668
|
+
all_of=all_of_components_irs,
|
|
669
|
+
required=sorted(list(final_required_fields_set)),
|
|
670
|
+
description=schema_node.get("description"),
|
|
671
|
+
format=schema_node.get("format") if isinstance(schema_node.get("format"), str) else None,
|
|
672
|
+
enum=schema_node.get("enum") if isinstance(schema_node.get("enum"), list) else None,
|
|
673
|
+
default=schema_node.get("default"),
|
|
674
|
+
example=schema_node.get("example"),
|
|
675
|
+
is_nullable=is_nullable_overall,
|
|
676
|
+
items=items_ir,
|
|
677
|
+
additional_properties=additional_properties_value,
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
# Re-parse items for complex array types that need special handling
|
|
681
|
+
# Skip if items was already set to null type (typeless items that resolve to Any)
|
|
682
|
+
if (
|
|
683
|
+
schema_ir.type == "array"
|
|
684
|
+
and isinstance(schema_node.get("items"), Mapping)
|
|
685
|
+
and not (schema_ir.items and schema_ir.items.type == "null") # Skip if already set to null/Any
|
|
686
|
+
):
|
|
687
|
+
raw_items_node = schema_node["items"]
|
|
688
|
+
item_schema_context_name_for_reparse: str | None
|
|
689
|
+
|
|
690
|
+
# Avoid generating synthetic names for $ref items - let the ref resolve naturally
|
|
691
|
+
if "$ref" in raw_items_node:
|
|
692
|
+
item_schema_context_name_for_reparse = None
|
|
693
|
+
elif raw_items_node.get("type") in ["string", "integer", "number", "boolean"]:
|
|
694
|
+
# Primitive items should NOT get names - they should remain inline as List[str] etc.
|
|
695
|
+
item_schema_context_name_for_reparse = None
|
|
696
|
+
elif (
|
|
697
|
+
not raw_items_node.get("type")
|
|
698
|
+
and "$ref" not in raw_items_node
|
|
699
|
+
and "allOf" not in raw_items_node
|
|
700
|
+
and "anyOf" not in raw_items_node
|
|
701
|
+
and "oneOf" not in raw_items_node
|
|
702
|
+
and "properties" not in raw_items_node
|
|
703
|
+
):
|
|
704
|
+
# Typeless items (e.g., just {nullable: true}) - don't create synthetic name
|
|
705
|
+
# These will resolve to Any type - but this case should be caught above
|
|
706
|
+
item_schema_context_name_for_reparse = None
|
|
707
|
+
else:
|
|
708
|
+
base_name_for_reparse_item = schema_name or "AnonymousArray"
|
|
709
|
+
item_schema_context_name_for_reparse = NameSanitizer.sanitize_class_name(
|
|
710
|
+
f"{base_name_for_reparse_item}Item"
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
# Ensure unique names for anonymous array items to avoid schema overwrites
|
|
714
|
+
# Only check for collision if this specific name already exists
|
|
715
|
+
if not schema_name and item_schema_context_name_for_reparse in context.parsed_schemas:
|
|
716
|
+
counter = 2 # Start from 2 since original is 1
|
|
717
|
+
original_name = item_schema_context_name_for_reparse
|
|
718
|
+
while item_schema_context_name_for_reparse in context.parsed_schemas:
|
|
719
|
+
item_schema_context_name_for_reparse = f"{original_name}{counter}"
|
|
720
|
+
counter += 1
|
|
721
|
+
|
|
722
|
+
direct_reparsed_item_ir = _parse_schema(
|
|
723
|
+
item_schema_context_name_for_reparse, raw_items_node, context, max_depth_override, allow_self_reference
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
is_promoted_inline_object_in_reparse_block = (
|
|
727
|
+
isinstance(raw_items_node, Mapping)
|
|
728
|
+
and raw_items_node.get("type") == "object"
|
|
729
|
+
and "$ref" not in raw_items_node
|
|
730
|
+
and direct_reparsed_item_ir.name == item_schema_context_name_for_reparse
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
if is_promoted_inline_object_in_reparse_block:
|
|
734
|
+
ref_holder_for_reparse_ir = IRSchema(
|
|
735
|
+
name=None,
|
|
736
|
+
type=direct_reparsed_item_ir.name,
|
|
737
|
+
description=direct_reparsed_item_ir.description or raw_items_node.get("description"),
|
|
738
|
+
)
|
|
739
|
+
ref_holder_for_reparse_ir._refers_to_schema = direct_reparsed_item_ir
|
|
740
|
+
schema_ir.items = ref_holder_for_reparse_ir
|
|
741
|
+
else:
|
|
742
|
+
schema_ir.items = direct_reparsed_item_ir
|
|
743
|
+
|
|
744
|
+
if schema_name and schema_name in context.parsed_schemas:
|
|
745
|
+
existing_in_context = context.parsed_schemas[schema_name]
|
|
746
|
+
|
|
747
|
+
if existing_in_context._is_circular_ref and existing_in_context is not schema_ir:
|
|
748
|
+
return existing_in_context
|
|
749
|
+
|
|
750
|
+
# Don't register synthetic primitive type schemas as standalone schemas
|
|
751
|
+
# They should remain inline to avoid unnecessary schema proliferation
|
|
752
|
+
# BUT: Top-level schemas defined in components/schemas should always be registered,
|
|
753
|
+
# even if they are primitive types
|
|
754
|
+
# NOTE: Schemas with enums are NOT primitives - they need to be generated as enum classes
|
|
755
|
+
is_primitive_schema = schema_ir.type in ["string", "integer", "number", "boolean"] and not schema_ir.enum
|
|
756
|
+
is_top_level_schema = schema_name and schema_name in context.raw_spec_schemas
|
|
757
|
+
is_synthetic_primitive = is_primitive_schema and not is_top_level_schema
|
|
758
|
+
should_register = (
|
|
759
|
+
schema_name
|
|
760
|
+
and not schema_ir._from_unresolved_ref
|
|
761
|
+
and not schema_ir._max_depth_exceeded_marker
|
|
762
|
+
and not is_synthetic_primitive
|
|
763
|
+
)
|
|
764
|
+
if should_register and schema_name:
|
|
765
|
+
context.parsed_schemas[schema_name] = schema_ir
|
|
766
|
+
|
|
767
|
+
# Set generation_name and final_module_stem for schemas that will be generated as separate files
|
|
768
|
+
# Skip for synthetic primitives (inline types) - they should remain without these attributes
|
|
769
|
+
# so the type resolver doesn't try to import them as separate modules
|
|
770
|
+
# Note: Boolean enums are handled inline as Literal[True/False] and should not be extracted
|
|
771
|
+
should_set_generation_names = (
|
|
772
|
+
schema_ir.name
|
|
773
|
+
and not is_synthetic_primitive
|
|
774
|
+
and not (schema_ir.type == "boolean" and schema_ir.enum) # Boolean enums are inline Literal types
|
|
775
|
+
)
|
|
776
|
+
if should_set_generation_names and schema_ir.name: # Extra check to help mypy narrow the type
|
|
777
|
+
schema_ir.generation_name = NameSanitizer.sanitize_class_name(schema_ir.name)
|
|
778
|
+
schema_ir.final_module_stem = NameSanitizer.sanitize_module_name(schema_ir.name)
|
|
779
|
+
|
|
780
|
+
# Check if this schema was involved in any detected cycles and mark it accordingly
|
|
781
|
+
# This must happen before returning the schema
|
|
782
|
+
if schema_name:
|
|
783
|
+
for cycle_info in context.unified_cycle_context.detected_cycles:
|
|
784
|
+
if (
|
|
785
|
+
cycle_info.cycle_path
|
|
786
|
+
and cycle_info.cycle_path[0] == schema_name
|
|
787
|
+
and cycle_info.cycle_path[-1] == schema_name
|
|
788
|
+
):
|
|
789
|
+
# This schema is the start and end of a cycle
|
|
790
|
+
# Only mark as circular if it's a direct self-reference (via immediate intermediate schema)
|
|
791
|
+
# or if the test case specifically expects this behavior (array items)
|
|
792
|
+
is_direct_self_ref = len(cycle_info.cycle_path) == 2
|
|
793
|
+
is_array_item_self_ref = len(cycle_info.cycle_path) == 3 and "Item" in cycle_info.cycle_path[1]
|
|
794
|
+
|
|
795
|
+
if is_direct_self_ref or is_array_item_self_ref:
|
|
796
|
+
schema_ir._is_circular_ref = True
|
|
797
|
+
schema_ir._from_unresolved_ref = True
|
|
798
|
+
schema_ir._circular_ref_path = " -> ".join(cycle_info.cycle_path)
|
|
799
|
+
break
|
|
800
|
+
|
|
801
|
+
return schema_ir
|
|
802
|
+
|
|
803
|
+
finally:
|
|
804
|
+
context.unified_exit_schema(schema_name)
|