Skip to content

Commit 1f4f6c7

Browse files
author
Doug Borg
committed
feat: improve service generation
- Merge path-level parameters into operations - Add default tag fallback (fixes None_service filename) - Add path placeholder fallback param injection - Harden requestBody & response schema handling across 3.0/3.1
1 parent e83960e commit 1f4f6c7

File tree

1 file changed

+86
-62
lines changed

1 file changed

+86
-62
lines changed

src/openapi_python_generator/language_converters/python/service_generator.py

Lines changed: 86 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
Schema,
1414
Operation,
1515
Parameter,
16-
RequestBody,
1716
Response,
1817
PathItem,
1918
)
@@ -76,11 +75,6 @@ def is_reference_type(obj: Any) -> bool:
7675

7776

7877
def is_schema_type(obj: Any) -> bool:
79-
"""Check if object is a Schema type across different versions."""
80-
return isinstance(obj, (Schema, Schema30, Schema31))
81-
82-
83-
def is_schema_type(obj) -> bool:
8478
"""Check if object is a Schema from any OpenAPI version"""
8579
return isinstance(obj, (Schema30, Schema31))
8680

@@ -130,11 +124,14 @@ def generate_body_param(operation: Operation) -> Union[str, None]:
130124

131125

132126
def generate_params(operation: Operation) -> str:
133-
def _generate_params_from_content(content: Union[Reference, Schema]):
134-
if isinstance(content, Reference):
135-
return f"data : {content.ref.split('/')[-1]}"
136-
else:
137-
return f"data : {type_converter(content, True).converted_type}"
127+
def _generate_params_from_content(content: Any):
128+
# Accept reference from either 3.0 or 3.1
129+
if isinstance(content, (Reference, Reference30, Reference31)):
130+
return f"data : {content.ref.split('/')[-1]}" # type: ignore
131+
elif isinstance(content, (Schema, Schema30, Schema31)):
132+
return f"data : {type_converter(content, True).converted_type}" # type: ignore
133+
else: # pragma: no cover
134+
raise Exception(f"Unsupported request body schema type: {type(content)}")
138135

139136
if operation.parameters is None and operation.requestBody is None:
140137
return ""
@@ -178,42 +175,27 @@ def _generate_params_from_content(content: Union[Reference, Schema]):
178175
"application/octet-stream",
179176
]
180177

181-
if operation.requestBody is not None:
182-
# Check if this is a RequestBody (either v3.0 or v3.1) by checking for content attribute
183-
if (
184-
hasattr(operation.requestBody, "content")
185-
and isinstance(operation.requestBody.content, dict)
186-
and any(
187-
[
188-
operation.requestBody.content.get(i) is not None
189-
for i in operation_request_body_types
190-
]
191-
)
178+
if operation.requestBody is not None and not is_reference_type(
179+
operation.requestBody
180+
):
181+
# Safe access only if it's a concrete RequestBody object
182+
rb_content = getattr(operation.requestBody, "content", None)
183+
if isinstance(rb_content, dict) and any(
184+
rb_content.get(i) is not None for i in operation_request_body_types
192185
):
193-
get_keyword = [
194-
i
195-
for i in operation_request_body_types
196-
if operation.requestBody.content.get(i) is not None
197-
][0]
198-
content = operation.requestBody.content.get(get_keyword)
199-
if content is not None and (
200-
hasattr(content, "media_type_schema")
201-
and (
202-
hasattr(content.media_type_schema, "type")
203-
or hasattr(content.media_type_schema, "ref")
204-
)
205-
):
206-
params += (
207-
f"{_generate_params_from_content(content.media_type_schema)}, "
208-
)
209-
else:
210-
raise Exception(
211-
f"Unsupported media type schema for {str(operation)}"
212-
) # pragma: no cover
213-
else:
214-
raise Exception(
215-
f"Unsupported request body type: {type(operation.requestBody)}"
216-
)
186+
get_keyword = [i for i in operation_request_body_types if rb_content.get(i)][
187+
0
188+
]
189+
content = rb_content.get(get_keyword)
190+
if content is not None and hasattr(content, "media_type_schema"):
191+
mts = getattr(content, "media_type_schema", None)
192+
if isinstance(mts, (Reference, Reference30, Reference31, Schema, Schema30, Schema31)):
193+
params += f"{_generate_params_from_content(mts)}, "
194+
else: # pragma: no cover
195+
raise Exception(
196+
f"Unsupported media type schema for {str(operation)}: {type(mts)}"
197+
)
198+
# else: silently ignore unsupported body shapes (could extend later)
217199
# Replace - with _ in params
218200
params = params.replace("-", "_")
219201
default_params = default_params.replace("-", "_")
@@ -274,8 +256,8 @@ def generate_return_type(operation: Operation) -> OpReturnType:
274256

275257
if is_response_type(chosen_response):
276258
# It's a Response type, access content safely
277-
if hasattr(chosen_response, "content") and chosen_response.content is not None:
278-
media_type_schema = chosen_response.content.get("application/json")
259+
if hasattr(chosen_response, "content") and getattr(chosen_response, "content") is not None: # type: ignore
260+
media_type_schema = getattr(chosen_response, "content").get("application/json") # type: ignore
279261
elif is_reference_type(chosen_response):
280262
media_type_schema = create_media_type_for_reference(chosen_response)
281263

@@ -285,40 +267,44 @@ def generate_return_type(operation: Operation) -> OpReturnType:
285267
)
286268

287269
if is_media_type(media_type_schema):
288-
if is_reference_type(media_type_schema.media_type_schema):
270+
inner_schema = getattr(media_type_schema, "media_type_schema", None)
271+
if is_reference_type(inner_schema):
289272
type_conv = TypeConversion(
290-
original_type=media_type_schema.media_type_schema.ref,
291-
converted_type=media_type_schema.media_type_schema.ref.split("/")[-1],
292-
import_types=[media_type_schema.media_type_schema.ref.split("/")[-1]],
273+
original_type=inner_schema.ref, # type: ignore
274+
converted_type=inner_schema.ref.split("/")[-1], # type: ignore
275+
import_types=[inner_schema.ref.split("/")[-1]], # type: ignore
293276
)
294277
return OpReturnType(
295278
type=type_conv,
296279
status_code=good_responses[0][0],
297280
complex_type=True,
298281
)
299-
elif is_schema_type(media_type_schema.media_type_schema):
300-
converted_result = type_converter(media_type_schema.media_type_schema, True)
301-
if "array" in converted_result.original_type and isinstance(
302-
converted_result.import_types, list
282+
elif is_schema_type(inner_schema):
283+
converted_result = type_converter(inner_schema, True) # type: ignore
284+
if (
285+
"array" in converted_result.original_type
286+
and isinstance(converted_result.import_types, list)
303287
):
304288
matched = re.findall(r"List\[(.+)\]", converted_result.converted_type)
305289
if len(matched) > 0:
306290
list_type = matched[0]
307-
else:
291+
else: # pragma: no cover
308292
raise Exception(
309293
f"Unable to parse list type from {converted_result.converted_type}"
310-
) # pragma: no cover
294+
)
311295
else:
312296
list_type = None
313297
return OpReturnType(
314298
type=converted_result,
315299
status_code=good_responses[0][0],
316-
complex_type=converted_result.import_types is not None
317-
and len(converted_result.import_types) > 0,
300+
complex_type=bool(
301+
converted_result.import_types
302+
and len(converted_result.import_types) > 0
303+
),
318304
list_type=list_type,
319305
)
320-
else:
321-
raise Exception("Unknown media type schema type") # pragma: no cover
306+
else: # pragma: no cover
307+
raise Exception("Unknown media type schema type")
322308
elif media_type_schema is None:
323309
return OpReturnType(
324310
type=None,
@@ -342,7 +328,40 @@ def generate_services(
342328
def generate_service_operation(
343329
op: Operation, path_name: str, async_type: bool
344330
) -> ServiceOperation:
331+
# Merge path-level parameters (always required by spec) into the
332+
# operation-level parameters so they get turned into function args.
333+
try:
334+
path_level_params = []
335+
if hasattr(path, "parameters") and getattr(path, "parameters") is not None: # type: ignore
336+
path_level_params = [p for p in getattr(path, "parameters") if p is not None] # type: ignore
337+
if path_level_params:
338+
existing_names = set()
339+
if op.parameters is not None:
340+
for p in op.parameters: # type: ignore
341+
if isinstance(p, Parameter):
342+
existing_names.add(p.name)
343+
for p in path_level_params:
344+
if isinstance(p, Parameter) and p.name not in existing_names:
345+
if op.parameters is None:
346+
op.parameters = [] # type: ignore
347+
op.parameters.append(p) # type: ignore
348+
except Exception: # pragma: no cover
349+
pass
350+
345351
params = generate_params(op)
352+
# Fallback: ensure all {placeholders} in path are present as function params
353+
try:
354+
placeholder_names = [m.group(1) for m in re.finditer(r"\{([^}/]+)\}", path_name)]
355+
existing_param_names = {
356+
p.split(":")[0].strip()
357+
for p in params.split(",") if ":" in p
358+
}
359+
for ph in placeholder_names:
360+
norm_ph = common.normalize_symbol(ph)
361+
if norm_ph not in existing_param_names and norm_ph:
362+
params = f"{norm_ph}: Any, " + params
363+
except Exception: # pragma: no cover
364+
pass
346365
operation_id = generate_operation_id(op, http_operation, path_name)
347366
query_params = generate_query_params(op)
348367
header_params = generate_header_params(op)
@@ -395,6 +414,11 @@ def generate_service_operation(
395414
async_so = generate_service_operation(op, path_name, True)
396415
service_ops.append(async_so)
397416

417+
# Ensure every operation has a tag; fallback to "default" for untagged operations
418+
for so in service_ops:
419+
if not so.tag:
420+
so.tag = "default"
421+
398422
tags = set([so.tag for so in service_ops])
399423

400424
for tag in tags:

0 commit comments

Comments
 (0)