From c44f32d826fcd49be693e1c9d7374883d6c40453 Mon Sep 17 00:00:00 2001 From: Marvin K Date: Wed, 11 Oct 2023 10:45:20 +0200 Subject: [PATCH] feat: add support for additionalProperties (#272) (#273) * feat: Add support for additionalProperties (#272) * fix: Map imports (#272) * fix: Allow additionalProperties=false (#272) * test: Extend test case for boolean (#272) * test: Extend test case for oneOf (#272) * rework: Remove else-branch to check value (#272) * update from master, fix order of imports and tests * fix case when `additionalProperties = true` --------- Co-authored-by: Semen --- filters/all.js | 12 +- .../java/com/asyncapi/model/$$message$$.java | 3 +- .../com/asyncapi/model/$$objectSchema$$.java | 13 +- .../additional-formats.test.js.snap | 1 + tests/__snapshots__/kafka.test.js.snap | 4 +- tests/__snapshots__/map-format.test.js.snap | 454 ++++++++++++++++++ tests/__snapshots__/mqtt.test.js.snap | 16 +- tests/__snapshots__/parameters.test.js.snap | 4 +- tests/map-format.test.js | 36 ++ tests/mocks/map-format.yml | 98 ++++ 10 files changed, 632 insertions(+), 9 deletions(-) create mode 100644 tests/__snapshots__/map-format.test.js.snap create mode 100644 tests/map-format.test.js create mode 100644 tests/mocks/map-format.yml diff --git a/filters/all.js b/filters/all.js index 90462a96..316824cf 100644 --- a/filters/all.js +++ b/filters/all.js @@ -2,7 +2,17 @@ const filter = module.exports; const _ = require('lodash'); function defineType(prop, propName) { - if (prop.type() === 'object') { + if (prop.additionalProperties()) { + if (prop.additionalProperties() === true) { + return 'Map'; + } else if (prop.additionalProperties().type() === 'object') { + return 'Map'; + } else if (prop.additionalProperties().format()) { + return 'Map'; + } else if (prop.additionalProperties().type()) { + return 'Map'; + } + } else if (prop.type() === 'object') { return _.upperFirst(_.camelCase(prop.uid())); } else if (prop.type() === 'array') { if (prop.items().type() === 'object') { diff --git a/template/src/main/java/com/asyncapi/model/$$message$$.java b/template/src/main/java/com/asyncapi/model/$$message$$.java index b5b1cf05..343a0485 100644 --- a/template/src/main/java/com/asyncapi/model/$$message$$.java +++ b/template/src/main/java/com/asyncapi/model/$$message$$.java @@ -6,8 +6,9 @@ {% else %} import jakarta.validation.Valid; {%- endif %} -import java.util.Objects; import java.util.List; +import java.util.Map; +import java.util.Objects; {% if message.description() or message.examples()%}/**{% for line in message.description() | splitByLines %} * {{ line | safe}}{% endfor %}{% if message.examples() %} diff --git a/template/src/main/java/com/asyncapi/model/$$objectSchema$$.java b/template/src/main/java/com/asyncapi/model/$$objectSchema$$.java index d5d22422..6e6f88e4 100644 --- a/template/src/main/java/com/asyncapi/model/$$objectSchema$$.java +++ b/template/src/main/java/com/asyncapi/model/$$objectSchema$$.java @@ -14,6 +14,7 @@ import javax.annotation.processing.Generated; import java.util.List; +import java.util.Map; import java.util.Objects; {% if schema.description() or schema.examples() %}/**{% for line in schema.description() | splitByLines %} @@ -24,7 +25,17 @@ public class {{schemaName | camelCase | upperFirst}} { {% for propName, prop in schema.properties() %} {%- set isRequired = propName | isRequired(schema.required()) %} - {%- if prop.type() === 'object' %} + {%- if prop.additionalProperties() %} + {%- if prop.additionalProperties() === true %} + private @Valid Map {{propName | camelCase}}; + {%- elif prop.additionalProperties().type() === 'object' %} + private @Valid Map {{propName | camelCase}}; + {%- elif prop.additionalProperties().format() %} + private @Valid Map {{propName | camelCase}}; + {%- elif prop.additionalProperties().type() %} + private @Valid Map {{propName | camelCase}}; + {%- endif %} + {%- elif prop.type() === 'object' %} private @Valid {{prop.uid() | camelCase | upperFirst}} {{propName | camelCase}}; {%- elif prop.type() === 'array' %} {%- if prop.items().type() === 'object' %} diff --git a/tests/__snapshots__/additional-formats.test.js.snap b/tests/__snapshots__/additional-formats.test.js.snap index d2bb1854..93a7ce2f 100644 --- a/tests/__snapshots__/additional-formats.test.js.snap +++ b/tests/__snapshots__/additional-formats.test.js.snap @@ -13,6 +13,7 @@ import com.fasterxml.jackson.annotation.JsonValue; import javax.annotation.processing.Generated; import java.util.List; +import java.util.Map; import java.util.Objects; diff --git a/tests/__snapshots__/kafka.test.js.snap b/tests/__snapshots__/kafka.test.js.snap index b8e1a864..000fbdc4 100644 --- a/tests/__snapshots__/kafka.test.js.snap +++ b/tests/__snapshots__/kafka.test.js.snap @@ -180,6 +180,7 @@ import com.fasterxml.jackson.annotation.JsonValue; import javax.annotation.processing.Generated; import java.util.List; +import java.util.Map; import java.util.Objects; @@ -264,8 +265,9 @@ exports[`template integration tests for generated files using the generator and import javax.annotation.processing.Generated; import jakarta.validation.Valid; -import java.util.Objects; import java.util.List; +import java.util.Map; +import java.util.Objects; @Generated(value="com.asyncapi.generator.template.spring", date="AnyDate") diff --git a/tests/__snapshots__/map-format.test.js.snap b/tests/__snapshots__/map-format.test.js.snap new file mode 100644 index 00000000..a80f300b --- /dev/null +++ b/tests/__snapshots__/map-format.test.js.snap @@ -0,0 +1,454 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`template integration tests for map format should generate DTO file with proper map types 1`] = ` +"package com.asyncapi.model; + + +import jakarta.validation.constraints.*; +import jakarta.validation.Valid; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonValue; + +import javax.annotation.processing.Generated; +import java.util.List; +import java.util.Map; +import java.util.Objects; + + +@Generated(value="com.asyncapi.generator.template.spring", date="AnyDate") +public class SongMetaData { + + private @Valid Map tags; + + private @Valid Map stats; + + private @Valid Map flags; + + private @Valid Album album; + + private @Valid Map interprets; + + + + + /** + * Tags + */ + @JsonProperty("tags") + public Map getTags() { + return tags; + } + + public void setTags(Map tags) { + this.tags = tags; + } + + + /** + * Stats + */ + @JsonProperty("stats") + public Map getStats() { + return stats; + } + + public void setStats(Map stats) { + this.stats = stats; + } + + + /** + * Flags + */ + @JsonProperty("flags") + public Map getFlags() { + return flags; + } + + public void setFlags(Map flags) { + this.flags = flags; + } + + + /** + * Album + */ + @JsonProperty("album") + public Album getAlbum() { + return album; + } + + public void setAlbum(Album album) { + this.album = album; + } + + + /** + * Interprets + */ + @JsonProperty("interprets") + public Map getInterprets() { + return interprets; + } + + public void setInterprets(Map interprets) { + this.interprets = interprets; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SongMetaData songMetaData = (SongMetaData) o; + return + Objects.equals(this.tags, songMetaData.tags) && + Objects.equals(this.stats, songMetaData.stats) && + Objects.equals(this.flags, songMetaData.flags) && + Objects.equals(this.album, songMetaData.album) && + Objects.equals(this.interprets, songMetaData.interprets); + } + + @Override + public int hashCode() { + return Objects.hash(tags, stats, flags, album, interprets); + } + + @Override + public String toString() { + return "class SongMetaData {\\n" + + + " tags: " + toIndentedString(tags) + "\\n" + + " stats: " + toIndentedString(stats) + "\\n" + + " flags: " + toIndentedString(flags) + "\\n" + + " album: " + toIndentedString(album) + "\\n" + + " interprets: " + toIndentedString(interprets) + "\\n" + + "}"; + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\\n", "\\n "); + } +}" +`; + +exports[`template integration tests for map format should generate DTO file with proper map types 2`] = ` +"package com.asyncapi.model; + + +import jakarta.validation.constraints.*; +import jakarta.validation.Valid; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonValue; + +import javax.annotation.processing.Generated; +import java.util.List; +import java.util.Map; +import java.util.Objects; + + +@Generated(value="com.asyncapi.generator.template.spring", date="AnyDate") +public class SuccessResponse { + + private @Valid String originalEventId; + + private @Valid Boolean success; + + private @Valid Map meta; + + + + + /** + * Id of the original Event + */ + @JsonProperty("originalEventId") + public String getOriginalEventId() { + return originalEventId; + } + + public void setOriginalEventId(String originalEventId) { + this.originalEventId = originalEventId; + } + + + /** + * Shows whether or not the original Event was processed correctly + */ + @JsonProperty("success") + public Boolean getSuccess() { + return success; + } + + public void setSuccess(Boolean success) { + this.success = success; + } + + + /** + * Meta-Information + */ + @JsonProperty("meta") + public Map getMeta() { + return meta; + } + + public void setMeta(Map meta) { + this.meta = meta; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SuccessResponse successResponse = (SuccessResponse) o; + return + Objects.equals(this.originalEventId, successResponse.originalEventId) && + Objects.equals(this.success, successResponse.success) && + Objects.equals(this.meta, successResponse.meta); + } + + @Override + public int hashCode() { + return Objects.hash(originalEventId, success, meta); + } + + @Override + public String toString() { + return "class SuccessResponse {\\n" + + + " originalEventId: " + toIndentedString(originalEventId) + "\\n" + + " success: " + toIndentedString(success) + "\\n" + + " meta: " + toIndentedString(meta) + "\\n" + + "}"; + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\\n", "\\n "); + } +}" +`; + +exports[`template integration tests for map format should generate DTO file with proper map types 3`] = ` +"package com.asyncapi.model; + + +import jakarta.validation.constraints.*; +import jakarta.validation.Valid; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonValue; + +import javax.annotation.processing.Generated; +import java.util.List; +import java.util.Map; +import java.util.Objects; + + +@Generated(value="com.asyncapi.generator.template.spring", date="AnyDate") +public class FailureResponse { + + private @Valid String originalEventId; + + private @Valid Boolean success; + + private @Valid Map meta; + + + + + /** + * Id of the original Event + */ + @JsonProperty("originalEventId") + public String getOriginalEventId() { + return originalEventId; + } + + public void setOriginalEventId(String originalEventId) { + this.originalEventId = originalEventId; + } + + + /** + * Shows whether or not the original Event was processed correctly + */ + @JsonProperty("success") + public Boolean getSuccess() { + return success; + } + + public void setSuccess(Boolean success) { + this.success = success; + } + + + /** + * Meta-Information + */ + @JsonProperty("meta") + public Map getMeta() { + return meta; + } + + public void setMeta(Map meta) { + this.meta = meta; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + FailureResponse failureResponse = (FailureResponse) o; + return + Objects.equals(this.originalEventId, failureResponse.originalEventId) && + Objects.equals(this.success, failureResponse.success) && + Objects.equals(this.meta, failureResponse.meta); + } + + @Override + public int hashCode() { + return Objects.hash(originalEventId, success, meta); + } + + @Override + public String toString() { + return "class FailureResponse {\\n" + + + " originalEventId: " + toIndentedString(originalEventId) + "\\n" + + " success: " + toIndentedString(success) + "\\n" + + " meta: " + toIndentedString(meta) + "\\n" + + "}"; + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\\n", "\\n "); + } +}" +`; + +exports[`template integration tests for map format should generate DTO file with proper map types 4`] = ` +"package com.asyncapi.model; + + +import jakarta.validation.constraints.*; +import jakarta.validation.Valid; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonValue; + +import javax.annotation.processing.Generated; +import java.util.List; +import java.util.Map; +import java.util.Objects; + + +@Generated(value="com.asyncapi.generator.template.spring", date="AnyDate") +public class Interpret { + + private @Valid String name; + + private @Valid Map meta; + + + + + /** + * Interpret name + */ + @JsonProperty("name") + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + + /** + * Meta-Information + */ + @JsonProperty("meta") + public Map getMeta() { + return meta; + } + + public void setMeta(Map meta) { + this.meta = meta; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Interpret interpret = (Interpret) o; + return + Objects.equals(this.name, interpret.name) && + Objects.equals(this.meta, interpret.meta); + } + + @Override + public int hashCode() { + return Objects.hash(name, meta); + } + + @Override + public String toString() { + return "class Interpret {\\n" + + + " name: " + toIndentedString(name) + "\\n" + + " meta: " + toIndentedString(meta) + "\\n" + + "}"; + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\\n", "\\n "); + } +}" +`; diff --git a/tests/__snapshots__/mqtt.test.js.snap b/tests/__snapshots__/mqtt.test.js.snap index 07f23a8b..20323fe2 100644 --- a/tests/__snapshots__/mqtt.test.js.snap +++ b/tests/__snapshots__/mqtt.test.js.snap @@ -257,6 +257,7 @@ import com.fasterxml.jackson.annotation.JsonValue; import javax.annotation.processing.Generated; import java.util.List; +import java.util.Map; import java.util.Objects; @@ -348,6 +349,7 @@ import com.fasterxml.jackson.annotation.JsonValue; import javax.annotation.processing.Generated; import java.util.List; +import java.util.Map; import java.util.Objects; @@ -439,6 +441,7 @@ import com.fasterxml.jackson.annotation.JsonValue; import javax.annotation.processing.Generated; import java.util.List; +import java.util.Map; import java.util.Objects; @@ -556,8 +559,9 @@ exports[`template integration tests for generated files using the generator and import javax.annotation.processing.Generated; import jakarta.validation.Valid; -import java.util.Objects; import java.util.List; +import java.util.Map; +import java.util.Objects; @Generated(value="com.asyncapi.generator.template.spring", date="AnyDate") @@ -614,8 +618,9 @@ exports[`template integration tests for generated files using the generator and import javax.annotation.processing.Generated; import jakarta.validation.Valid; -import java.util.Objects; import java.util.List; +import java.util.Map; +import java.util.Objects; @Generated(value="com.asyncapi.generator.template.spring", date="AnyDate") @@ -672,8 +677,9 @@ exports[`template integration tests for generated files using the generator and import javax.annotation.processing.Generated; import jakarta.validation.Valid; -import java.util.Objects; import java.util.List; +import java.util.Map; +import java.util.Objects; @Generated(value="com.asyncapi.generator.template.spring", date="AnyDate") @@ -1128,6 +1134,7 @@ import com.fasterxml.jackson.annotation.JsonValue; import javax.annotation.processing.Generated; import java.util.List; +import java.util.Map; import java.util.Objects; @@ -1212,8 +1219,9 @@ exports[`template integration tests for generated files using the generator and import javax.annotation.processing.Generated; import jakarta.validation.Valid; -import java.util.Objects; import java.util.List; +import java.util.Map; +import java.util.Objects; @Generated(value="com.asyncapi.generator.template.spring", date="AnyDate") diff --git a/tests/__snapshots__/parameters.test.js.snap b/tests/__snapshots__/parameters.test.js.snap index c9b001b2..eb1ac552 100644 --- a/tests/__snapshots__/parameters.test.js.snap +++ b/tests/__snapshots__/parameters.test.js.snap @@ -187,6 +187,7 @@ import com.fasterxml.jackson.annotation.JsonValue; import javax.annotation.processing.Generated; import java.util.List; +import java.util.Map; import java.util.Objects; @@ -271,8 +272,9 @@ exports[`integration tests for generated files under different template paramete import javax.annotation.processing.Generated; import javax.validation.Valid; -import java.util.Objects; import java.util.List; +import java.util.Map; +import java.util.Objects; @Generated(value="com.asyncapi.generator.template.spring", date="AnyDate") diff --git a/tests/map-format.test.js b/tests/map-format.test.js new file mode 100644 index 00000000..3a4eaea9 --- /dev/null +++ b/tests/map-format.test.js @@ -0,0 +1,36 @@ +const path = require('path'); +const Generator = require('@asyncapi/generator'); +const { readFile } = require('fs').promises; + +const MAIN_TEST_RESULT_PATH = path.join('tests', 'temp', 'integrationTestResult'); + +const generateFolderName = () => { + // you always want to generate to new directory to make sure test runs in clear environment + return path.resolve(MAIN_TEST_RESULT_PATH, Date.now().toString()); +}; + +describe('template integration tests for map format', () => { + + jest.setTimeout(30000); + + it('should generate DTO file with proper map types', async() => { + const outputDir = generateFolderName(); + const params = {}; + const mapFormatExamplePath = './mocks/map-format.yml'; + + const generator = new Generator(path.normalize('./'), outputDir, { forceWrite: true, templateParams: params }); + await generator.generateFromFile(path.resolve('tests', mapFormatExamplePath)); + + const expectedFiles = [ + '/src/main/java/com/asyncapi/model/SongMetaData.java', + '/src/main/java/com/asyncapi/model/SuccessResponse.java', + '/src/main/java/com/asyncapi/model/FailureResponse.java', + '/src/main/java/com/asyncapi/model/Interpret.java', + ]; + for (const index in expectedFiles) { + const file = await readFile(path.join(outputDir, expectedFiles[index]), 'utf8'); + const fileWithAnyDate = file.replace(/date="\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)"/, 'date="AnyDate"'); + expect(fileWithAnyDate).toMatchSnapshot(); + } + }); +}); diff --git a/tests/mocks/map-format.yml b/tests/mocks/map-format.yml new file mode 100644 index 00000000..c7e6215b --- /dev/null +++ b/tests/mocks/map-format.yml @@ -0,0 +1,98 @@ +asyncapi: 2.0.0 +info: + title: Record Label Service + version: 1.0.0 + description: This service is in charge of processing music +servers: + production: + url: 'my-kafka-hostname:9092' + protocol: kafka + description: Production Instance 1 +channels: + song.metadata: + publish: + message: + $ref: '#/components/messages/metadata' + subscribe: + message: + oneOf: + - $ref: '#/components/messages/success-response' + - $ref: '#/components/messages/failure-response' +components: + messages: + success-response: + payload: + $id: SuccessResponse + type: object + properties: + originalEventId: + description: Id of the original Event + type: string + success: + description: Shows whether or not the original Event was processed correctly + type: boolean + example: true + meta: + description: Meta-Information + additionalProperties: true + failure-response: + payload: + $id: FailureResponse + type: object + properties: + originalEventId: + description: Id of the original Event + type: string + success: + description: Shows whether or not the original Event was processed correctly + type: boolean + example: false + meta: + description: Meta-Information + additionalProperties: + type: string + metadata: + payload: + $id: SongMetaData + type: object + properties: + tags: + description: Tags + additionalProperties: + type: string + stats: + description: Stats + additionalProperties: + type: integer + format: int64 + flags: + description: Flags + additionalProperties: + type: boolean + album: + $ref: '#/components/schemas/Album' + interprets: + description: Interprets + additionalProperties: + $ref: '#/components/schemas/Interpret' + schemas: + Album: + description: Album + type: object + properties: + name: + description: Name of the album + type: string + year: + description: Publishing year + type: integer + additionalProperties: false # do not allow for additional properties + Interpret: + type: object + properties: + name: + description: Interpret name + type: string + meta: + description: Meta-Information + additionalProperties: true \ No newline at end of file