Skip to content

Commit

Permalink
feat: generate schema from example message (#191)
Browse files Browse the repository at this point in the history
  • Loading branch information
dalelane authored Mar 12, 2024
1 parent 82ede4d commit 1e2c5e4
Show file tree
Hide file tree
Showing 9 changed files with 154 additions and 15 deletions.
4 changes: 2 additions & 2 deletions components/Common.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/

import { createJavaArgsFromProperties } from '../utils/Types.utils';
import { collateModelNames } from '../utils/Models.utils';
import { collateModelNames, getMessagePayload } from '../utils/Models.utils';
import { MQCipherToJava } from './Connection/MQTLS';

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

const messageNameTitleCase = targetMessageName.charAt(0).toUpperCase() + targetMessageName.slice(1);
Expand Down
3 changes: 2 additions & 1 deletion components/Demo/Demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { DemoProducer } from './DemoProducer';
import { javaPackageToPath, toJavaClassName } from '../../utils/String.utils';
import { File } from '@asyncapi/generator-react-sdk';
import { createJavaConstructorArgs } from '../../utils/Types.utils';
import { getMessagePayload } from '../../utils/Models.utils';
import { PackageDeclaration } from '../Common';

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

const messageNameTitleCase = toJavaClassName(targetMessageName);

Expand Down
4 changes: 2 additions & 2 deletions components/Files/Models.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { PackageDeclaration, ImportDeclaration, Class, ClassConstructor } from '
import { ModelClassVariables, ModelConstructor } from '../Model';
import { javaPackageToPath } from '../../utils/String.utils';
import { Indent, IndentationTypes } from '@asyncapi/generator-react-sdk';
import { collateModels } from '../../utils/Models.utils';
import { collateModels, getMessagePayload } from '../../utils/Models.utils';

export function Models(asyncapi, params) {
const models = collateModels(asyncapi);
Expand All @@ -40,7 +40,7 @@ export function Models(asyncapi, params) {
<ModelClassVariables message={message}></ModelClassVariables>
</Indent>

<ClassConstructor name={messageNameUpperCase} properties={message.payload().properties()}>
<ClassConstructor name={messageNameUpperCase} properties={getMessagePayload(message).properties()}>
<ModelConstructor message={message}/>
</ClassConstructor>
<ClassConstructor name={messageNameUpperCase}>
Expand Down
5 changes: 3 additions & 2 deletions components/Model.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,16 @@
*/

import { setLocalVariables, defineVariablesForProperties } from '../utils/Types.utils';
import { getMessagePayload } from '../utils/Models.utils';

export function ModelConstructor({ message }) {
// TODO: Supoort ofMany messages
return (setLocalVariables(message.payload().properties()).join(''));
return (setLocalVariables(getMessagePayload(message).properties()).join(''));
}

export function ModelClassVariables({ message }) {
// TODO: Supoort ofMany messages
const argsString = defineVariablesForProperties(message.payload());
const argsString = defineVariablesForProperties(getMessagePayload(message));

return argsString.join(`
`);
Expand Down
49 changes: 48 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
"dependencies": {
"@asyncapi/generator-filters": "^2.1.0",
"@asyncapi/generator-hooks": "^0.1.0",
"@asyncapi/generator-react-sdk": "^1.0.11"
"@asyncapi/generator-react-sdk": "^1.0.11",
"generate-schema": "^2.6.0"
},
"release": {
"branches": [
Expand Down
26 changes: 20 additions & 6 deletions test/Kafka.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ describe('kafka integration tests using the generator', () => {
`${PACKAGE_PATH}/ConnectionHelper.java`,
`${PACKAGE_PATH}/LoggingHelper.java`,
`${PACKAGE_PATH}/PubSubBase.java`,
`${PACKAGE_PATH}/models/ModelContract.java`,
];
for (const file of commonFiles) {
expect(existsSync(path.join(OUTPUT_DIR, file))).toBe(true);
Expand Down Expand Up @@ -64,7 +65,6 @@ describe('kafka integration tests using the generator', () => {
'DemoSubscriber.java',
'SongReleasedProducer.java',
'SongReleasedSubscriber.java',
'models/ModelContract.java',
'models/Song.java',
],
[
Expand All @@ -84,7 +84,6 @@ describe('kafka integration tests using the generator', () => {
[
'DemoProducer.java',
'SongReleasedProducer.java',
'models/ModelContract.java',
'models/Song.java',
],
[
Expand All @@ -105,7 +104,6 @@ describe('kafka integration tests using the generator', () => {
'DemoSubscriber.java',
'SongReleasedProducer.java',
'SongReleasedSubscriber.java',
'models/ModelContract.java',
'models/Song.java',
],
[
Expand All @@ -124,7 +122,6 @@ describe('kafka integration tests using the generator', () => {
[
'DemoSubscriber.java',
'SongReleasedSubscriber.java',
'models/ModelContract.java',
'models/Song.java',
],
[
Expand All @@ -149,7 +146,6 @@ describe('kafka integration tests using the generator', () => {
'SmartylightingStreetlights10EventStreetlightIdLightingMeasuredSubscriber.java',
'models/DimLight.java',
'models/LightMeasured.java',
'models/ModelContract.java',
'models/TurnOnOff.java',
],
[
Expand All @@ -173,7 +169,6 @@ describe('kafka integration tests using the generator', () => {
'LightTurnOnProducer.java',
'models/DimLight.java',
'models/LightMeasured.java',
'models/ModelContract.java',
'models/TurnOn.java',
],
[
Expand All @@ -182,4 +177,23 @@ describe('kafka integration tests using the generator', () => {
]);
expect(verified).toBe(true);
});

it('should generate code for an AsyncAPI doc without payload schema', async () => {
const verified = await generateJavaProject(
'com.eem',
{
server: 'gateway-group',
},
'mocks/kafka-orders-v3.yml',
[
'DemoSubscriber.java',
'ORDERSJSONSubscriber.java',
'models/Message.java',
],
[
'props.put("security.protocol", "SASL_SSL")',
'props.put("sasl.mechanism", "PLAIN")',
]);
expect(verified).toBe(true);
});
});
34 changes: 34 additions & 0 deletions test/mocks/kafka-orders-v3.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
asyncapi: 3.0.0
info:
title: ORDERS.JSON
version: 1.0.0
contact:
email: username@example.com
channels:
ORDERS.JSON:
address: ORDERS.JSON
bindings:
kafka:
partitions: 3
replicas: 3
messages:
message:
examples:
- 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"}
operations:
receiveMessage:
action: receive
channel:
$ref: '#/channels/ORDERS.JSON'
messages:
- $ref: '#/channels/ORDERS.JSON/messages/message'
servers:
gateway-group:
host: my-kafka-hostname:9092
protocol: kafka-secure
security:
- $ref: '#/components/securitySchemes/EGW-SECURITY'
components:
securitySchemes:
EGW-SECURITY:
type: plain
41 changes: 41 additions & 0 deletions utils/Models.utils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { toJavaClassName } from './String.utils';
import { json } from 'generate-schema';

export function collateModelNames(asyncapi) {
return Object.keys(collateModels(asyncapi));
Expand All @@ -13,4 +14,44 @@ export function collateModels(asyncapi) {
}

return models;
}


// The rest of the generator depends on a message object
// having a payload with properties. This is needed to
// be able to generate Java classes with attributes
// matching the expected properties.
//
// Some AsyncAPI documents don't include payload properties
// but provide a sample message instead. For these
// documents, we can attempt to derive a schema from
// the sample, and use that schema to generate a usable
// set of properties.
export function getMessagePayload(message) {
let payload = message.payload();
if (!payload) {
payload = {
required: () => { return false; }
};
}
if (!payload.properties || !payload.properties()) {
const generatedProperties = {};

const examples = message.examples().all();
if (examples && examples.length > 0) {
const example = examples[0];
const examplePayload = example.payload();
const jsonSchema = json('schema', examplePayload).properties;
Object.keys(jsonSchema).forEach((propertyName) => {
generatedProperties[propertyName] = {
type: () => { return jsonSchema[propertyName].type; },
format: () => { return; },
required: () => { return false; }
};
});
}

payload.properties = () => { return generatedProperties; };
}
return payload;
}

0 comments on commit 1e2c5e4

Please sign in to comment.