diff --git a/src/generators/java/index.ts b/src/generators/java/index.ts index ed06023bed..54f7eaa0ed 100644 --- a/src/generators/java/index.ts +++ b/src/generators/java/index.ts @@ -1,3 +1,4 @@ export * from './JavaGenerator'; export { JAVA_DEFAULT_PRESET } from './JavaPreset'; export type { JavaPreset } from './JavaPreset'; +export * from './presets'; diff --git a/src/generators/java/presets/CommonPreset.ts b/src/generators/java/presets/CommonPreset.ts new file mode 100644 index 0000000000..27dc89d85e --- /dev/null +++ b/src/generators/java/presets/CommonPreset.ts @@ -0,0 +1,110 @@ +import { JavaRenderer } from '../JavaRenderer'; +import { JavaPreset } from '../JavaPreset'; + +import { FormatHelpers } from '../../../helpers'; +import { CommonModel } from '../../../models'; + +export interface JavaCommonPresetOptions { + equal: boolean; + hash: boolean; + classToString: boolean; +} + +/** + * Render `equal` function based on model's properties + * + * @returns {string} + */ +function renderEqual({ renderer, model }: { + renderer: JavaRenderer, + model: CommonModel, +}): string { + const formattedModelName = model.$id && FormatHelpers.toPascalCase(model.$id); + const properties = model.properties || {}; + const equalProperties = Object.keys(properties).map(prop => { + const camelCasedProp = FormatHelpers.toCamelCase(prop); + return `Objects.equals(this.${camelCasedProp}, self.${camelCasedProp})`; + }).join(' &&\n'); + + return `${renderer.renderAnnotation('Override')} +public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ${formattedModelName} self = (${formattedModelName}) o; + return +${renderer.indent(equalProperties, 6)}; +}`; +} + +/** + * Render `hashCode` function based on model's properties + * + * @returns {string} + */ +function renderHashCode({ renderer, model }: { + renderer: JavaRenderer, + model: CommonModel, +}): string { + const properties = model.properties || {}; + const hashProperties = Object.keys(properties).map(prop => FormatHelpers.toCamelCase(prop)).join(', '); + + return `${renderer.renderAnnotation('Override')} +public int hashCode() { + return Objects.hash(${hashProperties}); +}`; +} + +/** + * Render `toString` function based on model's properties + * + * @returns {string} + */ +function renderToString({ renderer, model }: { + renderer: JavaRenderer, + model: CommonModel, +}): string { + const formattedModelName = model.$id && FormatHelpers.toPascalCase(model.$id); + const properties = model.properties || {}; + const toStringProperties = Object.keys(properties).map(prop => + `" ${prop}: " + toIndentedString(${FormatHelpers.toCamelCase(prop)}) + "\\n" +` + ); + + return `${renderer.renderAnnotation('Override')} +public String toString() { + return "class ${formattedModelName} {\\n" + +${renderer.indent(renderer.renderBlock(toStringProperties), 4)} + "}"; +} + +${renderer.renderComments(['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 "); +}`; +} + +/** + * Preset which adds `equal`, `hashCode`, `toString` functions to class. + * + * @implements {JavaPreset} + */ +export const JAVA_COMMON_PRESET: JavaPreset = { + class: { + additionalContent({ renderer, model, content, options }) { + options = options || {}; + const blocks: string[] = []; + + if (options.equal === undefined || options.equal === true) blocks.push(renderEqual({ renderer, model })); + if (options.hashCode === undefined || options.hashCode === true) blocks.push(renderHashCode({ renderer, model })); + if (options.classToString === undefined || options.classToString === true) blocks.push(renderToString({ renderer, model })); + + return renderer.renderBlock([content, ...blocks], 2); + }, + } +}; diff --git a/src/generators/java/presets/ConstraintsPreset.ts b/src/generators/java/presets/ConstraintsPreset.ts new file mode 100644 index 0000000000..b6fedc489f --- /dev/null +++ b/src/generators/java/presets/ConstraintsPreset.ts @@ -0,0 +1,62 @@ +import { JavaPreset } from '../JavaPreset'; + +import { CommonModel } from '../../../models'; + +/** + * Preset which extends class's getters with annotations from `javax.validation.constraints` package + * + * @implements {JavaPreset} + */ +export const JAVA_CONSTRAINTS_PRESET: JavaPreset = { + class: { + getter({ renderer, model, propertyName, property, content }) { + if (!(property instanceof CommonModel)) { + return content; + } + const annotations: string[] = []; + + const isRequired = model.isRequired(propertyName); + if (isRequired) { + annotations.push(renderer.renderAnnotation('NotNull')); + } + + // string + const pattern = property.getFromSchema('pattern'); + if (pattern !== undefined) { + annotations.push(renderer.renderAnnotation('Pattern', { regexp: `"${pattern}"` })); + } + const minLength = property.getFromSchema('minLength'); + const maxLength = property.getFromSchema('maxLength'); + if (minLength !== undefined || maxLength !== undefined) { + annotations.push(renderer.renderAnnotation('Size', { min: minLength, max: maxLength })); + } + + // number/integer + const minimum = property.getFromSchema('minimum'); + if (minimum !== undefined) { + annotations.push(renderer.renderAnnotation('Min', minimum)); + } + const exclusiveMinimum = property.getFromSchema('exclusiveMinimum'); + if (exclusiveMinimum !== undefined) { + annotations.push(renderer.renderAnnotation('Min', exclusiveMinimum + 1)); + } + const maximum = property.getFromSchema('maximum'); + if (maximum !== undefined) { + annotations.push(renderer.renderAnnotation('Max', maximum)); + } + const exclusiveMaximum = property.getFromSchema('exclusiveMaximum'); + if (exclusiveMaximum !== undefined) { + annotations.push(renderer.renderAnnotation('Max', exclusiveMaximum - 1)); + } + + // array + const minItems = property.getFromSchema('minItems'); + const maxItems = property.getFromSchema('maxItems'); + if (minItems !== undefined || maxItems !== undefined) { + annotations.push(renderer.renderAnnotation('Size', { min: minItems, max: maxItems })); + } + + return renderer.renderBlock([...annotations, content]); + }, + } +}; diff --git a/src/generators/java/presets/DescriptioPreset.ts b/src/generators/java/presets/DescriptioPreset.ts new file mode 100644 index 0000000000..4710434612 --- /dev/null +++ b/src/generators/java/presets/DescriptioPreset.ts @@ -0,0 +1,51 @@ +import { JavaRenderer } from '../JavaRenderer'; +import { JavaPreset } from '../JavaPreset'; + +import { FormatHelpers } from '../../../helpers'; +import { CommonModel } from '../../../models'; + +function renderDescription({ renderer, content, item }: { + renderer: JavaRenderer, + content: string, + item: CommonModel, +}): string { + if (!(item instanceof CommonModel)) { + return content; + } + + let desc = item.getFromSchema('description'); + const examples = item.getFromSchema('examples'); + + if (Array.isArray(examples)) { + const renderedExamples = FormatHelpers.renderJSONExamples(examples); + const exampleDesc = `Examples: ${renderedExamples}`; + desc = desc ? `${desc}\n${exampleDesc}` : exampleDesc; + } + + if (desc) { + const renderedDesc = renderer.renderComments(desc); + return `${renderedDesc}\n${content}`; + } + return content; +} + +/** + * Preset which adds description to rendered model. + * + * @implements {JavaPreset} + */ +export const JAVA_DESCRIPTION_PRESET: JavaPreset = { + class: { + self({ renderer, model, content }) { + return renderDescription({ renderer, content, item: model }); + }, + getter({ renderer, property, content }) { + return renderDescription({ renderer, content, item: property }); + } + }, + enum: { + self({ renderer, model, content }) { + return renderDescription({ renderer, content, item: model }); + }, + } +}; diff --git a/src/generators/java/presets/JacksonPreset.ts b/src/generators/java/presets/JacksonPreset.ts new file mode 100644 index 0000000000..e5ab938b8a --- /dev/null +++ b/src/generators/java/presets/JacksonPreset.ts @@ -0,0 +1,15 @@ +import { JavaPreset } from '../JavaPreset'; + +/** + * Preset which adds `com.fasterxml.jackson` related annotations to class's getters. + * + * @implements {JavaPreset} + */ +export const JAVA_JACKSON_PRESET: JavaPreset = { + class: { + getter({ renderer, propertyName, content }) { + const annotation = renderer.renderAnnotation('JsonProperty', `"${propertyName}"`); + return renderer.renderBlock([annotation, content]); + }, + } +}; diff --git a/src/generators/java/presets/index.ts b/src/generators/java/presets/index.ts new file mode 100644 index 0000000000..8474ba7b8e --- /dev/null +++ b/src/generators/java/presets/index.ts @@ -0,0 +1,4 @@ +export * from './CommonPreset'; +export * from './DescriptioPreset'; +export * from './JacksonPreset'; +export * from './ConstraintsPreset'; diff --git a/src/generators/java/renderers/EnumRenderer.ts b/src/generators/java/renderers/EnumRenderer.ts index 9c3563d638..d7df6a27da 100644 --- a/src/generators/java/renderers/EnumRenderer.ts +++ b/src/generators/java/renderers/EnumRenderer.ts @@ -71,7 +71,7 @@ export const JAVA_DEFAULT_ENUM_PRESET: EnumPreset = { return `${key}(${value})`; }, additionalContent({ renderer, model }) { - const enumName = model.$id; + const enumName = model.$id && FormatHelpers.toPascalCase(model.$id); const type = Array.isArray(model.type) ? 'Object' : model.type; const classType = renderer.toClassType(renderer.toJavaType(type, model)); diff --git a/src/helpers/FormatHelpers.ts b/src/helpers/FormatHelpers.ts index effc66dba5..06b4ed72bd 100644 --- a/src/helpers/FormatHelpers.ts +++ b/src/helpers/FormatHelpers.ts @@ -93,4 +93,29 @@ export class FormatHelpers { const whitespaceChar = type === IndentationTypes.SPACES ? ' ' : '\t'; return Array(size).fill(whitespaceChar).join(''); } + + /** + * Render given JSON Schema example to string + * + * @param {Array} examples to render + * @returns {string} + */ + static renderJSONExamples(examples: any[]): string { + let renderedExamples = ''; + if (Array.isArray(examples)) { + examples.forEach(example => { + if (renderedExamples !== '') {renderedExamples += ', ';} + if (typeof example === 'object') { + try { + renderedExamples += JSON.stringify(example); + } catch (ignore) { + renderedExamples += example; + } + } else { + renderedExamples += example; + } + }); + } + return renderedExamples; + } } diff --git a/test/generators/java/presets/CommonPreset.spec.ts b/test/generators/java/presets/CommonPreset.spec.ts new file mode 100644 index 0000000000..0c27644174 --- /dev/null +++ b/test/generators/java/presets/CommonPreset.spec.ts @@ -0,0 +1,108 @@ +import { JavaGenerator, JAVA_COMMON_PRESET } from '../../../../src/generators'; + +describe('JAVA_COMMON_PRESET', function() { + test('should render common function in class by common preset', async function() { + const doc = { + $id: "Clazz", + type: "object", + properties: { + stringProp: { type: "string" }, + numberProp: { type: "number" }, + }, + }; + const expected = `public class Clazz { + private String stringProp; + private Double numberProp; + + public String getStringProp() { return this.stringProp; } + public void setStringProp(String stringProp) { this.stringProp = stringProp; } + + public Double getNumberProp() { return this.numberProp; } + public void setNumberProp(Double numberProp) { this.numberProp = numberProp; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Clazz self = (Clazz) o; + return + Objects.equals(this.stringProp, self.stringProp) && + Objects.equals(this.numberProp, self.numberProp); + } + + @Override + public int hashCode() { + return Objects.hash(stringProp, numberProp); + } + + @Override + public String toString() { + return "class Clazz {\\n" + + " stringProp: " + toIndentedString(stringProp) + "\\n" + + " numberProp: " + toIndentedString(numberProp) + "\\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 "); + } +}`; + + const generator = new JavaGenerator({ presets: [JAVA_COMMON_PRESET] }); + const inputModel = await generator.process(doc); + const model = inputModel.models["Clazz"]; + + let classModel = await generator.renderClass(model, inputModel); + expect(classModel).toEqual(expected); + }); + + test('should skip rendering of disabled functions', async function() { + const doc = { + $id: "Clazz", + type: "object", + properties: { + stringProp: { type: "string" }, + numberProp: { type: "number" }, + }, + }; + const expected = `public class Clazz { + private String stringProp; + private Double numberProp; + + public String getStringProp() { return this.stringProp; } + public void setStringProp(String stringProp) { this.stringProp = stringProp; } + + public Double getNumberProp() { return this.numberProp; } + public void setNumberProp(Double numberProp) { this.numberProp = numberProp; } + + @Override + public int hashCode() { + return Objects.hash(stringProp, numberProp); + } +}`; + + const generator = new JavaGenerator({ presets: [{ + preset: JAVA_COMMON_PRESET, + options: { + equal: false, + classToString: false, + } + }] }); + const inputModel = await generator.process(doc); + const model = inputModel.models["Clazz"]; + + let classModel = await generator.renderClass(model, inputModel); + expect(classModel).toEqual(expected); + }); +}); diff --git a/test/generators/java/presets/ConstraintsPreset.spec.ts b/test/generators/java/presets/ConstraintsPreset.spec.ts new file mode 100644 index 0000000000..8c6595ff5c --- /dev/null +++ b/test/generators/java/presets/ConstraintsPreset.spec.ts @@ -0,0 +1,53 @@ +import { JavaGenerator, JAVA_CONSTRAINTS_PRESET } from '../../../../src/generators'; + +describe('JAVA_DESCRIPTION_PRESET', function() { + let generator: JavaGenerator; + beforeEach(() => { + generator = new JavaGenerator({ presets: [JAVA_CONSTRAINTS_PRESET] }); + }); + + test('should render constaints annotations', async function() { + const doc = { + $id: "Clazz", + type: "object", + properties: { + min_number_prop: { type: "number", minimum: 0 }, + max_number_prop: { type: "number", exclusiveMaximum: 100 }, + array_prop: { type: "array", minItems: 2, maxItems: 3, }, + string_prop: { type: "string", pattern: "^I_", minLength: 3 } + }, + required: ['min_number_prop', 'max_number_prop'] + }; + const expected = `public class Clazz { + private Double minNumberProp; + private Double maxNumberProp; + private Object[] arrayProp; + private String stringProp; + + @NotNull + @Min(0) + public Double getMinNumberProp() { return this.minNumberProp; } + public void setMinNumberProp(Double minNumberProp) { this.minNumberProp = minNumberProp; } + + @NotNull + @Max(99) + public Double getMaxNumberProp() { return this.maxNumberProp; } + public void setMaxNumberProp(Double maxNumberProp) { this.maxNumberProp = maxNumberProp; } + + @Size(min=2, max=3) + public Object[] getArrayProp() { return this.arrayProp; } + public void setArrayProp(Object[] arrayProp) { this.arrayProp = arrayProp; } + + @Pattern(regexp="^I_") + @Size(min=3) + public String getStringProp() { return this.stringProp; } + public void setStringProp(String stringProp) { this.stringProp = stringProp; } +}`; + + const inputModel = await generator.process(doc); + const model = inputModel.models["Clazz"]; + + let classModel = await generator.renderClass(model, inputModel); + expect(classModel).toEqual(expected); + }); +}); diff --git a/test/generators/java/presets/DescriptionPreset.spec.ts b/test/generators/java/presets/DescriptionPreset.spec.ts new file mode 100644 index 0000000000..44c7b7a91e --- /dev/null +++ b/test/generators/java/presets/DescriptionPreset.spec.ts @@ -0,0 +1,92 @@ +import { JavaGenerator, JAVA_DESCRIPTION_PRESET } from '../../../../src/generators'; + +describe('JAVA_DESCRIPTION_PRESET', function() { + let generator: JavaGenerator; + beforeEach(() => { + generator = new JavaGenerator({ presets: [JAVA_DESCRIPTION_PRESET] }); + }); + + test('should render description and examples for class', async function() { + const doc = { + $id: "Clazz", + type: "object", + description: "Description for class", + examples: [{ prop: "value" }], + properties: { + prop: { type: "string", description: "Description for prop", examples: ["exampleValue"] }, + }, + }; + const expected = `/** + * Description for class + * Examples: {"prop":"value"} + */ +public class Clazz { + private String prop; + + /** + * Description for prop + * Examples: exampleValue + */ + public String getProp() { return this.prop; } + public void setProp(String prop) { this.prop = prop; } +}`; + + const inputModel = await generator.process(doc); + const model = inputModel.models["Clazz"]; + + let classModel = await generator.renderClass(model, inputModel); + expect(classModel).toEqual(expected); + }); + + test('should render description and examples for enum', async function() { + const doc = { + $id: "Enum", + type: "string", + description: "Description for enum", + examples: ['value'], + enum: [ + 'on', + 'off', + ] + }; + const expected = `/** + * Description for enum + * Examples: value + */ +public enum Enum { + ON("on"), OFF("off"); + + private String value; + + Enum(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + + @JsonCreator + public static Enum fromValue(String value) { + for (Enum e : Enum.values()) { + if (e.value.equals(value)) { + return e; + } + } + throw new IllegalArgumentException("Unexpected value '" + value + "'"); + } +}`; + + const inputModel = await generator.process(doc); + const model = inputModel.models["Enum"]; + + let enumModel = await generator.renderEnum(model, inputModel); + expect(enumModel).toEqual(expected); + }); +}); diff --git a/test/generators/java/presets/JacksonPreset.spec.ts b/test/generators/java/presets/JacksonPreset.spec.ts new file mode 100644 index 0000000000..6a0e46510e --- /dev/null +++ b/test/generators/java/presets/JacksonPreset.spec.ts @@ -0,0 +1,37 @@ +import { JavaGenerator, JAVA_JACKSON_PRESET } from '../../../../src/generators'; + +describe('JAVA_DESCRIPTION_PRESET', function() { + let generator: JavaGenerator; + beforeEach(() => { + generator = new JavaGenerator({ presets: [JAVA_JACKSON_PRESET] }); + }); + + test('should render Jackson annotations', async function() { + const doc = { + $id: "Clazz", + type: "object", + properties: { + min_number_prop: { type: "number" }, + max_number_prop: { type: "number" }, + }, + }; + const expected = `public class Clazz { + private Double minNumberProp; + private Double maxNumberProp; + + @JsonProperty("min_number_prop") + public Double getMinNumberProp() { return this.minNumberProp; } + public void setMinNumberProp(Double minNumberProp) { this.minNumberProp = minNumberProp; } + + @JsonProperty("max_number_prop") + public Double getMaxNumberProp() { return this.maxNumberProp; } + public void setMaxNumberProp(Double maxNumberProp) { this.maxNumberProp = maxNumberProp; } +}`; + + const inputModel = await generator.process(doc); + const model = inputModel.models["Clazz"]; + + let classModel = await generator.renderClass(model, inputModel); + expect(classModel).toEqual(expected); + }); +});