135 lines
4.4 KiB
Python
135 lines
4.4 KiB
Python
from __future__ import annotations
|
|
|
|
from copy import deepcopy
|
|
from typing import Any, cast
|
|
|
|
from .models import JsonObject, OpenApiOperation
|
|
|
|
PARAMETER_LOCATIONS = ("path", "query", "header", "cookie")
|
|
|
|
|
|
def input_schema_for_operation(operation: OpenApiOperation) -> JsonObject:
|
|
"""Build the node input JSON Schema from operation params and JSON body.
|
|
|
|
OpenAPI schemas containing `$ref` are not self-contained; validate them
|
|
with the original document context. This adapter intentionally does not
|
|
resolve references.
|
|
"""
|
|
properties: dict[str, Any] = {}
|
|
required: list[str] = []
|
|
|
|
for group_name, group_schema in _parameter_group_schemas(operation).items():
|
|
properties[group_name] = group_schema
|
|
if group_schema.get("required"):
|
|
required.append(group_name)
|
|
|
|
body_schema = _json_request_body_schema(operation)
|
|
if body_schema is not None:
|
|
properties["body"] = body_schema
|
|
if _request_body_required(operation):
|
|
required.append("body")
|
|
|
|
return {
|
|
"type": "object",
|
|
"properties": properties,
|
|
"required": required,
|
|
"additionalProperties": False,
|
|
}
|
|
|
|
|
|
def output_schema_for_operation(operation: OpenApiOperation) -> JsonObject:
|
|
"""Build generic transport output schema for one OpenAPI operation."""
|
|
body_schema = _first_success_json_response_schema(operation) or {}
|
|
return {
|
|
"type": "object",
|
|
"properties": {
|
|
"status_code": {"type": "integer"},
|
|
"headers": {
|
|
"type": "object",
|
|
"additionalProperties": {"type": "string"},
|
|
},
|
|
"body": body_schema,
|
|
},
|
|
"required": ["status_code", "headers", "body"],
|
|
"additionalProperties": False,
|
|
}
|
|
|
|
|
|
def _parameter_group_schemas(operation: OpenApiOperation) -> dict[str, JsonObject]:
|
|
grouped: dict[str, dict[str, Any]] = {}
|
|
for parameter in operation.effective_parameters:
|
|
if not isinstance(parameter, dict):
|
|
continue
|
|
location = parameter.get("in")
|
|
name = parameter.get("name")
|
|
schema = parameter.get("schema")
|
|
if location not in PARAMETER_LOCATIONS:
|
|
continue
|
|
if not isinstance(name, str) or not isinstance(schema, dict):
|
|
continue
|
|
|
|
# Group by OpenAPI parameter location so workflow inputs stay explicit:
|
|
# {"path": {...}, "query": {...}} instead of one ambiguous flat object.
|
|
group = grouped.setdefault(
|
|
location,
|
|
{
|
|
"type": "object",
|
|
"properties": {},
|
|
"required": [],
|
|
"additionalProperties": False,
|
|
},
|
|
)
|
|
group["properties"][name] = deepcopy(schema)
|
|
if parameter.get("required") is True:
|
|
group["required"].append(name)
|
|
|
|
return {
|
|
name: cast(JsonObject, grouped[name])
|
|
for name in PARAMETER_LOCATIONS
|
|
if name in grouped
|
|
}
|
|
|
|
|
|
def _json_request_body_schema(operation: OpenApiOperation) -> JsonObject | None:
|
|
request_body = operation.raw_operation.get("requestBody")
|
|
if not isinstance(request_body, dict):
|
|
return None
|
|
content = request_body.get("content")
|
|
if not isinstance(content, dict):
|
|
return None
|
|
json_media = content.get("application/json")
|
|
if not isinstance(json_media, dict):
|
|
return None
|
|
schema = json_media.get("schema")
|
|
return cast(JsonObject, deepcopy(schema)) if isinstance(schema, dict) else None
|
|
|
|
|
|
def _request_body_required(operation: OpenApiOperation) -> bool:
|
|
request_body = operation.raw_operation.get("requestBody")
|
|
return isinstance(request_body, dict) and request_body.get("required") is True
|
|
|
|
|
|
def _first_success_json_response_schema(
|
|
operation: OpenApiOperation,
|
|
) -> JsonObject | None:
|
|
responses = operation.raw_operation.get("responses")
|
|
if not isinstance(responses, dict):
|
|
return None
|
|
|
|
for code in sorted(responses):
|
|
if not str(code).startswith("2"):
|
|
continue
|
|
response = responses[code]
|
|
if not isinstance(response, dict):
|
|
continue
|
|
content = response.get("content")
|
|
if not isinstance(content, dict):
|
|
continue
|
|
json_media = content.get("application/json")
|
|
if not isinstance(json_media, dict):
|
|
continue
|
|
schema = json_media.get("schema")
|
|
if isinstance(schema, dict):
|
|
return cast(JsonObject, deepcopy(schema))
|
|
return None
|