Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add option to generate named exports from Typescript generator #622

Merged
merged 10 commits into from
Mar 22, 2022
46 changes: 45 additions & 1 deletion docs/generators.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Options are passed as the first argument to the generator's constructor. Check t
const generator = new TypeScriptGenerator({ ...options });
```

Default options contain:
Default generator options (common to all generators) are as follows:

| Option | Type | Description | Default value |
|---|---|---|---|
Expand All @@ -36,10 +36,15 @@ Default options contain:
| `defaultPreset` | Object | Default preset for generator. For more information, read [customization](./customization.md) document. | _Implemented by generator_ |
| `presets` | Array | Array contains **presets**. For more information, read [customization](./customization.md) document. | `[]` |

In addition, generators take additional options when calling their `renderCompleteModel(input, options)` functions.
This allows the caller to specify additional options when generating a multi-file model from the input with cross dependencies.

Below is a list of additional options available for a given generator.

### [TypeScript](./languages/TypeScript.md)

#### Generator options

| Option | Type | Description | Default value |
|---|---|---|---|
| `renderTypes` | Boolean | Render signature for types. | `true` |
Expand All @@ -49,39 +54,78 @@ Below is a list of additional options available for a given generator.
| `namingConvention.type` | Function | A function that returns the format of the type. | _Returns pascal cased name, and ensures that reserved keywords are never rendered__ |
| `namingConvention.property` | Function | A function that returns the format of the property. | _Returns camel cased name, and ensures that names of properties does not clash against reserved keywords for TS, as well as JS to ensure painless transpilation_ |

#### Render complete model options

| Option | Type | Description | Default value |
|----------------|--------------------------|----------------------------------------------------------------------------|---------------|
| `moduleSystem` | 'ESM' | 'CJS' | Which module system the generated files should use (`import` or `require`) | 'CJS' |
| `exportType` | 'default' | 'named' | Whether the exports should be default or named exports | 'default' |

### [Java](./languages/Java.md)

#### Generator options

| Option | Type | Description | Default value |
|---|---|---|---|
| `collectionType` | String | It indicates with which signature should be rendered the `array` type. Its value can be either `List` (`List<{type}>`) or `Array` (`{type}[]`). | `List` |
| `namingConvention` | Object | Options for naming conventions. | - |
| `namingConvention.type` | Function | A function that returns the format of the type. | _Returns pascal cased name, and ensures that reserved keywords are never rendered__ |
| `namingConvention.property` | Function | A function that returns the format of the property. | _Returns camel cased name, and ensures that names of properties does not clash against reserved keywords_ |

#### Render complete model options

| Option | Type | Description | Default value |
|---------------|--------|-----------------------------------------------|---------------|
| `packageName` | string | The package name to generate the models under | [required] |

### [JavaScript](./languages/JavaScript.md)

#### Generator options

| Option | Type | Description | Default value |
|---|---|---|---|
| `namingConvention` | Object | Options for naming conventions. | - |
| `namingConvention.type` | Function | A function that returns the format of the type. | _Returns pascal cased name, and ensures that reserved keywords are never rendered_ |
| `namingConvention.property` | Function | A function that returns the format of the property. | _Returns camel cased name, and ensures that names of properties does not clash against reserved keywords_ |

#### Render complete model options

| Option | Type | Description | Default value |
|----------------|--------------------------|----------------------------------------------------------------------------|---------------|
| `moduleSystem` | 'ESM' &#124; 'CJS' | Which module system the generated files should use (`import` or `require`) | 'CJS' |

### [Go](./languages/Go.md)

#### Generator options

| Option | Type | Description | Default value |
|---|---|---|---|
| `namingConvention` | Object | Options for naming conventions. | - |
| `namingConvention.type` | Function | A function that returns the format of the type. | _Returns pascal cased name_ |
| `namingConvention.field` | Function | A function that returns the format of the field. | _Returns pascal cased name_ |

#### Render complete model options

| Option | Type | Description | Default value |
|---------------|--------|-----------------------------------------------|---------------|
| `packageName` | string | The package name to generate the models under | [required] |

### [C#](./languages/Csharp.md)

#### Generator options

| Option | Type | Description | Default value |
|---|---|---|---|
| `namingConvention` | Object | Options for naming conventions. | - |
| `namingConvention.type` | Function | A function that returns the format of the type. | _Returns pascal cased name, and ensures that reserved keywords are never rendered__ |
| `namingConvention.property` | Function | A function that returns the format of the property. | _Returns camel cased name, and ensures that names of properties does not clash against reserved keywords_ |

#### Render complete model options

| Option | Type | Description | Default value |
|-------------|--------|--------------------------------------------|---------------|
| `namespace` | string | The namespace to generate the models under | [required] |

## Custom generator

The minimum set of required actions to create a new generator are:
Expand Down
2 changes: 2 additions & 0 deletions docs/languages/TypeScript.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ Check out this [example out for a live demonstration](../../examples/typescript-
## Rendering complete models to a specific module system
In some cases you might need to render the complete models to a specific module system such as ESM and CJS.

You can choose between default exports and named exports when using either, with the `exportType` option.

Check out this [example for a live demonstration how to generate the complete TypeScript models to use ESM module system](../../examples/typescript-use-esm).

Check out this [example for a live demonstration how to generate the complete TypeScript models to use CJS module system](../../examples/typescript-use-cjs).
71 changes: 49 additions & 22 deletions src/generators/typescript/TypeScriptGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {
AbstractGenerator,
import {
AbstractGenerator,
CommonGeneratorOptions,
defaultGeneratorOptions
} from '../AbstractGenerator';
import { CommonModel, CommonInputModel, RenderOutput } from '../../models';
import { CommonModel, CommonInputModel, RenderOutput, PresetWithOptions } from '../../models';
import { TypeHelpers, ModelKind, CommonNamingConvention, CommonNamingConventionImplementation } from '../../helpers';
import { TS_EXPORT_KEYWORD_PRESET } from './presets';
import { TypeScriptPreset, TS_DEFAULT_PRESET } from './TypeScriptPreset';
import { ClassRenderer } from './renderers/ClassRenderer';
import { InterfaceRenderer } from './renderers/InterfaceRenderer';
Expand All @@ -19,6 +20,7 @@ export interface TypeScriptOptions extends CommonGeneratorOptions<TypeScriptPres
}
export interface TypeScriptRenderCompleteModelOptions {
moduleSystem?: 'ESM' | 'CJS';
exportType?: 'default' | 'named';
}

/**
Expand All @@ -39,15 +41,29 @@ export class TypeScriptGenerator extends AbstractGenerator<TypeScriptOptions,Typ
) {
super('TypeScript', TypeScriptGenerator.defaultOptions, options);
}

/**
* Render a complete model result where the model code, library and model dependencies are all bundled appropriately.
*
* @param model
* @param inputModel
* @param options
*/
async renderCompleteModel(model: CommonModel, inputModel: CommonInputModel, options: TypeScriptRenderCompleteModelOptions): Promise<RenderOutput> {
async renderCompleteModel(model: CommonModel, inputModel: CommonInputModel, {moduleSystem = 'ESM', exportType = 'default'}: TypeScriptRenderCompleteModelOptions): Promise<RenderOutput> {
// Shallow copy presets so that we can restore it once we are done
const originalPresets = [...(this.options.presets ? this.options.presets : [])];

// Add preset that adds the `export` keyword if needed
if (
moduleSystem === 'ESM' &&
exportType === 'named' &&
(originalPresets[0] !== TS_EXPORT_KEYWORD_PRESET ||
(Object.prototype.hasOwnProperty.call(originalPresets[0], 'preset') &&
(originalPresets[0] as PresetWithOptions).preset !== TS_EXPORT_KEYWORD_PRESET))
) {
gabormagyar marked this conversation as resolved.
Show resolved Hide resolved
this.options.presets = [TS_EXPORT_KEYWORD_PRESET, ...originalPresets];
}
gabormagyar marked this conversation as resolved.
Show resolved Hide resolved

const outputModel = await this.render(model, inputModel);
let modelDependencies = model.getNearestDependencies();
//Ensure model dependencies have their rendered name
Expand All @@ -58,26 +74,37 @@ export class TypeScriptGenerator extends AbstractGenerator<TypeScriptOptions,Typ
modelDependencies = modelDependencies.filter((dependencyModelName) => {
return dependencyModelName !== outputModel.renderedName;
});

//Create the correct dependency imports
modelDependencies = modelDependencies.map((formattedDependencyModelName) => {
if (options.moduleSystem === 'CJS') {
return `const ${formattedDependencyModelName} = require('./${formattedDependencyModelName}');`;
modelDependencies = modelDependencies.map(
(dependencyName) => {
const dependencyObject =
exportType === 'named' ? `{${dependencyName}}` : dependencyName;

return moduleSystem === 'CJS'
? `const ${dependencyObject} = require('./${dependencyName}');`
: `import ${dependencyObject} from './${dependencyName}';`;
}
return `import ${formattedDependencyModelName} from './${formattedDependencyModelName}';`;
});
);

//Ensure we expose the model correctly, based on the module system
let modelCode = `${outputModel.result}
export default ${outputModel.renderedName};
`;
if (options.moduleSystem === 'CJS') {
modelCode = `${outputModel.result}
module.exports = ${outputModel.renderedName};`;
}
//Ensure we expose the model correctly, based on the module system and export type
const cjsExport =
exportType === 'default'
? `module.exports = ${outputModel.renderedName};`
: `exports.${outputModel.renderedName} = ${outputModel.renderedName};`;
const esmExport =
exportType === 'default'
? `export default ${outputModel.renderedName};\n`
: '';
const modelCode = `${outputModel.result}\n${moduleSystem === 'CJS' ? cjsExport : esmExport}`;

const outputContent = `${[...modelDependencies, ...outputModel.dependencies].join('\n')}

${modelCode}`;

// Restore presets array from original copy
this.options.presets = originalPresets;

return RenderOutput.toRenderOutput({ result: outputContent, renderedName: outputModel.renderedName, dependencies: outputModel.dependencies });
}

Expand All @@ -98,31 +125,31 @@ ${modelCode}`;
}

async renderClass(model: CommonModel, inputModel: CommonInputModel): Promise<RenderOutput> {
const presets = this.getPresets('class');
const presets = this.getPresets('class');
const renderer = new ClassRenderer(this.options, this, presets, model, inputModel);
const result = await renderer.runSelfPreset();
const renderedName = renderer.nameType(model.$id, model);
return RenderOutput.toRenderOutput({result, renderedName, dependencies: renderer.dependencies});
}

async renderInterface(model: CommonModel, inputModel: CommonInputModel): Promise<RenderOutput> {
const presets = this.getPresets('interface');
const presets = this.getPresets('interface');
const renderer = new InterfaceRenderer(this.options, this, presets, model, inputModel);
const result = await renderer.runSelfPreset();
const renderedName = renderer.nameType(model.$id, model);
return RenderOutput.toRenderOutput({result, renderedName, dependencies: renderer.dependencies});
}

async renderEnum(model: CommonModel, inputModel: CommonInputModel): Promise<RenderOutput> {
const presets = this.getPresets('enum');
const presets = this.getPresets('enum');
const renderer = new EnumRenderer(this.options, this, presets, model, inputModel);
const result = await renderer.runSelfPreset();
const renderedName = renderer.nameType(model.$id, model);
return RenderOutput.toRenderOutput({result, renderedName, dependencies: renderer.dependencies});
}

async renderType(model: CommonModel, inputModel: CommonInputModel): Promise<RenderOutput> {
const presets = this.getPresets('type');
const presets = this.getPresets('type');
const renderer = new TypeRenderer(this.options, this, presets, model, inputModel);
const result = await renderer.runSelfPreset();
const renderedName = renderer.nameType(model.$id, model);
Expand Down
39 changes: 39 additions & 0 deletions src/generators/typescript/presets/ExportKeywordPreset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { CommonModel } from '../../../models';
import { TypeScriptPreset } from '../TypeScriptPreset';
import { TypeScriptRenderer } from '../TypeScriptRenderer';

const renderWithExportKeyword = ({
content,
}: {
renderer: TypeScriptRenderer;
content: string;
item: CommonModel;
}): string => `export ${content}`;

/**
* Preset which adds export keyword wherever applicable (named exports)
*
* @implements {TypeScriptPreset}
*/
export const TS_EXPORT_KEYWORD_PRESET: TypeScriptPreset = {
class: {
self({ renderer, model, content }) {
return renderWithExportKeyword({ renderer, content, item: model });
},
},
interface: {
self({ renderer, model, content }) {
return renderWithExportKeyword({ renderer, content, item: model });
},
},
type: {
self({ renderer, model, content }) {
return renderWithExportKeyword({ renderer, content, item: model });
},
},
enum: {
self({ renderer, model, content }) {
return renderWithExportKeyword({ renderer, content, item: model });
},
},
};
1 change: 1 addition & 0 deletions src/generators/typescript/presets/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './CommonPreset';
export * from './ExportKeywordPreset';
77 changes: 37 additions & 40 deletions test/generators/typescript/TypeScriptGenerator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -490,56 +490,53 @@ ${content}`;
expect(arrayModel.result).toMatchSnapshot();
expect(arrayModel.dependencies).toEqual([]);
});

const doc = {
$id: 'Address',
type: 'object',
properties: {
street_name: { type: 'string' },
city: { type: 'string', description: 'City description' },
state: { type: 'string' },
house_number: { type: 'number' },
marriage: { type: 'boolean', description: 'Status if marriage live in given house' },
members: { oneOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }], },
array_type: { type: 'array', items: [{ type: 'string' }, { type: 'number' }] },
other_model: { type: 'object', $id: 'OtherModel', properties: { street_name: { type: 'string' } } },
},
patternProperties: {
'^S(.?*)test&': {
type: 'string'
}
},
required: ['street_name', 'city', 'state', 'house_number', 'array_type'],
};

test('should render models and their dependencies for CJS module system', async () => {
const doc = {
$id: 'Address',
type: 'object',
properties: {
street_name: { type: 'string' },
city: { type: 'string', description: 'City description' },
state: { type: 'string' },
house_number: { type: 'number' },
marriage: { type: 'boolean', description: 'Status if marriage live in given house' },
members: { oneOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }], },
array_type: { type: 'array', items: [{ type: 'string' }, { type: 'number' }] },
other_model: { type: 'object', $id: 'OtherModel', properties: { street_name: { type: 'string' } } },
},
patternProperties: {
'^S(.?*)test&': {
type: 'string'
}
},
required: ['street_name', 'city', 'state', 'house_number', 'array_type'],
};
const models = await generator.generateCompleteModels(doc, {moduleSystem: 'CJS'});
expect(models).toHaveLength(2);
expect(models[0].result).toMatchSnapshot();
expect(models[1].result).toMatchSnapshot();
});

test('should render models and their dependencies for CJS module system with named exports', async () => {
const models = await generator.generateCompleteModels(doc, {moduleSystem: 'CJS', exportType: 'named'});
expect(models).toHaveLength(2);
expect(models[0].result).toMatchSnapshot();
expect(models[1].result).toMatchSnapshot();
});

test('should render models and their dependencies for ESM module system', async () => {
const doc = {
$id: 'Address',
type: 'object',
properties: {
street_name: { type: 'string' },
city: { type: 'string', description: 'City description' },
state: { type: 'string' },
house_number: { type: 'number' },
marriage: { type: 'boolean', description: 'Status if marriage live in given house' },
members: { oneOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }], },
array_type: { type: 'array', items: [{ type: 'string' }, { type: 'number' }] },
other_model: { type: 'object', $id: 'OtherModel', properties: { street_name: { type: 'string' } } },
},
patternProperties: {
'^S(.?*)test&': {
type: 'string'
}
},
required: ['street_name', 'city', 'state', 'house_number', 'array_type'],
};
const models = await generator.generateCompleteModels(doc, {moduleSystem: 'ESM'});
expect(models).toHaveLength(2);
expect(models[0].result).toMatchSnapshot();
expect(models[1].result).toMatchSnapshot();
});

test('should render models and their dependencies for ESM module system with named exports', async () => {
const models = await generator.generateCompleteModels(doc, {moduleSystem: 'ESM', exportType: 'named'});
expect(models).toHaveLength(2);
expect(models[0].result).toMatchSnapshot();
expect(models[1].result).toMatchSnapshot();
});
});
Loading