Skip to content

Commit

Permalink
chore: refactored csharp generator (#770)
Browse files Browse the repository at this point in the history
  • Loading branch information
jonaslagoni authored Jun 26, 2022
1 parent 0a677d4 commit 3f524e3
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 432 deletions.
8 changes: 4 additions & 4 deletions src/generators/csharp/CSharpConstrainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { TypeMapping } from '../../helpers';
import { defaultEnumKeyConstraints, defaultEnumValueConstraints } from './constrainer/EnumConstrainer';
import { defaultModelNameConstraints } from './constrainer/ModelNameConstrainer';
import { defaultPropertyKeyConstraints } from './constrainer/PropertyKeyConstrainer';
import { CSharpRenderer } from './CSharpRenderer';
import { CSharpOptions } from './CSharpGenerator';

export const CSharpDefaultTypeMapping: TypeMapping<CSharpRenderer> = {
export const CSharpDefaultTypeMapping: TypeMapping<CSharpOptions> = {
Object ({constrainedModel}): string {
return constrainedModel.name;
},
Expand Down Expand Up @@ -32,8 +32,8 @@ export const CSharpDefaultTypeMapping: TypeMapping<CSharpRenderer> = {
});
return `(${tupleTypes.join(', ')})`;
},
Array ({constrainedModel, renderer}): string {
if (renderer.options.collectionType && renderer.options.collectionType === 'List') {
Array ({constrainedModel, options}): string {
if (options.collectionType && options.collectionType === 'List') {
return `IEnumerable<${constrainedModel.valueModel.type}>`;
}
return `${constrainedModel.valueModel.type}[]`;
Expand Down
64 changes: 39 additions & 25 deletions src/generators/csharp/CSharpGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,21 @@ import {
CommonGeneratorOptions,
defaultGeneratorOptions
} from '../AbstractGenerator';
import { CommonModel, CommonInputModel, RenderOutput } from '../../models';
import { TypeHelpers, ModelKind, CommonNamingConvention, CommonNamingConventionImplementation, FormatHelpers, TypeMapping, Constraints } from '../../helpers';
import { ConstrainedEnumModel, ConstrainedMetaModel, ConstrainedObjectModel, InputMetaModel, MetaModel, RenderOutput } from '../../models';
import { CommonNamingConvention, CommonNamingConventionImplementation, FormatHelpers, TypeMapping, Constraints, constrainMetaModel, split } from '../../helpers';
import { CSharpPreset, CSHARP_DEFAULT_PRESET } from './CSharpPreset';
import { EnumRenderer } from './renderers/EnumRenderer';
import { ClassRenderer } from './renderers/ClassRenderer';
import { isReservedCSharpKeyword } from './Constants';
import { Logger } from '../../index';
import { CSharpRenderer } from './CSharpRenderer';
import { CSharpDefaultConstraints, CSharpDefaultTypeMapping } from './CSharpConstrainer';

export interface CSharpOptions extends CommonGeneratorOptions<CSharpPreset> {
collectionType?: 'List' | 'Array';
namingConvention?: CommonNamingConvention;
typeMapping: TypeMapping<CSharpRenderer>;
typeMapping: TypeMapping<CSharpOptions>;
constraints: Constraints;
autoImplementedProperties: boolean;
}

export interface CSharpRenderCompleteModelOptions {
Expand All @@ -34,15 +34,37 @@ export class CSharpGenerator extends AbstractGenerator<CSharpOptions, CSharpRend
defaultPreset: CSHARP_DEFAULT_PRESET,
namingConvention: CommonNamingConventionImplementation,
typeMapping: CSharpDefaultTypeMapping,
constraints: CSharpDefaultConstraints
constraints: CSharpDefaultConstraints,
autoImplementedProperties: false
};

constructor(
options: Partial<CSharpOptions> = CSharpGenerator.defaultOptions,
) {
const mergedOptions = {...CSharpGenerator.defaultOptions, ...options};
const realizedOptions = {...CSharpGenerator.defaultOptions, ...options};

super('CSharp', CSharpGenerator.defaultOptions, mergedOptions);
super('CSharp', realizedOptions);
}

splitMetaModel(model: MetaModel): MetaModel[] {
//These are the models that we have separate renderers for
const metaModelsToSplit = {
splitEnum: true,
splitObject: true
};
return split(model, metaModelsToSplit);
}

constrainToMetaModel(model: MetaModel): ConstrainedMetaModel {
return constrainMetaModel(
this.options.typeMapping,
this.options.constraints,
{
metaModel: model,
options: this.options,
constrainedName: '' //This is just a placeholder, it will be constrained within the function
}
);
}

/**
Expand All @@ -54,7 +76,7 @@ export class CSharpGenerator extends AbstractGenerator<CSharpOptions, CSharpRend
* @param inputModel
* @param options used to render the full output
*/
async renderCompleteModel(model: CommonModel, inputModel: CommonInputModel, options: CSharpRenderCompleteModelOptions): Promise<RenderOutput> {
async renderCompleteModel(model: ConstrainedMetaModel, inputModel: InputMetaModel, options: CSharpRenderCompleteModelOptions): Promise<RenderOutput> {
if (isReservedCSharpKeyword(options.namespace)) {
throw new Error(`You cannot use reserved CSharp keyword (${options.namespace}) as namespace, please use another.`);
}
Expand All @@ -71,35 +93,27 @@ ${FormatHelpers.indent(outputDependencies + outputModel.result, this.options.ind
return RenderOutput.toRenderOutput({ result: outputContent, renderedName: outputModel.renderedName, dependencies: outputModel.dependencies });
}

render(model: CommonModel, inputModel: CommonInputModel): Promise<RenderOutput> {
const kind = TypeHelpers.extractKind(model);
switch (kind) {
case ModelKind.UNION:
//We dont support union in Csharp generator, however, if union is an object, we render it as a class.
if (!model.type?.includes('object')) { break; }
return this.renderClass(model, inputModel);
case ModelKind.OBJECT:
render(model: ConstrainedMetaModel, inputModel: InputMetaModel): Promise<RenderOutput> {
if (model instanceof ConstrainedObjectModel) {
return this.renderClass(model, inputModel);
case ModelKind.ENUM:
} else if (model instanceof ConstrainedEnumModel) {
return this.renderEnum(model, inputModel);
}
Logger.warn(`C# generator, cannot generate this type of model, ${model.$id}`);
}
Logger.warn(`C# generator, cannot generate this type of model, ${model.name}`);
return Promise.resolve(RenderOutput.toRenderOutput({ result: '', renderedName: '', dependencies: [] }));
}

async renderEnum(model: CommonModel, inputModel: CommonInputModel): Promise<RenderOutput> {
async renderEnum(model: ConstrainedEnumModel, inputModel: InputMetaModel): Promise<RenderOutput> {
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 });
return RenderOutput.toRenderOutput({ result, renderedName: model.name, dependencies: renderer.dependencies });
}

async renderClass(model: CommonModel, inputModel: CommonInputModel): Promise<RenderOutput> {
async renderClass(model: ConstrainedObjectModel, inputModel: InputMetaModel): Promise<RenderOutput> {
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 });
return RenderOutput.toRenderOutput({ result, renderedName: model.name, dependencies: renderer.dependencies });
}
}
15 changes: 9 additions & 6 deletions src/generators/csharp/CSharpPreset.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
/* eslint-disable @typescript-eslint/ban-types */
import { Preset, EnumPreset, ClassPreset, PresetArgs, PropertyArgs } from '../../models';
import { Preset, EnumPreset, ClassPreset, PresetArgs, PropertyArgs, ConstrainedObjectModel } from '../../models';
import { CSharpOptions } from './CSharpGenerator';
import { ClassRenderer, CSHARP_DEFAULT_CLASS_PRESET } from './renderers/ClassRenderer';
import { CSHARP_DEFAULT_ENUM_PRESET, EnumRenderer } from './renderers/EnumRenderer';

// Our class preset uses custom `accessor` hook to craft getter and setters.
export interface CsharpClassPreset extends ClassPreset<ClassRenderer> {
accessor?: (args: PresetArgs<ClassRenderer, any> & PropertyArgs) => Promise<string> | string;
export interface CsharpClassPreset<O> extends ClassPreset<ClassRenderer, O> {
accessor?: (args: PresetArgs<ClassRenderer, O, ConstrainedObjectModel> & PropertyArgs) => Promise<string> | string;
}

export type CSharpPreset<O extends object = any> = Preset<{
class: CsharpClassPreset;
export type ClassPresetType<O> = CsharpClassPreset<O>;
export type EnumPresetType<O> = EnumPreset<EnumRenderer, O>;

export type CSharpPreset<O = CSharpOptions> = Preset<{
class: CsharpClassPreset<O>;
enum: EnumPreset<EnumRenderer, O>
}>;

Expand Down
99 changes: 4 additions & 95 deletions src/generators/csharp/CSharpRenderer.ts
Original file line number Diff line number Diff line change
@@ -1,117 +1,26 @@
import { AbstractRenderer } from '../AbstractRenderer';
import { CSharpGenerator, CSharpOptions } from './CSharpGenerator';
import { CommonModel, CommonInputModel, Preset, PropertyType } from '../../models';
import { Preset, ConstrainedMetaModel, InputMetaModel } from '../../models';
import { FormatHelpers } from '../../helpers/FormatHelpers';
import { isReservedCSharpKeyword } from './Constants';

/**
* Common renderer for CSharp types
*
* @extends AbstractRenderer
*/
export abstract class CSharpRenderer extends AbstractRenderer<CSharpOptions> {
export abstract class CSharpRenderer<RendererModelType extends ConstrainedMetaModel> extends AbstractRenderer<CSharpOptions, CSharpGenerator, RendererModelType> {
constructor(
options: CSharpOptions,
generator: CSharpGenerator,
presets: Array<[Preset, unknown]>,
model: CommonModel,
inputModel: CommonInputModel,
model: RendererModelType,
inputModel: InputMetaModel,
) {
super(options, generator, presets, model, inputModel);
}

/**
* Renders the name of a type based on provided generator option naming convention type function.
*
* This is used to render names of models and then later used if that class is referenced from other models.
*
* @param name
* @param model
*/
nameType(name: string | undefined, model?: CommonModel): string {
return this.options?.namingConvention?.type
? this.options.namingConvention.type(name, { model: model || this.model, inputModel: this.inputModel, reservedKeywordCallback: isReservedCSharpKeyword })
: name || '';
}

/**
* Renders the name of a property based on provided generator option naming convention property function.
*
* @param propertyName
* @param property
*/
nameProperty(propertyName: string | undefined, property?: CommonModel): string {
return this.options?.namingConvention?.property
? this.options.namingConvention.property(propertyName, { model: this.model, inputModel: this.inputModel, property, reservedKeywordCallback: isReservedCSharpKeyword })
: propertyName || '';
}

runPropertyPreset(propertyName: string, property: CommonModel, options: any, type: PropertyType = PropertyType.property): Promise<string> {
return this.runPreset('property', { propertyName, property, options, type });
}

renderType(model: CommonModel): string {
if (model.$ref !== undefined) {
return this.nameType(model.$ref);
}

if (Array.isArray(model.type)) {
return model.type.length > 1 ? 'dynamic' : `${this.toCSharpType(model.type[0], model)}`;
}

return this.toCSharpType(model.type, model);
}

renderComments(lines: string | string[]): string {
lines = FormatHelpers.breakLines(lines);
return lines.map(line => `// ${line}`).join('\n');
}

toCSharpType(type: string | undefined, model: CommonModel): string {
switch (type) {
case 'integer':
case 'int32':
return 'int?';
case 'long':
case 'int64':
return 'long?';
case 'boolean':
return 'bool?';
case 'date':
case 'time':
case 'dateTime':
case 'date-time':
return 'System.DateTime?';
case 'string':
case 'password':
case 'byte':
return 'string';
case 'float':
return 'float?';
case 'double':
case 'number':
return 'double?';
case 'binary':
return 'byte[]';
case 'object':
return 'object';
case 'array': {
let arrayItemModel = model.items;
if (Array.isArray(model.items)) {
arrayItemModel = model.items.reduce((prevModel, currentModel) => {
return CommonModel.mergeCommonModels(CommonModel.toCommonModel(prevModel), CommonModel.toCommonModel(currentModel), {});
});
if (model.additionalItems !== undefined) {
arrayItemModel = CommonModel.mergeCommonModels(arrayItemModel, model.additionalItems, {});
}
}
const newType = arrayItemModel ? this.renderType(arrayItemModel as CommonModel) : 'dynamic';
if (this.options.collectionType && this.options.collectionType === 'List') {
return `IEnumerable<${newType}>`;
}
return `${newType}[]`;
}
default: return 'dynamic';
}
}
}
36 changes: 11 additions & 25 deletions src/generators/csharp/presets/CommonPreset.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { CSharpRenderer } from '../CSharpRenderer';
import { CSharpPreset } from '../CSharpPreset';

import { getUniquePropertyName, FormatHelpers, DefaultPropertyNames } from '../../../helpers';
import { CommonModel } from '../../../models';
import { FormatHelpers } from '../../../helpers';
import { ConstrainedObjectModel } from '../../../models';

export interface CSharpCommonPresetOptions {
equal: boolean;
Expand All @@ -13,24 +12,17 @@ export interface CSharpCommonPresetOptions {
* Render `equal` function based on model's properties
*/
function renderEqual({ renderer, model }: {
renderer: CSharpRenderer,
model: CommonModel,
renderer: CSharpRenderer<any>,
model: ConstrainedObjectModel,
}): string {
const formattedModelName = renderer.nameType(model.$id);
const properties = model.properties || {};
const propertyKeys = Object.keys(properties);
if (model.additionalProperties) {
propertyKeys.push(getUniquePropertyName(model, DefaultPropertyNames.additionalProperties));
}
for (const [pattern, patternModel] of Object.entries(model.patternProperties || {})) {
propertyKeys.push(getUniquePropertyName(patternModel, `${pattern}${DefaultPropertyNames.patternProperties}`));
}
let equalProperties = propertyKeys.map(prop => {
const accessorMethodProp = FormatHelpers.upperFirst(renderer.nameProperty(prop));
let equalProperties = propertyKeys.map(propertyName => {
const accessorMethodProp = FormatHelpers.upperFirst(propertyName);
return `${accessorMethodProp} == model.${accessorMethodProp}`;
}).join(' && \n');
equalProperties = `return ${equalProperties !== '' ? equalProperties : 'true'}`;
const methodContent = `if(obj is ${formattedModelName} model)
const methodContent = `if(obj is ${model.name} model)
{
${renderer.indent('if(ReferenceEquals(this, model)) { return true; }')}
${renderer.indent(equalProperties)};
Expand All @@ -48,18 +40,12 @@ ${renderer.indent(methodContent)}
* Render `hashCode` function based on model's properties
*/
function renderHashCode({ renderer, model }: {
renderer: CSharpRenderer,
model: CommonModel,
renderer: CSharpRenderer<any>,
model: ConstrainedObjectModel,
}): string {
const properties = model.properties || {};
const propertyKeys = Object.keys(properties);
if (model.additionalProperties) {
propertyKeys.push(getUniquePropertyName(model, DefaultPropertyNames.additionalProperties));
}
for (const [pattern, patternModel] of Object.entries(model.patternProperties || {})) {
propertyKeys.push(getUniquePropertyName(patternModel, `${pattern}${DefaultPropertyNames.patternProperties}`));
}
const hashProperties = propertyKeys.map(prop => `hash.Add(${FormatHelpers.upperFirst(renderer.nameProperty(prop))});`).join('\n');
const hashProperties = propertyKeys.map(propertyName => `hash.Add(${FormatHelpers.upperFirst(propertyName)});`).join('\n');

return `public override int GetHashCode()
{
Expand All @@ -81,7 +67,7 @@ export const CSHARP_COMMON_PRESET: CSharpPreset<CSharpCommonPresetOptions> = {
const blocks: string[] = [];

if (options.equal === undefined || options.equal === true) { blocks.push(renderEqual({ renderer, model })); }
if (options.hashCode === undefined || options.hashCode === true) {
if (options.hash === undefined || options.hash === true) {
renderer.addDependency('using System;');
blocks.push(renderHashCode({ renderer, model }));
}
Expand Down
Loading

0 comments on commit 3f524e3

Please sign in to comment.