Skip to content

Commit fc20f80

Browse files
fix(csharp): skip mock test examples with empty path parameters (#14879)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent d8cee99 commit fc20f80

19 files changed

Lines changed: 97 additions & 69 deletions

File tree

generators/csharp/base/src/context/CsharpTypeMapper.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ export class CsharpTypeMapper extends WithGeneration {
5959
return property.isOptional ? this.Types.FileParameter.asOptional() : this.Types.FileParameter;
6060
}
6161
case "fileArray": {
62-
return property.isOptional ? this.Types.FileParameter.asOptional() : this.Types.FileParameter;
62+
const listType = this.Collection.list(this.Types.FileParameter);
63+
return property.isOptional ? listType.asOptional() : listType;
6364
}
6465
default:
6566
assertNever(property);

generators/csharp/dynamic-snippets/src/context/DynamicLiteralMapper.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,11 @@ export class DynamicLiteralMapper extends WithGeneration {
140140
value: unknown;
141141
fallbackToDefault?: string;
142142
}): ast.Literal {
143+
// When generateLiterals is enabled, inline literal properties use `= new()`
144+
// default initializers in the C# model and must not be set in the snippet.
145+
if (this.settings.generateLiterals) {
146+
return this.csharp.Literal.nop();
147+
}
143148
switch (literal.type) {
144149
case "boolean": {
145150
const bool = this.context.getValueAsBoolean({ value });
@@ -240,6 +245,21 @@ export class DynamicLiteralMapper extends WithGeneration {
240245
}): ast.Literal {
241246
switch (named.type) {
242247
case "alias":
248+
// When generateLiterals is enabled and the alias resolves to a literal,
249+
// the C# model emits a readonly struct (e.g. `FormatMp3`) instead of a
250+
// raw string/bool. Instantiate it with `new TypeName()` rather than
251+
// emitting a plain literal value.
252+
if (this.settings.generateLiterals && named.typeReference.type === "literal") {
253+
return this.csharp.Literal.reference(
254+
this.csharp.instantiateClass({
255+
classReference: this.csharp.classReference({
256+
origin: named.declaration,
257+
namespace: this.context.getNamespace(named.declaration.fernFilepath)
258+
}),
259+
arguments_: []
260+
})
261+
);
262+
}
243263
return this.convert({ typeReference: named.typeReference, value, as, fallbackToDefault });
244264
case "discriminatedUnion":
245265
if (this.settings.shouldGeneratedDiscriminatedUnions) {

generators/csharp/dynamic-snippets/src/context/DynamicTypeMapper.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ export class DynamicTypeMapper extends WithGeneration {
6868
private convertNamed({ named }: { named: FernIr.dynamic.NamedType }): ast.Type {
6969
switch (named.type) {
7070
case "alias":
71+
if (this.settings.generateLiterals && named.typeReference.type === "literal") {
72+
return this.csharp.classReference({
73+
origin: named.declaration,
74+
namespace: this.context.getNamespace(named.declaration.fernFilepath)
75+
});
76+
}
7177
return this.convert({ typeReference: named.typeReference });
7278
case "enum":
7379
case "object":

generators/csharp/sdk/src/generateSdkTests.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,17 @@ function generateMockServerTests({ context }: { context: SdkGeneratorContext }):
3030
// TODO: support other response body types
3131
const useableExamples = allExamples.filter((example): example is FernIr.ExampleEndpointCall => {
3232
const response = example?.response;
33-
return response?.type === "ok" && response.value.type === "body";
33+
if (response?.type !== "ok" || response.value.type !== "body") {
34+
return false;
35+
}
36+
// Skip examples with empty string path parameters. An empty path parameter
37+
// causes a URL mismatch: the mock server registers a collapsed path (e.g.
38+
// /v0/tools/version/1) while the SDK client sends a double-slash path (e.g.
39+
// /v0/tools//version/1), resulting in a 404 from WireMock.
40+
if (example != null && hasEmptyPathParameter(example)) {
41+
return false;
42+
}
43+
return true;
3444
});
3545
if (useableExamples.length === 0) {
3646
continue;
@@ -49,6 +59,15 @@ function generateMockServerTests({ context }: { context: SdkGeneratorContext }):
4959
return files;
5060
}
5161

62+
function hasEmptyPathParameter(example: FernIr.ExampleEndpointCall): boolean {
63+
const allPathParams = [
64+
...example.rootPathParameters,
65+
...example.servicePathParameters,
66+
...example.endpointPathParameters
67+
];
68+
return allPathParams.some((param) => param.value.jsonExample === "");
69+
}
70+
5271
function shouldSkipMockServerTestForEndpoint({ endpoint }: { endpoint: FernIr.HttpEndpoint }): boolean {
5372
const responseBodyType = endpoint.response?.body?.type;
5473
if (responseBodyType === "fileDownload" || responseBodyType === "streamParameter") {

generators/csharp/sdk/versions.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,26 @@
11
# yaml-language-server: $schema=../../../fern-versions-yml.schema.json
2+
- version: 2.59.5
3+
changelogEntry:
4+
- summary: |
5+
Skip mock server test examples with empty string path parameters. An
6+
empty path parameter causes the mock server to register a collapsed URL
7+
(e.g. `/v0/tools/version/1`) while the SDK client sends a double-slash
8+
URL (e.g. `/v0/tools//version/1`), resulting in a 404 from WireMock.
9+
type: fix
10+
- summary: |
11+
Fix dynamic snippet generation for literal types when `generate-literals`
12+
(or `experimental-readonly-constants`) is enabled. Inline literal
13+
properties now correctly omit the value (relying on the `= new()`
14+
default initializer), and named literal alias types emit `new TypeName()`
15+
instead of a raw string, preventing CS0029 compilation errors.
16+
type: fix
17+
- summary: |
18+
Fix file upload request properties declared as `list<file>` generating
19+
a single `FileParameter` field instead of `List<FileParameter>`.
20+
type: fix
21+
createdAt: "2026-04-10"
22+
irVersion: 66
23+
224
- version: 2.59.4
325
changelogEntry:
426
- summary: |

seed/csharp-sdk/file-upload/src/SeedFileUpload/Service/Requests/MyOtherRequest.cs

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

seed/csharp-sdk/file-upload/src/SeedFileUpload/Service/Requests/MyRequest.cs

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

seed/csharp-sdk/file-upload/src/SeedFileUpload/Service/ServiceClient.cs

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example0.cs

Lines changed: 0 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

seed/csharp-sdk/literal/readonly-constants/src/SeedApi.DynamicSnippets/Example1.cs

Lines changed: 0 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)