From 36fb142c67410d89b89950a3cc0e6c3daffe3ee7 Mon Sep 17 00:00:00 2001 From: Jonas Lagoni Date: Thu, 2 Sep 2021 16:44:11 +0200 Subject: [PATCH] feat: add c# serializer and deserializer functionality (#334) --- src/generators/csharp/index.ts | 1 + .../csharp/presets/JsonSerializerPreset.ts | 247 +++++++++++++++ src/generators/csharp/presets/index.ts | 1 + src/helpers/FormatHelpers.ts | 3 +- .../presets/JsonSerializerPreset.spec.ts | 35 +++ .../JsonSerializerPreset.spec.ts.snap | 280 ++++++++++++++++++ 6 files changed, 565 insertions(+), 2 deletions(-) create mode 100644 src/generators/csharp/presets/JsonSerializerPreset.ts create mode 100644 src/generators/csharp/presets/index.ts create mode 100644 test/generators/csharp/presets/JsonSerializerPreset.spec.ts create mode 100644 test/generators/csharp/presets/__snapshots__/JsonSerializerPreset.spec.ts.snap diff --git a/src/generators/csharp/index.ts b/src/generators/csharp/index.ts index e1456f7175..a65644089d 100644 --- a/src/generators/csharp/index.ts +++ b/src/generators/csharp/index.ts @@ -1,3 +1,4 @@ export * from './CSharpGenerator'; export { CSHARP_DEFAULT_PRESET } from './CSharpPreset'; export type { CSharpPreset } from './CSharpPreset'; +export * from './presets'; diff --git a/src/generators/csharp/presets/JsonSerializerPreset.ts b/src/generators/csharp/presets/JsonSerializerPreset.ts new file mode 100644 index 0000000000..2fcb14f20a --- /dev/null +++ b/src/generators/csharp/presets/JsonSerializerPreset.ts @@ -0,0 +1,247 @@ +import { CSharpRenderer } from '../CSharpRenderer'; +import { CSharpPreset } from '../CSharpPreset'; +import { getUniquePropertyName, DefaultPropertyNames, FormatHelpers } from '../../../helpers'; +import { CommonModel } from '../../../models'; + +function renderSerializeAdditionalProperties(model: CommonModel, renderer: CSharpRenderer) { + 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}) + { + //Ignore any additional properties which might already be part of the core properties + if (properties.Any(prop => prop.Name == additionalProperty.Key)) + { + continue; + } + // write property name and let the serializer serialize the value itself + writer.WritePropertyName(additionalProperty.Key); + JsonSerializer.Serialize(writer, additionalProperty.Value); + } +}`; + } + return serializeAdditionalProperties; +} + +function renderSerializeProperties(model: CommonModel, renderer: CSharpRenderer) { + let serializeProperties = ''; + if (model.properties !== undefined) { + for (const [propertyName, propertyModel] of Object.entries(model.properties)) { + const formattedPropertyName = FormatHelpers.upperFirst(renderer.nameProperty(propertyName, propertyModel)); + serializeProperties += `if(value.${formattedPropertyName} != null) { + // write property name and let the serializer serialize the value itself + writer.WritePropertyName("${propertyName}"); + JsonSerializer.Serialize(writer, value.${formattedPropertyName}); +}\n`; + } + } + return serializeProperties; +} +function renderSerializePatternProperties(model: CommonModel, renderer: CSharpRenderer) { + 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); + JsonSerializer.Serialize(writer, patternProp.Value); + } +}`; + } + } + 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}"`); + } + let propertiesList = 'var properties = value.GetType().GetProperties();'; + if (propertyFilter.length > 0) { + renderer.addDependency('using System.Linq;'); + propertiesList = `var properties = value.GetType().GetProperties().Where(prop => ${propertyFilter.join(' && ')});`; + } + return propertiesList; +} +/** + * Render `serialize` function based on model + */ +function renderSerialize({ renderer, model }: { + renderer: CSharpRenderer, + model: CommonModel, +}): string { + const formattedModelName = renderer.nameType(model.$id); + const serializeProperties = renderSerializeProperties(model, renderer); + const serializePatternProperties = renderSerializePatternProperties(model, renderer); + const serializeAdditionalProperties = renderSerializeAdditionalProperties(model, renderer); + const propertiesList = renderPropertiesList(model, renderer); + + return `public override void Write(Utf8JsonWriter writer, ${formattedModelName} value, JsonSerializerOptions options) +{ + if (value == null) + { + JsonSerializer.Serialize(writer, null); + return; + } + ${propertiesList} + + writer.WriteStartObject(); + +${renderer.indent(serializeProperties)} + +${renderer.indent(serializePatternProperties)} + +${renderer.indent(serializeAdditionalProperties)} + + writer.WriteEndObject(); +}`; +} + +function renderDeserializeProperties(model: CommonModel, renderer: CSharpRenderer) { + 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); + return `if (propertyName == "${prop}") +{ + var value = JsonSerializer.Deserialize<${propertyModelType}>(ref reader, options); + instance.${formattedPropertyName} = value; + continue; +}`; + }); + return deserializeProperties.join('\n'); +} + +function renderDeserializePatternProperties(model: CommonModel, renderer: CSharpRenderer) { + 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 = JsonSerializer.Deserialize<${patternPropertyType}>(ref reader, options); + instance.${patternPropertyName}.Add(propertyName, deserializedValue); + continue; +}`; + }); + return patternProperties.join('\n'); +} + +function renderDeserializeAdditionalProperties(model: CommonModel, renderer: CSharpRenderer) { + 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 = JsonSerializer.Deserialize<${additionalPropertyType}>(ref reader, options); +instance.${additionalPropertyName}.Add(propertyName, deserializedValue); +continue;`; +} + +/** + * Render `deserialize` function based on model + */ +function renderDeserialize({ renderer, model }: { + renderer: CSharpRenderer, + model: CommonModel, +}): string { + const formattedModelName = renderer.nameType(model.$id); + const deserializeProperties = renderDeserializeProperties(model, renderer); + const deserializePatternProperties = renderDeserializePatternProperties(model, renderer); + const deserializeAdditionalProperties = renderDeserializeAdditionalProperties(model, renderer); + return `public override ${formattedModelName} Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) +{ + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException(); + } + + var instance = new ${formattedModelName}(); + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + return instance; + } + + // Get the key. + if (reader.TokenType != JsonTokenType.PropertyName) + { + throw new JsonException(); + } + + string propertyName = reader.GetString(); +${renderer.indent(deserializeProperties, 4)} + +${renderer.indent(deserializePatternProperties, 4)} + +${renderer.indent(deserializeAdditionalProperties, 4)} + } + + throw new JsonException(); +}`; +} + +/** + * Preset which adds `serialize` and `deserialize` functions to class. + * + * @implements {CSharpPreset} + */ +export const CSHARP_JSON_SERIALIZER_PRESET: CSharpPreset = { + class: { + 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}); + const serialize = renderSerialize({renderer, model}); + + return `[JsonConverter(typeof(${formattedModelName}Converter))] +${content} + +internal class ${formattedModelName}Converter : JsonConverter<${formattedModelName}> +{ + public override bool CanConvert(Type objectType) + { + // this converter can be applied to any type + return true; + } +${renderer.indent(deserialize)} +${renderer.indent(serialize)} + +} +`; + } + } +}; diff --git a/src/generators/csharp/presets/index.ts b/src/generators/csharp/presets/index.ts new file mode 100644 index 0000000000..7748b92548 --- /dev/null +++ b/src/generators/csharp/presets/index.ts @@ -0,0 +1 @@ +export * from './JsonSerializerPreset'; diff --git a/src/helpers/FormatHelpers.ts b/src/helpers/FormatHelpers.ts index 446f7c1e80..02edaccf27 100644 --- a/src/helpers/FormatHelpers.ts +++ b/src/helpers/FormatHelpers.ts @@ -1,9 +1,8 @@ -/* eslint-disable no-unused-vars */ import { camelCase, pascalCase, paramCase, - constantCase, + constantCase } from 'change-case'; export enum IndentationTypes { diff --git a/test/generators/csharp/presets/JsonSerializerPreset.spec.ts b/test/generators/csharp/presets/JsonSerializerPreset.spec.ts new file mode 100644 index 0000000000..0c0634c4d2 --- /dev/null +++ b/test/generators/csharp/presets/JsonSerializerPreset.spec.ts @@ -0,0 +1,35 @@ +import { CSharpGenerator, CSHARP_JSON_SERIALIZER_PRESET } from '../../../../src/generators'; +const doc = { + $id: 'Test', + type: 'object', + additionalProperties: true, + required: ['string prop'], + properties: { + 'string prop': { type: 'string' }, + numberProp: { type: 'number' }, + objectProp: { type: 'object', $id: 'NestedTest', properties: {stringProp: { type: 'string' }}} + }, + patternProperties: { + '^S(.?)test': { + type: 'string' + } + }, +}; +describe('JSON serializer preset', () => { + test('should render serialize and deserialize converters', async () => { + const generator = new CSharpGenerator({ + presets: [ + CSHARP_JSON_SERIALIZER_PRESET + ] + }); + 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/csharp/presets/__snapshots__/JsonSerializerPreset.spec.ts.snap b/test/generators/csharp/presets/__snapshots__/JsonSerializerPreset.spec.ts.snap new file mode 100644 index 0000000000..888522d6b1 --- /dev/null +++ b/test/generators/csharp/presets/__snapshots__/JsonSerializerPreset.spec.ts.snap @@ -0,0 +1,280 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`JSON serializer preset should render serialize and deserialize converters 1`] = ` +"[JsonConverter(typeof(TestConverter))] +public class Test { + private string stringProp; + private float? numberProp; + private NestedTest objectProp; + private Dictionary additionalProperties; + private Dictionary sTestPatternProperties; + + public string StringProp + { + get { return stringProp; } + set { stringProp = value; } + } + + public float? NumberProp + { + get { return numberProp; } + set { numberProp = value; } + } + + public NestedTest ObjectProp + { + get { return objectProp; } + set { objectProp = value; } + } + + public Dictionary AdditionalProperties + { + get { return additionalProperties; } + set { additionalProperties = value; } + } + + public Dictionary STestPatternProperties + { + get { return sTestPatternProperties; } + set { sTestPatternProperties = value; } + } +} + +internal class TestConverter : JsonConverter +{ + public override bool CanConvert(Type objectType) + { + // this converter can be applied to any type + return true; + } + public override Test Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException(); + } + + var instance = new Test(); + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + return instance; + } + + // Get the key. + if (reader.TokenType != JsonTokenType.PropertyName) + { + throw new JsonException(); + } + + string propertyName = reader.GetString(); + if (propertyName == \\"string prop\\") + { + var value = JsonSerializer.Deserialize(ref reader, options); + instance.StringProp = value; + continue; + } + if (propertyName == \\"numberProp\\") + { + var value = JsonSerializer.Deserialize(ref reader, options); + instance.NumberProp = value; + continue; + } + if (propertyName == \\"objectProp\\") + { + var value = JsonSerializer.Deserialize(ref reader, options); + instance.ObjectProp = value; + continue; + } + + if(instance.STestPatternProperties == null) { instance.STestPatternProperties = new Dictionary(); } + var match = Regex.Match(propertyName, @\\"^S(.?)test\\"); + if (match.Success) + { + var deserializedValue = JsonSerializer.Deserialize(ref reader, options); + instance.STestPatternProperties.Add(propertyName, deserializedValue); + continue; + } + + if(instance.AdditionalProperties == null) { instance.AdditionalProperties = new Dictionary(); } + var deserializedValue = JsonSerializer.Deserialize(ref reader, options); + instance.AdditionalProperties.Add(propertyName, deserializedValue); + continue; + } + + throw new JsonException(); + } + public override void Write(Utf8JsonWriter writer, Test value, JsonSerializerOptions options) + { + if (value == null) + { + JsonSerializer.Serialize(writer, null); + return; + } + var properties = value.GetType().GetProperties().Where(prop => prop.Name != \\"AdditionalProperties\\" && prop.Name != \\"STestPatternProperties\\"); + + writer.WriteStartObject(); + + if(value.StringProp != null) { + // write property name and let the serializer serialize the value itself + writer.WritePropertyName(\\"string prop\\"); + JsonSerializer.Serialize(writer, value.StringProp); + } + if(value.NumberProp != null) { + // write property name and let the serializer serialize the value itself + writer.WritePropertyName(\\"numberProp\\"); + JsonSerializer.Serialize(writer, value.NumberProp); + } + if(value.ObjectProp != null) { + // write property name and let the serializer serialize the value itself + writer.WritePropertyName(\\"objectProp\\"); + JsonSerializer.Serialize(writer, value.ObjectProp); + } + + + // Unwrap pattern properties in object + if(value.STestPatternProperties != null) { + foreach (var patternProp in value.STestPatternProperties) + { + //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); + JsonSerializer.Serialize(writer, patternProp.Value); + } + } + + // Unwrap additional properties in object + if (value.AdditionalProperties != null) { + foreach (var additionalProperty in value.AdditionalProperties) + { + //Ignore any additional properties which might already be part of the core properties + if (properties.Any(prop => prop.Name == additionalProperty.Key)) + { + continue; + } + // write property name and let the serializer serialize the value itself + writer.WritePropertyName(additionalProperty.Key); + JsonSerializer.Serialize(writer, additionalProperty.Value); + } + } + + writer.WriteEndObject(); + } + +} +" +`; + +exports[`JSON serializer preset should render serialize and deserialize converters 2`] = ` +"[JsonConverter(typeof(NestedTestConverter))] +public class NestedTest { + private string stringProp; + private Dictionary additionalProperties; + + public string StringProp + { + get { return stringProp; } + set { stringProp = value; } + } + + public Dictionary AdditionalProperties + { + get { return additionalProperties; } + set { additionalProperties = value; } + } +} + +internal class NestedTestConverter : JsonConverter +{ + public override bool CanConvert(Type objectType) + { + // this converter can be applied to any type + return true; + } + public override NestedTest Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException(); + } + + var instance = new NestedTest(); + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + return instance; + } + + // Get the key. + if (reader.TokenType != JsonTokenType.PropertyName) + { + throw new JsonException(); + } + + string propertyName = reader.GetString(); + if (propertyName == \\"stringProp\\") + { + var value = JsonSerializer.Deserialize(ref reader, options); + instance.StringProp = value; + continue; + } + + + + if(instance.AdditionalProperties == null) { instance.AdditionalProperties = new Dictionary(); } + var deserializedValue = JsonSerializer.Deserialize(ref reader, options); + instance.AdditionalProperties.Add(propertyName, deserializedValue); + continue; + } + + throw new JsonException(); + } + public override void Write(Utf8JsonWriter writer, NestedTest value, JsonSerializerOptions options) + { + if (value == null) + { + JsonSerializer.Serialize(writer, null); + return; + } + var properties = value.GetType().GetProperties().Where(prop => prop.Name != \\"AdditionalProperties\\"); + + writer.WriteStartObject(); + + if(value.StringProp != null) { + // write property name and let the serializer serialize the value itself + writer.WritePropertyName(\\"stringProp\\"); + JsonSerializer.Serialize(writer, value.StringProp); + } + + + + + // Unwrap additional properties in object + if (value.AdditionalProperties != null) { + foreach (var additionalProperty in value.AdditionalProperties) + { + //Ignore any additional properties which might already be part of the core properties + if (properties.Any(prop => prop.Name == additionalProperty.Key)) + { + continue; + } + // write property name and let the serializer serialize the value itself + writer.WritePropertyName(additionalProperty.Key); + JsonSerializer.Serialize(writer, additionalProperty.Value); + } + } + + writer.WriteEndObject(); + } + +} +" +`;