Skip to content

Commit

Permalink
feat: add common presets for Java (#112)
Browse files Browse the repository at this point in the history
  • Loading branch information
magicmatatjahu authored Mar 9, 2021
1 parent a36811a commit 5798501
Show file tree
Hide file tree
Showing 12 changed files with 559 additions and 1 deletion.
1 change: 1 addition & 0 deletions src/generators/java/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './JavaGenerator';
export { JAVA_DEFAULT_PRESET } from './JavaPreset';
export type { JavaPreset } from './JavaPreset';
export * from './presets';
110 changes: 110 additions & 0 deletions src/generators/java/presets/CommonPreset.ts
Original file line number Diff line number Diff line change
@@ -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);
},
}
};
62 changes: 62 additions & 0 deletions src/generators/java/presets/ConstraintsPreset.ts
Original file line number Diff line number Diff line change
@@ -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]);
},
}
};
51 changes: 51 additions & 0 deletions src/generators/java/presets/DescriptioPreset.ts
Original file line number Diff line number Diff line change
@@ -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 });
},
}
};
15 changes: 15 additions & 0 deletions src/generators/java/presets/JacksonPreset.ts
Original file line number Diff line number Diff line change
@@ -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]);
},
}
};
4 changes: 4 additions & 0 deletions src/generators/java/presets/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './CommonPreset';
export * from './DescriptioPreset';
export * from './JacksonPreset';
export * from './ConstraintsPreset';
2 changes: 1 addition & 1 deletion src/generators/java/renderers/EnumRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export const JAVA_DEFAULT_ENUM_PRESET: EnumPreset<EnumRenderer> = {
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));

Expand Down
25 changes: 25 additions & 0 deletions src/helpers/FormatHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Any>} 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;
}
}
108 changes: 108 additions & 0 deletions test/generators/java/presets/CommonPreset.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading

0 comments on commit 5798501

Please sign in to comment.