Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: request/response support in both operation and message objects #594

Closed
wants to merge 2 commits into from
Closed
Changes from 1 commit
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
27 changes: 27 additions & 0 deletions spec/asyncapi.md
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,7 @@ Field Name | Type | Description
<a name="operationObjectBindings"></a>bindings | [Operation Bindings Object](#operationBindingsObject) \| [Reference Object](#referenceObject) | A map where the keys describe the name of the protocol and the values describe protocol-specific definitions for the operation.
<a name="operationObjectTraits"></a>traits | [[Operation Trait Object](#operationTraitObject) &#124; [Reference Object](#referenceObject) ] | A list of traits to apply to the operation object. Traits MUST be merged into the operation object using the [JSON Merge Patch](https://tools.ietf.org/html/rfc7386) algorithm in the same order they are defined here.
<a name="operationObjectMessage"></a>message | [[Message Object](#messageObject) &#124; [Reference Object](#referenceObject)] | A definition of the message that will be published or received on this channel. `oneOf` is allowed here to specify multiple messages, however, **a message MUST be valid only against one of the referenced message objects.**
<a name="operationObjectResponse"></a>response | [[Message Object](#messageObject) &#124; [Reference Object](#referenceObject)] | A OPTIONAL definition of the message that will be produced as direct response to publish or subscribe operation on this channel. `oneOf` is allowed here to specify multiple messages, however, **a message MUST be valid ony against on of the referenced message objects.**
smarek marked this conversation as resolved.
Show resolved Hide resolved

This object can be extended with [Specification Extensions](#specificationExtensions).

Expand Down Expand Up @@ -700,6 +701,18 @@ This object can be extended with [Specification Extensions](#specificationExtens
}
}
},
"response": {
"payload": {
"type": "object",
"properties": {
"operationStatusCode": {
"type": "integer",
"format": "int32",
"description": "Operation result status code"
}
}
}
},
"bindings": {
"amqp": {
"ack": false
Expand Down Expand Up @@ -733,6 +746,14 @@ message:
$ref: "#/components/schemas/userCreate"
signup:
$ref: "#/components/schemas/signup"
response:
payload:
type: object
properties:
operationStatusCode:
type: integer
format: int32
description: Operation result status code
bindings:
amqp:
ack: false
Expand Down Expand Up @@ -1018,6 +1039,7 @@ Field Name | Type | Description
<a name="messageObjectBindings"></a>bindings | [Message Bindings Object](#messageBindingsObject) \| [Reference Object](#referenceObject) | A map where the keys describe the name of the protocol and the values describe protocol-specific definitions for the message.
<a name="messageObjectExamples"></a>examples | [Map[`string`, `any`]] | An array of key/value pairs where keys MUST be either **headers** and/or **payload**. Values MUST contain examples that validate against the [headers](#messageObjectHeaders) or [payload](#messageObjectPayload) fields, respectively. Example MAY also have the **name** and **summary** additional keys to provide respectively a machine-friendly name and a short summary of what the example is about.
<a name="messageObjectTraits"></a>traits | [[Message Trait Object](#messageTraitObject) &#124; [Reference Object](#referenceObject)] | A list of traits to apply to the message object. Traits MUST be merged into the message object using the [JSON Merge Patch](https://tools.ietf.org/html/rfc7386) algorithm in the same order they are defined here. The resulting object MUST be a valid [Message Object](#messageObject).
<a name="messageObjectResponse"></a>response | [[Message Object](#messageObject) &#124; [Reference Object](#referenceObject)] | A OPTIONAL definition of the message that the other party can expect to receive as a result to publish operation using this Message Object or as a result to receiving this Message Object in subscription. oneOf is allowed here to specify multiple messages, however, **a message MUST be valid ony against on of the referenced message objects.**
Copy link
Member

@fmvilas fmvilas Jul 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reasoning behind having response inside Message Object? Isn't it enough to have it at the operation level?

Also, how does it play with the response field on the operation? E.g.:

channels:
  mychannel:
    publish:
      message:
        response: ...
      response: ...

What does it mean? Does it mean the one at the operation level prevails over the one at the message level? Maybe both could be expected? If so, how does it work when you have two oneOfs on each response field?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in initial "Identification of potential concerns, challenges, and drawbacks:" number (2).

The reasons are 2

  • Variability to support as wide spectrum of protocol binding as possible
  • Backward compatibility with current x-response extension as documented by @derberg in blog websocket-part2

About the implementation, if both operation and message level response are filled in (even with oneOf) the parser/interpretation shall evaluate/validate/list all possible messages (merge of both response oneOf lists). So this design is intentional.

This also allows for operation level error responses and message level typed responses, eg. as DRY as possible

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't worry about x-response, it is just concept, not even official extension.

I think having it on operation level is anyway not clear. In the end we talk about reply to a message and not the operation, so intuition automatically tells me to look for it inside the message. It also makes sense for oneOf use case, where you have multiple different messages and depending on the message, you can have different reply. This is why I did it this way in my blog post. In the world of WebSocket in cryptocurrency trading APIs, you have one channel, and multiple different messages, when you send message ping you get pong in reply, and when you send message subscriptionStatus, you set status message in response. So depending on the message, you get different reply.

messages:
  ping:
    summary: Ping server to determine whether connection is alive
    description: Client can ping server to determine whether connection is alive, server responds with pong. This is an application level ping as opposed to default ping in websockets standard which is server initiated
    payload:
      $ref: '#/components/schemas/ping'
    x-response:
      $ref: '#/components/messages/pong'

Keep in mind that my blog post is based on real API, Kraken API, and Gemini in v2 follows the same pattern.

@jonaslagoni how does it work with NATS? what would be better there?

Copy link
Member

@jonaslagoni jonaslagoni Aug 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@derberg each response could in theory be a response to a specific message. There are no limitations there (guess it comes down to implementation) 🙂 So the response operation keyword does not make much sense there either.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@derberg understood, however what i had in mind, was allowing to put the general responses in channel (channel can report authentication, authorization, availability or different status itself, not corresponding to sent message) and message-specific responses linked with message, not channel.

@jonaslagoni i don't really have preference over the naming, so i'm open to suggestions

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@smarek I think what @jonaslagoni meant was that from his perspective and how it worked in NATS is that response is enough on message level and not on operation level. At least this is my assumption 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be, but that would require linking all the operation-level responses to all the possible messages that can be processes in that operation/channel, which seemed not-DRY to me, which is also why i proposed operation response as deduplication approach.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but when I specify that pong is a response to ping and that subscriptionStatus is response to subscription I do not validate any DRY principles, right?

message can be one or oneOf this response on operation level only confuses. If I have just one message, I'll specify it in this one message, but if I have oneOf then I will specify it there in every message. When would I do it on operation level?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well for your consideration, which of these seems better to you? (please ignore invalid schema, this is just to illustrate the RFC intent)

just-message

channels:
  basic:
    message:
      oneOf:
        - $ref: '#/components/messages/ping'
        - $ref: '#/components/messages/config'
        - $ref: '#/components/messages/update'
components:
  messages:
    ping:
      payload:
        $ref: '#/components/schemas/ping'
      response:
        oneOf:
          - $ref: '#/components/messages/pong'
          - $ref: '#/components/messages/unauthorized'
          - $ref: '#/components/messages/unauthenticated'
          - $ref: '#/components/messages/service_restarting'
    config:
      payload:
        $ref: '#/components/schemas/config'
      response:
        oneOf:
          - $ref: '#/components/messages/config_result'
          - $ref: '#/components/messages/unauthorized'
          - $ref: '#/components/messages/unauthenticated'
          - $ref: '#/components/messages/service_restarting'
    update:
      payload:
        $ref: '#/components/schemas/update'
      response:
        oneOf:
          - $ref: '#/components/messages/update_result'
          - $ref: '#/components/messages/unauthorized'
          - $ref: '#/components/messages/unauthenticated'
          - $ref: '#/components/messages/service_restarting'

channel and message

channels:
  basic:
    message:
      oneOf:
        - $ref: '#/components/messages/ping'
        - $ref: '#/components/messages/config'
        - $ref: '#/components/messages/update'
    response:
      oneOf:
        - $ref: '#/components/messages/unauthorized'
        - $ref: '#/components/messages/unauthenticated'
        - $ref: '#/components/messages/service_restarting'
components:
  messages:
    ping:
      payload:
        $ref: '#/components/schemas/ping'
      response:
        $ref: '#/components/messages/pong'
    config:
      payload:
        $ref: '#/components/schemas/config'
      response:
        $ref: '#/components/messages/config_result'
    update:
      payload:
        $ref: '#/components/schemas/update'
      response:
        $ref: '#/components/messages/update_result'

In my opinion the channel and message configuration is more clear about response messages belonging to channel operation status and also linking the specific request to one or multiple valid responses. It is not mandatory to define responses on channel or message, but many types/implementations of API are layered (and for good reasons) and individual layers can/may produce responses not relevant to request message but relevant to service/user/operational status/level

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I do see your use-case @smarek, I would suggest focusing on what matters here, enabling request/reply, not improving a feature we don't have. Request/reply discussion is complex enough, that adding more into the discussion, won't benefit or focus on what matters 🙂

I find message level response makes the most sense to introduce in this PR, as operation level response is more of a nice-to-have feature. I see this because we need to be able to describe for a specific message what the desired response is, yes it is a bit cumbersome to define layer responses, but I think that feature belongs in its own issue, once the core issue has been resolved.

What do you think about this? 🙂


This object can be extended with [Specification Extensions](#specificationExtensions).

Expand Down Expand Up @@ -1084,6 +1106,9 @@ Name | Allowed values | Notes
"traits": [
{ "$ref": "#/components/messageTraits/commonHeaders" }
],
"response": {
"$ref": "#/components/messages/userSignedUp"
},
"examples": [
{
"name": "SimpleSignup",
Expand Down Expand Up @@ -1136,6 +1161,8 @@ correlationId:
location: $message.header#/correlationId
traits:
- $ref: "#/components/messageTraits/commonHeaders"
response:
- $ref: "#/components/messages/userSignedUp"
examples:
- name: SimpleSignup
summary: A simple UserSignup example message
Expand Down