Skip to content

Commit 11a1b8d

Browse files
Adi-204derberg
andauthored
feat: automatic routing of message for slack example using discriminators (#1814)
Co-authored-by: Adi-204 <adiboghawala@gmail.com> Co-authored-by: Lukasz Gornicki <lpgornicki@gmail.com>
1 parent a640160 commit 11a1b8d

File tree

20 files changed

+905
-47
lines changed

20 files changed

+905
-47
lines changed

.changeset/slack-auto-routing.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
"@asyncapi/generator": minor
3+
"@asyncapi/generator-components": minor
4+
"@asyncapi/generator-helpers": minor
5+
---
6+
7+
- **Updated Component**: `OnMessage` (Python) - Added discriminator-based routing logic that automatically dispatches messages to operation-specific handlers before falling back to generic handlers
8+
- **New Helpers**:
9+
- `getMessageDiscriminatorData` - Extracts discriminator key and value from individual messages
10+
- `getMessageDiscriminatorsFromOperations` - Collects all discriminator metadata from receive operations
11+
- Enhanced Python webSocket client generation with **automatic operation-based message routing**:
12+
13+
## How python routing works
14+
15+
- Generated WebSocket clients now automatically route incoming messages to operation-specific handlers based on message discriminators. Users can register handlers for specific message types without manually parsing or filtering messages.
16+
- When a message arrives, the client checks it against registered discriminators (e.g., `type: "hello"`, `type: "events_api"`)
17+
- If a match is found, the message is routed to the specific operation handler (e.g., `onHelloMessage`, `onEvent`)
18+
- If no match is found, the message falls back to generic message handlers
19+
- This enables clean separation of message handling logic based on message types
20+
21+
> `discriminator` is a `string` field that you can add to any AsyncAPI Schema. This also means that it is limited to AsyncAPI Schema only, and it won't work with other schema formats, like for example, Avro.
22+
23+
The implementation automatically derives discriminator information from your AsyncAPI document:
24+
- Discriminator `key` is extracted from the `discriminator` field in your AsyncAPI spec
25+
- Discriminator `value` is extracted from the `const` property defined in message schemas
26+
27+
Example AsyncAPI Schema with `discriminator` and `const`:
28+
```yaml
29+
schemas:
30+
hello:
31+
type: object
32+
discriminator: type # you specify name of property
33+
properties:
34+
type:
35+
type: string
36+
const: hello # you specify the value of the discriminator property that is used for routing
37+
description: A hello string confirming WebSocket connection
38+
```
39+
40+
## Fallback
41+
42+
When defaults aren't available in the AsyncAPI document, users must provide **both** `discriminator_key` and `discriminator_value` when registering handlers. Providing only one parameter is not supported - you must provide either both or neither.
43+
44+
> **Why this limitation exists**: When a receive operation has multiple messages sharing the same discriminator key (e.g., all use `"type"` field), we need the specific value (e.g., `"hello"`, `"disconnect"`) to distinguish between them. Without both pieces of information, the routing becomes ambiguous.
45+
46+
Example:
47+
48+
```python
49+
# Default case - discriminator info auto-derived from AsyncAPI doc
50+
client.register_on_hello_message_handler(my_handler)
51+
52+
# Custom case - must provide both key AND value
53+
client.register_on_hello_message_handler(
54+
my_handler,
55+
discriminator_key="message_type",
56+
discriminator_value="custom_hello"
57+
)
58+
```

packages/components/src/components/OnMessage.js

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,33 @@ const websocketOnMessageMethod = {
3232
python: () => {
3333
return {
3434
onMessageMethod: `def on_message(self, ws, message):
35-
self.handle_message(message)`
35+
# Parse message for routing
36+
try:
37+
parsed_message = json.loads(message)
38+
except:
39+
parsed_message = message
40+
41+
handled = False
42+
43+
# Check each operation's discriminator
44+
for discriminator in self.receive_operation_discriminators:
45+
key = discriminator.get("key")
46+
value = discriminator.get("value")
47+
operation_id = discriminator.get("operation_id")
48+
49+
# Check if message matches this discriminator
50+
if key and isinstance(parsed_message, dict) and parsed_message.get(key) == value:
51+
handler = self.receive_operation_handlers.get(operation_id)
52+
if handler:
53+
try:
54+
handler(message)
55+
handled = True
56+
except Exception as error:
57+
print(f"Error in {operation_id} handler: {error}")
58+
59+
# Fallback to generic handlers if not handled
60+
if not handled:
61+
self.handle_message(message)`
3662
};
3763
},
3864
dart: () => {

packages/components/src/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,4 @@ export { CoreMethods } from './components/readme/CoreMethods';
1717
export { Installation } from './components/readme/Installation';
1818
export { Usage } from './components/readme/Usage';
1919
export { Overview } from './components/readme/Overview';
20-
export { Readme } from './components/readme/Readme';
20+
export { Readme } from './components/readme/Readme';

packages/components/test/components/__snapshots__/AvailableOperations.test.js.snap

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,5 +157,11 @@ client.mixed_message_examples(123)
157157
158158
159159
160-
#### \`operation_with_snake_case(payload)\`"
160+
#### \`operation_with_snake_case(payload)\`
161+
162+
163+
164+
165+
#### \`receiveMarketUpdate(payload)\`
166+
Receive market update message"
161167
`;

packages/components/test/components/__snapshots__/Connect.test.js.snap

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,33 @@ exports[`Testing of Connect function render python Connect method 1`] = `
9898
print(\\"Connected to HoppscotchEchoWebSocketClient server\\")
9999
100100
def on_message(self, ws, message):
101-
self.handle_message(message)
101+
# Parse message for routing
102+
try:
103+
parsed_message = json.loads(message)
104+
except:
105+
parsed_message = message
106+
107+
handled = False
108+
109+
# Check each operation's discriminator
110+
for discriminator in self.receive_operation_discriminators:
111+
key = discriminator.get(\\"key\\")
112+
value = discriminator.get(\\"value\\")
113+
operation_id = discriminator.get(\\"operation_id\\")
114+
115+
# Check if message matches this discriminator
116+
if key and isinstance(parsed_message, dict) and parsed_message.get(key) == value:
117+
handler = self.receive_operation_handlers.get(operation_id)
118+
if handler:
119+
try:
120+
handler(message)
121+
handled = True
122+
except Exception as error:
123+
print(f\\"Error in {operation_id} handler: {error}\\")
124+
125+
# Fallback to generic handlers if not handled
126+
if not handled:
127+
self.handle_message(message)
102128
103129
def on_error(self, ws, error):
104130
print(\\"WebSocket Error:\\", error)

packages/components/test/components/__snapshots__/OnMessage.test.js.snap

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,31 @@ exports[`OnMessage renders per language renders javascript OnMessage method 1`]
3131
3232
exports[`OnMessage renders per language renders python OnMessage method 1`] = `
3333
"def on_message(self, ws, message):
34-
self.handle_message(message)"
34+
# Parse message for routing
35+
try:
36+
parsed_message = json.loads(message)
37+
except:
38+
parsed_message = message
39+
40+
handled = False
41+
42+
# Check each operation's discriminator
43+
for discriminator in self.receive_operation_discriminators:
44+
key = discriminator.get(\\"key\\")
45+
value = discriminator.get(\\"value\\")
46+
operation_id = discriminator.get(\\"operation_id\\")
47+
48+
# Check if message matches this discriminator
49+
if key and isinstance(parsed_message, dict) and parsed_message.get(key) == value:
50+
handler = self.receive_operation_handlers.get(operation_id)
51+
if handler:
52+
try:
53+
handler(message)
54+
handled = True
55+
except Exception as error:
56+
print(f\\"Error in {operation_id} handler: {error}\\")
57+
58+
# Fallback to generic handlers if not handled
59+
if not handled:
60+
self.handle_message(message)"
3561
`;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* Extracts discriminator metadata from a message for operation routing.
3+
*
4+
* @param {object} message - The AsyncAPI message object containing payload and discriminator info.
5+
* @param {string} operationId - The operation ID associated with this message.
6+
* @returns {object|null} An object with key, value, and operation_id if discriminator is valid; otherwise null.
7+
*/
8+
const getMessageDiscriminatorData = (message, operationId) => {
9+
const payload = message.payload();
10+
if (!payload) {
11+
return null;
12+
}
13+
const discriminator = payload.discriminator();
14+
15+
if (!discriminator) {
16+
return null;
17+
}
18+
19+
const discriminator_key = discriminator;
20+
const properties = payload.properties();
21+
22+
if (!properties || !properties[discriminator_key]) {
23+
return null;
24+
}
25+
26+
const discriminatorProperty = properties[discriminator_key];
27+
const discriminator_value = discriminatorProperty.const();
28+
29+
if (!discriminator_value) {
30+
return null;
31+
}
32+
33+
return {
34+
key: discriminator_key,
35+
value: discriminator_value,
36+
operation_id: operationId
37+
};
38+
};
39+
40+
/**
41+
* Get discriminator metadata from all messages across a list of AsyncAPI operations.
42+
*
43+
* @param {Array<object>} operations - List of AsyncAPI Operation objects.
44+
* @returns {Array<object>} Array of discriminator metadata objects with key, value, and operation_id.
45+
*/
46+
const getMessageDiscriminatorsFromOperations = (operations) => {
47+
if (!operations || !Array.isArray(operations)) {
48+
return [];
49+
}
50+
const operationDiscriminators = [];
51+
52+
operations.forEach((operation) => {
53+
const operationId = operation.id();
54+
const messages = operation.messages().all();
55+
56+
messages
57+
.filter(message => message.hasPayload())
58+
.forEach(message => {
59+
const discriminatorData = getMessageDiscriminatorData(message, operationId);
60+
if (discriminatorData) {
61+
operationDiscriminators.push(discriminatorData);
62+
}
63+
});
64+
});
65+
66+
return operationDiscriminators;
67+
};
68+
69+
module.exports = {
70+
getMessageDiscriminatorData,
71+
getMessageDiscriminatorsFromOperations
72+
};

packages/helpers/src/index.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const { getMessageExamples, getOperationMessages } = require('./operations');
22
const { getServerUrl, getServer, getServerHost, getServerProtocol } = require('./servers');
3-
const { getClientName, getInfo, toSnakeCase, toCamelCase, getTitle, lowerFirst, upperFirst} = require('./utils');
3+
const { getClientName, getInfo, toSnakeCase, toCamelCase, getTitle, lowerFirst, upperFirst } = require('./utils');
4+
const { getMessageDiscriminatorData, getMessageDiscriminatorsFromOperations } = require('./discriminators');
45
const { getQueryParams } = require('./bindings');
56
const { cleanTestResultPaths, verifyDirectoryStructure, getDirElementsRecursive, buildParams, listFiles, hasNestedConfig} = require('./testing');
67
const { JavaModelsPresets } = require('./ModelsPresets');
@@ -26,5 +27,7 @@ module.exports = {
2627
getDirElementsRecursive,
2728
buildParams,
2829
hasNestedConfig,
29-
JavaModelsPresets
30+
JavaModelsPresets,
31+
getMessageDiscriminatorData,
32+
getMessageDiscriminatorsFromOperations
3033
};

packages/helpers/test/__fixtures__/asyncapi-websocket-query.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,36 @@ channels:
7474
payload: false
7575
- name: number
7676
payload: 123
77+
MarketUpdateMessage:
78+
summary: Market update message with discriminator
79+
payload:
80+
type: object
81+
discriminator: messageType
82+
properties:
83+
messageType:
84+
type: string
85+
const: marketUpdate
86+
data:
87+
type: string
88+
MessageWithoutDiscriminator:
89+
summary: Message without discriminator
90+
payload:
91+
type: object
92+
properties:
93+
messageType:
94+
type: string
95+
data:
96+
type: string
97+
MessageWithDiscriminatorNoConst:
98+
summary: Message with discriminator but no const value
99+
payload:
100+
type: object
101+
discriminator: messageType
102+
properties:
103+
messageType:
104+
type: string
105+
data:
106+
type: string
77107
wsBindingNoQuery:
78108
address: '/no-query'
79109
bindings:
@@ -163,6 +193,16 @@ operations:
163193
$ref: '#/channels/marketDataV1'
164194
messages:
165195
- $ref: '#/channels/marketDataV1/messages/noExamples'
196+
197+
receiveMarketUpdate:
198+
action: receive
199+
channel:
200+
$ref: '#/channels/marketDataV1'
201+
summary: Receive market update message
202+
messages:
203+
- $ref: '#/channels/marketDataV1/messages/MarketUpdateMessage'
204+
- $ref: '#/channels/marketDataV1/messages/MessageWithoutDiscriminator'
205+
- $ref: '#/channels/marketDataV1/messages/MessageWithDiscriminatorNoConst'
166206

167207
components:
168208
parameters:

0 commit comments

Comments
 (0)