Skip to content

Commit db13007

Browse files
authored
Issue/4 - Handles the case where there are no /components/schemas but there is an anonymous one in an operation. (#13)
* First commit with samples. * Work in progress. * Work in progress. * First commit with code worth sharing. * Fixed merge messages in README. * fixes #4 This template now works when the payload schema is embedded inside an operation, as is the case with the Streetlights demo.
1 parent 9b1be7f commit db13007

File tree

9 files changed

+163
-36
lines changed

9 files changed

+163
-36
lines changed

README.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,13 @@ Support for arrays, specifying function names and other features are yet to be i
99

1010
In the samples directory there is an example of code that is close to what we intend to generate.
1111

12-
To run the sample code, copy config-template.ini to config.ini and enter your host and password.
12+
## Configuration
13+
14+
To run the sample code, copy config-template.ini to config.ini and enter the connection details.
15+
16+
If no authentication is needed, you need not provide the username and password values.
17+
18+
If no host is given, the template attempts to find an mqtt host in the servers section of the AsyncAPI document to use instead.
1319

1420
## Specification Conformance
1521
Note that this template interprets the AsyncAPI document in conformance with the [AsyncAPI Specification](https://www.asyncapi.com/docs/specifications/2.0.0/).
@@ -29,6 +35,7 @@ Run the Generator using the Python Paho Template
2935
ag ~/AsyncApiDocument.yaml @asyncapi/python-paho-template
3036
```
3137

38+
Copy config-template.ini to config.ini and edit it to provide the connection details.
3239

3340
### Parameters
3441

@@ -47,5 +54,3 @@ Extension | Parameter | Default | Description
4754
info.x-view | view | client | By default, this template generates publisher code for subscribe operations and vice versa. You can switch this by setting this parameter to 'provider'.
4855

4956

50-
51-

filters/all.js

Lines changed: 75 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ function functionName([channelName, channel]) {
2020
}
2121
filter.functionName = functionName;
2222

23+
function getAnonymousSchema(asyncapi) {
24+
return anonymousSchema(asyncapi);
25+
}
26+
filter.getAnonymousSchema = getAnonymousSchema;
27+
2328
function getRealSubscriber([info, params, channel]) {
2429
let pub = templateUtil.getRealSubscriber(info, params, channel);
2530
return pub
@@ -51,6 +56,45 @@ function identifierName(str) {
5156
}
5257
filter.identifierName = identifierName;
5358

59+
// For files like the streetlights tutorial that don't have schemas, this finds the first anonymous schema in a message payload.
60+
function anonymousSchema(asyncapi) {
61+
let ret = null;
62+
for (channelName in asyncapi.channels()) {
63+
let channel = asyncapi.channel(channelName);
64+
let sub = channel.subscribe();
65+
//console.log("anonymousSchema " + channelName);
66+
//console.log("Sub:");
67+
//console.log(sub);
68+
69+
if (sub) {
70+
ret = anonymouseSchemaFromOperation(sub);
71+
}
72+
73+
if (!ret) {
74+
let pub = channel.publish();
75+
//console.log("Pub:");
76+
//console.log(pub)
77+
if (pub) {
78+
ret = anonymouseSchemaFromOperation(pub);
79+
}
80+
}
81+
82+
if (ret) {
83+
return ret;
84+
}
85+
}
86+
}
87+
88+
function anonymouseSchemaFromOperation(operation) {
89+
let ret = null;
90+
let payloadClass = filter.payloadClass(operation);
91+
//console.log('anonymouseSchemaFromOperation ' + payloadClass);
92+
if (payloadClass === 'Payload') {
93+
ret = operation.message().payload();
94+
}
95+
return ret;
96+
}
97+
5498
function logFull(obj) {
5599
console.log(obj);
56100
if (obj) {
@@ -62,13 +106,27 @@ function logFull(obj) {
62106
filter.logFull = logFull;
63107

64108

65-
// This returns the class name of the payload.
109+
// This returns the class name of the payload. If the schema is embedded, rather than a reference
110+
// to something in components/schemas, we return the name 'Payload' which will be the class we use for the payload.
66111
function payloadClass(operation) {
112+
//console.log("payloadClass............");
67113
let ret = operation.message().payload().ext('x-parser-schema-id');
114+
//console.log(ret);
115+
if (ret.includes("anonymous-schema")) {
116+
ret = "Payload";
117+
}
68118
return ret;
69119
}
70120
filter.payloadClass = payloadClass;
71121

122+
// This returns the first server it can find in the servers section, mainly to
123+
// support the streetlights tutorial.
124+
function server(asyncapi) {
125+
return templateUtil.getServer(asyncapi)
126+
}
127+
filter.server = server;
128+
129+
72130
// This returns an object containing information the template needs to render topic strings.
73131
function topicInfo([channelName, channel]) {
74132
let p = channel.parameters();
@@ -174,7 +232,7 @@ function dump(obj) {
174232

175233
function getImports(schema) {
176234
let ret = '';
177-
//console.log("getImports ----------------------------:");
235+
//console.log("getImports");
178236
//console.log(getMethods(schema))
179237
//console.log(schema);
180238
var properties = schema.properties();
@@ -205,7 +263,7 @@ function getImports(schema) {
205263
}
206264
} else {
207265
let ref = property.ext('x-parser-schema-id');
208-
if (ref) {
266+
if (ref && !ref.includes('anonymous')) {
209267
let importName = _.lowerFirst(ref);
210268
ret += `from ${importName} import ${ref}\n`
211269
}
@@ -250,7 +308,10 @@ function getMessengers([params, asyncapi]) {
250308
messenger.functionName = getFunctionNameByChannel(channelName, channel);
251309
messenger.subscribeTopic = topicInfo.subscribeTopic;
252310
messenger.payload = sub.message().payload();
253-
messenger.payloadClass = messenger.payload.ext('x-parser-schema-id');
311+
//console.log("payload:");
312+
//console.log(messenger.payload);
313+
messenger.payloadClass = filter.payloadClass(sub);
314+
//console.log(messenger.payloadClass);
254315
//console.log(messenger);
255316
ret.push(messenger);
256317
}
@@ -266,26 +327,27 @@ function getMessengers([params, asyncapi]) {
266327
filter.getMessengers = getMessengers;
267328

268329
function getFirstPublisherMessenger([params, asyncapi]) {
330+
//console.log('getFirstPublisherMessenger');
331+
let ret = null;
269332
for (channelName in asyncapi.channels()) {
270333
let channel = asyncapi.channel(channelName);
271334
let pub = templateUtil.getRealPublisher(asyncapi.info(), params, channel);
272-
//(pub);
335+
273336
if (pub) {
274337
let messenger = {};
275338
messenger.name = _.camelCase(channelName) + "Messenger";
276339
messenger.functionName = getFunctionNameByChannel(channelName, channel);
277340
messenger.publishTopic = channelName;
278341
messenger.payload = pub.message().payload();
279-
messenger.payloadClass = messenger.payload.ext('x-parser-schema-id');
280-
//console.log(messenger.payloadClass);
281-
//console.log(messenger.name);
282-
return messenger;
342+
messenger.payloadClass = filter.payloadClass(pub);
343+
//console.log("getFirstPublisherMessenger messenger.payloadClass: " + messenger.payloadClass);
344+
//console.log("getFirstPublisherMessenger messenger.name: " + messenger.name);
345+
ret = messenger;
346+
break;
283347
}
284348
}
285-
286-
let messenger = {};
287-
messenger.name = "messenger";
288-
return messenger;
349+
//console.log('getFirstPublisherMessenger ' + ret);
350+
return ret;
289351
}
290352
filter.getFirstPublisherMessenger = getFirstPublisherMessenger;
291353

hooks/post-process.js

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,24 @@ const _ = require('lodash');
55
module.exports = {
66
'generate:after': generator => {
77
const asyncapi = generator.asyncapi;
8+
let hasSchema = false;
89

9-
for (schema in asyncapi.components().schemas()) {
10-
let oldName = schema + ".py";
11-
let newName = _.lowerFirst(oldName);
12-
if (newName !== schema) {
13-
fs.renameSync(path.resolve(generator.targetDir, oldName), path.resolve(generator.targetDir, newName));
10+
if (asyncapi.components()) {
11+
12+
for (schema in asyncapi.components().schemas()) {
13+
hasSchema = true;
14+
let oldName = schema + ".py";
15+
let newName = _.lowerFirst(oldName);
16+
if (newName !== schema) {
17+
fs.renameSync(path.resolve(generator.targetDir, oldName), path.resolve(generator.targetDir, newName));
18+
}
1419
}
1520
}
21+
22+
// If there are no schemas, we expect to find an anonymous one embedded in a payload. If we do have schemas we assume we don't need this.
23+
// This will turn out to be a bug if we ever have a file with schemas, but which also has an anonymous schema embedded in an operation.
24+
if (hasSchema) {
25+
fs.unlinkSync(path.resolve(generator.targetDir, 'payload.py'));
26+
}
1627
}
1728
}

lib/templateUtil.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,23 @@ class TemplateUtil {
6767
return isProvider ? channel.subscribe() : channel.publish();
6868
}
6969

70+
// This returns the first mqtt server we can find in the document.
71+
getServer(asyncapi) {
72+
let ret = 'None';
73+
let servers = asyncapi.servers();
74+
for (const name in servers) {
75+
let server = asyncapi.server(name);
76+
if (server.protocol() === 'mqtt') {
77+
let url = server.url();
78+
let i = url.lastIndexOf('/');
79+
ret = url.substring(i + 1);
80+
//console.log("getServer: " + url + " " + i + " " + ret);
81+
break;
82+
}
83+
}
84+
return ret;
85+
}
86+
7087
}
7188

7289
// This is the set of Python reserved words, to ensure that we don't generate reserved words.

template/$$schema$$.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
from entity import Entity
55
{% set imports = schema | getImports %}
66
{{ imports }}
7-
{{ modelClass(schemaName, schema.properties(), schema.required(), 0 ) }}
7+
{{ modelClass(schemaName, schema.properties(), schema.required(), 0 ) }}

template/config-template.ini

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[DEFAULT]
2-
host=our-host.messaging.solace.cloud
3-
password=your-password
4-
port=1883
5-
username=solace-cloud-client
2+
host=
3+
password=
4+
port=
5+
username=

template/main.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@
55

66
import messaging
77

8+
{% if asyncapi.components() -%}
89
{% for schemaName, schema in asyncapi.components().schemas() -%}
910
{% set moduleName = schemaName | lowerFirst -%}
1011
from {{ moduleName }} import {{ schemaName }}
11-
{% endfor %}
12+
{% endfor -%}
13+
{% else -%}
14+
from payload import Payload
15+
{% endif %}
1216

1317

1418
# Config has the connection properties.
@@ -36,23 +40,28 @@ def main():
3640
logging.basicConfig(level=logging.INFO)
3741
logging.info('Start of main.')
3842
config = getConfig()
43+
{% set publishMessenger = [params, asyncapi] | getFirstPublisherMessenger -%}
3944
{% set messengers = [params, asyncapi] | getMessengers -%}
4045
{%- for messenger in messengers -%}
4146
{%- if messenger.subscribeTopic %}
42-
{{ messenger.name }} = messaging.Messaging(config, '{{ messenger.topic }}', {{ messenger.functionName }})
47+
{{ messenger.name }} = messaging.Messaging(config, '{{ messenger.subscribeTopic }}', {{ messenger.functionName }})
4348
{%- else %}
4449
{{ messenger.name }} = messaging.Messaging(config)
4550
{%- endif %}
51+
{%- if publishMessenger %}
4652
{{ messenger.name }}.loop_start()
53+
{%- else %}
54+
{{ messenger.name }}.loop_forever()
55+
{%- endif %}
4756
{%- endfor %}
48-
{% set messenger = [params, asyncapi] | getFirstPublisherMessenger -%}
49-
{%- if messenger %}
50-
# Example of how to publish a message:
51-
payload = {{ messenger.payloadClass }}()
57+
{%- if publishMessenger %}
58+
59+
# Example of how to publish a message. You will have to add arguments to the constructor on the next line:
60+
payload = {{ publishMessenger.payloadClass }}()
5261
payloadJson = payload.to_json()
5362

5463
while (True):
55-
{{ messenger.name }}.publish('{{ messenger.publishTopic }}', payloadJson)
64+
{{ publishMessenger.name }}.publish('{{ publishMessenger.publishTopic }}', payloadJson)
5665
time.sleep(1)
5766
{%- endif %}
5867

template/messaging.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ class Messaging:
1010
def __init__(self, config, subscription = None, on_message = None, clientId = None):
1111
global on_connect
1212
self.config = config
13+
defaultHost = '{{ asyncapi | server }}'
14+
logging.info("Default host:", defaultHost)
1315

1416
if (clientId):
1517
self.client = mqtt.Client(clientId)
@@ -25,9 +27,20 @@ def __init__(self, config, subscription = None, on_message = None, clientId = No
2527
if (on_message):
2628
self.client.on_message = on_message
2729

28-
self.client.username_pw_set(config['username'], config['password'])
29-
port = int(config['port'])
30-
self.client.connect(config['host'], port)
30+
username = config.get('username', None)
31+
password = config.get('password', None)
32+
33+
if username is not None:
34+
self.client.username_pw_set(username, password)
35+
36+
port = int(config.get('port', '1883'))
37+
host = config.get('host', defaultHost)
38+
print("Host: ", host, "port: ", port)
39+
40+
if host is None:
41+
raise Exception("Host must be defined in the config file or in the servers section.")
42+
43+
self.client.connect(host, port)
3144

3245
def publish(self, topic, payload, qos = 0, retain = False):
3346
self.client.publish(topic, payload, qos, retain)

template/payload.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{% from "partials/model-class" import modelClass -%}
2+
from enum import Enum
3+
from typing import Sequence
4+
from entity import Entity
5+
{% set schema = asyncapi | getAnonymousSchema -%}
6+
{% if schema %}
7+
{% set imports = schema | getImports -%}
8+
{{ imports }}
9+
{{ modelClass('Payload', schema.properties(), schema.required(), 0 ) }}
10+
{% endif %}

0 commit comments

Comments
 (0)