Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion src/generators/typescript/TypeScriptGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { ClassRenderer } from './renderers/ClassRenderer';
import { InterfaceRenderer } from './renderers/InterfaceRenderer';
import { EnumRenderer } from './renderers/EnumRenderer';
import { TypeRenderer } from './renderers/TypeRenderer';
import { ConstValueRenderer } from './renderers/ConstValueRenderer';
import {
TypeScriptDefaultConstraints,
TypeScriptDefaultTypeMapping
Expand Down Expand Up @@ -301,6 +302,15 @@ ${modelCode}`;
...options
});
const dependencyManagerToUse = this.getDependencyManager(optionsToUse);

// Render const values first (if any exist)
const constValuesOutput = await this.renderConstValue(
model,
inputModel,
optionsToUse
);

// Render the class
const presets = this.getPresets('class');
const renderer = new ClassRenderer(
optionsToUse,
Expand All @@ -310,14 +320,53 @@ ${modelCode}`;
inputModel,
dependencyManagerToUse
);
const result = await renderer.runSelfPreset();
const classResult = await renderer.runSelfPreset();

// Combine const values and class
const result = constValuesOutput
? `${constValuesOutput.result}\n\n${classResult}`
: classResult;

return RenderOutput.toRenderOutput({
result,
renderedName: model.name,
dependencies: dependencyManagerToUse.dependencies
});
}

async renderConstValue(
model: ConstrainedObjectModel,
inputModel: InputMetaModel,
options?: DeepPartial<TypeScriptOptions>
): Promise<RenderOutput | null> {
const optionsToUse = TypeScriptGenerator.getOptions({
...this.options,
...options
});
const dependencyManagerToUse = this.getDependencyManager(optionsToUse);
const presets = this.getPresets('constValue');
const renderer = new ConstValueRenderer(
optionsToUse,
this,
presets,
model,
inputModel,
dependencyManagerToUse
);

// Check if there are any const properties to render
if (renderer.getConstProperties().length === 0) {
return null;
}

const result = await renderer.runSelfPreset();
return RenderOutput.toRenderOutput({
result,
renderedName: `${model.name}Constants`,
dependencies: dependencyManagerToUse.dependencies
});
}

async renderInterface(
model: ConstrainedObjectModel,
inputModel: InputMetaModel,
Expand Down
12 changes: 10 additions & 2 deletions src/generators/typescript/TypeScriptPreset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {
InterfacePreset,
EnumPreset,
CommonPreset,
ConstrainedMetaModel
ConstrainedMetaModel,
ConstValuePreset
} from '../../models';
import {
ClassRenderer,
Expand All @@ -16,6 +17,10 @@ import {
} from './renderers/InterfaceRenderer';
import { EnumRenderer, TS_DEFAULT_ENUM_PRESET } from './renderers/EnumRenderer';
import { TypeRenderer, TS_DEFAULT_TYPE_PRESET } from './renderers/TypeRenderer';
import {
ConstValueRenderer,
TS_DEFAULT_CONST_VALUE_PRESET
} from './renderers/ConstValueRenderer';
import { TypeScriptOptions } from './TypeScriptGenerator';

export type ClassPresetType<O> = ClassPreset<ClassRenderer, O>;
Expand All @@ -26,17 +31,20 @@ export type TypePresetType<O> = CommonPreset<
O,
ConstrainedMetaModel
>;
export type ConstValuePresetType<O> = ConstValuePreset<ConstValueRenderer, O>;

export type TypeScriptPreset<O = any> = Preset<{
class: ClassPresetType<O>;
interface: InterfacePresetType<O>;
enum: EnumPresetType<O>;
type: TypePresetType<O>;
constValue: ConstValuePresetType<O>;
}>;

export const TS_DEFAULT_PRESET: TypeScriptPreset<TypeScriptOptions> = {
class: TS_DEFAULT_CLASS_PRESET,
interface: TS_DEFAULT_INTERFACE_PRESET,
enum: TS_DEFAULT_ENUM_PRESET,
type: TS_DEFAULT_TYPE_PRESET
type: TS_DEFAULT_TYPE_PRESET,
constValue: TS_DEFAULT_CONST_VALUE_PRESET
};
4 changes: 4 additions & 0 deletions src/generators/typescript/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ export * from './TypeScriptFileGenerator';
export { TS_DEFAULT_PRESET } from './TypeScriptPreset';
export type { TypeScriptPreset } from './TypeScriptPreset';
export * from './presets';
export {
ConstValueRenderer,
TS_DEFAULT_CONST_VALUE_PRESET
} from './renderers/ConstValueRenderer';

export {
defaultEnumKeyConstraints as typeScriptDefaultEnumKeyConstraints,
Expand Down
29 changes: 16 additions & 13 deletions src/generators/typescript/renderers/ClassRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export const TS_DEFAULT_CLASS_PRESET: ClassPresetType<TypeScriptOptions> = {
continue;
}
assignments.push(`this._${propertyName} = input.${propertyName};`);
ctorProperties.push(renderer.renderProperty(property).replace(';', ','));
ctorProperties.push(renderer.renderProperty(property).replaceAll(';', ','));
}

return `constructor(input: {
Expand All @@ -76,24 +76,27 @@ ${renderer.indent(renderer.renderBlock(assignments))}
return `private _${renderer.renderProperty(property)}`;
},
getter({ property }): string {
return `get ${property.propertyName}(): ${
property.property.options.const?.value
? property.property.options.const.value
: property.property.type
}${property.required === false ? ' | undefined' : ''} { return this._${
property.propertyName
}; }`;
const constVal = property.property.options.const?.value;
// Use JSON.stringify for non-string values to avoid [object Object] issues
let returnType: string;
if (constVal === undefined) {
returnType = property.property.type;
} else if (typeof constVal === 'string') {
returnType = constVal;
} else {
returnType = JSON.stringify(constVal);
}
const optionalSuffix = property.required === false ? ' | undefined' : '';
return `get ${property.propertyName}(): ${returnType}${optionalSuffix} { return this._${property.propertyName}; }`;
},
setter({ property }): string {
// if const value exists we should not render a setter
if (property.property.options.const?.value) {
return '';
}

return `set ${property.propertyName}(${property.propertyName}: ${
property.property.type
}${property.required === false ? ' | undefined' : ''}) { this._${
property.propertyName
} = ${property.propertyName}; }`;
return `set ${property.propertyName}(${property.propertyName}: ${property.property.type
}${property.required === false ? ' | undefined' : ''}) { this._${property.propertyName
} = ${property.propertyName}; }`;
}
};
80 changes: 80 additions & 0 deletions src/generators/typescript/renderers/ConstValueRenderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { TypeScriptRenderer } from '../TypeScriptRenderer';
import {
ConstrainedObjectModel,
ConstrainedObjectPropertyModel
} from '../../../models';
import { ConstValuePresetType } from '../TypeScriptPreset';
import { TypeScriptOptions } from '../TypeScriptGenerator';

/**
* Renderer for TypeScript's exported const values
*
* This renderer generates exported const declarations for properties
* that have const values defined in the schema.
*
* @extends TypeScriptRenderer
*/
export class ConstValueRenderer extends TypeScriptRenderer<ConstrainedObjectModel> {
async defaultSelf(): Promise<string> {
const content = [
await this.renderItems(),
await this.runAdditionalContentPreset()
];

return this.renderBlock(content);
}

/**
* Get all properties that have const values defined
*/
getConstProperties(): ConstrainedObjectPropertyModel[] {
return Object.values(this.model.properties).filter(
(prop) => prop.property.options.const?.value !== undefined
);
}

/**
* Converts a property name from camelCase to UPPER_SNAKE_CASE
*/
toConstName(propertyName: string): string {
return propertyName.replaceAll(/([a-z])([A-Z])/g, '$1_$2').toUpperCase();
}

async renderItems(): Promise<string> {
const constProperties = this.getConstProperties();
const items: string[] = [];

for (const property of constProperties) {
const renderedItem = await this.runItemPreset(property);
if (renderedItem) {
items.push(renderedItem);
}
}

return this.renderBlock(items);
}

runItemPreset(property: ConstrainedObjectPropertyModel): Promise<string> {
return this.runPreset('item', { property });
}
}

export const TS_DEFAULT_CONST_VALUE_PRESET: ConstValuePresetType<TypeScriptOptions> =
{
self({ renderer }) {
return renderer.defaultSelf();
},
item({ property, renderer }): string {
const constValue = property.property.options.const?.value;
if (constValue === undefined) {
return '';
}

const constName = renderer.toConstName(property.propertyName);
// Use proper formatting based on value type
const formattedValue =
typeof constValue === 'string' ? constValue : JSON.stringify(constValue);

return `export const ${constName} = ${formattedValue};`;
}
};
7 changes: 7 additions & 0 deletions src/models/Preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,13 @@ export interface EnumPreset<R extends AbstractRenderer, O>
item?: (args: PresetArgs<R, O, ConstrainedEnumModel> & EnumArgs) => string;
}

export interface ConstValuePreset<R extends AbstractRenderer, O>
extends CommonPreset<R, O, ConstrainedObjectModel> {
item?: (
args: PresetArgs<R, O, ConstrainedObjectModel> & PropertyArgs
) => string;
}

export type Preset<
C extends Record<string, CommonPreset<any, any, any>> = any
> = Partial<C>;
Expand Down
Loading