From 3f524e3669a184ca3767c19e7dd9fa128bd625af Mon Sep 17 00:00:00 2001 From: Jonas Lagoni Date: Sun, 26 Jun 2022 11:39:18 +0200 Subject: [PATCH] chore: refactored csharp generator (#770) --- src/generators/csharp/CSharpConstrainer.ts | 8 +- src/generators/csharp/CSharpGenerator.ts | 64 +++-- src/generators/csharp/CSharpPreset.ts | 15 +- src/generators/csharp/CSharpRenderer.ts | 99 +------- src/generators/csharp/presets/CommonPreset.ts | 36 +-- .../csharp/presets/JsonSerializerPreset.ts | 221 ++++++------------ .../csharp/renderers/ClassRenderer.ts | 102 +++----- .../csharp/renderers/EnumRenderer.ts | 72 ++---- 8 files changed, 185 insertions(+), 432 deletions(-) diff --git a/src/generators/csharp/CSharpConstrainer.ts b/src/generators/csharp/CSharpConstrainer.ts index 47738b33de..bfd33c5e34 100644 --- a/src/generators/csharp/CSharpConstrainer.ts +++ b/src/generators/csharp/CSharpConstrainer.ts @@ -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 = { +export const CSharpDefaultTypeMapping: TypeMapping = { Object ({constrainedModel}): string { return constrainedModel.name; }, @@ -32,8 +32,8 @@ export const CSharpDefaultTypeMapping: TypeMapping = { }); 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}[]`; diff --git a/src/generators/csharp/CSharpGenerator.ts b/src/generators/csharp/CSharpGenerator.ts index ed97b7b2a9..37bc9c7039 100644 --- a/src/generators/csharp/CSharpGenerator.ts +++ b/src/generators/csharp/CSharpGenerator.ts @@ -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 { collectionType?: 'List' | 'Array'; namingConvention?: CommonNamingConvention; - typeMapping: TypeMapping; + typeMapping: TypeMapping; constraints: Constraints; + autoImplementedProperties: boolean; } export interface CSharpRenderCompleteModelOptions { @@ -34,15 +34,37 @@ export class CSharpGenerator extends AbstractGenerator = 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 + } + ); } /** @@ -54,7 +76,7 @@ export class CSharpGenerator extends AbstractGenerator { + async renderCompleteModel(model: ConstrainedMetaModel, inputModel: InputMetaModel, options: CSharpRenderCompleteModelOptions): Promise { if (isReservedCSharpKeyword(options.namespace)) { throw new Error(`You cannot use reserved CSharp keyword (${options.namespace}) as namespace, please use another.`); } @@ -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 { - 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 { + 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 { + async renderEnum(model: ConstrainedEnumModel, inputModel: InputMetaModel): Promise { 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 { + async renderClass(model: ConstrainedObjectModel, inputModel: InputMetaModel): Promise { 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 }); } } diff --git a/src/generators/csharp/CSharpPreset.ts b/src/generators/csharp/CSharpPreset.ts index 8528ebb24e..60eaa9c977 100644 --- a/src/generators/csharp/CSharpPreset.ts +++ b/src/generators/csharp/CSharpPreset.ts @@ -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 { - accessor?: (args: PresetArgs & PropertyArgs) => Promise | string; +export interface CsharpClassPreset extends ClassPreset { + accessor?: (args: PresetArgs & PropertyArgs) => Promise | string; } -export type CSharpPreset = Preset<{ - class: CsharpClassPreset; +export type ClassPresetType = CsharpClassPreset; +export type EnumPresetType = EnumPreset; + +export type CSharpPreset = Preset<{ + class: CsharpClassPreset; enum: EnumPreset }>; diff --git a/src/generators/csharp/CSharpRenderer.ts b/src/generators/csharp/CSharpRenderer.ts index 948f3eec90..7074e5b199 100644 --- a/src/generators/csharp/CSharpRenderer.ts +++ b/src/generators/csharp/CSharpRenderer.ts @@ -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 { +export abstract class CSharpRenderer extends AbstractRenderer { 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 { - 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'; - } - } } diff --git a/src/generators/csharp/presets/CommonPreset.ts b/src/generators/csharp/presets/CommonPreset.ts index 1ba550e6b9..2790086621 100644 --- a/src/generators/csharp/presets/CommonPreset.ts +++ b/src/generators/csharp/presets/CommonPreset.ts @@ -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; @@ -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, + 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)}; @@ -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, + 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() { @@ -81,7 +67,7 @@ export const CSHARP_COMMON_PRESET: CSharpPreset = { 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 })); } diff --git a/src/generators/csharp/presets/JsonSerializerPreset.ts b/src/generators/csharp/presets/JsonSerializerPreset.ts index d913051ae0..9a803bb2ad 100644 --- a/src/generators/csharp/presets/JsonSerializerPreset.ts +++ b/src/generators/csharp/presets/JsonSerializerPreset.ts @@ -1,118 +1,72 @@ import { CSharpRenderer } from '../CSharpRenderer'; import { CSharpPreset } from '../CSharpPreset'; -import { getUniquePropertyName, DefaultPropertyNames, FormatHelpers, TypeHelpers, ModelKind } from '../../../helpers'; -import { CommonInputModel, CommonModel } from '../../../models'; +import { ConstrainedDictionaryModel, ConstrainedEnumModel, ConstrainedObjectModel, ConstrainedObjectPropertyModel, ConstrainedReferenceModel } from '../../../models'; -function renderSerializeProperty(modelInstanceVariable: string, model: CommonModel, inputModel: CommonInputModel) { +function renderSerializeProperty(modelInstanceVariable: string, model: ConstrainedObjectPropertyModel) { let value = modelInstanceVariable; - if (model.$ref) { - const resolvedModel = inputModel.models[model.$ref]; - const propertyModelKind = TypeHelpers.extractKind(resolvedModel); - //Referenced enums is the only one who need custom serialization - if (propertyModelKind === ModelKind.ENUM) { - value = `${value}.GetValue()`; - } + //Special case where a referenced enum model need to be accessed + if (model.property instanceof ConstrainedReferenceModel && model.property.ref instanceof ConstrainedEnumModel) { + value = `${model.property.type}.GetValue()`; } return `JsonSerializer.Serialize(writer, ${value});`; } -function renderSerializeAdditionalProperties(model: CommonModel, renderer: CSharpRenderer, inputModel: CommonInputModel) { - const serializeAdditionalProperties = ''; - if (model.additionalProperties !== undefined) { - let additionalPropertyName = getUniquePropertyName(model, DefaultPropertyNames.additionalProperties); - additionalPropertyName = FormatHelpers.upperFirst(renderer.nameProperty(additionalPropertyName, model.additionalProperties)); - return `// Unwrap additional properties in object -if (value.AdditionalProperties != null) { - foreach (var additionalProperty in value.${additionalPropertyName}) +function renderSerializeProperties(model: ConstrainedObjectModel) { + let serializeProperties = ''; + if (model.properties !== undefined) { + for (const [propertyName, propertyModel] of Object.entries(model.properties)) { + const modelInstanceVariable = `value.${propertyName}`; + if (propertyModel.property instanceof ConstrainedDictionaryModel && propertyModel.property.serializationType === 'unwrap') { + serializeProperties += `// Unwrap dictionary properties +if (${modelInstanceVariable} != null) { + foreach (var unwrappedProperty in ${modelInstanceVariable}) { - //Ignore any additional properties which might already be part of the core properties - if (properties.Any(prop => prop.Name == additionalProperty.Key)) + // Ignore any unwrapped properties which might already be part of the core properties + if (properties.Any(prop => prop.Name == unwrappedProperty.Key)) { continue; } - // write property name and let the serializer serialize the value itself - writer.WritePropertyName(additionalProperty.Key); - ${renderSerializeProperty('additionalProperty.Value', model.additionalProperties, inputModel)} + // Write property name and let the serializer serialize the value itself + writer.WritePropertyName(unwrappedProperty.Key); + ${renderSerializeProperty('unwrappedProperty.Value', propertyModel)} } }`; - } - return serializeAdditionalProperties; -} - -function renderSerializeProperties(model: CommonModel, renderer: CSharpRenderer, inputModel: CommonInputModel) { - let serializeProperties = ''; - if (model.properties !== undefined) { - for (const [propertyName, propertyModel] of Object.entries(model.properties)) { - const formattedPropertyName = FormatHelpers.upperFirst(renderer.nameProperty(propertyName, propertyModel)); - const modelInstanceVariable = `value.${formattedPropertyName}`; + } serializeProperties += `if(${modelInstanceVariable} != null) { // write property name and let the serializer serialize the value itself writer.WritePropertyName("${propertyName}"); - ${renderSerializeProperty(modelInstanceVariable, propertyModel, inputModel)} + ${renderSerializeProperty(modelInstanceVariable, propertyModel)} }\n`; } } return serializeProperties; } -function renderSerializePatternProperties(model: CommonModel, renderer: CSharpRenderer, inputModel: CommonInputModel) { - let serializePatternProperties = ''; - if (model.patternProperties !== undefined) { - for (const [pattern, patternModel] of Object.entries(model.patternProperties)) { - let patternPropertyName = getUniquePropertyName(model, `${pattern}${DefaultPropertyNames.patternProperties}`); - patternPropertyName = FormatHelpers.upperFirst(renderer.nameProperty(patternPropertyName, patternModel)); - serializePatternProperties += `// Unwrap pattern properties in object -if(value.${patternPropertyName} != null) { - foreach (var patternProp in value.${patternPropertyName}) - { - //Ignore any pattern properties which might already be part of the core properties - if (properties.Any(prop => prop.Name == patternProp.Key)) - { - continue; - } - // write property name and let the serializer serialize the value itself - writer.WritePropertyName(patternProp.Key); - ${renderSerializeProperty('patternProp.Value', patternModel, inputModel)} - } -}`; - } - } - return serializePatternProperties; -} -function renderPropertiesList(model: CommonModel, renderer: CSharpRenderer) { - const propertyFilter: string[] = []; - if (model.additionalProperties !== undefined) { - let additionalPropertyName = getUniquePropertyName(model, DefaultPropertyNames.additionalProperties); - additionalPropertyName = FormatHelpers.upperFirst(renderer.nameProperty(additionalPropertyName, model.additionalProperties)); - propertyFilter.push(`prop.Name != "${additionalPropertyName}"`); - } - for (const [pattern, patternModel] of Object.entries(model.patternProperties || {})) { - let patternPropertyName = getUniquePropertyName(model, `${pattern}${DefaultPropertyNames.patternProperties}`); - patternPropertyName = FormatHelpers.upperFirst(renderer.nameProperty(patternPropertyName, patternModel)); - propertyFilter.push(`prop.Name != "${patternPropertyName}"`); - } +function renderPropertiesList(model: ConstrainedObjectModel, renderer: CSharpRenderer) { + const unwrappedDictionaryProperties = Object.values(model.properties).filter((model) => { + return model.property instanceof ConstrainedDictionaryModel && model.property.serializationType === 'unwrap'; + }).map((value) => { + return value.propertyName; + }); + let propertiesList = 'var properties = value.GetType().GetProperties();'; - if (propertyFilter.length > 0) { + if (unwrappedDictionaryProperties.length > 0) { renderer.addDependency('using System.Linq;'); - propertiesList = `var properties = value.GetType().GetProperties().Where(prop => ${propertyFilter.join(' && ')});`; + propertiesList = `var properties = value.GetType().GetProperties().Where(prop => ${unwrappedDictionaryProperties.join(' && ')});`; } return propertiesList; } /** * Render `serialize` function based on model */ -function renderSerialize({ renderer, model, inputModel }: { - renderer: CSharpRenderer, - model: CommonModel, - inputModel: CommonInputModel +function renderSerialize({ renderer, model }: { + renderer: CSharpRenderer, + model: ConstrainedObjectModel }): string { - const formattedModelName = renderer.nameType(model.$id); - const serializeProperties = renderSerializeProperties(model, renderer, inputModel); - const serializePatternProperties = renderSerializePatternProperties(model, renderer, inputModel); - const serializeAdditionalProperties = renderSerializeAdditionalProperties(model, renderer, inputModel); + const serializeProperties = renderSerializeProperties(model); const propertiesList = renderPropertiesList(model, renderer); - return `public override void Write(Utf8JsonWriter writer, ${formattedModelName} value, JsonSerializerOptions options) + return `public override void Write(Utf8JsonWriter writer, ${model.name} value, JsonSerializerOptions options) { if (value == null) { @@ -125,94 +79,56 @@ function renderSerialize({ renderer, model, inputModel }: { ${renderer.indent(serializeProperties)} -${renderer.indent(serializePatternProperties)} - -${renderer.indent(serializeAdditionalProperties)} - writer.WriteEndObject(); }`; } -function renderDeserializeProperty(type: string, model: CommonModel, inputModel: CommonInputModel) { - if (model.$ref) { - const resolvedModel = inputModel.models[model.$ref]; - const propertyModelKind = TypeHelpers.extractKind(resolvedModel); - //Referenced enums is the only one who need custom serialization - if (propertyModelKind === ModelKind.ENUM) { - return `${type}Extension.To${type}(JsonSerializer.Deserialize(ref reader, options))`; - } +function renderDeserializeProperty(model: ConstrainedObjectPropertyModel) { + //Referenced enums is the only one who need custom serialization + if (model.property instanceof ConstrainedReferenceModel && + model.property.ref instanceof ConstrainedEnumModel) { + return `${model.property.type}Extension.To${model.property.type}(JsonSerializer.Deserialize(ref reader, options))`; } - return `JsonSerializer.Deserialize<${type}>(ref reader, options)`; + return `JsonSerializer.Deserialize<${model.property.type}>(ref reader, options)`; } -function renderDeserializeProperties(model: CommonModel, renderer: CSharpRenderer, inputModel: CommonInputModel) { +function renderDeserializeProperties(model: ConstrainedObjectModel) { const propertyEntries = Object.entries(model.properties || {}); const deserializeProperties = propertyEntries.map(([prop, propModel]) => { - const formattedPropertyName = FormatHelpers.upperFirst(renderer.nameProperty(prop, propModel)); - const propertyModelType = renderer.renderType(propModel); + //Unwrapped dictionary properties, need to be unwrapped in JSON + if (propModel.property instanceof ConstrainedDictionaryModel && + propModel.property.serializationType === 'unwrap') { + return `if(instance.${prop} == null) { instance.${prop} = new Dictionary<${propModel.property.key.type}, ${propModel.property.key.type}>(); } + var deserializedValue = ${renderDeserializeProperty(propModel)}; + instance.${prop}.Add(propertyName, deserializedValue); + continue;`; + } return `if (propertyName == "${prop}") -{ - var value = ${renderDeserializeProperty(propertyModelType, propModel, inputModel)}; - instance.${formattedPropertyName} = value; - continue; -}`; + { + var value = ${renderDeserializeProperty(propModel)}; + instance.${prop} = value; + continue; + }`; }); return deserializeProperties.join('\n'); } -function renderDeserializePatternProperties(model: CommonModel, renderer: CSharpRenderer, inputModel: CommonInputModel) { - if (model.patternProperties === undefined) { - return ''; - } - const patternProperties = Object.entries(model.patternProperties).map(([pattern, patternModel]) => { - let patternPropertyName = getUniquePropertyName(model, `${pattern}${DefaultPropertyNames.patternProperties}`); - patternPropertyName = FormatHelpers.upperFirst(renderer.nameProperty(patternPropertyName, patternModel)); - const patternPropertyType = renderer.renderType(patternModel); - return `if(instance.${patternPropertyName} == null) { instance.${patternPropertyName} = new Dictionary(); } -var match = Regex.Match(propertyName, @"${pattern}"); -if (match.Success) -{ - var deserializedValue = ${renderDeserializeProperty(patternPropertyType, patternModel, inputModel)}; - instance.${patternPropertyName}.Add(propertyName, deserializedValue); - continue; -}`; - }); - return patternProperties.join('\n'); -} - -function renderDeserializeAdditionalProperties(model: CommonModel, renderer: CSharpRenderer, inputModel: CommonInputModel) { - if (model.additionalProperties === undefined) { - return ''; - } - let additionalPropertyName = getUniquePropertyName(model, DefaultPropertyNames.additionalProperties); - additionalPropertyName = FormatHelpers.upperFirst(renderer.nameProperty(additionalPropertyName, model.additionalProperties)); - const additionalPropertyType = renderer.renderType(model.additionalProperties); - return `if(instance.${additionalPropertyName} == null) { instance.${additionalPropertyName} = new Dictionary(); } -var deserializedValue = ${renderDeserializeProperty(additionalPropertyType, model.additionalProperties, inputModel)}; -instance.${additionalPropertyName}.Add(propertyName, deserializedValue); -continue;`; -} - /** * Render `deserialize` function based on model */ -function renderDeserialize({ renderer, model, inputModel }: { - renderer: CSharpRenderer, - model: CommonModel, - inputModel: CommonInputModel +function renderDeserialize({ renderer, model }: { + renderer: CSharpRenderer, + model: ConstrainedObjectModel }): string { - const formattedModelName = renderer.nameType(model.$id); - const deserializeProperties = renderDeserializeProperties(model, renderer, inputModel); - const deserializePatternProperties = renderDeserializePatternProperties(model, renderer, inputModel); - const deserializeAdditionalProperties = renderDeserializeAdditionalProperties(model, renderer, inputModel); - return `public override ${formattedModelName} Read(ref Utf8JsonReader reader, System.Type typeToConvert, JsonSerializerOptions options) + const deserializeProperties = renderDeserializeProperties(model); + return `public override ${model.name} Read(ref Utf8JsonReader reader, System.Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType != JsonTokenType.StartObject) { throw new JsonException(); } - var instance = new ${formattedModelName}(); + var instance = new ${model.name}(); while (reader.Read()) { @@ -229,10 +145,6 @@ function renderDeserialize({ renderer, model, inputModel }: { string propertyName = reader.GetString(); ${renderer.indent(deserializeProperties, 4)} - -${renderer.indent(deserializePatternProperties, 4)} - -${renderer.indent(deserializeAdditionalProperties, 4)} } throw new JsonException(); @@ -246,19 +158,18 @@ ${renderer.indent(deserializeAdditionalProperties, 4)} */ export const CSHARP_JSON_SERIALIZER_PRESET: CSharpPreset = { class: { - self({ renderer, model, content, inputModel}) { + self({ renderer, model, content}) { renderer.addDependency('using System.Text.Json;'); renderer.addDependency('using System.Text.Json.Serialization;'); renderer.addDependency('using System.Text.RegularExpressions;'); - const formattedModelName = renderer.nameType(model.$id); - const deserialize = renderDeserialize({renderer, model, inputModel}); - const serialize = renderSerialize({renderer, model, inputModel}); + const deserialize = renderDeserialize({renderer, model}); + const serialize = renderSerialize({renderer, model}); - return `[JsonConverter(typeof(${formattedModelName}Converter))] + return `[JsonConverter(typeof(${model.name}Converter))] ${content} -internal class ${formattedModelName}Converter : JsonConverter<${formattedModelName}> +internal class ${model.name}Converter : JsonConverter<${model.name}> { public override bool CanConvert(System.Type objectType) { diff --git a/src/generators/csharp/renderers/ClassRenderer.ts b/src/generators/csharp/renderers/ClassRenderer.ts index 99f3d661da..71f7652971 100644 --- a/src/generators/csharp/renderers/ClassRenderer.ts +++ b/src/generators/csharp/renderers/ClassRenderer.ts @@ -1,15 +1,15 @@ import { CSharpRenderer } from '../CSharpRenderer'; -import { CommonModel, PropertyType } from '../../../models'; -import { DefaultPropertyNames, getUniquePropertyName } from '../../../helpers'; +import { ConstrainedObjectModel, ConstrainedObjectPropertyModel} from '../../../models'; import { pascalCase } from 'change-case'; import { CsharpClassPreset } from '../CSharpPreset'; +import { CSharpOptions } from '../CSharpGenerator'; /** * Renderer for CSharp's `struct` type * * @extends CSharpRenderer */ -export class ClassRenderer extends CSharpRenderer { +export class ClassRenderer extends CSharpRenderer { public async defaultSelf(): Promise { const content = [ await this.renderProperties(), @@ -24,8 +24,7 @@ export class ClassRenderer extends CSharpRenderer { this.addDependency('using System.Collections.Generic;'); } - const formattedName = this.nameType(this.model.$id); - return `public class ${formattedName} + return `public class ${this.model.name} { ${this.indent(this.renderBlock(content, 2))} }`; @@ -35,25 +34,11 @@ ${this.indent(this.renderBlock(content, 2))} const properties = this.model.properties || {}; const content: string[] = []; - for (const [propertyName, property] of Object.entries(properties)) { - const rendererProperty = await this.runPropertyPreset(propertyName, property, this.options); + for (const property of Object.values(properties)) { + const rendererProperty = await this.runPropertyPreset(property); content.push(rendererProperty); } - if (this.model.additionalProperties !== undefined) { - const propertyName = getUniquePropertyName(this.model, DefaultPropertyNames.additionalProperties); - const additionalProperty = await this.runPropertyPreset(propertyName, this.model.additionalProperties, this.options, PropertyType.additionalProperty); - content.push(additionalProperty); - } - - if (this.model.patternProperties !== undefined) { - for (const [pattern, patternModel] of Object.entries(this.model.patternProperties)) { - const propertyName = getUniquePropertyName(this.model, `${pattern}${DefaultPropertyNames.patternProperties}`); - const renderedPatternProperty = await this.runPropertyPreset(propertyName, patternModel, this.options, PropertyType.patternProperties); - content.push(renderedPatternProperty); - } - } - return this.renderBlock(content); } @@ -61,20 +46,8 @@ ${this.indent(this.renderBlock(content, 2))} const properties = this.model.properties || {}; const content: string[] = []; - for (const [propertyName, property] of Object.entries(properties)) { - content.push(await this.runAccessorPreset(propertyName, property, this.options, PropertyType.property)); - } - - if (this.model.additionalProperties !== undefined) { - const propertyName = getUniquePropertyName(this.model, DefaultPropertyNames.additionalProperties); - content.push(await this.runAccessorPreset(propertyName, this.model.additionalProperties, this.options, PropertyType.additionalProperty)); - } - - if (this.model.patternProperties !== undefined) { - for (const [pattern, patternModel] of Object.entries(this.model.patternProperties)) { - const propertyName = getUniquePropertyName(this.model, `${pattern}${DefaultPropertyNames.patternProperties}`); - content.push(await this.runAccessorPreset(propertyName, patternModel, this.options, PropertyType.patternProperties)); - } + for (const property of Object.values(properties)) { + content.push(await this.runAccessorPreset(property)); } return this.renderBlock(content, 2); @@ -84,68 +57,57 @@ ${this.indent(this.renderBlock(content, 2))} return this.runPreset('ctor'); } - runAccessorPreset(propertyName: string, property: CommonModel, options?: any, type: PropertyType = PropertyType.property): Promise { - return this.runPreset('accessor', { propertyName, property, options, type }); + runAccessorPreset(property: ConstrainedObjectPropertyModel): Promise { + return this.runPreset('accessor', { property }); } - runPropertyPreset(propertyName: string, property: CommonModel, options?: any, type: PropertyType = PropertyType.property): Promise { - return this.runPreset('property', { propertyName, property, options, type }); + runPropertyPreset(property: ConstrainedObjectPropertyModel): Promise { + return this.runPreset('property', { property }); } - runGetterPreset(propertyName: string, property: CommonModel, options?: any, type: PropertyType = PropertyType.property): Promise { - return this.runPreset('getter', { propertyName, property, options, type }); + runGetterPreset(property: ConstrainedObjectPropertyModel): Promise { + return this.runPreset('getter', { property }); } - runSetterPreset(propertyName: string, property: CommonModel, options?: any, type: PropertyType = PropertyType.property): Promise { - return this.runPreset('setter', { propertyName, property, options, type }); + runSetterPreset(property: ConstrainedObjectPropertyModel): Promise { + return this.runPreset('setter', { property }); } } -export const CSHARP_DEFAULT_CLASS_PRESET: CsharpClassPreset = { +export const CSHARP_DEFAULT_CLASS_PRESET: CsharpClassPreset = { self({ renderer }) { return renderer.defaultSelf(); }, - async property({ renderer, propertyName, options, property, type }) { - propertyName = renderer.nameProperty(propertyName, property); - let propertyType = renderer.renderType(property); - if (type === PropertyType.additionalProperty || type === PropertyType.patternProperties) { - propertyType = `Dictionary`; + async property({ renderer, property, options }) { + if (options.autoImplementedProperties) { + const getter = await renderer.runGetterPreset(property, options); + const setter = await renderer.runSetterPreset(property, options); + return `public ${property.property.type} ${pascalCase(property.propertyName)} { ${getter} ${setter} }`; } - if (options?.autoImplementedProperties) { - const getter = await renderer.runGetterPreset(propertyName, property, options, type); - const setter = await renderer.runSetterPreset(propertyName, property, options, type); - return `public ${propertyType} ${pascalCase(propertyName)} { ${getter} ${setter} }`; - } - return `private ${propertyType} ${propertyName};`; + return `private ${property.property.type} ${property.propertyName};`; }, - async accessor({ renderer, propertyName, options, property, type }) { - const formattedAccessorName = pascalCase(renderer.nameProperty(propertyName, property)); - let propertyType = renderer.renderType(property); - if (type === PropertyType.additionalProperty || type === PropertyType.patternProperties) { - propertyType = `Dictionary`; - } + async accessor({ renderer, options, property }) { + const formattedAccessorName = pascalCase(property.propertyName); if (options?.autoImplementedProperties) { return ''; } - return `public ${propertyType} ${formattedAccessorName} + return `public ${property.property.type} ${formattedAccessorName} { - ${await renderer.runGetterPreset(propertyName, property, options, type)} - ${await renderer.runSetterPreset(propertyName, property, options, type)} + ${await renderer.runGetterPreset(property, options)} + ${await renderer.runSetterPreset(property, options)} }`; }, - getter({ renderer, propertyName, options, property }) { + getter({ options, property }) { if (options?.autoImplementedProperties) { return 'get;'; } - const formattedPropertyName = renderer.nameProperty(propertyName, property); - return `get { return ${formattedPropertyName}; }`; + return `get { return ${property.propertyName}; }`; }, - setter({ renderer, propertyName, options, property }) { + setter({ options, property }) { if (options?.autoImplementedProperties) { return 'set;'; } - const formattedPropertyName = renderer.nameProperty(propertyName, property); - return `set { ${formattedPropertyName} = value; }`; + return `set { ${property.propertyName} = value; }`; } }; diff --git a/src/generators/csharp/renderers/EnumRenderer.ts b/src/generators/csharp/renderers/EnumRenderer.ts index f199ed4ea0..8978ea4eee 100644 --- a/src/generators/csharp/renderers/EnumRenderer.ts +++ b/src/generators/csharp/renderers/EnumRenderer.ts @@ -1,19 +1,18 @@ import { CSharpRenderer } from '../CSharpRenderer'; -import { EnumPreset } from '../../../models'; -import { pascalCase } from 'change-case'; -import { FormatHelpers } from '../../../helpers'; +import { ConstrainedEnumModel, ConstrainedEnumValueModel } from '../../../models'; +import { EnumPresetType } from '../CSharpPreset'; +import { CSharpOptions } from '../CSharpGenerator'; /** * Renderer for C#'s `enum` type * * @extends CSharpRenderer */ -export class EnumRenderer extends CSharpRenderer { +export class EnumRenderer extends CSharpRenderer { async defaultSelf(): Promise { const enumItems = await this.renderItems(); - const formattedName = this.nameType(this.model.$id); - const getValueCaseItemValues = await this.getValueCaseItemValues(); - const toEnumCaseItemValues = await this.toEnumCaseItemValues(); + const getValueCaseItemValues = this.getValueCaseItemValues(); + const toEnumCaseItemValues = this.toEnumCaseItemValues(); const enumValueSwitch = `switch (enumValue) { ${this.indent(getValueCaseItemValues)} @@ -24,22 +23,22 @@ return null;`; ${this.indent(toEnumCaseItemValues)} } return null;`; - const classContent = `public static dynamic GetValue(this ${formattedName} enumValue) + const classContent = `public static dynamic GetValue(this ${this.model.name} enumValue) { ${this.indent(enumValueSwitch)} } -public static ${formattedName}? To${formattedName}(dynamic value) +public static ${this.model.name}? To${this.model.name}(dynamic value) { ${this.indent(valueSwitch)} }`; - return `public enum ${formattedName} + return `public enum ${this.model.name} { ${this.indent(enumItems)} } -public static class ${formattedName}Extensions +public static class ${this.model.name}Extensions { ${this.indent(classContent)} } @@ -47,7 +46,7 @@ ${this.indent(classContent)} } async renderItems(): Promise { - const enums = this.model.enum || []; + const enums = this.model.values || []; const items: string[] = []; for (const value of enums) { @@ -59,70 +58,39 @@ ${this.indent(classContent)} return `${content}`; } - /** - * Some enum values require custom value conversion - */ - getEnumValue(enumValue: any): any { - switch (typeof enumValue) { - case 'number': - case 'bigint': - case 'boolean': - return enumValue; - case 'object': - return `"${JSON.stringify(enumValue).replace(/"/g, '\\"')}"`; - default: - return `"${enumValue}"`; - } - } - - async toEnumCaseItemValues(): Promise { - const enums = this.model.enum || []; + toEnumCaseItemValues(): string { + const enums = this.model.values || []; const items: string[] = []; - const formattedName = this.nameType(this.model.$id); for (const enumValue of enums) { - const renderedItem = await this.runItemPreset(enumValue); - const value = this.getEnumValue(enumValue); - items.push(`case ${value}: return ${formattedName}.${renderedItem};`); + items.push(`case ${enumValue.value}: return ${this.model.name}.${enumValue.key};`); } const content = items.join('\n'); return `${content}`; } - async getValueCaseItemValues(): Promise { - const enums = this.model.enum || []; + getValueCaseItemValues(): string { + const enums = this.model.values || []; const items: string[] = []; - const formattedName = this.nameType(this.model.$id); for (const enumValue of enums) { - const renderedItem = await this.runItemPreset(enumValue); - const value = this.getEnumValue(enumValue); - items.push(`case ${formattedName}.${renderedItem}: return ${value};`); + items.push(`case ${this.model.name}.${enumValue.key}: return ${enumValue.value};`); } const content = items.join('\n'); return `${content}`; } - runItemPreset(item: any): Promise { + runItemPreset(item: ConstrainedEnumValueModel): Promise { return this.runPreset('item', { item }); } } -export const CSHARP_DEFAULT_ENUM_PRESET: EnumPreset = { +export const CSHARP_DEFAULT_ENUM_PRESET: EnumPresetType = { self({ renderer }) { return renderer.defaultSelf(); }, item({ item }) { - let itemName = FormatHelpers.replaceSpecialCharacters(String(item), { exclude: [' '], separator: '_' }); - if (typeof item === 'number' || typeof item === 'bigint') { - itemName = `Number_${itemName}`; - } else if (typeof item === 'object') { - itemName = `${JSON.stringify(item)}`; - } else if (!(/^[a-zA-Z]+$/).test(itemName.charAt(0))) { - itemName = `String_${itemName}`; - } - - return pascalCase(itemName); + return item.key; }, };