Skip to content

Commit 1e2c5e4

Browse files
authored
feat: generate schema from example message (#191)
1 parent 82ede4d commit 1e2c5e4

File tree

9 files changed

+154
-15
lines changed

9 files changed

+154
-15
lines changed

components/Common.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616

1717
import { createJavaArgsFromProperties } from '../utils/Types.utils';
18-
import { collateModelNames } from '../utils/Models.utils';
18+
import { collateModelNames, getMessagePayload } from '../utils/Models.utils';
1919
import { MQCipherToJava } from './Connection/MQTLS';
2020

2121
export function Class({ childrenContent, name, implementsClass, extendsClass }) {
@@ -180,7 +180,7 @@ import ${params.package}.models.${messageName};`;
180180
/* Used to resolve a channel object to message name */
181181
export function ChannelToMessage(channel, asyncapi) {
182182
const message = channel.messages().all()[0];
183-
const targetPayloadProperties = message.payload().properties();
183+
const targetPayloadProperties = getMessagePayload(message).properties();
184184
const targetMessageName = message.name();
185185

186186
const messageNameTitleCase = targetMessageName.charAt(0).toUpperCase() + targetMessageName.slice(1);

components/Demo/Demo.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { DemoProducer } from './DemoProducer';
1919
import { javaPackageToPath, toJavaClassName } from '../../utils/String.utils';
2020
import { File } from '@asyncapi/generator-react-sdk';
2121
import { createJavaConstructorArgs } from '../../utils/Types.utils';
22+
import { getMessagePayload } from '../../utils/Models.utils';
2223
import { PackageDeclaration } from '../Common';
2324

2425
export function Demo(asyncapi, params) {
@@ -39,7 +40,7 @@ export function Demo(asyncapi, params) {
3940
// Get payload from either publish or subscribe
4041
const message = channel.messages().all()[0];
4142
const targetMessageName = message.id() || message.name();
42-
const targetPayloadProperties = message.payload().properties();
43+
const targetPayloadProperties = getMessagePayload(message).properties();
4344

4445
const messageNameTitleCase = toJavaClassName(targetMessageName);
4546

components/Files/Models.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { PackageDeclaration, ImportDeclaration, Class, ClassConstructor } from '
1919
import { ModelClassVariables, ModelConstructor } from '../Model';
2020
import { javaPackageToPath } from '../../utils/String.utils';
2121
import { Indent, IndentationTypes } from '@asyncapi/generator-react-sdk';
22-
import { collateModels } from '../../utils/Models.utils';
22+
import { collateModels, getMessagePayload } from '../../utils/Models.utils';
2323

2424
export function Models(asyncapi, params) {
2525
const models = collateModels(asyncapi);
@@ -40,7 +40,7 @@ export function Models(asyncapi, params) {
4040
<ModelClassVariables message={message}></ModelClassVariables>
4141
</Indent>
4242

43-
<ClassConstructor name={messageNameUpperCase} properties={message.payload().properties()}>
43+
<ClassConstructor name={messageNameUpperCase} properties={getMessagePayload(message).properties()}>
4444
<ModelConstructor message={message}/>
4545
</ClassConstructor>
4646
<ClassConstructor name={messageNameUpperCase}>

components/Model.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,16 @@
1515
*/
1616

1717
import { setLocalVariables, defineVariablesForProperties } from '../utils/Types.utils';
18+
import { getMessagePayload } from '../utils/Models.utils';
1819

1920
export function ModelConstructor({ message }) {
2021
// TODO: Supoort ofMany messages
21-
return (setLocalVariables(message.payload().properties()).join(''));
22+
return (setLocalVariables(getMessagePayload(message).properties()).join(''));
2223
}
2324

2425
export function ModelClassVariables({ message }) {
2526
// TODO: Supoort ofMany messages
26-
const argsString = defineVariablesForProperties(message.payload());
27+
const argsString = defineVariablesForProperties(getMessagePayload(message));
2728

2829
return argsString.join(`
2930
`);

package-lock.json

Lines changed: 48 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@
3333
"dependencies": {
3434
"@asyncapi/generator-filters": "^2.1.0",
3535
"@asyncapi/generator-hooks": "^0.1.0",
36-
"@asyncapi/generator-react-sdk": "^1.0.11"
36+
"@asyncapi/generator-react-sdk": "^1.0.11",
37+
"generate-schema": "^2.6.0"
3738
},
3839
"release": {
3940
"branches": [

test/Kafka.test.js

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ describe('kafka integration tests using the generator', () => {
3333
`${PACKAGE_PATH}/ConnectionHelper.java`,
3434
`${PACKAGE_PATH}/LoggingHelper.java`,
3535
`${PACKAGE_PATH}/PubSubBase.java`,
36+
`${PACKAGE_PATH}/models/ModelContract.java`,
3637
];
3738
for (const file of commonFiles) {
3839
expect(existsSync(path.join(OUTPUT_DIR, file))).toBe(true);
@@ -64,7 +65,6 @@ describe('kafka integration tests using the generator', () => {
6465
'DemoSubscriber.java',
6566
'SongReleasedProducer.java',
6667
'SongReleasedSubscriber.java',
67-
'models/ModelContract.java',
6868
'models/Song.java',
6969
],
7070
[
@@ -84,7 +84,6 @@ describe('kafka integration tests using the generator', () => {
8484
[
8585
'DemoProducer.java',
8686
'SongReleasedProducer.java',
87-
'models/ModelContract.java',
8887
'models/Song.java',
8988
],
9089
[
@@ -105,7 +104,6 @@ describe('kafka integration tests using the generator', () => {
105104
'DemoSubscriber.java',
106105
'SongReleasedProducer.java',
107106
'SongReleasedSubscriber.java',
108-
'models/ModelContract.java',
109107
'models/Song.java',
110108
],
111109
[
@@ -124,7 +122,6 @@ describe('kafka integration tests using the generator', () => {
124122
[
125123
'DemoSubscriber.java',
126124
'SongReleasedSubscriber.java',
127-
'models/ModelContract.java',
128125
'models/Song.java',
129126
],
130127
[
@@ -149,7 +146,6 @@ describe('kafka integration tests using the generator', () => {
149146
'SmartylightingStreetlights10EventStreetlightIdLightingMeasuredSubscriber.java',
150147
'models/DimLight.java',
151148
'models/LightMeasured.java',
152-
'models/ModelContract.java',
153149
'models/TurnOnOff.java',
154150
],
155151
[
@@ -173,7 +169,6 @@ describe('kafka integration tests using the generator', () => {
173169
'LightTurnOnProducer.java',
174170
'models/DimLight.java',
175171
'models/LightMeasured.java',
176-
'models/ModelContract.java',
177172
'models/TurnOn.java',
178173
],
179174
[
@@ -182,4 +177,23 @@ describe('kafka integration tests using the generator', () => {
182177
]);
183178
expect(verified).toBe(true);
184179
});
180+
181+
it('should generate code for an AsyncAPI doc without payload schema', async () => {
182+
const verified = await generateJavaProject(
183+
'com.eem',
184+
{
185+
server: 'gateway-group',
186+
},
187+
'mocks/kafka-orders-v3.yml',
188+
[
189+
'DemoSubscriber.java',
190+
'ORDERSJSONSubscriber.java',
191+
'models/Message.java',
192+
],
193+
[
194+
'props.put("security.protocol", "SASL_SSL")',
195+
'props.put("sasl.mechanism", "PLAIN")',
196+
]);
197+
expect(verified).toBe(true);
198+
});
185199
});

test/mocks/kafka-orders-v3.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
asyncapi: 3.0.0
2+
info:
3+
title: ORDERS.JSON
4+
version: 1.0.0
5+
contact:
6+
email: username@example.com
7+
channels:
8+
ORDERS.JSON:
9+
address: ORDERS.JSON
10+
bindings:
11+
kafka:
12+
partitions: 3
13+
replicas: 3
14+
messages:
15+
message:
16+
examples:
17+
- payload: {"id":"973fb57a-4fcc-42df-8710-440c7c3ec32c","customer":"Dionne Howell","customerid":"26b87be0-2be7-4e2d-b5de-43d83d51ee49","description":"M Acid-washed Capri Jeans","price":47.85,"quantity":7,"region":"EMEA","ordertime":"2024-03-09 15:37:19.769"}
18+
operations:
19+
receiveMessage:
20+
action: receive
21+
channel:
22+
$ref: '#/channels/ORDERS.JSON'
23+
messages:
24+
- $ref: '#/channels/ORDERS.JSON/messages/message'
25+
servers:
26+
gateway-group:
27+
host: my-kafka-hostname:9092
28+
protocol: kafka-secure
29+
security:
30+
- $ref: '#/components/securitySchemes/EGW-SECURITY'
31+
components:
32+
securitySchemes:
33+
EGW-SECURITY:
34+
type: plain

utils/Models.utils.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { toJavaClassName } from './String.utils';
2+
import { json } from 'generate-schema';
23

34
export function collateModelNames(asyncapi) {
45
return Object.keys(collateModels(asyncapi));
@@ -13,4 +14,44 @@ export function collateModels(asyncapi) {
1314
}
1415

1516
return models;
17+
}
18+
19+
20+
// The rest of the generator depends on a message object
21+
// having a payload with properties. This is needed to
22+
// be able to generate Java classes with attributes
23+
// matching the expected properties.
24+
//
25+
// Some AsyncAPI documents don't include payload properties
26+
// but provide a sample message instead. For these
27+
// documents, we can attempt to derive a schema from
28+
// the sample, and use that schema to generate a usable
29+
// set of properties.
30+
export function getMessagePayload(message) {
31+
let payload = message.payload();
32+
if (!payload) {
33+
payload = {
34+
required: () => { return false; }
35+
};
36+
}
37+
if (!payload.properties || !payload.properties()) {
38+
const generatedProperties = {};
39+
40+
const examples = message.examples().all();
41+
if (examples && examples.length > 0) {
42+
const example = examples[0];
43+
const examplePayload = example.payload();
44+
const jsonSchema = json('schema', examplePayload).properties;
45+
Object.keys(jsonSchema).forEach((propertyName) => {
46+
generatedProperties[propertyName] = {
47+
type: () => { return jsonSchema[propertyName].type; },
48+
format: () => { return; },
49+
required: () => { return false; }
50+
};
51+
});
52+
}
53+
54+
payload.properties = () => { return generatedProperties; };
55+
}
56+
return payload;
1657
}

0 commit comments

Comments
 (0)