Skip to content

Commit

Permalink
feat: un/marshal helper function for JS (#591)
Browse files Browse the repository at this point in the history
  • Loading branch information
Samridhi-98 authored Jan 26, 2022
1 parent 57948b5 commit b2683cd
Show file tree
Hide file tree
Showing 6 changed files with 428 additions and 5 deletions.
10 changes: 5 additions & 5 deletions src/generators/javascript/JavaScriptRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export abstract class JavaScriptRenderer extends AbstractRenderer<JavaScriptOpti
options: JavaScriptOptions,
generator: JavaScriptGenerator,
presets: Array<[Preset, unknown]>,
model: CommonModel,
model: CommonModel,
inputModel: CommonInputModel,
) {
super(options, generator, presets, model, inputModel);
Expand All @@ -29,7 +29,7 @@ export abstract class JavaScriptRenderer extends AbstractRenderer<JavaScriptOpti
* @param model
*/
nameType(name: string | undefined, model?: CommonModel): string {
return this.options?.namingConvention?.type
return this.options?.namingConvention?.type
? this.options.namingConvention.type(name, { model: model || this.model, inputModel: this.inputModel, reservedKeywordCallback: isReservedJavaScriptKeyword })
: name || '';
}
Expand All @@ -41,7 +41,7 @@ export abstract class JavaScriptRenderer extends AbstractRenderer<JavaScriptOpti
* @param property
*/
nameProperty(propertyName: string | undefined, property?: CommonModel): string {
return this.options?.namingConvention?.property
return this.options?.namingConvention?.property
? this.options.namingConvention.property(propertyName, { model: this.model, inputModel: this.inputModel, property, reservedKeywordCallback: isReservedJavaScriptKeyword })
: propertyName || '';
}
Expand Down Expand Up @@ -76,11 +76,11 @@ ${content}
content.push(renderedPatternProperty);
}
}

return this.renderBlock(content);
}

runPropertyPreset(propertyName: string, property: CommonModel, type: PropertyType = PropertyType.property): Promise<string> {
return this.runPreset('property', { propertyName, property, type});
return this.runPreset('property', { propertyName, property, type });
}
}
1 change: 1 addition & 0 deletions src/generators/javascript/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './JavaScriptGenerator';
export * from './JavaScriptFileGenerator';
export { JS_DEFAULT_PRESET } from './JavaScriptPreset';
export type { JavaScriptPreset } from './JavaScriptPreset';
export * from './presets';
207 changes: 207 additions & 0 deletions src/generators/javascript/presets/CommonPreset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import { JavaScriptRenderer } from '../JavaScriptRenderer';
import { JavaScriptPreset } from '../JavaScriptPreset';
import { getUniquePropertyName, DefaultPropertyNames, TypeHelpers, ModelKind } from '../../../helpers';
import { CommonInputModel, CommonModel } from '../../../models';

export interface JavaScriptCommonPresetOptions {
marshalling: boolean;
}

function realizePropertyFactory(prop: string) {
return `$\{typeof ${prop} === 'number' || typeof ${prop} === 'boolean' ? ${prop} : JSON.stringify(${prop})}`;
}
function renderMarshalProperty(modelInstanceVariable: string, model: CommonModel, inputModel: CommonInputModel) {
if (model.$ref) {
const resolvedModel = inputModel.models[model.$ref];
const propertyModelKind = TypeHelpers.extractKind(resolvedModel);
//Referenced enums only need standard marshalling, so lets filter those away
if (propertyModelKind !== ModelKind.ENUM) {
return `$\{${modelInstanceVariable}.marshal()}`;
}
}
return realizePropertyFactory(modelInstanceVariable);
}
function renderMarshalProperties(model: CommonModel, renderer: JavaScriptRenderer, inputModel: CommonInputModel) {
const properties = model.properties || {};
const propertyKeys = [...Object.entries(properties)];
const marshalProperties = propertyKeys.map(([prop, propModel]) => {
const formattedPropertyName = renderer.nameProperty(prop, propModel);
const modelInstanceVariable = `this.${formattedPropertyName}`;
const propMarshalCode = renderMarshalProperty(modelInstanceVariable, propModel, inputModel);
const marshalCode = `json += \`"${prop}": ${propMarshalCode},\`;`;
return `if(${modelInstanceVariable} !== undefined) {
${marshalCode}
}`;
});
return marshalProperties.join('\n');
}

function renderMarshalPatternProperties(model: CommonModel, renderer: JavaScriptRenderer, inputModel: CommonInputModel) {
let marshalPatternProperties = '';
if (model.patternProperties !== undefined) {
for (const [pattern, patternModel] of Object.entries(model.patternProperties)) {
let patternPropertyName = getUniquePropertyName(model, `${pattern}${DefaultPropertyNames.patternProperties}`);
patternPropertyName = renderer.nameProperty(patternPropertyName, patternModel);
const modelInstanceVariable = 'value';
const patternPropertyMarshalCode = renderMarshalProperty(modelInstanceVariable, patternModel, inputModel);
const marshalCode = `json += \`"$\{key}": ${patternPropertyMarshalCode},\`;`;
marshalPatternProperties += `if(this.${patternPropertyName} !== undefined) {
for (const [key, value] of this.${patternPropertyName}.entries()) {
//Only render pattern properties which are not already a property
if(Object.keys(this).includes(String(key))) continue;
${marshalCode}
}
}`;
}
}
return marshalPatternProperties;
}

function renderMarshalAdditionalProperties(model: CommonModel, renderer: JavaScriptRenderer, inputModel: CommonInputModel) {
let marshalAdditionalProperties = '';
if (model.additionalProperties !== undefined) {
let additionalPropertyName = getUniquePropertyName(model, DefaultPropertyNames.additionalProperties);
additionalPropertyName = renderer.nameProperty(additionalPropertyName, model.additionalProperties);
const modelInstanceVariable = 'value';
const patternPropertyMarshalCode = renderMarshalProperty(modelInstanceVariable, model.additionalProperties, inputModel);
const marshalCode = `json += \`"$\{key}": ${patternPropertyMarshalCode},\`;`;
marshalAdditionalProperties = `if(this.${additionalPropertyName} !== undefined) {
for (const [key, value] of this.${additionalPropertyName}.entries()) {
//Only render additionalProperties which are not already a property
if(Object.keys(this).includes(String(key))) continue;
${marshalCode}
}
}`;
}
return marshalAdditionalProperties;
}

/**
* Render `marshal` function based on model
*/
function renderMarshal({ renderer, model, inputModel }: {
renderer: JavaScriptRenderer,
model: CommonModel,
inputModel: CommonInputModel
}): string {
return `marshal(){
let json = '{'
${renderer.indent(renderMarshalProperties(model, renderer, inputModel))}
${renderer.indent(renderMarshalPatternProperties(model, renderer, inputModel))}
${renderer.indent(renderMarshalAdditionalProperties(model, renderer, inputModel))}
//Remove potential last comma
return \`$\{json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}\`;
}`;
}

function renderUnmarshalProperty(modelInstanceVariable: string, model: CommonModel, inputModel: CommonInputModel, renderer: JavaScriptRenderer) {
if (model.$ref) {
const resolvedModel = inputModel.models[model.$ref];
const propertyModelKind = TypeHelpers.extractKind(resolvedModel);
//Referenced enums only need standard marshalling, so lets filter those away
if (propertyModelKind !== ModelKind.ENUM) {
return `${renderer.nameType(model.$ref)}.unmarshal(${modelInstanceVariable})`;
}
}
return `${modelInstanceVariable}`;
}
function renderUnmarshalProperties(model: CommonModel, renderer: JavaScriptRenderer, inputModel: CommonInputModel) {
const properties = model.properties || {};
const propertyKeys = [...Object.entries(properties)];
const unmarshalProperties = propertyKeys.map(([prop, propModel]) => {
const formattedPropertyName = renderer.nameProperty(prop, propModel);
const modelInstanceVariable = `obj["${prop}"]`;
const unmarshalCode = renderUnmarshalProperty(modelInstanceVariable, propModel, inputModel, renderer);
return `if (${modelInstanceVariable} !== undefined) {
instance.${formattedPropertyName} = ${unmarshalCode};
}`;
});
return unmarshalProperties.join('\n');
}

function renderUnmarshalPatternProperties(model: CommonModel, renderer: JavaScriptRenderer, inputModel: CommonInputModel) {
let unmarshalPatternProperties = '';
let setPatternPropertiesMap = '';
if (model.patternProperties !== undefined) {
for (const [pattern, patternModel] of Object.entries(model.patternProperties)) {
let patternPropertyName = getUniquePropertyName(model, `${pattern}${DefaultPropertyNames.patternProperties}`);
patternPropertyName = renderer.nameProperty(patternPropertyName, patternModel);
const modelInstanceVariable = 'value';
const unmarshalCode = renderUnmarshalProperty(modelInstanceVariable, patternModel, inputModel, renderer);
setPatternPropertiesMap += `if (instance.${patternPropertyName} === undefined) {instance.${patternPropertyName} = new Map();}\n`;
unmarshalPatternProperties += `//Check all pattern properties
if (key.match(new RegExp('${pattern}'))) {
instance.${patternPropertyName}.set(key, ${unmarshalCode});
continue;
}`;
}
}
return { unmarshalPatternProperties, setPatternPropertiesMap };
}

function renderUnmarshalAdditionalProperties(model: CommonModel, renderer: JavaScriptRenderer, inputModel: CommonInputModel) {
let unmarshalAdditionalProperties = '';
let setAdditionalPropertiesMap = '';
if (model.additionalProperties !== undefined) {
let additionalPropertyName = getUniquePropertyName(model, DefaultPropertyNames.additionalProperties);
additionalPropertyName = renderer.nameProperty(additionalPropertyName, model.additionalProperties);
const modelInstanceVariable = 'value';
const unmarshalCode = renderUnmarshalProperty(modelInstanceVariable, model.additionalProperties, inputModel, renderer);
setAdditionalPropertiesMap = `if (instance.${additionalPropertyName} === undefined) {instance.${additionalPropertyName} = new Map();}`;
unmarshalAdditionalProperties = `instance.${additionalPropertyName}.set(key, ${unmarshalCode});`;
}
return { unmarshalAdditionalProperties, setAdditionalPropertiesMap };
}

/**
* Render `unmarshal` function based on model
*/
function renderUnmarshal({ renderer, model, inputModel }: {
renderer: JavaScriptRenderer,
model: CommonModel,
inputModel: CommonInputModel
}): string {
const properties = model.properties || {};
const { unmarshalPatternProperties, setPatternPropertiesMap } = renderUnmarshalPatternProperties(model, renderer, inputModel);
const { unmarshalAdditionalProperties, setAdditionalPropertiesMap } = renderUnmarshalAdditionalProperties(model, renderer, inputModel);
const unmarshalProperties = renderUnmarshalProperties(model, renderer, inputModel);
const formattedModelName = renderer.nameType(model.$id);
const propertyNames = Object.keys(properties).map((prop => `"${prop}"`));
return `unmarshal(json){
const obj = typeof json === "object" ? json : JSON.parse(json);
const instance = new ${formattedModelName}({});
${renderer.indent(unmarshalProperties)}
//Not part of core properties
${setPatternPropertiesMap}
${setAdditionalPropertiesMap}
for (const [key, value] of Object.entries(obj).filter((([key,]) => {return ![${propertyNames}].includes(key);}))) {
${renderer.indent(unmarshalPatternProperties, 4)}
${renderer.indent(unmarshalAdditionalProperties, 4)}
}
return instance;
}`;
}

/**
* Preset which adds `marshal`, `unmarshal` functions to class.
*
* @implements {JavaScriptPreset}
*/
export const JS_COMMON_PRESET: JavaScriptPreset = {
class: {
additionalContent({ renderer, model, content, options, inputModel }) {
options = options || {};
const blocks: string[] = [];

if (options.marshalling === true) {
blocks.push(renderMarshal({ renderer, model, inputModel }));
blocks.push(renderUnmarshal({ renderer, model, inputModel }));
}

return renderer.renderBlock([content, ...blocks], 2);
},
}
};
1 change: 1 addition & 0 deletions src/generators/javascript/presets/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './CommonPreset';
49 changes: 49 additions & 0 deletions test/generators/javascript/preset/MarshallingPreset.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/* eslint-disable */

import { JavaScriptGenerator, JS_COMMON_PRESET } from '../../../../src/generators';

import Ajv from 'ajv';
const doc = {
definitions: {
'NestedTest': {
type: 'object', $id: 'NestedTest', properties: {stringProp: { type: 'string' }}
}
},
$id: 'Test',
type: 'object',
additionalProperties: {$ref: '#/definitions/NestedTest'},
required: ['string prop'],
properties: {
'string prop': { type: 'string' },
numberProp: { type: 'number' },
objectProp: { $ref: '#/definitions/NestedTest' }
},
patternProperties: {
'^S(.?)test': { type: 'string' },
'^S(.?)AnotherTest': { $ref: '#/definitions/NestedTest' },
},
};
describe('Marshalling preset', () => {
test('should render un/marshal code', async () => {
const generator = new JavaScriptGenerator({
presets: [
{
preset: JS_COMMON_PRESET,
options: {
marshalling: true
}
}
]
});
const inputModel = await generator.process(doc);

const testModel = inputModel.models['Test'];
const nestedTestModel = inputModel.models['NestedTest'];

const testClass = await generator.renderClass(testModel, inputModel);
const nestedTestClass = await generator.renderClass(nestedTestModel, inputModel);

expect(testClass.result).toMatchSnapshot();
expect(nestedTestClass.result).toMatchSnapshot();
});
});
Loading

0 comments on commit b2683cd

Please sign in to comment.