diff --git a/components/Common.js b/components/Common.js index 77e00d1..7602d52 100644 --- a/components/Common.js +++ b/components/Common.js @@ -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 }) { @@ -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); diff --git a/components/Demo/Demo.js b/components/Demo/Demo.js index 04ef95b..47d88f3 100644 --- a/components/Demo/Demo.js +++ b/components/Demo/Demo.js @@ -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) { @@ -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); diff --git a/components/Files/Models.js b/components/Files/Models.js index 018882c..8fe7d6f 100644 --- a/components/Files/Models.js +++ b/components/Files/Models.js @@ -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); @@ -40,7 +40,7 @@ export function Models(asyncapi, params) { - + diff --git a/components/Model.js b/components/Model.js index 18ed077..0a47f15 100644 --- a/components/Model.js +++ b/components/Model.js @@ -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(` `); diff --git a/package-lock.json b/package-lock.json index 7a42ce6..1bcaaca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,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" }, "devDependencies": { "@asyncapi/generator": "^1.17.7", @@ -7084,6 +7085,23 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/generate-schema": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/generate-schema/-/generate-schema-2.6.0.tgz", + "integrity": "sha512-EUBKfJNzT8f91xUk5X5gKtnbdejZeE065UAJ3BCzE8VEbvwKI9Pm5jaWmqVeK1MYc1g5weAVFDTSJzN7ymtTqA==", + "dependencies": { + "commander": "^2.9.0", + "type-of-is": "^3.4.0" + }, + "bin": { + "generate-schema": "bin/generate-schema" + } + }, + "node_modules/generate-schema/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -17335,6 +17353,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-of-is": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/type-of-is/-/type-of-is-3.5.1.tgz", + "integrity": "sha512-SOnx8xygcAh8lvDU2exnK2bomASfNjzB3Qz71s2tw9QnX8fkAo7aC+D0H7FV0HjRKj94CKV2Hi71kVkkO6nOxg==", + "engines": { + "node": ">=0.10.5" + } + }, "node_modules/typed-array-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", @@ -23351,6 +23377,22 @@ "wide-align": "^1.1.5" } }, + "generate-schema": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/generate-schema/-/generate-schema-2.6.0.tgz", + "integrity": "sha512-EUBKfJNzT8f91xUk5X5gKtnbdejZeE065UAJ3BCzE8VEbvwKI9Pm5jaWmqVeK1MYc1g5weAVFDTSJzN7ymtTqA==", + "requires": { + "commander": "^2.9.0", + "type-of-is": "^3.4.0" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + } + } + }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -31003,6 +31045,11 @@ "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "dev": true }, + "type-of-is": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/type-of-is/-/type-of-is-3.5.1.tgz", + "integrity": "sha512-SOnx8xygcAh8lvDU2exnK2bomASfNjzB3Qz71s2tw9QnX8fkAo7aC+D0H7FV0HjRKj94CKV2Hi71kVkkO6nOxg==" + }, "typed-array-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", diff --git a/package.json b/package.json index bee1d73..b141415 100644 --- a/package.json +++ b/package.json @@ -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": [ diff --git a/test/Kafka.test.js b/test/Kafka.test.js index 1dd3b8b..273e138 100644 --- a/test/Kafka.test.js +++ b/test/Kafka.test.js @@ -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); @@ -64,7 +65,6 @@ describe('kafka integration tests using the generator', () => { 'DemoSubscriber.java', 'SongReleasedProducer.java', 'SongReleasedSubscriber.java', - 'models/ModelContract.java', 'models/Song.java', ], [ @@ -84,7 +84,6 @@ describe('kafka integration tests using the generator', () => { [ 'DemoProducer.java', 'SongReleasedProducer.java', - 'models/ModelContract.java', 'models/Song.java', ], [ @@ -105,7 +104,6 @@ describe('kafka integration tests using the generator', () => { 'DemoSubscriber.java', 'SongReleasedProducer.java', 'SongReleasedSubscriber.java', - 'models/ModelContract.java', 'models/Song.java', ], [ @@ -124,7 +122,6 @@ describe('kafka integration tests using the generator', () => { [ 'DemoSubscriber.java', 'SongReleasedSubscriber.java', - 'models/ModelContract.java', 'models/Song.java', ], [ @@ -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', ], [ @@ -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', ], [ @@ -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); + }); }); diff --git a/test/mocks/kafka-orders-v3.yml b/test/mocks/kafka-orders-v3.yml new file mode 100644 index 0000000..5642cce --- /dev/null +++ b/test/mocks/kafka-orders-v3.yml @@ -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 diff --git a/utils/Models.utils.js b/utils/Models.utils.js index 989fce9..d2c7f5c 100644 --- a/utils/Models.utils.js +++ b/utils/Models.utils.js @@ -1,4 +1,5 @@ import { toJavaClassName } from './String.utils'; +import { json } from 'generate-schema'; export function collateModelNames(asyncapi) { return Object.keys(collateModels(asyncapi)); @@ -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; } \ No newline at end of file