Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"""

import json
from typing import Any, Dict, List, Optional, Tuple
from typing import Any, Dict, List, Optional, Tuple, Literal, cast
import sys
import uuid
from urllib.parse import urlparse
Expand Down Expand Up @@ -248,14 +248,14 @@ def _is_auth_compatible(self, openapi_auth: Optional[Auth], auth_tools: Optional

# For API Key auth, check header name and location compatibility
if hasattr(openapi_auth, 'var_name') and hasattr(auth_tools, 'var_name'):
openapi_var = openapi_auth.var_name.lower() if openapi_auth.var_name else ""
tools_var = auth_tools.var_name.lower() if auth_tools.var_name else ""
openapi_var = getattr(openapi_auth, 'var_name', "").lower() if getattr(openapi_auth, 'var_name', None) else ""
tools_var = getattr(auth_tools, 'var_name', "").lower() if getattr(auth_tools, 'var_name', None) else ""

if openapi_var != tools_var:
return False

if hasattr(openapi_auth, 'location') and hasattr(auth_tools, 'location'):
if openapi_auth.location != auth_tools.location:
if getattr(openapi_auth, 'location', None) != getattr(auth_tools, 'location', None):
return False

return True
Expand Down Expand Up @@ -300,6 +300,32 @@ def _resolve_ref_obj(self, obj: Any, visited: Optional[set] = None) -> Any:
if isinstance(obj, dict) and "$ref" in obj:
return self._resolve_ref_path(obj["$ref"], visited)
return obj

def _extract_examples(self, obj: Dict[str, Any]) -> Optional[List[Any]]:
"""
Extract examples from an OpenAPI parameter or Media Type Object (Parameter, Media Type, Schema).

Supports both 'example' (single value) and 'examples' (map of Example Objects).
Returns a list of example values suitable for JSON Schema 'examples' keyword.
"""
examples = []

# Handle single 'example' field
if "example" in obj and obj["example"] is not None:
examples.append(obj["example"])

# Handle 'examples' map (OpenAPI 3.0+)
if "examples" in obj and isinstance(obj["examples"], dict):
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
for example_obj in obj["examples"].values():
if isinstance(example_obj, dict) and "$ref" in example_obj:
example_obj = self._resolve_ref_obj(example_obj, set()) or {}
if isinstance(example_obj, dict):
# Example Object can have 'value' or 'externalValue'
if "value" in example_obj:
examples.append(example_obj["value"])
# Note: externalValue is a URI reference, we skip it as it's not inline

return examples if examples else None

def _create_auth_from_scheme(self, scheme: Dict[str, Any], scheme_name: str) -> Optional[Auth]:
"""Creates an Auth object from an OpenAPI security scheme."""
Expand Down Expand Up @@ -417,7 +443,7 @@ def _create_tool(self, path: str, method: str, operation: Dict[str, Any], base_u

call_template = HttpCallTemplate(
name=self.call_template_name,
http_method=method.upper(),
http_method=cast(Literal["GET", "POST", "PUT", "DELETE", "PATCH"], method.upper()),
url=full_url,
body_field=body_field if body_field else None,
header_fields=header_fields if header_fields else None,
Expand Down Expand Up @@ -466,10 +492,18 @@ def _extract_inputs(self, path: str, operation: Dict[str, Any]) -> Tuple[JsonSch
if param.get("in") == "body":
body_field = "body"
json_schema = self._resolve_ref_obj(param.get("schema", {}), set()) or {}
properties[body_field] = {

# Extract examples from body parameter
body_examples = self._extract_examples(param)

prop = {
"description": param.get("description", "Request body"),
**json_schema,
}
if body_examples:
prop["examples"] = body_examples

properties[body_field] = prop
if param.get("required"):
required.append(body_field)
continue
Expand All @@ -484,10 +518,18 @@ def _extract_inputs(self, path: str, operation: Dict[str, Any]) -> Tuple[JsonSch
schema["items"] = param.get("items")
if "enum" in param:
schema["enum"] = param.get("enum")
properties[param_name] = {

# Extract examples from parameter
param_examples = self._extract_examples(param)

prop = {
"description": param.get("description", ""),
**schema,
}
if param_examples:
prop["examples"] = param_examples

properties[param_name] = prop
if param.get("required"):
required.append(param_name)

Expand All @@ -497,13 +539,22 @@ def _extract_inputs(self, path: str, operation: Dict[str, Any]) -> Tuple[JsonSch
content = request_body.get("content", {})
json_schema = content.get("application/json", {}).get("schema")
json_schema = self._resolve_ref_obj(json_schema, set()) if json_schema else None

# Extract examples from request body media type
media_type_obj = content.get("application/json", {})
body_examples = self._extract_examples(media_type_obj)

if json_schema:
# Add a single 'body' field to represent the request body
body_field = "body"
properties[body_field] = {
prop = {
"description": json_schema.get("description", "Request body"),
**json_schema
}
if body_examples:
prop["examples"] = body_examples

properties[body_field] = prop
if json_schema.get("required"):
required.append(body_field)

Expand All @@ -518,14 +569,17 @@ def _extract_outputs(self, operation: Dict[str, Any]) -> JsonSchema:
return JsonSchema()

json_schema = None
media_type_obj = None
if "content" in success_response:
content = success_response.get("content", {})
json_schema = content.get("application/json", {}).get("schema")
media_type_obj = content.get("application/json", {})
# Fallback to any content type if application/json missing
if json_schema is None and isinstance(content, dict):
for v in content.values():
if isinstance(v, dict) and "schema" in v:
json_schema = v.get("schema")
media_type_obj = v
break
elif "schema" in success_response: # OpenAPI 2.0
json_schema = success_response.get("schema")
Expand All @@ -536,6 +590,13 @@ def _extract_outputs(self, operation: Dict[str, Any]) -> JsonSchema:
# Resolve $ref in response schema
json_schema = self._resolve_ref_obj(json_schema, set()) or {}

# Extract examples from response media type and schema level
response_examples = list(self._extract_examples(media_type_obj) or []) if media_type_obj else []
for ex in self._extract_examples(json_schema) or []:
if ex not in response_examples:
response_examples.append(ex)
response_examples = response_examples or None

schema_args = {
"type": json_schema.get("type", "object"),
"properties": json_schema.get("properties", {}),
Expand All @@ -552,5 +613,9 @@ def _extract_outputs(self, operation: Dict[str, Any]) -> JsonSchema:
for attr in ["enum", "minimum", "maximum", "format"]:
if attr in json_schema:
schema_args[attr] = json_schema.get(attr)

# Add examples if present
if response_examples:
schema_args["examples"] = response_examples

return JsonSchema(**schema_args)
162 changes: 162 additions & 0 deletions plugins/communication_protocols/http/tests/test_openapi_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,165 @@ async def test_openapi_converter_with_auth_tools():

# Verify auth_tools is stored
assert converter.auth_tools == auth_tools


def test_openapi_converter_parameter_examples():
"""Test that parameter examples are correctly extracted from OpenAPI spec."""
# Create a minimal OpenAPI spec with parameter examples
openapi_spec = {
"openapi": "3.0.0",
"info": {
"title": "Test API",
"version": "1.0.0"
},
"paths": {
"/users/{userId}": {
"get": {
"operationId": "getUser",
"parameters": [
{
"name": "userId",
"in": "path",
"description": "ID of the user",
"required": True,
"schema": {
"type": "string"
},
"example": "user123"
},
{
"name": "includeDetails",
"in": "query",
"description": "Include detailed information",
"required": False,
"schema": {
"type": "boolean"
},
"examples": {
"trueExample": {
"summary": "Include details",
"value": True
},
"falseExample": {
"summary": "Exclude details",
"value": False
}
}
}
],
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"id": {"type": "string"},
"name": {"type": "string"}
}
},
"examples": {
"userExample": {
"summary": "Example user",
"value": {
"id": "user123",
"name": "John Doe"
}
}
}
}
}
}
}
}
},
"/users": {
"post": {
"operationId": "createUser",
"requestBody": {
"description": "User to create",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"email": {"type": "string"}
},
"required": ["name", "email"]
},
"examples": {
"newUser": {
"summary": "New user example",
"value": {
"name": "Jane Smith",
"email": "jane@example.com"
}
}
}
}
}
},
"responses": {
"201": {
"description": "User created",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"id": {"type": "string"},
"name": {"type": "string"},
"email": {"type": "string"}
}
}
}
}
}
}
}
}
}
}

converter = OpenApiConverter(openapi_spec)
manual = converter.convert()

assert len(manual.tools) == 2

# Test getUser tool - path and query parameter examples
get_user_tool = next((tool for tool in manual.tools if tool.name == "getUser"), None)
assert get_user_tool is not None

# Check path parameter example
user_id_param = get_user_tool.inputs.properties.get("userId")
assert user_id_param is not None
assert user_id_param.examples is not None
assert "user123" in user_id_param.examples

# Check query parameter examples
include_details_param = get_user_tool.inputs.properties.get("includeDetails")
assert include_details_param is not None
assert include_details_param.examples is not None
assert True in include_details_param.examples
assert False in include_details_param.examples

# Check response examples
assert get_user_tool.outputs.examples is not None
assert len(get_user_tool.outputs.examples) > 0
example_value = get_user_tool.outputs.examples[0]
assert example_value["id"] == "user123"
assert example_value["name"] == "John Doe"

# Test createUser tool - request body examples
create_user_tool = next((tool for tool in manual.tools if tool.name == "createUser"), None)
assert create_user_tool is not None

body_param = create_user_tool.inputs.properties.get("body")
assert body_param is not None
assert body_param.examples is not None
assert len(body_param.examples) > 0
example_value = body_param.examples[0]
assert example_value["name"] == "Jane Smith"
assert example_value["email"] == "jane@example.com"