Skip to content

Commit

Permalink
feat: add C# Newtonsoft preset (#970)
Browse files Browse the repository at this point in the history
  • Loading branch information
jonaslagoni authored Nov 9, 2022
1 parent dd613c3 commit 7ceb980
Show file tree
Hide file tree
Showing 19 changed files with 532 additions and 4 deletions.
11 changes: 10 additions & 1 deletion docs/languages/Csharp.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,20 @@ Here are all the supported presets and the libraries they use:

To include functionality that convert the models using the [System.Text.Json](https://devblogs.microsoft.com/dotnet/try-the-new-system-text-json-apis/), to use this, use the preset `CSHARP_JSON_SERIALIZER_PRESET`.

Check out this [example for a live demonstration](../../examples/csharp-generate-serializer).
Check out this [example for a live demonstration](../../examples/csharp-generate-json-serializer).

**External dependencies**
Requires [System.Text.Json](https://devblogs.microsoft.com/dotnet/try-the-new-system-text-json-apis/), [System.Text.Json.Serialization](https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-how-to?pivots=dotnet-6-0) and [System.Text.RegularExpressions](https://docs.microsoft.com/en-us/dotnet/api/system.text.regularexpressions?view=net-6.0) to work.

#### Using Newtonsoft/Json.NET

To include functionality that convert the models using the [Newtonsoft/Json.NET](https://www.newtonsoft.com/json) framework, to use this, use the preset `CSHARP_NEWTONSOFT_SERIALIZER_PRESET`.

Check out this [example for a live demonstration](../../examples/csharp-generate-newtonsoft-serializer).

**External dependencies**
Requires [`Newtonsoft.Json`, `Newtonsoft.Json.Linq`](https://www.newtonsoft.com/json) and [System.Collections.Generic](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic?view=net-7.0).

### To and from XML
Currently not supported, [let everyone know you need it](https://github.com/asyncapi/modelina/issues/new?assignees=&labels=enhancement&template=enhancement.md)!

Expand Down
3 changes: 2 additions & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ This directory contains a series of self-contained examples that you can use as
- [TEMPLATE](./TEMPLATE) - A basic template used to create new examples.
- [java-generate-tostring](./java-generate-tostring) - A basic example that shows how to generate models that overwrite the `toString` method
- [csharp-generate-equals-and-hashcode](./csharp-generate-equals-and-hashcode) - A basic example on how to generate models that overwrite the `Equal` and `GetHashCode` methods
- [csharp-generate-serializer](./csharp-generate-serializer) - A basic example on how to generate models that include function to serialize the data models to JSON
- [csharp-generate-json-serializer](./csharp-generate-json-serializer) - A basic example on how to generate models that include function to serialize the data models to and from JSON with System.Text.Json.
- [csharp-generate-newtonsoft-serializer](./csharp-generate-newtonsoft-serializer) - A basic example on how to generate models that include function to serialize the data models to and form JSON with Newtonsoft.
- [csharp-overwrite-enum-naming](./csharp-overwrite-enum-naming) - A basic example on how to generate enum value names.
- [csharp-use-inheritance](./csharp-use-inheritance) - A basic example that shows how to introduce inheritance to classes
- [generate-javascript-models](./generate-javascript-models) - A basic example to generate JavaScript data models
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"config" : {
"example_name" : "csharp-generate-serializer"
"example_name" : "csharp-generate-json-serializer"
},
"scripts": {
"install": "cd ../.. && npm i",
Expand Down
17 changes: 17 additions & 0 deletions examples/csharp-generate-newtonsoft-serializer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# C# Generate serializer functions for Newtonsoft

A basic example of how to generate models and which includes a way to serialize them into and from JSON using Newtonsoft.

## How to run this example

Run this example using:

```sh
npm i && npm run start
```

If you are on Windows, use the `start:windows` script instead:

```sh
npm i && npm run start:windows
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Should be able to generate a model with functions to serialize the data model into JSON and should log expected output to console 1`] = `
Array [
"[JsonConverter(typeof(RootConverter))]
public class Root
{
private string email;
public string Email
{
get { return email; }
set { email = value; }
}
}
public class RootConverter : JsonConverter<Root>
{
public override Root ReadJson(JsonReader reader, Type objectType, Root existingValue, bool hasExistingValue, JsonSerializer serializer)
{
JObject jo = JObject.Load(reader);
Root value = new Root();
if(jo[\\"email\\" != null) {
value.Email = jo[\\"email\\"].ToObject<string>(serializer);
}
return value;
}
public override void WriteJson(JsonWriter writer, Root value, JsonSerializer serializer)
{
JObject jo = new JObject();
if (value.email != null)
{
jo.Add(\\"email\\", JToken.FromObject(value.Email, serializer));
}
jo.WriteTo(writer);
}
public override bool CanRead => true;
public override bool CanWrite => true;
}",
]
`;
14 changes: 14 additions & 0 deletions examples/csharp-generate-newtonsoft-serializer/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const spy = jest.spyOn(global.console, 'log').mockImplementation(() => { return; });
import {generate} from './index';

describe('Should be able to generate a model with functions to serialize the data model into JSON ', () => {
afterAll(() => {
jest.restoreAllMocks();
});

test('and should log expected output to console', async () => {
await generate();
expect(spy.mock.calls.length).toEqual(1);
expect(spy.mock.calls[0]).toMatchSnapshot();
});
});
29 changes: 29 additions & 0 deletions examples/csharp-generate-newtonsoft-serializer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { CSharpGenerator, CSHARP_NEWTONSOFT_SERIALIZER_PRESET } from '../../src';

const generator = new CSharpGenerator({
presets: [
CSHARP_NEWTONSOFT_SERIALIZER_PRESET
]
});

const jsonSchemaDraft7 = {
$schema: 'http://json-schema.org/draft-07/schema#',
type: 'object',
additionalProperties: false,
properties: {
email: {
type: 'string',
format: 'email'
}
}
};

export async function generate() : Promise<void> {
const models = await generator.generate(jsonSchemaDraft7);
for (const model of models) {
console.log(model.result);
}
}
if (require.main === module) {
generate();
}
10 changes: 10 additions & 0 deletions examples/csharp-generate-newtonsoft-serializer/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions examples/csharp-generate-newtonsoft-serializer/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"config" : {
"example_name" : "csharp-generate-newtonsoft-serializer"
},
"scripts": {
"install": "cd ../.. && npm i",
"start": "../../node_modules/.bin/ts-node --cwd ../../ ./examples/$npm_package_config_example_name/index.ts",
"start:windows": "..\\..\\node_modules\\.bin\\ts-node --cwd ..\\..\\ .\\examples\\%npm_package_config_example_name%\\index.ts",
"test": "../../node_modules/.bin/jest --config=../../jest.config.js ./examples/$npm_package_config_example_name/index.spec.ts",
"test:windows": "..\\..\\node_modules\\.bin\\jest --config=..\\..\\jest.config.js examples/%npm_package_config_example_name%/index.spec.ts"
}
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@
"docker:test:blackbox": "npm run docker:build && docker run asyncapi/modelina npm run test:blackbox",
"test": "npm run test:library && npm run test:examples",
"test:library": "cross-env CI=true jest --coverage --testPathIgnorePatterns ./test/blackbox --testPathIgnorePatterns ./examples",
"test:examples": "cross-env CI=true jest ./examples --testPathIgnorePatterns ./examples/integrate-with-react && npm run test:examples:websites",
"test:examples": "npm run test:examples:regular && npm run test:examples:websites",
"test:examples:regular": "cross-env CI=true jest ./examples --testPathIgnorePatterns ./examples/integrate-with-react",
"test:examples:websites": "cd ./examples/integrate-with-react && npm i && npm run test",
"test:blackbox": "cross-env CI=true jest ./test/blackbox",
"test:watch": "jest --watch",
Expand Down
135 changes: 135 additions & 0 deletions src/generators/csharp/presets/NewtonsoftSerializerPreset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { CSharpPreset } from '../CSharpPreset';
import { ConstrainedDictionaryModel, ConstrainedEnumModel, ConstrainedObjectModel, ConstrainedReferenceModel } from '../../../models';
import { CSharpOptions } from '../CSharpGenerator';
import { pascalCase } from 'change-case';

/**
* Render `serialize` function based on model
*/
function renderSerialize({ model }: {
model: ConstrainedObjectModel
}): string {
const corePropsWrite = Object.values(model.properties)
.filter((prop) => !(prop.property instanceof ConstrainedDictionaryModel) || prop.property.serializationType === 'normal')
.map((prop) => {
const propertyAccessor = pascalCase(prop.propertyName);
let toJson = `jo.Add("${prop.unconstrainedPropertyName}", JToken.FromObject(value.${propertyAccessor}, serializer));`;
if (prop.property instanceof ConstrainedReferenceModel
&& prop.property.ref instanceof ConstrainedEnumModel) {
toJson = `var enumValue = ${prop.property.type}Extensions.GetValue((${prop.property.type})value.${propertyAccessor});
var stringEnumValue = enumValue.ToString();
// C# converts booleans to uppercase True and False, which newtonsoft cannot understand
var jsonStringCompliant = stringEnumValue == "True" || stringEnumValue == "False" ? stringEnumValue.ToLower() : stringEnumValue;
var jsonToken = JToken.Parse(jsonStringCompliant);
jo.Add("${prop.unconstrainedPropertyName}", jsonToken);`;
}
return `if (value.${prop.propertyName} != null)
{
${toJson}
}`;
});
const unwrapPropsWrite = Object.values(model.properties)
.filter((prop) => prop.property instanceof ConstrainedDictionaryModel && prop.property.serializationType === 'unwrap')
.map((prop) => {
const propertyAccessor = pascalCase(prop.propertyName);
return `if (value.${propertyAccessor} != null)
{
foreach (var unwrapProperty in value.${propertyAccessor})
{
var hasProp = jo[unwrapProperty.Key];
if (hasProp != null) continue;
jo.Add(unwrapProperty.Key, JToken.FromObject(unwrapProperty.Value, serializer));
}
}`;
});
return `public override void WriteJson(JsonWriter writer, ${model.name} value, JsonSerializer serializer)
{
JObject jo = new JObject();
${corePropsWrite.join('\n')}
${unwrapPropsWrite.join('\n')}
jo.WriteTo(writer);
}`;
}

/**
* Render `deserialize` function based on model
*/
function renderDeserialize({ model }: {
model: ConstrainedObjectModel
}): string {
const unwrapDictionaryProps = Object.values(model.properties)
.filter((prop) => prop.property instanceof ConstrainedDictionaryModel && prop.property.serializationType === 'unwrap');
const coreProps = Object.values(model.properties)
.filter((prop) => !(prop.property instanceof ConstrainedDictionaryModel) || prop.property.serializationType === 'normal');
const corePropsRead = coreProps.map((prop) => {
const propertyAccessor = pascalCase(prop.propertyName);
let toValue = `jo["${prop.unconstrainedPropertyName}"].ToObject<${prop.property.type}>(serializer)`;
if (prop.property instanceof ConstrainedReferenceModel
&& prop.property.ref instanceof ConstrainedEnumModel) {
toValue = `${prop.property.type}Extensions.To${prop.property.type}(jo["${prop.unconstrainedPropertyName}"])`;
}
return `if(jo["${prop.unconstrainedPropertyName}" != null) {
value.${propertyAccessor} = ${toValue};
}`;
});
const nonDictionaryPropCheck = coreProps.map((prop) => {
return `prop.Name != "${prop.unconstrainedPropertyName}"`;
});
const dictionaryInitializers = unwrapDictionaryProps.map((prop) => {
const propertyAccessor = pascalCase(prop.propertyName);
return `value.${propertyAccessor} = new Dictionary<${(prop.property as ConstrainedDictionaryModel).key.type}, ${(prop.property as ConstrainedDictionaryModel).value.type}>();`;
});
const unwrapDictionaryRead = unwrapDictionaryProps.map((prop) => {
const propertyAccessor = pascalCase(prop.propertyName);
return `value.${propertyAccessor}[additionalProperty.Name] = additionalProperty.Value.ToObject<${(prop.property as ConstrainedDictionaryModel).value.type}>(serializer);`;
});
const additionalPropertiesCode = unwrapDictionaryProps.length !== 0 ? `var additionalProperties = jo.Properties().Where((prop) => ${nonDictionaryPropCheck.join(' || ')});
${dictionaryInitializers}
foreach (var additionalProperty in additionalProperties)
{
${unwrapDictionaryRead.join('\n')}
}` : '';
return `public override ${model.name} ReadJson(JsonReader reader, Type objectType, ${model.name} existingValue, bool hasExistingValue, JsonSerializer serializer)
{
JObject jo = JObject.Load(reader);
${model.name} value = new ${model.name}();
${corePropsRead.join('\n')}
${additionalPropertiesCode}
return value;
}`;
}

/**
* Preset which adds Newtonsoft/JSON.net converters for serializing and deserializing the data models
*
* @implements {CSharpPreset}
*/
export const CSHARP_NEWTONSOFT_SERIALIZER_PRESET: CSharpPreset<CSharpOptions> = {
class: {
self: ({ renderer, content, model }) => {
renderer.addDependency('using Newtonsoft.Json;');
renderer.addDependency('using Newtonsoft.Json.Linq;');
renderer.addDependency('using System.Collections.Generic;');

const deserialize = renderDeserialize({ model });
const serialize = renderSerialize({ model });

return `[JsonConverter(typeof(${model.name}Converter))]
${content}
public class ${model.name}Converter : JsonConverter<${model.name}>
{
${deserialize}
${serialize}
public override bool CanRead => true;
public override bool CanWrite => true;
}`;
},
},
};
1 change: 1 addition & 0 deletions src/generators/csharp/presets/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './JsonSerializerPreset';
export * from './NewtonsoftSerializerPreset';
export * from './CommonPreset';
33 changes: 33 additions & 0 deletions test/generators/csharp/presets/NewtonsoftSerializerPreset.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { CSharpGenerator, CSHARP_NEWTONSOFT_SERIALIZER_PRESET } from '../../../../src/generators';
const doc = {
$id: 'Test',
type: 'object',
additionalProperties: true,
required: ['string prop'],
properties: {
'string prop': { type: 'string' },
numberProp: { type: 'number' },
enumProp: { $id: 'EnumTest', enum: ['Some enum String', true, {test: 'test'}, 2]},
objectProp: { type: 'object', $id: 'NestedTest', properties: {stringProp: { type: 'string' }}}
},
patternProperties: {
'^S(.?)test': {
type: 'string'
}
},
};
describe('Newtonsoft JSON serializer preset', () => {
test('should render serialize and deserialize converters', async () => {
const generator = new CSharpGenerator({
presets: [
CSHARP_NEWTONSOFT_SERIALIZER_PRESET
]
});

const outputModels = await generator.generate(doc);
expect(outputModels).toHaveLength(3);
expect(outputModels[0].result).toMatchSnapshot();
expect(outputModels[1].result).toMatchSnapshot();
expect(outputModels[2].result).toMatchSnapshot();
});
});
Loading

0 comments on commit 7ceb980

Please sign in to comment.