From 602f8b73cb6fe5d30fd45af23f321519690c12ca Mon Sep 17 00:00:00 2001 From: Cameron Rushton Date: Fri, 25 Feb 2022 10:02:20 -0500 Subject: [PATCH] fix: generated code missing import statements when using template extension (#244) * import consumer when functionAsConsumer is true * clean up integration tests; move common code into their own homes * clean up code smells * added missing semicolon because it really mattered (not) --- filters/all.js | 4 +- test/__snapshots__/integration.test.js.snap | 133 ++++++++++------ test/integration.test.js | 143 +++++++++--------- .../animals-same-function-name.yaml | 38 +++++ .../dynamic-topic-same-function-name.yaml} | 0 5 files changed, 191 insertions(+), 127 deletions(-) create mode 100644 test/mocks/scs-function-name/animals-same-function-name.yaml rename test/mocks/{test-scs-function-name.yaml => scs-function-name/dynamic-topic-same-function-name.yaml} (100%) diff --git a/filters/all.js b/filters/all.js index 38243c2c..e7643fce 100644 --- a/filters/all.js +++ b/filters/all.js @@ -256,11 +256,11 @@ function addtoExtraIncludesFromFunctionSpecs(asyncapi, params, extraIncludes) { extraIncludes.needBean = true; extraIncludes.needFunction = true; } - if ((spec.type === 'supplier' && !(spec.dynamic && spec.dynamicType === 'streamBridge')) || spec.functionAsConsumer) { + if ((spec.type === 'supplier' && !(spec.dynamic && spec.dynamicType === 'streamBridge'))) { extraIncludes.needBean = true; extraIncludes.needSupplier = true; } - if (spec.type === 'consumer') { + if (spec.type === 'consumer' || spec.functionAsConsumer) { extraIncludes.needBean = true; extraIncludes.needConsumer = true; } diff --git a/test/__snapshots__/integration.test.js.snap b/test/__snapshots__/integration.test.js.snap index 6c499da5..21107851 100644 --- a/test/__snapshots__/integration.test.js.snap +++ b/test/__snapshots__/integration.test.js.snap @@ -177,6 +177,89 @@ public class Application { " `; +exports[`template integration tests using the generator should generate a consumer and return a payload when using x-scs-function-name and dynamic topic binding 1`] = ` +" +import java.util.function.Consumer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.stream.function.StreamBridge; +import org.springframework.context.annotation.Bean; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; + +@SpringBootApplication +public class Application { + + private static final Logger logger = LoggerFactory.getLogger(Application.class); + + @Autowired + private StreamBridge streamBridge; + + public static void main(String[] args) { + SpringApplication.run(Application.class); + } + + // This is a consumer that calls a send method, instead of a function, because it has a dynamic channel and we need streamBridge. + @Bean + public Consumer sameFunctionName() { + return data -> { + // Add business logic here. + logger.info(data.toString()); + String messageId = \\"string\\"; + String operation = \\"string\\"; + MyOtherSchema payload = new MyOtherSchema(); + sendSameFunctionName(payload, messageId, operation); + }; + } + + public void sendSameFunctionName( + MyOtherSchema payload, String messageId, String operation + ) { + String topic = String.format(\\"testLevel1/%s/%s\\", + messageId, operation); + streamBridge.send(topic, payload); + } +} +" +`; + +exports[`template integration tests using the generator should generate a function and return a payload when using x-scs-function-name and a static topic 1`] = ` +" +import java.util.function.Function; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.messaging.Message; + +@SpringBootApplication +public class Application { + + private static final Logger logger = LoggerFactory.getLogger(Application.class); + + public static void main(String[] args) { + SpringApplication.run(Application.class); + } + + @Bean + public Function, Message> sameFunctionName() { + return data -> { + // Add business logic here. + logger.info(data.toString()); + return new Message(); + }; + } + +} +" +`; + exports[`template integration tests using the generator should generate a model subclass when it sees an allOf 1`] = ` "package com.acme; @@ -1501,53 +1584,3 @@ public class Debtor { } " `; - -exports[`template integration tests using the generator should return payload when using x-scs-function-name instead of logging the message 1`] = ` -" -import java.util.function.Supplier; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.cloud.stream.function.StreamBridge; -import org.springframework.context.annotation.Bean; -import org.springframework.messaging.Message; -import org.springframework.messaging.support.MessageBuilder; - -@SpringBootApplication -public class Application { - - private static final Logger logger = LoggerFactory.getLogger(Application.class); - - @Autowired - private StreamBridge streamBridge; - - public static void main(String[] args) { - SpringApplication.run(Application.class); - } - - // This is a consumer that calls a send method, instead of a function, because it has a dynamic channel and we need streamBridge. - @Bean - public Consumer sameFunctionName() { - return data -> { - // Add business logic here. - logger.info(data.toString()); - String messageId = \\"string\\"; - String operation = \\"string\\"; - MyOtherSchema payload = new MyOtherSchema(); - sendSameFunctionName(payload, messageId, operation); - }; - } - - public void sendSameFunctionName( - MyOtherSchema payload, String messageId, String operation - ) { - String topic = String.format(\\"testLevel1/%s/%s\\", - messageId, operation); - streamBridge.send(topic, payload); - } -} -" -`; diff --git a/test/integration.test.js b/test/integration.test.js index 55974027..58031444 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -3,69 +3,85 @@ const Generator = require('@asyncapi/generator'); const { readFile } = require('fs').promises; const crypto = require('crypto'); -const MAIN_TEST_RESULT_PATH = path.join('test', 'temp', 'integrationTestResult'); +// Constants not overridden per test +const TEST_FOLDER_NAME = 'test'; +const MAIN_TEST_RESULT_PATH = path.join(TEST_FOLDER_NAME, 'temp', 'integrationTestResult'); describe('template integration tests using the generator', () => { jest.setTimeout(30000); + // Constants that may be overridden per test + const DEFAULT_PACKAGE = 'com.acme'; + const DEFAULT_PACKAGE_PATH = path.join(...DEFAULT_PACKAGE.split('.')); + + let outputDirectory; + const generateFolderName = () => { - // you always want to generate to new directory to make sure test runs in clear environment + // we always want to generate to new directory to make sure test runs in clear environment return path.resolve(MAIN_TEST_RESULT_PATH, crypto.randomBytes(4).toString('hex')); }; - const assertExpectedFiles = async (outputDirectory, expectedFiles) => { + const generate = (asyncApiFilePath, params) => { + const generator = new Generator(path.normalize('./'), outputDirectory, { forceWrite: true, templateParams: params }); + return generator.generateFromFile(path.resolve(TEST_FOLDER_NAME, asyncApiFilePath)); + }; + + const assertExpectedFiles = async (expectedFiles) => { for (const index in expectedFiles) { const file = await readFile(path.join(outputDirectory, expectedFiles[index]), 'utf8'); expect(file).toMatchSnapshot(); } }; + beforeEach(() => { + outputDirectory = generateFolderName(); + }); + it('should generate application files using the solace binder', async () => { - const OUTPUT_DIR = generateFolderName(); - const PACKAGE = 'com.acme'; - const PACKAGE_PATH = path.join(...PACKAGE.split('.')); const params = { binder: 'solace', - javaPackage: PACKAGE, + javaPackage: DEFAULT_PACKAGE, host: 'testVmrUri', username: 'user', password: 'test', //NOSONAR msgVpn: 'vpnName', artifactId: 'asyncApiFileName' }; - - const generator = new Generator(path.normalize('./'), OUTPUT_DIR, { forceWrite: true, templateParams: params }); - await generator.generateFromFile(path.resolve('test', 'mocks/solace-test-app.yaml')); - const expectedFiles = [ + await generate('mocks/solace-test-app.yaml', params); + + const validatedFiles = [ 'pom.xml', 'README.md', - `src/main/java/${PACKAGE_PATH}/Application.java`, - `src/main/java/${PACKAGE_PATH}/MySchema.java`, + `src/main/java/${DEFAULT_PACKAGE_PATH}/Application.java`, + `src/main/java/${DEFAULT_PACKAGE_PATH}/MySchema.java`, 'src/main/resources/application.yml' ]; - await assertExpectedFiles(OUTPUT_DIR, expectedFiles); + await assertExpectedFiles(validatedFiles); }); - it('should return payload when using x-scs-function-name instead of logging the message', async () => { - const OUTPUT_DIR = generateFolderName(); - - const generator = new Generator(path.normalize('./'), OUTPUT_DIR, { forceWrite: true }); - await generator.generateFromFile(path.resolve('test', 'mocks/test-scs-function-name.yaml')); + it('should generate a consumer and return a payload when using x-scs-function-name and dynamic topic binding', async () => { + await generate('mocks/scs-function-name/dynamic-topic-same-function-name.yaml'); - const expectedFiles = [ + const validatedFiles = [ 'src/main/java/Application.java' ]; - await assertExpectedFiles(OUTPUT_DIR, expectedFiles); + await assertExpectedFiles(validatedFiles); + }); + + it('should generate a function and return a payload when using x-scs-function-name and a static topic', async () => { + await generate('mocks/scs-function-name/animals-same-function-name.yaml'); + + const validatedFiles = [ + 'src/main/java/Application.java' + ]; + await assertExpectedFiles(validatedFiles); }); it('should generate extra config when using the paramatersToHeaders parameter', async () => { - const OUTPUT_DIR = generateFolderName(); - const PACKAGE = 'com.acme'; - const PACKAGE_PATH = path.join(...PACKAGE.split('.')); const params = { binder: 'solace', - javaPackage: PACKAGE, + javaPackage: DEFAULT_PACKAGE, host: 'testVmrUri', username: 'user', password: 'test', //NOSONAR @@ -73,90 +89,70 @@ describe('template integration tests using the generator', () => { artifactId: 'asyncApiFileName', parametersToHeaders: true }; - - const generator = new Generator(path.normalize('./'), OUTPUT_DIR, { forceWrite: true, templateParams: params }); - await generator.generateFromFile(path.resolve('test', 'mocks/solace-test-app.yaml')); - const expectedFiles = [ - `src/main/java/${PACKAGE_PATH}/Application.java`, + await generate('mocks/solace-test-app.yaml', params); + + const validatedFiles = [ + `src/main/java/${DEFAULT_PACKAGE_PATH}/Application.java`, 'src/main/resources/application.yml' ]; - await assertExpectedFiles(OUTPUT_DIR, expectedFiles); + await assertExpectedFiles(validatedFiles); }); it('should generate a comment for a consumer receiving multiple messages', async () => { - const OUTPUT_DIR = generateFolderName(); - - const generator = new Generator(path.normalize('./'), OUTPUT_DIR, { forceWrite: true }); - await generator.generateFromFile(path.resolve('test', 'mocks/animals.yaml')); + await generate('mocks/animals.yaml'); - const expectedFiles = [ + const validatedFiles = [ 'src/main/java/Application.java' ]; - await assertExpectedFiles(OUTPUT_DIR, expectedFiles); + await assertExpectedFiles(validatedFiles); }); it('avro schemas should appear in a package based on their namespace, if any.', async () => { // Note that this file has 2 Avro schemas named User, but one has the namespace 'userpublisher.' - const OUTPUT_DIR = generateFolderName(); - const PACKAGE = 'com.acme'; - const PACKAGE_PATH = path.join(...PACKAGE.split('.')); const AVRO_PACKAGE_PATH = 'userpublisher'; const params = { binder: 'kafka', - javaPackage: PACKAGE, + javaPackage: DEFAULT_PACKAGE, artifactId: 'asyncApiFileName' }; - - const generator = new Generator(path.normalize('./'), OUTPUT_DIR, { forceWrite: true, templateParams: params }); - await generator.generateFromFile(path.resolve('test', 'mocks/kafka-avro.yaml')); + await generate('mocks/kafka-avro.yaml', params); - const expectedFiles = [ - `src/main/java/${PACKAGE_PATH}/User.java`, + const validatedFiles = [ + `src/main/java/${DEFAULT_PACKAGE_PATH}/User.java`, `src/main/java/${AVRO_PACKAGE_PATH}/User.java`, ]; - await assertExpectedFiles(OUTPUT_DIR, expectedFiles); + await assertExpectedFiles(validatedFiles); }); it('should generate a model subclass when it sees an allOf', async () => { - const OUTPUT_DIR = generateFolderName(); - const PACKAGE = 'com.acme'; - const PACKAGE_PATH = path.join(...PACKAGE.split('.')); const params = { - javaPackage: PACKAGE, + javaPackage: DEFAULT_PACKAGE, artifactId: 'asyncApiFileName' }; - - const generator = new Generator(path.normalize('./'), OUTPUT_DIR, { forceWrite: true, templateParams: params }); - await generator.generateFromFile(path.resolve('test', 'mocks/error-reporter.yaml')); + await generate('mocks/error-reporter.yaml', params); - const expectedFiles = [ - `src/main/java/${PACKAGE_PATH}/ExtendedErrorModel.java` + const validatedFiles = [ + `src/main/java/${DEFAULT_PACKAGE_PATH}/ExtendedErrorModel.java` ]; - await assertExpectedFiles(OUTPUT_DIR, expectedFiles); + await assertExpectedFiles(validatedFiles); }); it('should generate schemas with nested arrays', async () => { - const OUTPUT_DIR = generateFolderName(); - - const generator = new Generator(path.normalize('./'), OUTPUT_DIR, { forceWrite: true }); - await generator.generateFromFile(path.resolve('test', 'mocks/nested-arrays.yaml')); + await generate('mocks/nested-arrays.yaml'); - const expectedFiles = [ + const validatedFiles = [ 'src/main/java/Application.java', 'src/main/java/Dossier.java', 'src/main/java/Debtor.java' ]; - await assertExpectedFiles(OUTPUT_DIR, expectedFiles); + await assertExpectedFiles(validatedFiles); }); it('should generate code from the smarty lighting streetlights example', async () => { - const OUTPUT_DIR = generateFolderName(); - - const generator = new Generator(path.normalize('./'), OUTPUT_DIR, { forceWrite: true }); - await generator.generateFromFile(path.resolve('test', 'mocks/smarty-lighting-streetlights.yaml')); + await generate('mocks/smarty-lighting-streetlights.yaml'); - const expectedFiles = [ + const validatedFiles = [ 'src/main/java/Application.java', 'src/main/java/DimLightPayload.java', 'src/main/java/LightMeasuredPayload.java', @@ -164,19 +160,16 @@ describe('template integration tests using the generator', () => { 'src/main/java/TurnOnOffPayload.java', 'src/main/java/SubObject.java' ]; - await assertExpectedFiles(OUTPUT_DIR, expectedFiles); + await assertExpectedFiles(validatedFiles); }); it('should generate code using schemas that have $id set', async () => { - const OUTPUT_DIR = generateFolderName(); - - const generator = new Generator(path.normalize('./'), OUTPUT_DIR, { forceWrite: true }); - await generator.generateFromFile(path.resolve('test', 'mocks/using-$id-field.yaml')); + await generate('mocks/using-$id-field.yaml'); - const expectedFiles = [ + const validatedFiles = [ 'src/main/java/Application.java', 'src/main/java/DefaultMessageSchema.java' ]; - await assertExpectedFiles(OUTPUT_DIR, expectedFiles); + await assertExpectedFiles(validatedFiles); }); }); diff --git a/test/mocks/scs-function-name/animals-same-function-name.yaml b/test/mocks/scs-function-name/animals-same-function-name.yaml new file mode 100644 index 00000000..04acadae --- /dev/null +++ b/test/mocks/scs-function-name/animals-same-function-name.yaml @@ -0,0 +1,38 @@ +components: + schemas: + Dog: + type: object + properties: + name: + type: string + Cat: + type: object + properties: + name: + type: string + messages: + DogMessage: + payload: + $ref: '#/components/schemas/Dog' + CatMessage: + payload: + $ref: '#/components/schemas/Cat' +channels: + 'animals': + publish: + x-scs-function-name: sameFunctionName + message: + oneOf: + - $ref: '#/components/messages/CatMessage' + - $ref: '#/components/messages/DogMessage' + subscribe: + x-scs-function-name: sameFunctionName + message: + oneOf: + - $ref: '#/components/messages/CatMessage' + - $ref: '#/components/messages/DogMessage' +asyncapi: 2.0.0 +info: + description: Testing oneOf + title: animals + version: 0.0.1 diff --git a/test/mocks/test-scs-function-name.yaml b/test/mocks/scs-function-name/dynamic-topic-same-function-name.yaml similarity index 100% rename from test/mocks/test-scs-function-name.yaml rename to test/mocks/scs-function-name/dynamic-topic-same-function-name.yaml