Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: properly implement constants in c# #1801

Merged
merged 8 commits into from
Feb 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion docs/migrations/version-3-to-4.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,34 @@ but will now generate:
interface AnonymousSchema_1 {
aa_00_testAttribute?: string;
}
```
```

## C#

### Constant values are now properly rendered as const properties

This example used to generate a `string` with a getter and setter, but will now generate a const string that is initialized to the const value provided.

```yaml
type: object
properties:
property:
type: string
const: 'abc'
```

will generate

```csharp
public class TestClass {
private const string property = "test";

public string Property
{
get { return property; }
}
...
}
```

Notice that `Property` no longer has a `set` method. This might break existing models.
48 changes: 47 additions & 1 deletion src/generators/csharp/constrainer/ConstantConstrainer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,53 @@
import {
ConstrainedEnumModel,
ConstrainedMetaModel,
ConstrainedMetaModelOptionsConst,
ConstrainedReferenceModel,
ConstrainedStringModel
} from '../../../models';
import { CSharpConstantConstraint } from '../CSharpGenerator';

const getConstrainedEnumModelConstant = (args: {
constrainedMetaModel: ConstrainedMetaModel;
constrainedEnumModel: ConstrainedEnumModel;
constOptions: ConstrainedMetaModelOptionsConst;
}) => {
const constrainedEnumValueModel = args.constrainedEnumModel.values.find(
(value) => value.originalInput === args.constOptions.originalInput
);

if (constrainedEnumValueModel) {
return `${args.constrainedMetaModel.type}.${constrainedEnumValueModel.key}`;
}
};

export function defaultConstantConstraints(): CSharpConstantConstraint {
return () => {
return ({ constrainedMetaModel }) => {
const constOptions = constrainedMetaModel.options.const;

if (!constOptions) {
return undefined;
}

if (
constrainedMetaModel instanceof ConstrainedReferenceModel &&
constrainedMetaModel.ref instanceof ConstrainedEnumModel
) {
return getConstrainedEnumModelConstant({
constrainedMetaModel,
constrainedEnumModel: constrainedMetaModel.ref,
constOptions
});
} else if (constrainedMetaModel instanceof ConstrainedEnumModel) {
return getConstrainedEnumModelConstant({
constrainedMetaModel,
constrainedEnumModel: constrainedMetaModel,
constOptions
});
} else if (constrainedMetaModel instanceof ConstrainedStringModel) {
return `"${constOptions.originalInput}"`;
}

return undefined;
};
}
31 changes: 18 additions & 13 deletions src/generators/csharp/presets/JsonSerializerPreset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,27 +131,32 @@ function renderDeserializeProperty(model: ConstrainedObjectPropertyModel) {

function renderDeserializeProperties(model: ConstrainedObjectModel) {
const propertyEntries = Object.entries(model.properties || {});
const deserializeProperties = propertyEntries.map(([prop, propModel]) => {
const pascalProp = pascalCase(prop);
//Unwrapped dictionary properties, need to be unwrapped in JSON
if (
propModel.property instanceof ConstrainedDictionaryModel &&
propModel.property.serializationType === 'unwrap'
) {
return `if(instance.${pascalProp} == null) { instance.${pascalProp} = new Dictionary<${
propModel.property.key.type
}, ${propModel.property.value.type}>(); }
const deserializeProperties = propertyEntries
.map(([prop, propModel]) => {
const pascalProp = pascalCase(prop);
//Unwrapped dictionary properties, need to be unwrapped in JSON
if (
propModel.property instanceof ConstrainedDictionaryModel &&
propModel.property.serializationType === 'unwrap'
) {
return `if(instance.${pascalProp} == null) { instance.${pascalProp} = new Dictionary<${
propModel.property.key.type
}, ${propModel.property.value.type}>(); }
var deserializedValue = ${renderDeserializeProperty(propModel)};
instance.${pascalProp}.Add(propertyName, deserializedValue);
continue;`;
}
return `if (propertyName == "${propModel.unconstrainedPropertyName}")
}
if (propModel.property.options.const) {
return undefined;
}
return `if (propertyName == "${propModel.unconstrainedPropertyName}")
{
var value = ${renderDeserializeProperty(propModel)};
instance.${pascalProp} = value;
continue;
}`;
});
})
.filter((prop): prop is string => !!prop);
return deserializeProperties.join('\n');
}

Expand Down
27 changes: 16 additions & 11 deletions src/generators/csharp/presets/NewtonsoftSerializerPreset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,19 +85,24 @@ function renderDeserialize({
!(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.name}Extensions.To${prop.property.name}(jo["${prop.unconstrainedPropertyName}"].ToString())`;
}
return `if(jo["${prop.unconstrainedPropertyName}"] != null) {
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.name}Extensions.To${prop.property.name}(jo["${prop.unconstrainedPropertyName}"].ToString())`;
}
if (prop.property.options.const) {
return undefined;
}
return `if(jo["${prop.unconstrainedPropertyName}"] != null) {
value.${propertyAccessor} = ${toValue};
}`;
});
})
.filter((prop): prop is string => !!prop);
const nonDictionaryPropCheck = coreProps.map((prop) => {
return `prop.Name != "${prop.unconstrainedPropertyName}"`;
});
Expand Down
17 changes: 17 additions & 0 deletions src/generators/csharp/renderers/ClassRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,21 @@ export const CSHARP_DEFAULT_CLASS_PRESET: CsharpClassPreset<CSharpOptions> = {
const getter = await renderer.runGetterPreset(property);
const setter = await renderer.runSetterPreset(property);

if (property.property.options.const) {
return `public const ${property.property.type} ${pascalCase(
property.propertyName
)} { ${getter} } = ${property.property.options.const.value};`;
}

const semiColon = nullablePropertyEnding !== '' ? ';' : '';
return `public ${property.property.type} ${pascalCase(
property.propertyName
)} { ${getter} ${setter} }${nullablePropertyEnding}${semiColon}`;
}

if (property.property.options.const) {
return `private const ${property.property.type} ${property.propertyName} = ${property.property.options.const.value};`;
}
return `private ${property.property.type} ${property.propertyName}${nullablePropertyEnding};`;
},
async accessor({ renderer, options, property }) {
Expand All @@ -128,6 +138,13 @@ export const CSHARP_DEFAULT_CLASS_PRESET: CsharpClassPreset<CSharpOptions> = {
return '';
}

if (property.property.options.const) {
return `public ${property.property.type} ${formattedAccessorName}
{
${await renderer.runGetterPreset(property)}
}`;
}

return `public ${property.property.type} ${formattedAccessorName}
{
${await renderer.runGetterPreset(property)}
Expand Down
5 changes: 5 additions & 0 deletions src/generators/csharp/renderers/RecordRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ export const CSHARP_DEFAULT_RECORD_PRESET: CsharpRecordPreset<CSharpOptions> = {
async property({ renderer, property }) {
const getter = await renderer.runGetterPreset(property);
const setter = await renderer.runSetterPreset(property);
if (property.property.options.const) {
return `public const ${property.property.type} ${pascalCase(
property.propertyName
)} = ${property.property.options.const.value};`;
}
return `public ${property.required ? 'required ' : ''}${
property.property.type
} ${pascalCase(property.propertyName)} { ${getter} ${setter} }`;
Expand Down
48 changes: 46 additions & 2 deletions test/generators/csharp/CSharpGenerator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,28 @@ describe('CSharpGenerator', () => {
]);
});

test('should generate a const string in record', async () => {
const doc = {
$id: '_address',
type: 'object',
properties: {
property: { type: 'string', const: 'test' }
},
required: ['property'],
additionalProperties: {
type: 'string'
}
};

generator.options.modelType = 'record';
const models = await generator.generate(doc);
expect(models).toHaveLength(1);
expect(models[0].result).toMatchSnapshot();
expect(models[0].dependencies).toEqual([
'using System.Collections.Generic;'
]);
});

test('should render `enum` type', async () => {
const doc = {
$id: 'Things',
Expand All @@ -166,8 +188,7 @@ describe('CSharpGenerator', () => {
enum: ['test+', 'test', 'test-', 'test?!', '*test']
};

generator = new CSharpGenerator();

generator.options.modelType = 'record';
const models = await generator.generate(doc);
expect(models).toHaveLength(1);
expect(models[0].result).toMatchSnapshot();
Expand Down Expand Up @@ -304,5 +325,28 @@ describe('CSharpGenerator', () => {
'using System.Collections.Generic;'
]);
});

test('should generate a const string', async () => {
const doc = {
$id: 'CustomClass',
type: 'object',
properties: {
property: { type: 'string', const: 'test' }
},
additionalProperties: {
type: 'string'
},
required: ['property']
};

generator = new CSharpGenerator();

const models = await generator.generate(doc);
expect(models).toHaveLength(1);
expect(models[0].result).toMatchSnapshot();
expect(models[0].dependencies).toEqual([
'using System.Collections.Generic;'
]);
});
});
});
27 changes: 27 additions & 0 deletions test/generators/csharp/__snapshots__/CSharpGenerator.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,33 @@ exports[`CSharpGenerator class renderer should be able to overwrite property pre
}"
`;

exports[`CSharpGenerator class renderer should generate a const string 1`] = `
"public partial class CustomClass
{
private const string property = \\"test\\";
private Dictionary<string, string>? additionalProperties;

public string Property
{
get { return property; }
}

public Dictionary<string, string>? AdditionalProperties
{
get { return additionalProperties; }
set { additionalProperties = value; }
}
}"
`;

exports[`CSharpGenerator should generate a const string in record 1`] = `
"public partial record Address
{
public const string Property = \\"test\\";
public Dictionary<string, string>? AdditionalProperties { get; init; }
}"
`;

exports[`CSharpGenerator should render \`class\` type 1`] = `
"public partial class Address
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const doc = {
required: ['string prop'],
properties: {
'string prop': { type: 'string' },
'const string prop': { type: 'string', const: 'abc' },
numberProp: { type: 'number' },
enumProp: {
$id: 'EnumTest',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const doc = {
required: ['string prop'],
properties: {
'string prop': { type: 'string' },
'const string prop': { type: 'string', const: 'abc' },
numberProp: { type: 'number' },
enumProp: {
$id: 'EnumTest',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ exports[`JSON serializer preset should render serialize and deserialize converte
public partial class Test
{
private string stringProp;
private const string? constStringProp = \\"abc\\";
private double? numberProp;
private EnumTest? enumProp;
private NestedTest? objectProp;
Expand All @@ -16,6 +17,11 @@ public partial class Test
set { stringProp = value; }
}

public string? ConstStringProp
{
get { return constStringProp; }
}

public double? NumberProp
{
get { return numberProp; }
Expand Down Expand Up @@ -119,6 +125,11 @@ internal class TestConverter : JsonConverter<Test>
writer.WritePropertyName(\\"string prop\\");
JsonSerializer.Serialize(writer, value.StringProp, options);
}
if(value.ConstStringProp != null) {
// write property name and let the serializer serialize the value itself
writer.WritePropertyName(\\"const string prop\\");
JsonSerializer.Serialize(writer, value.ConstStringProp, options);
}
if(value.NumberProp != null) {
// write property name and let the serializer serialize the value itself
writer.WritePropertyName(\\"numberProp\\");
Expand Down
Loading
Loading