diff --git a/faststream/_internal/endpoint/subscriber/specification.py b/faststream/_internal/endpoint/subscriber/specification.py index 369649713c5..985cfacf0cd 100644 --- a/faststream/_internal/endpoint/subscriber/specification.py +++ b/faststream/_internal/endpoint/subscriber/specification.py @@ -7,7 +7,10 @@ from faststream._internal.configs import BrokerConfig, SubscriberSpecificationConfig from faststream.exceptions import SetupError -from faststream.specification.asyncapi.message import parse_handler_params +from faststream.specification.asyncapi.message import ( + parse_handler_params, + parse_handler_return, +) from faststream.specification.asyncapi.utils import to_camelcase if TYPE_CHECKING: @@ -78,6 +81,34 @@ def get_payloads(self) -> list[tuple["dict[str, Any]", str]]: return payloads + def get_reply_payloads(self) -> list[tuple["dict[str, Any]", str]]: + payloads: list[tuple[dict[str, Any], str]] = [] + + call_name = self.call_name + + for h in self.calls: + if h.dependant is None: + msg = "You should setup `Handler` at first." + raise SetupError(msg) + + reply_body = parse_handler_return( + h.dependant, + prefix=f"{self.config.title_ or call_name}:ReplyMessage", + ) + payloads.append((reply_body, to_camelcase(h.name))) + + if not self.calls: + payloads.append( + ( + { + "title": f"{self.config.title_ or call_name}:ReplyMessage:Payload", + }, + to_camelcase(call_name), + ), + ) + + return payloads + @property @abstractmethod def name(self) -> str: diff --git a/faststream/confluent/subscriber/specification.py b/faststream/confluent/subscriber/specification.py index 95680561a18..86386038e27 100644 --- a/faststream/confluent/subscriber/specification.py +++ b/faststream/confluent/subscriber/specification.py @@ -31,6 +31,7 @@ def name(self) -> str: def get_schema(self) -> dict[str, SubscriberSpec]: payloads = self.get_payloads() + reply_payloads = self.get_reply_payloads() channels = {} for t in self.topics: @@ -43,6 +44,10 @@ def get_schema(self) -> dict[str, SubscriberSpec]: title=f"{handler_name}:Message", payload=resolve_payloads(payloads), ), + reply_message=Message( + title=f"{handler_name}:ReplyMessage", + payload=resolve_payloads(reply_payloads), + ), bindings=None, ), bindings=ChannelBinding( diff --git a/faststream/kafka/subscriber/specification.py b/faststream/kafka/subscriber/specification.py index 4071bb81635..bcb3916412b 100644 --- a/faststream/kafka/subscriber/specification.py +++ b/faststream/kafka/subscriber/specification.py @@ -34,6 +34,7 @@ def name(self) -> str: def get_schema(self) -> dict[str, SubscriberSpec]: payloads = self.get_payloads() + reply_payloads = self.get_reply_payloads() channels = {} for t in self.topics: @@ -46,6 +47,10 @@ def get_schema(self) -> dict[str, SubscriberSpec]: title=f"{handler_name}:Message", payload=resolve_payloads(payloads), ), + reply_message=Message( + title=f"{handler_name}:ReplyMessage", + payload=resolve_payloads(reply_payloads), + ), bindings=None, ), bindings=ChannelBinding( diff --git a/faststream/nats/subscriber/specification.py b/faststream/nats/subscriber/specification.py index 31f3f8b4459..25152aa13b8 100644 --- a/faststream/nats/subscriber/specification.py +++ b/faststream/nats/subscriber/specification.py @@ -23,6 +23,7 @@ def name(self) -> str: def get_schema(self) -> dict[str, SubscriberSpec]: payloads = self.get_payloads() + reply_payloads = self.get_reply_payloads() return { self.name: SubscriberSpec( @@ -32,6 +33,10 @@ def get_schema(self) -> dict[str, SubscriberSpec]: title=f"{self.name}:Message", payload=resolve_payloads(payloads), ), + reply_message=Message( + title=f"{self.name}:ReplyMessage", + payload=resolve_payloads(reply_payloads), + ), bindings=None, ), bindings=ChannelBinding( diff --git a/faststream/rabbit/subscriber/specification.py b/faststream/rabbit/subscriber/specification.py index 78408c8b5dd..737634cb60c 100644 --- a/faststream/rabbit/subscriber/specification.py +++ b/faststream/rabbit/subscriber/specification.py @@ -31,6 +31,7 @@ def name(self) -> str: def get_schema(self) -> dict[str, SubscriberSpec]: payloads = self.get_payloads() + reply_payloads = self.get_reply_payloads() queue = self.config.queue.add_prefix(self._outer_config.prefix) @@ -59,6 +60,10 @@ def get_schema(self) -> dict[str, SubscriberSpec]: title=f"{channel_name}:Message", payload=resolve_payloads(payloads), ), + reply_message=Message( + title=f"{channel_name}:ReplyMessage", + payload=resolve_payloads(reply_payloads), + ), ), bindings=ChannelBinding( amqp=amqp.ChannelBinding( diff --git a/faststream/redis/subscriber/specification.py b/faststream/redis/subscriber/specification.py index 1ea1721b1d6..83c49f39236 100644 --- a/faststream/redis/subscriber/specification.py +++ b/faststream/redis/subscriber/specification.py @@ -20,6 +20,7 @@ class RedisSubscriberSpecification( ): def get_schema(self) -> dict[str, SubscriberSpec]: payloads = self.get_payloads() + reply_payloads = self.get_reply_payloads() return { self.name: SubscriberSpec( @@ -29,6 +30,10 @@ def get_schema(self) -> dict[str, SubscriberSpec]: title=f"{self.name}:Message", payload=resolve_payloads(payloads), ), + reply_message=Message( + title=f"{self.name}:ReplyMessage", + payload=resolve_payloads(reply_payloads), + ), bindings=None, ), bindings=ChannelBinding( diff --git a/faststream/specification/asyncapi/message.py b/faststream/specification/asyncapi/message.py index 16946a69da9..d1a2e06f703 100644 --- a/faststream/specification/asyncapi/message.py +++ b/faststream/specification/asyncapi/message.py @@ -36,6 +36,29 @@ def parse_handler_params(call: "CallModel", prefix: str = "") -> dict[str, Any]: return body +def parse_handler_return(call: "CallModel", prefix: str = "") -> dict[str, Any]: + """Parses the handler parameters.""" + model_container = getattr(call, "serializer", call) + response_option = getattr(model_container, "response_option", None) + if not response_option: + return {"title": "EmptyPayload", "type": "null"} + out = response_option["return"] + + body = get_model_schema( + create_model( + "", + **{out.field_name: (out.field_type, out.default_value)}, # type: ignore[call-overload] + ), + prefix=prefix, + exclude=tuple(call.custom_fields.keys()), + ) + + if body is None: + return {"title": "EmptyPayload", "type": "null"} + + return body + + @overload def get_response_schema(call: None, prefix: str = "") -> None: ... diff --git a/faststream/specification/asyncapi/v3_0_0/generate.py b/faststream/specification/asyncapi/v3_0_0/generate.py index 98fc64f3031..4982d7a2522 100644 --- a/faststream/specification/asyncapi/v3_0_0/generate.py +++ b/faststream/specification/asyncapi/v3_0_0/generate.py @@ -17,6 +17,7 @@ License, Message, Operation, + OperationReply, Reference, Server, Tag, @@ -25,10 +26,13 @@ OperationBinding, http as http_bindings, ) +from faststream.specification.asyncapi.v3_0_0.schema.operation_reply import ( + OperationReplyAddress, +) from faststream.specification.asyncapi.v3_0_0.schema.operations import Action if TYPE_CHECKING: - from faststream._internal.basic_types import AnyHttpUrl + from faststream._internal.basic_types import AnyCallable, AnyHttpUrl from faststream._internal.broker import BrokerUsecase from faststream._internal.types import ConnectionType, MsgType from faststream.asgi.handlers import HttpHandler @@ -64,6 +68,7 @@ def get_app_schema( channels, operations = get_broker_channels(broker) messages: dict[str, Message] = {} + reply_messages: dict[str, Message] = {} payloads: dict[str, dict[str, Any]] = {} for channel in channels.values(): @@ -89,6 +94,23 @@ def get_app_schema( channel.messages = msgs + for operation_name, operation in operations.items(): + reply_msgs: dict[str, Message | Reference] = {} + if not operation.reply: + continue + for message in operation.reply.messages: + assert isinstance(message, Message) + + reply_msgs["ReplyMessage"] = _resolve_reply_payloads( + f"{operation_name.removesuffix('Subscribe')}:ReplyMessage", + message, + payloads, + reply_messages, + ) + operation.reply.messages = list(reply_msgs.values()) + + messages.update(reply_messages) + return ApplicationSchema( info=ApplicationInfo( title=title, @@ -166,6 +188,7 @@ def get_broker_channels( """Get the broker channels for an application.""" channels = {} operations = {} + operations_by_handler: dict[AnyCallable, Operation] = {} for sub in filter(lambda s: s.specification.include_in_schema, broker.subscribers): for sub_key, sub_channel in sub.schema().items(): @@ -194,7 +217,7 @@ def get_broker_channels( stacklevel=1, ) - operations[operation_key] = Operation.from_sub( + operation = Operation.from_sub( messages=[ Reference(**{ "$ref": f"#/channels/{channel_key}/messages/{msg_name}", @@ -203,7 +226,22 @@ def get_broker_channels( ], channel=Reference(**{"$ref": f"#/channels/{channel_key}"}), operation=sub_channel.operation, + reply=OperationReply( + messages=[Message.from_spec(sub_channel.operation.reply_message)] + if sub_channel.operation.reply_message + else [], + address=OperationReplyAddress( + description=None, + location="$message.header#/replyTo", + ), + channel=None, + ) + if not sub._no_reply + else None, ) + operations[operation_key] = operation + for call in sub.specification.calls: + operations_by_handler[call.handler._original_call] = operation for pub in filter(lambda p: p.specification.include_in_schema, broker.publishers): for pub_key, pub_channel in pub.schema().items(): @@ -228,6 +266,13 @@ def get_broker_channels( channel=Reference(**{"$ref": f"#/channels/{channel_key}"}), operation=pub_channel.operation, ) + for call in pub.specification.calls: + sub_operation = operations_by_handler.get(call) + if sub_operation is None or sub_operation.reply is None: + continue + sub_operation.reply.channel = Reference(**{ + "$ref": f"#/channels/{channel_key}" + }) return channels, operations @@ -270,20 +315,18 @@ def _get_http_binding_method(methods: Sequence[str]) -> str: return next((method for method in methods if method != "HEAD"), "HEAD") -def _resolve_msg_payloads( - message_name: str, - m: Message, - channel_name: str, +def _resolve_payloads_common( + *, + m: "Message", payloads: dict[str, Any], - messages: dict[str, Any], -) -> Reference: + messages_target: dict[str, Any], + message_ref: str, + default_payload_title: str, +) -> "Reference": assert isinstance(m.payload, dict) m.payload = move_pydantic_refs(m.payload, DEF_KEY) - message_name = clear_key(message_name) - channel_name = clear_key(channel_name) - if DEF_KEY in m.payload: payloads.update(m.payload.pop(DEF_KEY)) @@ -298,19 +341,21 @@ def _resolve_msg_payloads( defs = payload.pop(DEF_KEY) or {} for def_name, def_schema in defs.items(): payloads[clear_key(def_name)] = def_schema + processed_payloads[clear_key(name)] = payload one_of_list.append(Reference(**{"$ref": f"#/components/schemas/{name}"})) payloads.update(processed_payloads) m.payload["oneOf"] = one_of_list + assert m.title - messages[clear_key(m.title)] = m - return Reference( - **{"$ref": f"#/components/messages/{channel_name}:{message_name}"}, - ) + messages_target[clear_key(m.title)] = m + + return Reference(**{"$ref": message_ref}) payloads.update(m.payload.pop(DEF_KEY, {})) - payload_name = m.payload.get("title", f"{channel_name}:{message_name}:Payload") + + payload_name = m.payload.get("title", default_payload_title) payload_name = clear_key(payload_name) if payload_name in payloads and payloads[payload_name] != m.payload: @@ -322,8 +367,44 @@ def _resolve_msg_payloads( payloads[payload_name] = m.payload m.payload = {"$ref": f"#/components/schemas/{payload_name}"} + assert m.title - messages[clear_key(m.title)] = m - return Reference( - **{"$ref": f"#/components/messages/{channel_name}:{message_name}"}, + messages_target[clear_key(m.title)] = m + + return Reference(**{"$ref": message_ref}) + + +def _resolve_reply_payloads( + message_name: str, + m: "Message", + payloads: dict[str, Any], + reply_messages: dict[str, Any], +) -> "Reference": + message_name = clear_key(message_name) + + return _resolve_payloads_common( + m=m, + payloads=payloads, + messages_target=reply_messages, + message_ref=f"#/components/messages/{message_name}", + default_payload_title=f"{message_name}:Payload", + ) + + +def _resolve_msg_payloads( + message_name: str, + m: "Message", + channel_name: str, + payloads: dict[str, Any], + messages: dict[str, Any], +) -> "Reference": + message_name = clear_key(message_name) + channel_name = clear_key(channel_name) + + return _resolve_payloads_common( + m=m, + payloads=payloads, + messages_target=messages, + message_ref=f"#/components/messages/{channel_name}:{message_name}", + default_payload_title=f"{channel_name}:{message_name}:Payload", ) diff --git a/faststream/specification/asyncapi/v3_0_0/schema/__init__.py b/faststream/specification/asyncapi/v3_0_0/schema/__init__.py index 0fa8f24a669..9b095f33ad0 100644 --- a/faststream/specification/asyncapi/v3_0_0/schema/__init__.py +++ b/faststream/specification/asyncapi/v3_0_0/schema/__init__.py @@ -5,6 +5,7 @@ from .info import ApplicationInfo from .license import License from .message import CorrelationId, Message +from .operation_reply import OperationReply from .operations import Operation from .schema import ApplicationSchema from .servers import Server, ServerVariable @@ -22,6 +23,7 @@ "License", "Message", "Operation", + "OperationReply", "Parameter", "Reference", "Server", diff --git a/faststream/specification/asyncapi/v3_0_0/schema/operation_reply.py b/faststream/specification/asyncapi/v3_0_0/schema/operation_reply.py new file mode 100644 index 00000000000..4cc9fb59857 --- /dev/null +++ b/faststream/specification/asyncapi/v3_0_0/schema/operation_reply.py @@ -0,0 +1,24 @@ +from pydantic import BaseModel + +from faststream._internal._compat import PYDANTIC_V2 +from faststream.specification.asyncapi.v3_0_0.schema.message import Message + +from .utils import Reference + + +class OperationReplyAddress(BaseModel): + description: str | None = None + location: str + + +class OperationReply(BaseModel): + messages: list[Message | Reference] + channel: Reference | None = None + address: OperationReplyAddress | None = None + + if PYDANTIC_V2: + model_config = {"extra": "allow"} + else: + + class Config: + extra = "allow" diff --git a/faststream/specification/asyncapi/v3_0_0/schema/operations.py b/faststream/specification/asyncapi/v3_0_0/schema/operations.py index 05ca29fbf01..7bdfe9b2f44 100644 --- a/faststream/specification/asyncapi/v3_0_0/schema/operations.py +++ b/faststream/specification/asyncapi/v3_0_0/schema/operations.py @@ -9,6 +9,7 @@ from .bindings import OperationBinding from .channels import Channel +from .operation_reply import OperationReply from .tag import Tag from .utils import Reference @@ -43,6 +44,8 @@ class Operation(BaseModel): security: dict[str, list[str]] | None = None + reply: OperationReply | None = None + # TODO # traits @@ -62,12 +65,14 @@ def from_sub( messages: list[Reference], channel: Reference, operation: OperationSpec, + reply: OperationReply | None = None, ) -> Self: return cls( action=Action.RECEIVE, messages=messages, channel=channel, bindings=OperationBinding.from_sub(operation.bindings), + reply=reply, summary=None, description=None, security=None, @@ -86,6 +91,7 @@ def from_pub( messages=messages, channel=channel, bindings=OperationBinding.from_pub(operation.bindings), + reply=None, summary=None, description=None, security=None, diff --git a/faststream/specification/schema/operation/model.py b/faststream/specification/schema/operation/model.py index 58f426dc17e..b2780c3cdf3 100644 --- a/faststream/specification/schema/operation/model.py +++ b/faststream/specification/schema/operation/model.py @@ -8,3 +8,4 @@ class Operation: message: Message bindings: OperationBinding | None + reply_message: Message | None = None diff --git a/faststream/specification/schema/reply/__init__.py b/faststream/specification/schema/reply/__init__.py new file mode 100644 index 00000000000..09ef2e907f2 --- /dev/null +++ b/faststream/specification/schema/reply/__init__.py @@ -0,0 +1,3 @@ +from .model import OperationReply, OperationReplyAddress + +__all__ = ("OperationReply", "OperationReplyAddress") diff --git a/faststream/specification/schema/reply/model.py b/faststream/specification/schema/reply/model.py new file mode 100644 index 00000000000..3ec3441cfaf --- /dev/null +++ b/faststream/specification/schema/reply/model.py @@ -0,0 +1,15 @@ +from dataclasses import dataclass + +from faststream.specification.schema import Message + + +@dataclass +class OperationReplyAddress: + location: str + description: str | None = None + + +@dataclass +class OperationReply: + message: Message | None + address: OperationReplyAddress | None diff --git a/tests/asyncapi/base/v3_0_0/arguments.py b/tests/asyncapi/base/v3_0_0/arguments.py index 9c4b0cce953..1f4e39ff7b5 100644 --- a/tests/asyncapi/base/v3_0_0/arguments.py +++ b/tests/asyncapi/base/v3_0_0/arguments.py @@ -107,12 +107,11 @@ async def handle() -> None: ... payload = schema["components"]["schemas"] - for key, v in payload.items(): - assert key == "EmptyPayload" - assert v == { - "title": key, - "type": "null", - } + assert "EmptyPayload" in payload + assert payload["EmptyPayload"] == { + "title": "EmptyPayload", + "type": "null", + } def test_no_type(self) -> None: broker = self.broker_class() @@ -124,9 +123,8 @@ async def handle(msg) -> None: ... payload = schema["components"]["schemas"] - for key, v in payload.items(): - assert key == "Handle:Message:Payload" - assert v == {"title": key} + assert "Handle:Message:Payload" in payload + assert payload["Handle:Message:Payload"] == {"title": "Handle:Message:Payload"} def test_simple_type(self) -> None: broker = self.broker_class() @@ -139,9 +137,8 @@ async def handle(msg: int) -> None: ... payload = schema["components"]["schemas"] assert next(iter(schema["channels"].values())).get("description") is None - for key, v in payload.items(): - assert key == "Handle:Message:Payload" - assert v == {"title": key, "type": "integer"} + assert "Handle:Message:Payload" in payload + assert payload["Handle:Message:Payload"] == {"title": "Handle:Message:Payload", "type": "integer"} def test_simple_optional_type(self) -> None: broker = self.broker_class() @@ -153,19 +150,20 @@ async def handle(msg: int | None) -> None: ... payload = schema["components"]["schemas"] - for key, v in payload.items(): - assert key == "Handle:Message:Payload" - assert v == IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "title": key, - }, - ) | IsDict( - { # TODO: remove when deprecating PydanticV1 - "title": key, - "type": "integer", - }, - ), v + key = "Handle:Message:Payload" + assert key in payload + v = payload[key] + assert v == IsDict( + { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": key, + }, + ) | IsDict( + { # TODO: remove when deprecating PydanticV1 + "title": key, + "type": "integer", + }, + ), v def test_simple_type_with_default(self) -> None: broker = self.broker_class() @@ -177,13 +175,13 @@ async def handle(msg: int = 1) -> None: ... payload = schema["components"]["schemas"] - for key, v in payload.items(): - assert key == "Handle:Message:Payload" - assert v == { - "default": 1, - "title": key, - "type": "integer", - } + key = "Handle:Message:Payload" + assert key in payload + assert payload[key] == { + "default": 1, + "title": key, + "type": "integer", + } def test_multi_args_no_type(self) -> None: broker = self.broker_class() @@ -195,17 +193,17 @@ async def handle(msg, another) -> None: ... payload = schema["components"]["schemas"] - for key, v in payload.items(): - assert key == "Handle:Message:Payload" - assert v == { - "properties": { - "another": {"title": "Another"}, - "msg": {"title": "Msg"}, - }, - "required": ["msg", "another"], - "title": key, - "type": "object", - } + key = "Handle:Message:Payload" + assert key in payload + assert payload[key] == { + "properties": { + "another": {"title": "Another"}, + "msg": {"title": "Msg"}, + }, + "required": ["msg", "another"], + "title": key, + "type": "object", + } def test_multi_args_with_type(self) -> None: broker = self.broker_class() @@ -217,17 +215,17 @@ async def handle(msg: str, another: int) -> None: ... payload = schema["components"]["schemas"] - for key, v in payload.items(): - assert key == "Handle:Message:Payload" - assert v == { - "properties": { - "another": {"title": "Another", "type": "integer"}, - "msg": {"title": "Msg", "type": "string"}, - }, - "required": ["msg", "another"], - "title": key, - "type": "object", - } + key = "Handle:Message:Payload" + assert key in payload + assert payload[key] == { + "properties": { + "another": {"title": "Another", "type": "integer"}, + "msg": {"title": "Msg", "type": "string"}, + }, + "required": ["msg", "another"], + "title": key, + "type": "object", + } def test_multi_args_with_default(self) -> None: broker = self.broker_class() @@ -239,30 +237,31 @@ async def handle(msg: str, another: int | None = None) -> None: ... payload = schema["components"]["schemas"] - for key, v in payload.items(): - assert key == "Handle:Message:Payload" - - assert v == { - "properties": { - "another": IsDict( - { - "anyOf": [{"type": "integer"}, {"type": "null"}], - "default": None, - "title": "Another", - }, - ) - | IsDict( - { # TODO: remove when deprecating PydanticV1 - "title": "Another", - "type": "integer", - }, - ), - "msg": {"title": "Msg", "type": "string"}, - }, - "required": ["msg"], - "title": key, - "type": "object", - } + key = "Handle:Message:Payload" + assert key in payload + v = payload[key] + + assert v == { + "properties": { + "another": IsDict( + { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "default": None, + "title": "Another", + }, + ) + | IsDict( + { # TODO: remove when deprecating PydanticV1 + "title": "Another", + "type": "integer", + }, + ), + "msg": {"title": "Msg", "type": "string"}, + }, + "required": ["msg"], + "title": key, + "type": "object", + } def test_dataclass(self) -> None: @dataclass @@ -280,6 +279,8 @@ async def handle(user: User) -> None: ... payload = schema["components"]["schemas"] for key, v in payload.items(): + if key == "EmptyPayload" or key.endswith(":ReplyMessage:Payload"): + continue assert key == "User" assert v == { "properties": { @@ -306,6 +307,8 @@ async def handle(user: User) -> None: ... payload = schema["components"]["schemas"] for key, v in payload.items(): + if key == "EmptyPayload" or key.endswith(":ReplyMessage:Payload"): + continue assert key == "User" assert v == { "properties": { @@ -336,7 +339,7 @@ async def handle(user: User) -> None: ... payload = schema["components"]["schemas"] - assert payload == { + assert IsPartialDict({ "Status": IsPartialDict( { "enum": ["registered", "banned"], @@ -354,7 +357,7 @@ async def handle(user: User) -> None: ... "title": "User", "type": "object", }, - }, payload + }) == payload, payload def test_pydantic_model_mixed_regular(self) -> None: class Email(pydantic.BaseModel): @@ -374,7 +377,7 @@ async def handle(user: User, description: str = "") -> None: ... payload = schema["components"]["schemas"] - assert payload == { + assert IsPartialDict({ "Email": { "title": "Email", "type": "object", @@ -404,7 +407,7 @@ async def handle(user: User, description: str = "") -> None: ... }, "required": ["user"], }, - } + }) == payload def test_nested_models_in_union_should_be_in_schemas(self) -> None: """Test that nested Pydantic models in union types are promoted to components/schemas. @@ -507,18 +510,18 @@ async def handle(user: User) -> None: ... payload = schema["components"]["schemas"] - for key, v in payload.items(): - assert key == "User" - assert v == { - "examples": [{"id": 1, "name": "john"}], - "properties": { - "id": {"title": "Id", "type": "integer"}, - "name": {"default": "", "title": "Name", "type": "string"}, - }, - "required": ["id"], - "title": "User", - "type": "object", - } + key = "User" + assert key in payload + assert payload[key] == { + "examples": [{"id": 1, "name": "john"}], + "properties": { + "id": {"title": "Id", "type": "integer"}, + "name": {"default": "", "title": "Name", "type": "string"}, + }, + "required": ["id"], + "title": "User", + "type": "object", + } def test_with_filter(self) -> None: class User(pydantic.BaseModel): @@ -570,18 +573,18 @@ async def handle(id: int, message=message) -> None: ... payload = schema["components"]["schemas"] - for key, v in payload.items(): - assert key == "Handle:Message:Payload" - assert v == { - "properties": { - "id": {"title": "Id", "type": "integer"}, - "name": {"default": "", "title": "Name", "type": "string"}, - "name2": {"title": "Name2", "type": "string"}, - }, - "required": ["id", "name2"], - "title": key, - "type": "object", - }, v + key = "Handle:Message:Payload" + assert key in payload + assert payload[key] == { + "properties": { + "id": {"title": "Id", "type": "integer"}, + "name": {"default": "", "title": "Name", "type": "string"}, + "name2": {"title": "Name2", "type": "string"}, + }, + "required": ["id", "name2"], + "title": key, + "type": "object", + }, payload[key] @pydantic_v2 def test_discriminator(self) -> None: @@ -674,15 +677,15 @@ async def handle(user: Model) -> None: ... key = next(iter(schema["components"]["messages"].keys())) assert key == IsStr(regex=r"test[\w:]*:Handle:SubscribeMessage") - assert schema["components"] == { - "messages": { + assert schema["components"] == IsPartialDict({ + "messages": IsPartialDict({ key: { "title": key, "correlationId": {"location": "$message.header#/correlation_id"}, "payload": {"$ref": "#/components/schemas/Model"}, }, - }, - "schemas": { + }), + "schemas": IsPartialDict({ "Sub": { "properties": { "type": IsPartialDict({"const": "sub", "title": "Type"}), @@ -714,8 +717,8 @@ async def handle(user: Model) -> None: ... "title": "Model", "type": "object", }, - }, - }, schema["components"] + }), + }), schema["components"] class ArgumentsTestcase(FastAPICompatible): @@ -739,6 +742,8 @@ async def msg( payload = schema["components"]["schemas"] for key, v in payload.items(): + if key == "EmptyPayload" or key.endswith(":ReplyMessage:Payload"): + continue assert key == "Perfect" assert v == { @@ -765,6 +770,8 @@ async def handle( payload = schema["components"]["schemas"] for key, v in payload.items(): + if key == "EmptyPayload" or key.endswith(":ReplyMessage:Payload"): + continue assert v == IsDict( { "properties": { @@ -822,11 +829,11 @@ async def second_handle(user: User) -> None: ... payload = schema["components"]["schemas"] - assert len(payload) == 1 - - key, value = next(iter(payload.items())) + assert len(payload) == 3 - assert key == "User" + assert "User" in payload + key = "User" + value = payload[key] assert value == { "properties": IsDict({ "id": {"title": "Id", "type": "integer"}, diff --git a/tests/asyncapi/base/v3_0_0/naming.py b/tests/asyncapi/base/v3_0_0/naming.py index 30e347e80b8..74dad573f24 100644 --- a/tests/asyncapi/base/v3_0_0/naming.py +++ b/tests/asyncapi/base/v3_0_0/naming.py @@ -25,13 +25,15 @@ async def handle_user_created(msg: str) -> None: ... IsStr(regex=r"test[\w:]*:HandleUserCreated"), ] - assert list(schema["components"]["messages"].keys()) == [ + assert list(schema["components"]["messages"].keys()) == Contains( IsStr(regex=r"test[\w:]*:HandleUserCreated:SubscribeMessage"), - ] + IsStr(regex=r"test[\w:]*:HandleUserCreated:ReplyMessage"), + ) & HasLen(2) - assert list(schema["components"]["schemas"].keys()) == [ + assert list(schema["components"]["schemas"].keys()) == Contains( "HandleUserCreated:Message:Payload", - ] + "HandleUserCreated:ReplyMessage:Payload", + ) & HasLen(2) def test_pydantic_subscriber_naming(self) -> None: broker = self.broker_class() @@ -45,11 +47,15 @@ async def handle_user_created(msg: create_model("SimpleModel")) -> None: ... IsStr(regex=r"test[\w:]*:HandleUserCreated"), ] - assert list(schema["components"]["messages"].keys()) == [ + assert list(schema["components"]["messages"].keys()) == Contains( IsStr(regex=r"test[\w:]*:HandleUserCreated:SubscribeMessage"), - ] + IsStr(regex=r"test[\w:]*:HandleUserCreated:ReplyMessage"), + ) & HasLen(2) - assert list(schema["components"]["schemas"].keys()) == ["SimpleModel"] + assert list(schema["components"]["schemas"].keys()) == Contains( + "SimpleModel", + "HandleUserCreated:ReplyMessage:Payload", + ) & HasLen(2) def test_multi_subscribers_naming(self) -> None: broker = self.broker_class() @@ -68,11 +74,14 @@ async def handle_user_created(msg: str) -> None: ... assert list(schema["components"]["messages"].keys()) == Contains( IsStr(regex=r"test[\w:]*:HandleUserCreated:SubscribeMessage"), IsStr(regex=r"test2[\w:]*:HandleUserCreated:SubscribeMessage"), - ) & HasLen(2) + IsStr(regex=r"test[\w:]*:HandleUserCreated:ReplyMessage"), + IsStr(regex=r"test2[\w:]*:HandleUserCreated:ReplyMessage"), + ) & HasLen(4) - assert list(schema["components"]["schemas"].keys()) == [ + assert list(schema["components"]["schemas"].keys()) == Contains( "HandleUserCreated:Message:Payload", - ] + "HandleUserCreated:ReplyMessage:Payload", + ) & HasLen(2) def test_subscriber_naming_manual(self) -> None: broker = self.broker_class() @@ -84,13 +93,15 @@ async def handle_user_created(msg: str) -> None: ... assert list(schema["channels"].keys()) == ["custom"] - assert list(schema["components"]["messages"].keys()) == [ + assert list(schema["components"]["messages"].keys()) == Contains( "custom:SubscribeMessage", - ] + "custom:ReplyMessage", + ) & HasLen(2) - assert list(schema["components"]["schemas"].keys()) == [ + assert list(schema["components"]["schemas"].keys()) == Contains( "custom:Message:Payload", - ] + "custom:ReplyMessage:Payload", + ) & HasLen(2) def test_subscriber_naming_default(self) -> None: broker = self.broker_class() @@ -103,11 +114,14 @@ def test_subscriber_naming_default(self) -> None: IsStr(regex=r"test[\w:]*:Subscriber"), ] - assert list(schema["components"]["messages"].keys()) == [ + assert list(schema["components"]["messages"].keys()) == Contains( IsStr(regex=r"test[\w:]*:Subscriber:SubscribeMessage"), - ] + IsStr(regex=r"test[\w:]*:Subscriber:ReplyMessage"), + ) & HasLen(2) for key, v in schema["components"]["schemas"].items(): + if key == "Subscriber:ReplyMessage:Payload": + continue assert key == "Subscriber:Message:Payload" assert v == {"title": key} @@ -120,13 +134,15 @@ def test_subscriber_naming_default_with_title(self) -> None: assert list(schema["channels"].keys()) == ["custom"] - assert list(schema["components"]["messages"].keys()) == [ + assert list(schema["components"]["messages"].keys()) == Contains( "custom:SubscribeMessage", - ] + "custom:ReplyMessage", + ) & HasLen(2) - assert list(schema["components"]["schemas"].keys()) == [ + assert list(schema["components"]["schemas"].keys()) == Contains( "custom:Message:Payload", - ] + "custom:ReplyMessage:Payload", + ) & HasLen(2) assert schema["components"]["schemas"]["custom:Message:Payload"] == { "title": "custom:Message:Payload", @@ -153,12 +169,17 @@ async def handle_user_created(msg: str) -> None: ... IsStr(regex=r"test[\w:]*:HandleUserCreated:SubscribeMessage"), IsStr(regex=r"test2[\w:]*:Subscriber:SubscribeMessage"), IsStr(regex=r"test3[\w:]*:Subscriber:SubscribeMessage"), - ) & HasLen(3) + IsStr(regex=r"test[\w:]*:HandleUserCreated:ReplyMessage"), + IsStr(regex=r"test2[\w:]*:Subscriber:ReplyMessage"), + IsStr(regex=r"test3[\w:]*:Subscriber:ReplyMessage"), + ) & HasLen(6) assert list(schema["components"]["schemas"].keys()) == Contains( "HandleUserCreated:Message:Payload", "Subscriber:Message:Payload", - ) & HasLen(2) + "HandleUserCreated:ReplyMessage:Payload", + "Subscriber:ReplyMessage:Payload", + ) & HasLen(4) assert schema["components"]["schemas"]["Subscriber:Message:Payload"] == { "title": "Subscriber:Message:Payload", @@ -183,16 +204,21 @@ async def handle_user_id(msg: int) -> None: ... IsStr(regex=r"test[\w:]*:\[HandleUserCreated,HandleUserId\]"), ] - assert list(schema["components"]["messages"].keys()) == [ + assert list(schema["components"]["messages"].keys()) == Contains( IsStr( regex=r"test[\w:]*:\[HandleUserCreated,HandleUserId\]:SubscribeMessage", ), - ] + IsStr( + regex=r"test[\w:]*:\[HandleUserCreated,HandleUserId\]:ReplyMessage", + ), + ) & HasLen(2) - assert list(schema["components"]["schemas"].keys()) == [ + assert list(schema["components"]["schemas"].keys()) == Contains( "HandleUserCreated:Message:Payload", "HandleUserId:Message:Payload", - ] + "HandleUserCreated:ReplyMessage:Payload", + "HandleUserId:ReplyMessage:Payload", + ) & HasLen(4) def test_subscriber_filter_pydantic(self) -> None: broker = self.broker_class() @@ -211,16 +237,21 @@ async def handle_user_id(msg: int) -> None: ... IsStr(regex=r"test[\w:]*:\[HandleUserCreated,HandleUserId\]"), ] - assert list(schema["components"]["messages"].keys()) == [ + assert list(schema["components"]["messages"].keys()) == Contains( IsStr( regex=r"test[\w:]*:\[HandleUserCreated,HandleUserId\]:SubscribeMessage", ), - ] + IsStr( + regex=r"test[\w:]*:\[HandleUserCreated,HandleUserId\]:ReplyMessage", + ), + ) & HasLen(2) - assert list(schema["components"]["schemas"].keys()) == [ + assert list(schema["components"]["schemas"].keys()) == Contains( "SimpleModel", "HandleUserId:Message:Payload", - ] + "HandleUserCreated:ReplyMessage:Payload", + "HandleUserId:ReplyMessage:Payload", + ) & HasLen(4) def test_subscriber_filter_with_title(self) -> None: broker = self.broker_class() @@ -237,14 +268,17 @@ async def handle_user_id(msg: int) -> None: ... assert list(schema["channels"].keys()) == ["custom"] - assert list(schema["components"]["messages"].keys()) == [ + assert list(schema["components"]["messages"].keys()) == Contains( "custom:SubscribeMessage", - ] + "custom:ReplyMessage", + ) & HasLen(2) - assert list(schema["components"]["schemas"].keys()) == [ + assert list(schema["components"]["schemas"].keys()) == Contains( "HandleUserCreated:Message:Payload", "HandleUserId:Message:Payload", - ] + "HandleUserCreated:ReplyMessage:Payload", + "HandleUserId:ReplyMessage:Payload", + ) & HasLen(4) class PublisherNaming(BaseNaming): diff --git a/tests/asyncapi/base/v3_0_0/router.py b/tests/asyncapi/base/v3_0_0/router.py index ebcb531c1b1..af57476c106 100644 --- a/tests/asyncapi/base/v3_0_0/router.py +++ b/tests/asyncapi/base/v3_0_0/router.py @@ -30,6 +30,7 @@ async def handle(msg) -> None: ... schema = self.get_spec(broker).to_jsonable() payload = schema["components"]["schemas"] + payload = {k: v for k, v in payload.items() if k != "EmptyPayload"} key = list(payload.keys())[0] # noqa: RUF015 assert payload[key]["title"] == key == "Handle:Message:Payload" @@ -52,7 +53,7 @@ async def handle(msg) -> None: ... schema = self.get_spec(broker).to_jsonable() schemas = schema["components"]["schemas"] - del schemas["Handle:Message:Payload"] + schemas = {k: v for k, v in schemas.items() if k not in ("Handle:Message:Payload", "EmptyPayload", "Handle:ReplyMessage:Payload")} for i, j in schemas.items(): assert ( diff --git a/tests/asyncapi/confluent/v3_0_0/test_naming.py b/tests/asyncapi/confluent/v3_0_0/test_naming.py index 41b6de66645..27a7464312c 100644 --- a/tests/asyncapi/confluent/v3_0_0/test_naming.py +++ b/tests/asyncapi/confluent/v3_0_0/test_naming.py @@ -55,6 +55,16 @@ async def handle() -> None: ... "$ref": "#/channels/test:Handle/messages/SubscribeMessage", }, ], + "reply": { + "address": { + "location": "$message.header#/replyTo", + }, + "messages": [ + { + "$ref": "#/components/messages/test:Handle:ReplyMessage", + }, + ], + }, }, }, "components": { @@ -66,7 +76,15 @@ async def handle() -> None: ... }, "payload": {"$ref": "#/components/schemas/EmptyPayload"}, }, + "test:Handle:ReplyMessage": { + "title": "test:Handle:ReplyMessage", + "correlationId": { + "location": "$message.header#/correlation_id", + }, + "payload": {"$ref": "#/components/schemas/Handle:ReplyMessage:Payload"}, + }, }, - "schemas": {"EmptyPayload": {"title": "EmptyPayload", "type": "null"}}, + "schemas": {"EmptyPayload": {"title": "EmptyPayload", "type": "null"}, + "Handle:ReplyMessage:Payload": {"title": "Handle:ReplyMessage:Payload", "type": "null"}, "Handle:ReplyMessage:Payload": {"title": "Handle:ReplyMessage:Payload", "type": "null"}}, }, } diff --git a/tests/asyncapi/confluent/v3_0_0/test_router.py b/tests/asyncapi/confluent/v3_0_0/test_router.py index c7a8f672d42..1dde62eaa9f 100644 --- a/tests/asyncapi/confluent/v3_0_0/test_router.py +++ b/tests/asyncapi/confluent/v3_0_0/test_router.py @@ -64,6 +64,16 @@ async def handle(msg) -> None: ... }, ], "channel": {"$ref": "#/channels/test_test:Handle"}, + "reply": { + "address": { + "location": "$message.header#/replyTo", + }, + "messages": [ + { + "$ref": "#/components/messages/test_test:Handle:ReplyMessage", + }, + ], + }, }, }, "components": { @@ -77,9 +87,17 @@ async def handle(msg) -> None: ... "$ref": "#/components/schemas/Handle:Message:Payload", }, }, + "test_test:Handle:ReplyMessage": { + "title": "test_test:Handle:ReplyMessage", + "correlationId": { + "location": "$message.header#/correlation_id", + }, + "payload": {"$ref": "#/components/schemas/Handle:ReplyMessage:Payload"}, + }, }, "schemas": { "Handle:Message:Payload": {"title": "Handle:Message:Payload"}, + "Handle:ReplyMessage:Payload": {"title": "Handle:ReplyMessage:Payload", "type": "null"}, }, }, } diff --git a/tests/asyncapi/kafka/v3_0_0/test_naming.py b/tests/asyncapi/kafka/v3_0_0/test_naming.py index f3dd6b9244f..bcacdba9029 100644 --- a/tests/asyncapi/kafka/v3_0_0/test_naming.py +++ b/tests/asyncapi/kafka/v3_0_0/test_naming.py @@ -55,6 +55,16 @@ async def handle() -> None: ... "$ref": "#/channels/test:Handle/messages/SubscribeMessage", }, ], + "reply": { + "address": { + "location": "$message.header#/replyTo", + }, + "messages": [ + { + "$ref": "#/components/messages/test:Handle:ReplyMessage", + }, + ], + }, }, }, "components": { @@ -66,8 +76,16 @@ async def handle() -> None: ... }, "payload": {"$ref": "#/components/schemas/EmptyPayload"}, }, + "test:Handle:ReplyMessage": { + "title": "test:Handle:ReplyMessage", + "correlationId": { + "location": "$message.header#/correlation_id", + }, + "payload": {"$ref": "#/components/schemas/Handle:ReplyMessage:Payload"}, + }, }, - "schemas": {"EmptyPayload": {"title": "EmptyPayload", "type": "null"}}, + "schemas": {"EmptyPayload": {"title": "EmptyPayload", "type": "null"}, + "Handle:ReplyMessage:Payload": {"title": "Handle:ReplyMessage:Payload", "type": "null"}, "Handle:ReplyMessage:Payload": {"title": "Handle:ReplyMessage:Payload", "type": "null"}}, }, } diff --git a/tests/asyncapi/kafka/v3_0_0/test_router.py b/tests/asyncapi/kafka/v3_0_0/test_router.py index a26549c820a..1ff628ee708 100644 --- a/tests/asyncapi/kafka/v3_0_0/test_router.py +++ b/tests/asyncapi/kafka/v3_0_0/test_router.py @@ -64,6 +64,16 @@ async def handle(msg) -> None: ... }, ], "channel": {"$ref": "#/channels/test_test:Handle"}, + "reply": { + "address": { + "location": "$message.header#/replyTo", + }, + "messages": [ + { + "$ref": "#/components/messages/test_test:Handle:ReplyMessage", + }, + ], + }, }, }, "components": { @@ -77,9 +87,17 @@ async def handle(msg) -> None: ... "$ref": "#/components/schemas/Handle:Message:Payload", }, }, + "test_test:Handle:ReplyMessage": { + "title": "test_test:Handle:ReplyMessage", + "correlationId": { + "location": "$message.header#/correlation_id", + }, + "payload": {"$ref": "#/components/schemas/Handle:ReplyMessage:Payload"}, + }, }, "schemas": { "Handle:Message:Payload": {"title": "Handle:Message:Payload"}, + "Handle:ReplyMessage:Payload": {"title": "Handle:ReplyMessage:Payload", "type": "null"}, }, }, } diff --git a/tests/asyncapi/kafka/v3_0_0/test_security.py b/tests/asyncapi/kafka/v3_0_0/test_security.py index de3ceecd1f6..93d0c29af72 100644 --- a/tests/asyncapi/kafka/v3_0_0/test_security.py +++ b/tests/asyncapi/kafka/v3_0_0/test_security.py @@ -54,6 +54,17 @@ {"$ref": "#/channels/test_1:TestTopic/messages/SubscribeMessage"}, ], "channel": {"$ref": "#/channels/test_1:TestTopic"}, + "reply": { + "address": { + "location": "$message.header#/replyTo", + }, + "channel": {"$ref": "#/channels/test_2:Publisher"}, + "messages": [ + { + "$ref": "#/components/messages/test_1:TestTopic:ReplyMessage", + }, + ], + }, }, "test_2:Publisher": { "action": "send", @@ -68,6 +79,13 @@ "correlationId": {"location": "$message.header#/correlation_id"}, "payload": {"$ref": "#/components/schemas/TestTopic:Message:Payload"}, }, + "test_1:TestTopic:ReplyMessage": { + "title": "test_1:TestTopic:ReplyMessage", + "correlationId": {"location": "$message.header#/correlation_id"}, + "payload": { + "$ref": "#/components/schemas/TestTopic:ReplyMessage:Payload", + }, + }, "test_2:Publisher:Message": { "title": "test_2:Publisher:Message", "correlationId": {"location": "$message.header#/correlation_id"}, @@ -81,6 +99,10 @@ "title": "TestTopic:Message:Payload", "type": "string", }, + "TestTopic:ReplyMessage:Payload": { + "title": "TestTopic:ReplyMessage:Payload", + "type": "string", + }, "test_2:Publisher:Message:Payload": { "title": "test_2:Publisher:Message:Payload", "type": "string", diff --git a/tests/asyncapi/nats/v3_0_0/test_naming.py b/tests/asyncapi/nats/v3_0_0/test_naming.py index 79db9e2abcd..abfb0ba5f40 100644 --- a/tests/asyncapi/nats/v3_0_0/test_naming.py +++ b/tests/asyncapi/nats/v3_0_0/test_naming.py @@ -57,6 +57,16 @@ async def handle() -> None: ... "$ref": "#/channels/test:Handle/messages/SubscribeMessage", }, ], + "reply": { + "address": { + "location": "$message.header#/replyTo", + }, + "messages": [ + { + "$ref": "#/components/messages/test:Handle:ReplyMessage", + }, + ], + }, }, }, "components": { @@ -68,7 +78,15 @@ async def handle() -> None: ... }, "payload": {"$ref": "#/components/schemas/EmptyPayload"}, }, + "test:Handle:ReplyMessage": { + "title": "test:Handle:ReplyMessage", + "correlationId": { + "location": "$message.header#/correlation_id", + }, + "payload": {"$ref": "#/components/schemas/Handle:ReplyMessage:Payload"}, + }, }, - "schemas": {"EmptyPayload": {"title": "EmptyPayload", "type": "null"}}, + "schemas": {"EmptyPayload": {"title": "EmptyPayload", "type": "null"}, + "Handle:ReplyMessage:Payload": {"title": "Handle:ReplyMessage:Payload", "type": "null"}, "Handle:ReplyMessage:Payload": {"title": "Handle:ReplyMessage:Payload", "type": "null"}}, }, } diff --git a/tests/asyncapi/nats/v3_0_0/test_router.py b/tests/asyncapi/nats/v3_0_0/test_router.py index cd965d72986..3ed37affdf6 100644 --- a/tests/asyncapi/nats/v3_0_0/test_router.py +++ b/tests/asyncapi/nats/v3_0_0/test_router.py @@ -64,6 +64,16 @@ async def handle(msg) -> None: ... }, ], "channel": {"$ref": "#/channels/test_test:Handle"}, + "reply": { + "address": { + "location": "$message.header#/replyTo", + }, + "messages": [ + { + "$ref": "#/components/messages/test_test:Handle:ReplyMessage", + }, + ], + }, }, }, "components": { @@ -77,9 +87,17 @@ async def handle(msg) -> None: ... "$ref": "#/components/schemas/Handle:Message:Payload", }, }, + "test_test:Handle:ReplyMessage": { + "title": "test_test:Handle:ReplyMessage", + "correlationId": { + "location": "$message.header#/correlation_id", + }, + "payload": {"$ref": "#/components/schemas/Handle:ReplyMessage:Payload"}, + }, }, "schemas": { "Handle:Message:Payload": {"title": "Handle:Message:Payload"}, + "Handle:ReplyMessage:Payload": {"title": "Handle:ReplyMessage:Payload", "type": "null"}, }, }, } diff --git a/tests/asyncapi/rabbit/v3_0_0/test_naming.py b/tests/asyncapi/rabbit/v3_0_0/test_naming.py index 02d4c766ca8..a3e01d31640 100644 --- a/tests/asyncapi/rabbit/v3_0_0/test_naming.py +++ b/tests/asyncapi/rabbit/v3_0_0/test_naming.py @@ -20,6 +20,7 @@ async def handle() -> None: ... assert list(schema["components"]["messages"].keys()) == [ "test:exchange:Handle:SubscribeMessage", + "test:exchange:Handle:ReplyMessage", ] def test_publisher_with_exchange(self) -> None: @@ -104,6 +105,16 @@ async def handle() -> None: ... "$ref": "#/channels/test:_:Handle/messages/SubscribeMessage", }, ], + "reply": { + "address": { + "location": "$message.header#/replyTo", + }, + "messages": [ + { + "$ref": "#/components/messages/test:_:Handle:ReplyMessage", + }, + ], + }, }, }, "components": { @@ -115,7 +126,15 @@ async def handle() -> None: ... }, "payload": {"$ref": "#/components/schemas/EmptyPayload"}, }, + "test:_:Handle:ReplyMessage": { + "title": "test:_:Handle:ReplyMessage", + "correlationId": { + "location": "$message.header#/correlation_id", + }, + "payload": {"$ref": "#/components/schemas/Handle:ReplyMessage:Payload"}, + }, }, - "schemas": {"EmptyPayload": {"title": "EmptyPayload", "type": "null"}}, + "schemas": {"EmptyPayload": {"title": "EmptyPayload", "type": "null"}, + "Handle:ReplyMessage:Payload": {"title": "Handle:ReplyMessage:Payload", "type": "null"}, "Handle:ReplyMessage:Payload": {"title": "Handle:ReplyMessage:Payload", "type": "null"}}, }, } diff --git a/tests/asyncapi/rabbit/v3_0_0/test_router.py b/tests/asyncapi/rabbit/v3_0_0/test_router.py index c7a9590e6c7..73745a57ac4 100644 --- a/tests/asyncapi/rabbit/v3_0_0/test_router.py +++ b/tests/asyncapi/rabbit/v3_0_0/test_router.py @@ -89,6 +89,16 @@ async def handle(msg) -> None: ... }, ], "channel": {"$ref": "#/channels/test_test:_:Handle"}, + "reply": { + "address": { + "location": "$message.header#/replyTo", + }, + "messages": [ + { + "$ref": "#/components/messages/test_test:_:Handle:ReplyMessage", + }, + ], + }, }, }, "components": { @@ -102,9 +112,17 @@ async def handle(msg) -> None: ... "$ref": "#/components/schemas/Handle:Message:Payload", }, }, + "test_test:_:Handle:ReplyMessage": { + "title": "test_test:_:Handle:ReplyMessage", + "correlationId": { + "location": "$message.header#/correlation_id", + }, + "payload": {"$ref": "#/components/schemas/Handle:ReplyMessage:Payload"}, + }, }, "schemas": { "Handle:Message:Payload": {"title": "Handle:Message:Payload"}, + "Handle:ReplyMessage:Payload": {"title": "Handle:ReplyMessage:Payload", "type": "null"}, }, }, }, schema diff --git a/tests/asyncapi/redis/v3_0_0/test_naming.py b/tests/asyncapi/redis/v3_0_0/test_naming.py index 76497118028..e5b2f15b109 100644 --- a/tests/asyncapi/redis/v3_0_0/test_naming.py +++ b/tests/asyncapi/redis/v3_0_0/test_naming.py @@ -45,6 +45,16 @@ async def handle() -> None: ... "messages": [ {"$ref": "#/channels/test:Handle/messages/SubscribeMessage"}, ], + "reply": { + "address": { + "location": "$message.header#/replyTo", + }, + "messages": [ + { + "$ref": "#/components/messages/test:Handle:ReplyMessage", + }, + ], + }, }, }, "components": { @@ -56,8 +66,18 @@ async def handle() -> None: ... "payload": {"$ref": "#/components/schemas/EmptyPayload"}, "title": "test:Handle:SubscribeMessage", }, + "test:Handle:ReplyMessage": { + "correlationId": { + "location": "$message.header#/correlation_id", + }, + "payload": {"$ref": "#/components/schemas/Handle:ReplyMessage:Payload"}, + "title": "test:Handle:ReplyMessage", + }, + }, + "schemas": { + "EmptyPayload": {"title": "EmptyPayload", "type": "null"}, + "Handle:ReplyMessage:Payload": {"title": "Handle:ReplyMessage:Payload", "type": "null"}, }, - "schemas": {"EmptyPayload": {"title": "EmptyPayload", "type": "null"}}, }, "defaultContentType": "application/json", "info": {"title": "FastStream", "version": "0.1.0"}, diff --git a/tests/asyncapi/redis/v3_0_0/test_router.py b/tests/asyncapi/redis/v3_0_0/test_router.py index 3a668c90fc6..d0e029dc008 100644 --- a/tests/asyncapi/redis/v3_0_0/test_router.py +++ b/tests/asyncapi/redis/v3_0_0/test_router.py @@ -68,6 +68,16 @@ async def handle(msg) -> None: ... }, ], "channel": {"$ref": "#/channels/test_test:Handle"}, + "reply": { + "address": { + "location": "$message.header#/replyTo", + }, + "messages": [ + { + "$ref": "#/components/messages/test_test:Handle:ReplyMessage", + }, + ], + }, }, }, "components": { @@ -81,9 +91,17 @@ async def handle(msg) -> None: ... "$ref": "#/components/schemas/Handle:Message:Payload", }, }, + "test_test:Handle:ReplyMessage": { + "title": "test_test:Handle:ReplyMessage", + "correlationId": { + "location": "$message.header#/correlation_id", + }, + "payload": {"$ref": "#/components/schemas/Handle:ReplyMessage:Payload"}, + }, }, "schemas": { "Handle:Message:Payload": {"title": "Handle:Message:Payload"}, + "Handle:ReplyMessage:Payload": {"title": "Handle:ReplyMessage:Payload", "type": "null"}, }, }, } diff --git a/uv.lock b/uv.lock index f7067b55f26..c3338f4ef9d 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.13'", @@ -850,7 +850,7 @@ wheels = [ [[package]] name = "faststream" -version = "0.6.7" +version = "0.7.0rc0" source = { editable = "." } dependencies = [ { name = "anyio" },