Skip to content

Commit 8b9f85c

Browse files
authored
fix(csharp): Rename Custom method to FromCustom in String Enums (#6438)
1 parent 414c869 commit 8b9f85c

File tree

168 files changed

+1947
-291
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

168 files changed

+1947
-291
lines changed

generators/csharp/base/src/asIs/test/Json/StringEnumSerializerTests.Template.cs

+21-22
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,22 @@ namespace <%= namespace%>.Test.Core.Json;
99
[Parallelizable(ParallelScope.All)]
1010
public class StringEnumSerializerTests
1111
{
12-
private static readonly JsonSerializerOptions JsonOptions = new()
13-
{
14-
WriteIndented = true,
15-
};
12+
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
1613

1714
private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2;
18-
private static readonly DummyEnum UnknownEnumValue = DummyEnum.Custom("unknown_value");
19-
20-
private static readonly string JsonWithKnownEnum2 =
21-
$$"""
22-
{
23-
"enum_property": "{{KnownEnumValue2}}"
24-
}
25-
""";
26-
27-
private static readonly string JsonWithUnknownEnum =
28-
$$"""
29-
{
30-
"enum_property": "{{UnknownEnumValue}}"
31-
}
32-
""";
15+
private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value");
16+
17+
private static readonly string JsonWithKnownEnum2 = $$"""
18+
{
19+
"enum_property": "{{KnownEnumValue2}}"
20+
}
21+
""";
22+
23+
private static readonly string JsonWithUnknownEnum = $$"""
24+
{
25+
"enum_property": "{{UnknownEnumValue}}"
26+
}
27+
""";
3328

3429
[Test]
3530
public void ShouldParseKnownEnumValue2()
@@ -93,9 +88,9 @@ public DummyEnum(string value)
9388
/// </summary>
9489
public string Value { get; }
9590

96-
public static readonly DummyEnum KnownValue1 = Custom(Values.KnownValue1);
91+
public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1);
9792

98-
public static readonly DummyEnum KnownValue2 = Custom(Values.KnownValue2);
93+
public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2);
9994

10095
/// <summary>
10196
/// Constant strings for enum values
@@ -110,7 +105,7 @@ public static class Values
110105
/// <summary>
111106
/// Create a string enum with the given value.
112107
/// </summary>
113-
public static DummyEnum Custom(string value)
108+
public static DummyEnum FromCustom(string value)
114109
{
115110
return new DummyEnum(value);
116111
}
@@ -133,6 +128,10 @@ public override int GetHashCode()
133128
return Value.GetHashCode();
134129
}
135130

131+
public static explicit operator string(DummyEnum value) => value.Value;
132+
133+
public static explicit operator DummyEnum(string value) => new(value);
134+
136135
public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2);
137136

138137
public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2);

generators/csharp/codegen/src/ast/Class.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,9 @@ export class Class extends AstNode {
361361
if (operator.type === Class.CastOperator.Type.Explicit || operator.type === Class.CastOperator.Type.Implicit) {
362362
writer.write(`${operator.type} `);
363363
writer.write("operator ");
364-
writer.write(`${this.name}(`);
364+
const to = operator.to ?? this.reference;
365+
writer.writeNode(to);
366+
writer.write("(");
365367
operator.parameter.write(writer);
366368
} else {
367369
const normalOperator = operator as Class.NormalOperator;
@@ -444,6 +446,7 @@ export namespace Class {
444446
export interface CastOperator {
445447
parameter: Parameter;
446448
type: CastOperator.Type;
449+
to?: Type;
447450
body: CodeBlock;
448451
useExpressionBody?: boolean;
449452
}

generators/csharp/model/src/enum/StringEnumGenerator.ts

+29-7
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { ModelGeneratorContext } from "../ModelGeneratorContext";
99

1010
export class StringEnumGenerator extends FileGenerator<CSharpFile, ModelCustomConfigSchema, ModelGeneratorContext> {
1111
private readonly classReference: csharp.ClassReference;
12+
private readonly customMethodName: string;
1213

1314
constructor(
1415
context: ModelGeneratorContext,
@@ -17,6 +18,12 @@ export class StringEnumGenerator extends FileGenerator<CSharpFile, ModelCustomCo
1718
) {
1819
super(context);
1920
this.classReference = this.context.csharpTypeMapper.convertToClassReference(this.typeDeclaration.name);
21+
this.customMethodName = this.getCustomMethodName(enumDeclaration);
22+
}
23+
24+
private getCustomMethodName(enumDeclaration: EnumTypeDeclaration): string {
25+
const d = "FromCustom";
26+
return enumDeclaration.values.some((v) => v.name.name.pascalCase.safeName === d) ? "FromCustom_" : d;
2027
}
2128

2229
protected doGenerate(): CSharpFile {
@@ -106,12 +113,7 @@ export class StringEnumGenerator extends FileGenerator<CSharpFile, ModelCustomCo
106113
useRequired: false,
107114
static_: true,
108115
readonly: true,
109-
initializer: csharp.codeblock((writer) => {
110-
writer.write("Custom(");
111-
writer.write("Values.");
112-
writer.write(fieldName);
113-
writer.write(")");
114-
})
116+
initializer: csharp.codeblock(`new(Values.${fieldName})`)
115117
})
116118
);
117119
});
@@ -121,7 +123,7 @@ export class StringEnumGenerator extends FileGenerator<CSharpFile, ModelCustomCo
121123
stringEnum.addMethod(
122124
csharp.method({
123125
access: csharp.Access.Public,
124-
name: "Custom",
126+
name: this.customMethodName,
125127
parameters: [
126128
csharp.parameter({
127129
name: "value",
@@ -203,6 +205,26 @@ export class StringEnumGenerator extends FileGenerator<CSharpFile, ModelCustomCo
203205
useExpressionBody: true
204206
});
205207

208+
stringEnum.addOperator({
209+
type: "explicit",
210+
to: csharp.Type.string(),
211+
parameter: csharp.parameter({
212+
name: "value",
213+
type: csharp.Type.reference(this.classReference)
214+
}),
215+
body: csharp.codeblock("value.Value"),
216+
useExpressionBody: true
217+
});
218+
stringEnum.addOperator({
219+
type: "explicit",
220+
parameter: csharp.parameter({
221+
name: "value",
222+
type: csharp.Type.string()
223+
}),
224+
body: csharp.codeblock("new(value)"),
225+
useExpressionBody: true
226+
});
227+
206228
return new CSharpFile({
207229
clazz: stringEnum,
208230
directory: this.context.getDirectoryForTypeId(this.typeDeclaration.name.typeId),

generators/csharp/sdk/versions.yml

+18
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,24 @@
77
# Set `enable-forward-compatible-enums` to `false` in the configuration to generate the old enums.
88
# irVersion: 53
99

10+
- version: 1.13.1
11+
createdAt: "2025-03-13"
12+
irVersion: 57
13+
changelogEntry:
14+
- type: fix
15+
summary: |
16+
Forward compatible enums has a static method `Custom(string value)` that allows you to create a new instance of the enum with a custom value.
17+
"Custom" is a commonly used enum value, and we want to avoid conflicts with the static method, so we're renaming the static method to `FromCustom(string value)`.
18+
This feature is gated behind the `experimental-enable-forward-compatible-enums` configuration option, so we're accepting this as a breaking change without a major version bump.
19+
- type: feat
20+
summary: |
21+
Forward compatible enums can be explicitly casted from and to strings.
22+
```csharp
23+
string myEnumAsString = (string)MyEnum.Enum1;
24+
MyEnum myEnum = (MyEnum)"custom-value";
25+
```
26+
Note: We're not supporting implicit casts here because it could lead to behavior the user doesn't expect.
27+
1028
- version: 1.13.0
1129
createdAt: "2025-03-13"
1230
irVersion: 57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"type": "string",
3+
"enum": [
4+
"safe",
5+
"Custom"
6+
],
7+
"definitions": {}
8+
}

packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/enum.json

+75
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,81 @@
212212
}
213213
]
214214
},
215+
"type_:EnumWithCustom": {
216+
"type": "enum",
217+
"declaration": {
218+
"name": {
219+
"originalName": "EnumWithCustom",
220+
"camelCase": {
221+
"unsafeName": "enumWithCustom",
222+
"safeName": "enumWithCustom"
223+
},
224+
"snakeCase": {
225+
"unsafeName": "enum_with_custom",
226+
"safeName": "enum_with_custom"
227+
},
228+
"screamingSnakeCase": {
229+
"unsafeName": "ENUM_WITH_CUSTOM",
230+
"safeName": "ENUM_WITH_CUSTOM"
231+
},
232+
"pascalCase": {
233+
"unsafeName": "EnumWithCustom",
234+
"safeName": "EnumWithCustom"
235+
}
236+
},
237+
"fernFilepath": {
238+
"allParts": [],
239+
"packagePath": [],
240+
"file": null
241+
}
242+
},
243+
"values": [
244+
{
245+
"name": {
246+
"originalName": "SAFE",
247+
"camelCase": {
248+
"unsafeName": "safe",
249+
"safeName": "safe"
250+
},
251+
"snakeCase": {
252+
"unsafeName": "safe",
253+
"safeName": "safe"
254+
},
255+
"screamingSnakeCase": {
256+
"unsafeName": "SAFE",
257+
"safeName": "SAFE"
258+
},
259+
"pascalCase": {
260+
"unsafeName": "Safe",
261+
"safeName": "Safe"
262+
}
263+
},
264+
"wireValue": "safe"
265+
},
266+
{
267+
"name": {
268+
"originalName": "CUSTOM",
269+
"camelCase": {
270+
"unsafeName": "custom",
271+
"safeName": "custom"
272+
},
273+
"snakeCase": {
274+
"unsafeName": "custom",
275+
"safeName": "custom"
276+
},
277+
"screamingSnakeCase": {
278+
"unsafeName": "CUSTOM",
279+
"safeName": "CUSTOM"
280+
},
281+
"pascalCase": {
282+
"unsafeName": "Custom",
283+
"safeName": "Custom"
284+
}
285+
},
286+
"wireValue": "Custom"
287+
}
288+
]
289+
},
215290
"type_unknown:Status": {
216291
"type": "enum",
217292
"declaration": {

packages/cli/register/src/ir-to-fdr-converter/__test__/__snapshots__/enum.json

+18-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"types": {
33
"type_:Operand": {
4-
"description": "Tests enum name and value can be \ndifferent.",
4+
"description": "Tests enum name and value can be\ndifferent.",
55
"name": "Operand",
66
"shape": {
77
"type": "enum",
@@ -13,7 +13,7 @@
1313
"value": "="
1414
},
1515
{
16-
"description": "The name and value should be similar\nare similar for less than. ",
16+
"description": "The name and value should be similar\nare similar for less than.",
1717
"value": "less_than"
1818
}
1919
]
@@ -55,6 +55,20 @@
5555
]
5656
}
5757
},
58+
"type_:EnumWithCustom": {
59+
"name": "EnumWithCustom",
60+
"shape": {
61+
"type": "enum",
62+
"values": [
63+
{
64+
"value": "safe"
65+
},
66+
{
67+
"value": "Custom"
68+
}
69+
]
70+
}
71+
},
5872
"type_unknown:Status": {
5973
"name": "Status",
6074
"shape": {
@@ -416,7 +430,8 @@
416430
"types": [
417431
"type_:Operand",
418432
"type_:Color",
419-
"type_:ColorOrOperand"
433+
"type_:ColorOrOperand",
434+
"type_:EnumWithCustom"
420435
],
421436
"subpackages": [
422437
"subpackage_inlined-request",

seed/csharp-model/enum/forward-compatible-enums/.mock/definition/__package__.yml

+21-9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

seed/csharp-model/enum/forward-compatible-enums/.mock/definition/unknown.yml

+6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)