diff --git a/src/generators/javascript/JavaScriptRenderer.ts b/src/generators/javascript/JavaScriptRenderer.ts index b2042718ce..2c6de544e4 100644 --- a/src/generators/javascript/JavaScriptRenderer.ts +++ b/src/generators/javascript/JavaScriptRenderer.ts @@ -14,7 +14,7 @@ export abstract class JavaScriptRenderer extends AbstractRenderer, - model: CommonModel, + model: CommonModel, inputModel: CommonInputModel, ) { super(options, generator, presets, model, inputModel); @@ -29,7 +29,7 @@ export abstract class JavaScriptRenderer extends AbstractRenderer { - return this.runPreset('property', { propertyName, property, type}); + return this.runPreset('property', { propertyName, property, type }); } } diff --git a/src/generators/javascript/index.ts b/src/generators/javascript/index.ts index e9839415f3..586eb2b0c0 100644 --- a/src/generators/javascript/index.ts +++ b/src/generators/javascript/index.ts @@ -2,3 +2,4 @@ export * from './JavaScriptGenerator'; export * from './JavaScriptFileGenerator'; export { JS_DEFAULT_PRESET } from './JavaScriptPreset'; export type { JavaScriptPreset } from './JavaScriptPreset'; +export * from './presets'; diff --git a/src/generators/javascript/presets/CommonPreset.ts b/src/generators/javascript/presets/CommonPreset.ts new file mode 100644 index 0000000000..8117953efd --- /dev/null +++ b/src/generators/javascript/presets/CommonPreset.ts @@ -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); + }, + } +}; diff --git a/src/generators/javascript/presets/index.ts b/src/generators/javascript/presets/index.ts new file mode 100644 index 0000000000..58c93bfea2 --- /dev/null +++ b/src/generators/javascript/presets/index.ts @@ -0,0 +1 @@ +export * from './CommonPreset'; diff --git a/test/generators/javascript/preset/MarshallingPreset.spec.ts b/test/generators/javascript/preset/MarshallingPreset.spec.ts new file mode 100644 index 0000000000..23cadf7b0d --- /dev/null +++ b/test/generators/javascript/preset/MarshallingPreset.spec.ts @@ -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(); + }); +}); diff --git a/test/generators/javascript/preset/__snapshots__/MarshallingPreset.spec.ts.snap b/test/generators/javascript/preset/__snapshots__/MarshallingPreset.spec.ts.snap new file mode 100644 index 0000000000..31631df59f --- /dev/null +++ b/test/generators/javascript/preset/__snapshots__/MarshallingPreset.spec.ts.snap @@ -0,0 +1,165 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Marshalling preset should render un/marshal code 1`] = ` +"class Test { + stringProp; + numberProp; + objectProp; + additionalProperties; + sTestPatternProperties; + sAnotherTestPatternProperties; + + constructor(input) { + this.stringProp = input.stringProp; + if (input.hasOwnProperty('numberProp')) { + this.numberProp = input.numberProp; + } + if (input.hasOwnProperty('objectProp')) { + this.objectProp = input.objectProp; + } + } + + get stringProp() { return this.stringProp; } + set stringProp(stringProp) { this.stringProp = stringProp; } + + get numberProp() { return this.numberProp; } + set numberProp(numberProp) { this.numberProp = numberProp; } + + get objectProp() { return this.objectProp; } + set objectProp(objectProp) { this.objectProp = objectProp; } + + get additionalProperties() { return this.additionalProperties; } + set additionalProperties(additionalProperties) { this.additionalProperties = additionalProperties; } + + get sTestPatternProperties() { return this.sTestPatternProperties; } + set sTestPatternProperties(sTestPatternProperties) { this.sTestPatternProperties = sTestPatternProperties; } + + get sAnotherTestPatternProperties() { return this.sAnotherTestPatternProperties; } + set sAnotherTestPatternProperties(sAnotherTestPatternProperties) { this.sAnotherTestPatternProperties = sAnotherTestPatternProperties; } + + marshal(){ + let json = '{' + if(this.stringProp !== undefined) { + json += `"string prop": ${typeof this.stringProp === 'number' || typeof this.stringProp === 'boolean' ? this.stringProp : JSON.stringify(this.stringProp)},`; + } + if(this.numberProp !== undefined) { + json += `"numberProp": ${typeof this.numberProp === 'number' || typeof this.numberProp === 'boolean' ? this.numberProp : JSON.stringify(this.numberProp)},`; + } + if(this.objectProp !== undefined) { + json += `"objectProp": ${this.objectProp.marshal()},`; + } + if(this.sTestPatternProperties !== undefined) { + for (const [key, value] of this.sTestPatternProperties.entries()) { + //Only render pattern properties which are not already a property + if(Object.keys(this).includes(String(key))) continue; + json += `"${key}": ${typeof value === 'number' || typeof value === 'boolean' ? value : JSON.stringify(value)},`; + } + }if(this.sAnotherTestPatternProperties !== undefined) { + for (const [key, value] of this.sAnotherTestPatternProperties.entries()) { + //Only render pattern properties which are not already a property + if(Object.keys(this).includes(String(key))) continue; + json += `"${key}": ${value.marshal()},`; + } + } + if(this.additionalProperties !== undefined) { + for (const [key, value] of this.additionalProperties.entries()) { + //Only render additionalProperties which are not already a property + if(Object.keys(this).includes(String(key))) continue; + json += `"${key}": ${value.marshal()},`; + } + } + + //Remove potential last comma + return `${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}`; + } + + unmarshal(json){ + const obj = typeof json === "object" ? json : JSON.parse(json); + const instance = new Test({}); + + if (obj["string prop"] !== undefined) { + instance.stringProp = obj["string prop"]; + } + if (obj["numberProp"] !== undefined) { + instance.numberProp = obj["numberProp"]; + } + if (obj["objectProp"] !== undefined) { + instance.objectProp = NestedTest.unmarshal(obj["objectProp"]); + } + + //Not part of core properties + if (instance.sTestPatternProperties === undefined) {instance.sTestPatternProperties = new Map();} + if (instance.sAnotherTestPatternProperties === undefined) {instance.sAnotherTestPatternProperties = new Map();} + + if (instance.additionalProperties === undefined) {instance.additionalProperties = new Map();} + for (const [key, value] of Object.entries(obj).filter((([key,]) => {return !["string prop","numberProp","objectProp"].includes(key);}))) { + //Check all pattern properties + if (key.match(new RegExp('^S(.?)test'))) { + instance.sTestPatternProperties.set(key, value); + continue; + }//Check all pattern properties + if (key.match(new RegExp('^S(.?)AnotherTest'))) { + instance.sAnotherTestPatternProperties.set(key, NestedTest.unmarshal(value)); + continue; + } + instance.additionalProperties.set(key, NestedTest.unmarshal(value)); + } + return instance; + } +}" +`; + +exports[`Marshalling preset should render un/marshal code 2`] = ` +"class NestedTest { + stringProp; + additionalProperties; + + constructor(input) { + if (input.hasOwnProperty('stringProp')) { + this.stringProp = input.stringProp; + } + } + + get stringProp() { return this.stringProp; } + set stringProp(stringProp) { this.stringProp = stringProp; } + + get additionalProperties() { return this.additionalProperties; } + set additionalProperties(additionalProperties) { this.additionalProperties = additionalProperties; } + + marshal(){ + let json = '{' + if(this.stringProp !== undefined) { + json += `"stringProp": ${typeof this.stringProp === 'number' || typeof this.stringProp === 'boolean' ? this.stringProp : JSON.stringify(this.stringProp)},`; + } + + if(this.additionalProperties !== undefined) { + for (const [key, value] of this.additionalProperties.entries()) { + //Only render additionalProperties which are not already a property + if(Object.keys(this).includes(String(key))) continue; + json += `"${key}": ${typeof value === 'number' || typeof value === 'boolean' ? value : JSON.stringify(value)},`; + } + } + + //Remove potential last comma + return `${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}`; + } + + unmarshal(json){ + const obj = typeof json === "object" ? json : JSON.parse(json); + const instance = new NestedTest({}); + + if (obj["stringProp"] !== undefined) { + instance.stringProp = obj["stringProp"]; + } + + //Not part of core properties + + if (instance.additionalProperties === undefined) {instance.additionalProperties = new Map();} + for (const [key, value] of Object.entries(obj).filter((([key,]) => {return !["stringProp"].includes(key);}))) { + + instance.additionalProperties.set(key, value); + } + return instance; + } +}" +`;