Skip to content

Commit

Permalink
feat!: properly implement constants in c# (#1801)
Browse files Browse the repository at this point in the history
  • Loading branch information
marakalwa authored Feb 19, 2024
1 parent 8180327 commit 225f730
Show file tree
Hide file tree
Showing 12 changed files with 231 additions and 29 deletions.
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

0 comments on commit 225f730

Please sign in to comment.