Skip to content
Merged
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
2 changes: 1 addition & 1 deletion plugins/communication_protocols/http/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "utcp-http"
version = "1.1.9"
version = "1.1.11"
authors = [
{ name = "UTCP Contributors" },
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -360,20 +360,30 @@ def _resolve_ref_obj(self, obj: Any, visited: Optional[set] = None) -> Any:

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).
Extract examples from an OpenAPI Parameter, Media Type, or Schema object.

Handles all three shapes the spec allows:
- 'example' (single value) - OpenAPI Parameter / Media Type / 3.0 Schema.
- 'examples' as a map of named Example Objects - OpenAPI Parameter /
Media Type Object (each entry carries an inline 'value').
- 'examples' as a list of literal values - JSON Schema / OpenAPI 3.1
Schema Object.

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):
for example_obj in obj["examples"].values():

examples_obj = obj.get("examples")
if isinstance(examples_obj, list):
# JSON Schema / OpenAPI 3.1 Schema form: a plain list of example values.
examples.extend(examples_obj)
elif isinstance(examples_obj, dict):
# OpenAPI 3.0 form: a map of named Example Objects.
for example_obj in examples_obj.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):
Expand All @@ -391,13 +401,21 @@ def _merge_examples(self, *objs: Optional[Dict[str, Any]]) -> Optional[List[Any]
Used to combine examples that can appear at more than one level for the
same value, e.g. a Media Type Object and the Schema Object beneath it.
Returns a list suitable for the JSON Schema 'examples' keyword, or None.

De-duplication uses a canonical JSON serialization (sorted keys) as the
identity. This is order-insensitive for objects and type-aware, so it
does not collapse semantically distinct examples the way Python's ``==``
would (``True == 1``, ``False == 0``, ``1 == 1.0``).
"""
merged: List[Any] = []
seen: set = set()
for obj in objs:
if not isinstance(obj, dict):
continue
for ex in self._extract_examples(obj) or []:
if ex not in merged:
key = json.dumps(ex, sort_keys=True, default=str)
if key not in seen:
seen.add(key)
merged.append(ex)
return merged or None

Expand Down
107 changes: 107 additions & 0 deletions plugins/communication_protocols/http/tests/test_openapi_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,3 +316,110 @@ def test_openapi_converter_schema_level_examples_normalized():
assert "example" not in body_param.model_dump(by_alias=True)

assert tool.outputs.examples == [{"id": "w_1"}]


def test_openapi_converter_array_form_schema_examples():
"""Array-form (JSON Schema / OpenAPI 3.1) schema 'examples' are preserved, not dropped."""
openapi_spec = {
"openapi": "3.1.0",
"info": {"title": "Test API", "version": "1.0.0"},
"paths": {
"/gadgets": {
"post": {
"operationId": "createGadget",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {"name": {"type": "string"}},
# JSON Schema 'examples' keyword: a list of values
"examples": [{"name": "Gadget A"}, {"name": "Gadget B"}],
}
}
}
},
"responses": {
"200": {
"description": "ok",
"content": {
"application/json": {
"schema": {
"type": "string",
"examples": ["ok", "done"],
}
}
},
}
},
}
}
},
}

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

tool = next((t for t in manual.tools if t.name == "createGadget"), None)
assert tool is not None

body_param = tool.inputs.properties.get("body")
assert body_param is not None
assert body_param.examples == [{"name": "Gadget A"}, {"name": "Gadget B"}]
# examples surface in the normalized field on serialization
assert body_param.model_dump(by_alias=True).get("examples") == [{"name": "Gadget A"}, {"name": "Gadget B"}]

assert tool.outputs.examples == ["ok", "done"]


def test_openapi_converter_example_dedup_is_type_aware_and_order_insensitive():
"""De-dup keeps distinct JSON types (true vs 1) and collapses key-reordered objects."""
openapi_spec = {
"openapi": "3.0.0",
"info": {"title": "Test API", "version": "1.0.0"},
"paths": {
"/items": {
"post": {
"operationId": "createItem",
"requestBody": {
"content": {
"application/json": {
# media-type example with one key order...
"examples": {"e1": {"value": {"a": 1, "b": 2}}},
"schema": {
"type": "object",
# ...schema example with the other key order -> collapses to one
"examples": [{"b": 2, "a": 1}],
},
}
}
},
"responses": {
"200": {
"description": "ok",
"content": {
"application/json": {
"schema": {
"type": "object",
# true and 1 are distinct; duplicate true removed
"examples": [True, 1, True],
}
}
},
}
},
}
}
},
}

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

tool = next((t for t in manual.tools if t.name == "createItem"), None)
assert tool is not None

# {a:1,b:2} and {b:2,a:1} are the same example -> collapsed to one
assert tool.inputs.properties.get("body").examples == [{"a": 1, "b": 2}]
# True vs 1 kept distinct (== would have collapsed them); duplicate True removed
assert tool.outputs.examples == [True, 1]
Loading