@typespec/http-server-csharp 0.58.0-alpha.1 → 0.58.0-alpha.10-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/cmd/hscs.js +2 -0
  2. package/dist/src/cli/cli.d.ts +2 -0
  3. package/dist/src/cli/cli.d.ts.map +1 -0
  4. package/dist/src/cli/cli.js +124 -0
  5. package/dist/src/cli/cli.js.map +1 -0
  6. package/dist/src/{attributes.d.ts → lib/attributes.d.ts} +1 -1
  7. package/dist/src/lib/attributes.d.ts.map +1 -0
  8. package/dist/src/{attributes.js → lib/attributes.js} +48 -4
  9. package/dist/src/{attributes.js.map → lib/attributes.js.map} +1 -1
  10. package/dist/src/{boilerplate.d.ts → lib/boilerplate.d.ts} +2 -0
  11. package/dist/src/lib/boilerplate.d.ts.map +1 -0
  12. package/dist/src/{boilerplate.js → lib/boilerplate.js} +101 -36
  13. package/dist/src/lib/boilerplate.js.map +1 -0
  14. package/dist/src/lib/index.d.ts.map +1 -0
  15. package/dist/src/lib/index.js.map +1 -0
  16. package/dist/src/{interfaces.d.ts → lib/interfaces.d.ts} +10 -0
  17. package/dist/src/lib/interfaces.d.ts.map +1 -0
  18. package/dist/src/{interfaces.js → lib/interfaces.js} +7 -3
  19. package/dist/src/lib/interfaces.js.map +1 -0
  20. package/dist/src/{lib.d.ts → lib/lib.d.ts} +25 -1
  21. package/dist/src/lib/lib.d.ts.map +1 -0
  22. package/dist/src/{lib.js → lib/lib.js} +31 -0
  23. package/dist/src/lib/lib.js.map +1 -0
  24. package/dist/src/lib/scaffolding.d.ts +20 -0
  25. package/dist/src/lib/scaffolding.d.ts.map +1 -0
  26. package/dist/src/lib/scaffolding.js +388 -0
  27. package/dist/src/lib/scaffolding.js.map +1 -0
  28. package/dist/src/lib/service.d.ts.map +1 -0
  29. package/dist/src/{service.js → lib/service.js} +584 -125
  30. package/dist/src/lib/service.js.map +1 -0
  31. package/dist/src/lib/testing/index.d.ts.map +1 -0
  32. package/dist/src/{testing → lib/testing}/index.js +1 -0
  33. package/dist/src/lib/testing/index.js.map +1 -0
  34. package/dist/src/lib/type-helpers.d.ts.map +1 -0
  35. package/dist/src/lib/type-helpers.js.map +1 -0
  36. package/dist/src/{utils.d.ts → lib/utils.d.ts} +3 -3
  37. package/dist/src/lib/utils.d.ts.map +1 -0
  38. package/dist/src/{utils.js → lib/utils.js} +67 -50
  39. package/dist/src/lib/utils.js.map +1 -0
  40. package/package.json +32 -23
  41. package/dist/src/attributes.d.ts.map +0 -1
  42. package/dist/src/boilerplate.d.ts.map +0 -1
  43. package/dist/src/boilerplate.js.map +0 -1
  44. package/dist/src/index.d.ts.map +0 -1
  45. package/dist/src/index.js.map +0 -1
  46. package/dist/src/interfaces.d.ts.map +0 -1
  47. package/dist/src/interfaces.js.map +0 -1
  48. package/dist/src/lib.d.ts.map +0 -1
  49. package/dist/src/lib.js.map +0 -1
  50. package/dist/src/service.d.ts.map +0 -1
  51. package/dist/src/service.js.map +0 -1
  52. package/dist/src/testing/index.d.ts.map +0 -1
  53. package/dist/src/testing/index.js.map +0 -1
  54. package/dist/src/type-helpers.d.ts.map +0 -1
  55. package/dist/src/type-helpers.js.map +0 -1
  56. package/dist/src/utils.d.ts.map +0 -1
  57. package/dist/src/utils.js.map +0 -1
  58. /package/dist/src/{index.d.ts → lib/index.d.ts} +0 -0
  59. /package/dist/src/{index.js → lib/index.js} +0 -0
  60. /package/dist/src/{service.d.ts → lib/service.d.ts} +0 -0
  61. /package/dist/src/{testing → lib/testing}/index.d.ts +0 -0
  62. /package/dist/src/{type-helpers.d.ts → lib/type-helpers.d.ts} +0 -0
  63. /package/dist/src/{type-helpers.js → lib/type-helpers.js} +0 -0
@@ -1,25 +1,36 @@
1
- import { getDoc, getNamespaceFullName, getService, isErrorModel, isNeverType, isNullType, isVoidType, } from "@typespec/compiler";
2
- import { CodeTypeEmitter, Placeholder, StringBuilder, code, createAssetEmitter, } from "@typespec/compiler/emitter-framework";
3
- import { Visibility, createMetadataInfo, getHttpOperation, isStatusCode, } from "@typespec/http";
1
+ import { getDoc, getNamespaceFullName, getService, isErrorModel, isNeverType, isNullType, isTemplateDeclaration, isVoidType, } from "@typespec/compiler";
2
+ import { CodeTypeEmitter, StringBuilder, code, createAssetEmitter, } from "@typespec/compiler/emitter-framework";
3
+ import { createRekeyableMap } from "@typespec/compiler/utils";
4
+ import { Visibility, createMetadataInfo, getHeaderFieldName, getHttpOperation, getHttpPart, isHeader, isPathParam, isQueryParam, isStatusCode, } from "@typespec/http";
4
5
  import { getResourceOperation } from "@typespec/rest";
5
6
  import { execFile } from "child_process";
6
- import { getSerializationSourceFiles } from "./boilerplate.js";
7
+ import { GeneratedFileHeader, GeneratedFileHeaderWithNullable, getSerializationSourceFiles, } from "./boilerplate.js";
7
8
  import { CSharpSourceType, CSharpType, NameCasingType, } from "./interfaces.js";
8
9
  import { reportDiagnostic } from "./lib.js";
10
+ import { getBusinessLogicImplementations, getScaffoldingHelpers, } from "./scaffolding.js";
9
11
  import { getRecordType, isKnownReferenceType } from "./type-helpers.js";
10
- import { HttpMetadata, UnknownType, coalesceTypes, ensureCSharpIdentifier, ensureCleanDirectory, formatComment, getCSharpIdentifier, getCSharpStatusCode, getCSharpType, getCSharpTypeForScalar, getModelAttributes, getModelInstantiationName, getOperationVerbDecorator, isValueType, } from "./utils.js";
12
+ import { HttpMetadata, UnknownType, coalesceTypes, ensureCSharpIdentifier, ensureCleanDirectory, formatComment, getCSharpIdentifier, getCSharpStatusCode, getCSharpType, getCSharpTypeForIntrinsic, getCSharpTypeForScalar, getModelAttributes, getModelInstantiationName, getOperationVerbDecorator, isValueType, } from "./utils.js";
11
13
  export async function $onEmit(context) {
12
14
  let _unionCounter = 0;
13
15
  const controllers = new Map();
14
16
  const NoResourceContext = "RPCOperations";
17
+ const doNotEmit = context.program.compilerOptions.noEmit || false;
15
18
  class CSharpCodeEmitter extends CodeTypeEmitter {
16
19
  #metadateMap = new Map();
17
- #licenseHeader = `// Copyright (c) Microsoft Corporation. All rights reserved.
18
- // Licensed under the MIT License.`;
20
+ #generatedFileHeaderWithNullable = GeneratedFileHeaderWithNullable;
21
+ #generatedFileHeader = GeneratedFileHeader;
19
22
  #sourceTypeKey = "sourceType";
20
23
  #libraryFiles = getSerializationSourceFiles(this.emitter);
21
24
  #baseNamespace = undefined;
22
25
  #emitterOutputType = context.options["output-type"];
26
+ #emitMocks = context.options["emit-mocks"];
27
+ #useSwagger = context.options["use-swaggerui"] || false;
28
+ #openapiPath = context.options["openapi-path"] || "openapi/openapi.yaml";
29
+ #mockRegistrations = new Map();
30
+ #mockHelpers = this.#emitMocks === "all"
31
+ ? getScaffoldingHelpers(this.emitter, this.#useSwagger, this.#openapiPath)
32
+ : [];
33
+ #mockFiles = [];
23
34
  #metaInfo = createMetadataInfo(this.emitter.getProgram(), {
24
35
  canonicalVisibility: Visibility.Read,
25
36
  canShareProperty: (p) => true,
@@ -40,8 +51,17 @@ export async function $onEmit(context) {
40
51
  declarationName(declarationType) {
41
52
  switch (declarationType.kind) {
42
53
  case "Enum":
54
+ if (!declarationType.name)
55
+ return `Enum${_unionCounter++}`;
56
+ return getCSharpIdentifier(declarationType.name, NameCasingType.Class);
43
57
  case "Interface":
58
+ if (!declarationType.name)
59
+ return `Interface${_unionCounter++}`;
60
+ return getCSharpIdentifier(declarationType.name, NameCasingType.Class);
44
61
  case "Model":
62
+ if (!declarationType.name)
63
+ return `Model${_unionCounter++}`;
64
+ return getCSharpIdentifier(declarationType.name, NameCasingType.Class);
45
65
  case "Operation":
46
66
  return getCSharpIdentifier(declarationType.name, NameCasingType.Class);
47
67
  case "Union":
@@ -60,8 +80,7 @@ export async function $onEmit(context) {
60
80
  const doc = getDoc(this.emitter.getProgram(), en);
61
81
  const attributes = getModelAttributes(program, en, enumName);
62
82
  this.#metadateMap.set(en, new CSharpType({ name: enumName, namespace: namespace }));
63
- return this.emitter.result.declaration(enumName, code `${this.#licenseHeader}
64
- // <auto-generated />
83
+ return this.emitter.result.declaration(enumName, code `${this.#generatedFileHeader}
65
84
 
66
85
  ${this.#emitUsings()}
67
86
 
@@ -108,13 +127,15 @@ export async function $onEmit(context) {
108
127
  return this.emitter.result.rawCode(code `System.Text.Json.Nodes.JsonNode`);
109
128
  case "ErrorType":
110
129
  case "never":
111
- case "void":
112
130
  reportDiagnostic(this.emitter.getProgram(), {
113
131
  code: "invalid-intrinsic",
114
132
  target: intrinsic,
115
133
  format: { typeName: intrinsic.name },
116
134
  });
117
135
  return "";
136
+ case "void":
137
+ const type = getCSharpTypeForIntrinsic(this.emitter.getProgram(), intrinsic);
138
+ return this.emitter.result.rawCode(`${type?.type.getTypeReference()}`);
118
139
  }
119
140
  }
120
141
  #emitUsings(file) {
@@ -127,13 +148,17 @@ export async function $onEmit(context) {
127
148
  return builder.segments.join("\n");
128
149
  }
129
150
  modelDeclaration(model, name) {
151
+ const parts = this.#getMultipartParts(model);
152
+ if (parts.length > 0) {
153
+ parts.forEach((p) => this.emitter.emitType(p));
154
+ return "";
155
+ }
130
156
  const className = ensureCSharpIdentifier(this.emitter.getProgram(), model, name);
131
157
  const namespace = this.emitter.getContext().namespace;
132
158
  const doc = getDoc(this.emitter.getProgram(), model);
133
159
  const attributes = getModelAttributes(this.emitter.getProgram(), model, className);
134
160
  this.#metadateMap.set(model, new CSharpType({ name: className, namespace: namespace }));
135
- const decl = this.emitter.result.declaration(className, code `${this.#licenseHeader}
136
- // <auto-generated />
161
+ const decl = this.emitter.result.declaration(className, code `${this.#generatedFileHeader}
137
162
 
138
163
  ${this.#emitUsings()}
139
164
 
@@ -146,22 +171,31 @@ export async function $onEmit(context) {
146
171
  return decl;
147
172
  }
148
173
  modelDeclarationContext(model, name) {
174
+ if (this.#isMultipartModel(model))
175
+ return {};
149
176
  const modelName = ensureCSharpIdentifier(this.emitter.getProgram(), model, name);
150
177
  const modelFile = this.emitter.createSourceFile(`models/${modelName}.cs`);
151
178
  modelFile.meta[this.#sourceTypeKey] = CSharpSourceType.Model;
152
179
  const modelNamespace = `${this.#getOrSetBaseNamespace(model)}.Models`;
153
- return this.#createModelContext(modelNamespace, modelFile);
180
+ return this.#createModelContext(modelNamespace, modelFile, modelName);
154
181
  }
155
182
  modelInstantiationContext(model) {
183
+ if (this.#isMultipartModel(model))
184
+ return {};
156
185
  const modelName = getModelInstantiationName(this.emitter.getProgram(), model, model.name);
157
186
  const sourceFile = this.emitter.createSourceFile(`models/${modelName}.cs`);
158
187
  sourceFile.meta[this.#sourceTypeKey] = CSharpSourceType.Model;
159
188
  const modelNamespace = `${this.#getOrSetBaseNamespace(model)}.Models`;
160
- const context = this.#createModelContext(modelNamespace, sourceFile);
189
+ const context = this.#createModelContext(modelNamespace, sourceFile, model.name);
161
190
  context.instantiationName = modelName;
162
191
  return context;
163
192
  }
164
193
  modelInstantiation(model, name) {
194
+ const parts = this.#getMultipartParts(model);
195
+ if (parts.length > 0) {
196
+ parts.forEach((p) => this.emitter.emitType(p));
197
+ return "";
198
+ }
165
199
  const program = this.emitter.getProgram();
166
200
  const recordType = getRecordType(program, model);
167
201
  if (recordType !== undefined) {
@@ -171,54 +205,211 @@ export async function $onEmit(context) {
171
205
  const className = context.instantiationName ?? name;
172
206
  return this.modelDeclaration(model, className);
173
207
  }
208
+ #getMultipartParts(model) {
209
+ const parts = [...model.properties.values()]
210
+ .flatMap((p) => getHttpPart(this.emitter.getProgram(), p.type)?.type)
211
+ .filter((t) => t !== undefined);
212
+ if (model.baseModel) {
213
+ return parts.concat(this.#getMultipartParts(model.baseModel));
214
+ }
215
+ return parts;
216
+ }
217
+ #isMultipartModel(model) {
218
+ const multipartTypes = this.#getMultipartParts(model);
219
+ return multipartTypes.length > 0;
220
+ }
174
221
  modelProperties(model) {
175
222
  const result = new StringBuilder();
176
223
  for (const [_, prop] of model.properties) {
177
224
  if (!isVoidType(prop.type) &&
178
225
  !isNeverType(prop.type) &&
179
226
  !isNullType(prop.type) &&
180
- !isErrorModel(this.emitter.getProgram(), prop.type))
227
+ !isErrorModel(this.emitter.getProgram(), prop.type)) {
181
228
  result.push(code `${this.emitter.emitModelProperty(prop)}`);
229
+ }
182
230
  }
183
231
  return result.reduce();
184
232
  }
233
+ modelLiteralContext(model) {
234
+ const name = this.emitter.emitDeclarationName(model) || "";
235
+ return this.modelDeclarationContext(model, name);
236
+ }
237
+ modelLiteral(model) {
238
+ const modelName = this.emitter.getContext().name;
239
+ reportDiagnostic(this.emitter.getProgram(), {
240
+ code: "anonymous-model",
241
+ target: model,
242
+ format: { emittedName: modelName },
243
+ });
244
+ return this.modelDeclaration(model, modelName);
245
+ }
185
246
  #isRecord(type) {
186
247
  return type.kind === "Model" && type.name === "Record" && type.indexer !== undefined;
187
248
  }
249
+ #isInheritedProperty(property) {
250
+ const visited = [];
251
+ function isInherited(model, propertyName) {
252
+ if (visited.includes(model))
253
+ return false;
254
+ visited.push(model);
255
+ if (model.properties.has(propertyName))
256
+ return true;
257
+ if (model.baseModel === undefined)
258
+ return false;
259
+ return isInherited(model.baseModel, propertyName);
260
+ }
261
+ const model = property.model;
262
+ if (model === undefined || model.baseModel === undefined)
263
+ return false;
264
+ return isInherited(model.baseModel, property.name);
265
+ }
188
266
  modelPropertyLiteral(property) {
189
267
  if (isStatusCode(this.emitter.getProgram(), property))
190
268
  return "";
191
269
  const propertyName = ensureCSharpIdentifier(this.emitter.getProgram(), property, property.name);
192
- const [typeName, typeDefault] = this.#findPropertyType(property);
270
+ const [typeName, typeDefault, nullable] = this.#findPropertyType(property);
193
271
  const doc = getDoc(this.emitter.getProgram(), property);
194
- const attributes = getModelAttributes(this.emitter.getProgram(), property);
195
- // eslint-disable-next-line deprecation/deprecation
272
+ const attributes = getModelAttributes(this.emitter.getProgram(), property, propertyName);
273
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
196
274
  const defaultValue = property.default
197
- ? // eslint-disable-next-line deprecation/deprecation
275
+ ? // eslint-disable-next-line @typescript-eslint/no-deprecated
198
276
  code `${this.emitter.emitType(property.default)}`
199
277
  : typeDefault;
200
278
  return this.emitter.result
201
- .rawCode(code `${doc ? `${formatComment(doc)}\n` : ""}${`${attributes.map((attribute) => attribute.getApplicationString(this.emitter.getContext().scope)).join("\n")}${attributes?.length > 0 ? "\n" : ""}`}public ${typeName}${property.optional && isValueType(this.emitter.getProgram(), property.type) ? "?" : ""} ${propertyName} { get; ${typeDefault ? "}" : "set; }"}${defaultValue ? ` = ${defaultValue};\n` : "\n"}
279
+ .rawCode(code `${doc ? `${formatComment(doc)}\n` : ""}${`${attributes.map((attribute) => attribute.getApplicationString(this.emitter.getContext().scope)).join("\n")}${attributes?.length > 0 ? "\n" : ""}`}public ${this.#isInheritedProperty(property) ? "new " : ""}${typeName}${isValueType(this.emitter.getProgram(), property.type) && (property.optional || nullable)
280
+ ? "?"
281
+ : ""} ${propertyName} { get; ${typeDefault ? "}" : "set; }"}${defaultValue ? ` = ${defaultValue};\n` : "\n"}
202
282
  `);
203
283
  }
204
284
  #findPropertyType(property) {
205
- switch (property.type.kind) {
285
+ return this.#getTypeInfoForTsType(property.type);
286
+ }
287
+ #isMultipartRequest(operation) {
288
+ const body = operation.parameters.body;
289
+ if (body === undefined)
290
+ return false;
291
+ return body.bodyKind === "multipart";
292
+ }
293
+ #hasMultipartOperation(iface) {
294
+ for (const [_, operation] of iface.operations) {
295
+ const [httpOp, _] = getHttpOperation(this.emitter.getProgram(), operation);
296
+ if (this.#isMultipartRequest(httpOp))
297
+ return true;
298
+ }
299
+ return false;
300
+ }
301
+ #getTypeInfoForUnion(union) {
302
+ const propResult = this.#getNonNullableTsType(union);
303
+ if (propResult === undefined) {
304
+ return [
305
+ code `${emitter.emitTypeReference(union)}`,
306
+ undefined,
307
+ [...union.variants.values()].filter((v) => isNullType(v.type)).length > 0,
308
+ ];
309
+ }
310
+ const [typeName, typeDefault, _] = this.#getTypeInfoForTsType(propResult.type);
311
+ return [typeName, typeDefault, propResult.nullable];
312
+ }
313
+ #getTypeInfoForTsType(tsType) {
314
+ function extractStringValue(type, span) {
315
+ switch (type.kind) {
316
+ case "String":
317
+ return type.value;
318
+ case "Boolean":
319
+ return `${type.value}`;
320
+ case "Number":
321
+ return type.valueAsString;
322
+ case "StringTemplateSpan":
323
+ if (type.isInterpolated) {
324
+ return extractStringValue(type.type, span);
325
+ }
326
+ else {
327
+ return type.type.value;
328
+ }
329
+ case "ModelProperty":
330
+ return extractStringValue(type.type, span);
331
+ case "EnumMember":
332
+ if (type.value === undefined)
333
+ return type.name;
334
+ if (typeof type.value === "string")
335
+ return type.value;
336
+ if (typeof type.value === "number")
337
+ return `${type.value}`;
338
+ }
339
+ reportDiagnostic(emitter.getProgram(), {
340
+ code: "invalid-interpolation",
341
+ target: span,
342
+ format: {},
343
+ });
344
+ return "";
345
+ }
346
+ switch (tsType.kind) {
206
347
  case "String":
207
- return [code `string`, `"${property.type.value}"`];
348
+ return [code `string`, `"${tsType.value}"`, false];
349
+ case "StringTemplate":
350
+ const template = tsType;
351
+ if (template.stringValue !== undefined)
352
+ return [code `string`, `"${template.stringValue}"`, false];
353
+ const spanResults = [];
354
+ for (const span of template.spans) {
355
+ spanResults.push(extractStringValue(span, span));
356
+ }
357
+ return [code `string`, `"${spanResults.join("")}"`, false];
208
358
  case "Boolean":
209
- return [code `bool`, `${property.type.value === true ? true : false}`];
359
+ return [code `bool`, `${tsType.value === true ? true : false}`, false];
210
360
  case "Number":
361
+ const [type, value] = this.#findNumericType(tsType);
362
+ return [code `${type}`, `${value}`, false];
363
+ case "Tuple":
364
+ const defaults = [];
365
+ const [csharpType, isObject] = this.#coalesceTypes(tsType.values);
366
+ if (isObject)
367
+ return ["object[]", undefined, false];
368
+ for (const value of tsType.values) {
369
+ const [_, itemDefault] = this.#getTypeInfoForTsType(value);
370
+ defaults.push(itemDefault);
371
+ }
372
+ return [
373
+ code `${csharpType.getTypeReference()}[]`,
374
+ `[${defaults.join(", ")}]`,
375
+ csharpType.isNullable,
376
+ ];
211
377
  case "Object":
212
- return [code `object`, undefined];
378
+ return [code `object`, undefined, false];
213
379
  case "Model":
214
- if (this.#isRecord(property.type)) {
215
- return [code `JsonObject`, undefined];
380
+ if (this.#isRecord(tsType)) {
381
+ return [code `JsonObject`, undefined, false];
216
382
  }
217
- return [code `${this.emitter.emitTypeReference(property.type)}`, undefined];
383
+ return [code `${emitter.emitTypeReference(tsType)}`, undefined, false];
384
+ case "ModelProperty":
385
+ return this.#getTypeInfoForTsType(tsType.type);
386
+ case "Enum":
387
+ return [code `${emitter.emitTypeReference(tsType)}`, undefined, false];
388
+ case "EnumMember":
389
+ if (typeof tsType.value === "number") {
390
+ const stringValue = tsType.value.toString();
391
+ if (stringValue.includes(".") || stringValue.includes("e"))
392
+ return ["double", stringValue, false];
393
+ return ["int", stringValue, false];
394
+ }
395
+ if (typeof tsType.value === "string") {
396
+ return ["string", tsType.value, false];
397
+ }
398
+ return [code `object`, undefined, false];
399
+ case "Union":
400
+ return this.#getTypeInfoForUnion(tsType);
401
+ case "UnionVariant":
402
+ return this.#getTypeInfoForTsType(tsType.type);
218
403
  default:
219
- return [code `${this.emitter.emitTypeReference(property.type)}`, undefined];
404
+ return [code `${emitter.emitTypeReference(tsType)}`, undefined, false];
220
405
  }
221
406
  }
407
+ #findNumericType(type) {
408
+ const stringValue = type.valueAsString;
409
+ if (stringValue.includes(".") || stringValue.includes("e"))
410
+ return ["double", stringValue];
411
+ return ["int", stringValue];
412
+ }
222
413
  modelPropertyReference(property) {
223
414
  return code `${this.emitter.emitTypeReference(property.type)}`;
224
415
  }
@@ -232,8 +423,7 @@ export async function $onEmit(context) {
232
423
  const doc = getDoc(this.emitter.getProgram(), iface);
233
424
  const attributes = getModelAttributes(this.emitter.getProgram(), iface, ifaceName);
234
425
  this.#metadateMap.set(iface, new CSharpType({ name: ifaceName, namespace: namespace }));
235
- const decl = this.emitter.result.declaration(ifaceName, code `${this.#licenseHeader}
236
- // <auto-generated />
426
+ const decl = this.emitter.result.declaration(ifaceName, code `${this.#generatedFileHeaderWithNullable}
237
427
 
238
428
  ${this.#emitUsings()}
239
429
 
@@ -252,7 +442,7 @@ export async function $onEmit(context) {
252
442
  sourceFile.meta[this.#sourceTypeKey] = CSharpSourceType.Interface;
253
443
  const ifaceNamespace = this.#getOrSetBaseNamespace(iface);
254
444
  const modelNamespace = `${ifaceNamespace}.Models`;
255
- const context = this.#createModelContext(ifaceNamespace, sourceFile);
445
+ const context = this.#createModelContext(ifaceNamespace, sourceFile, ifaceName);
256
446
  context.file.imports.set("System", ["System"]);
257
447
  context.file.imports.set("System.Net", ["System.Net"]);
258
448
  context.file.imports.set("System.Text.Json", ["System.Text.Json"]);
@@ -261,6 +451,11 @@ export async function $onEmit(context) {
261
451
  ]);
262
452
  context.file.imports.set("System.Threading.Tasks", ["System.Threading.Tasks"]);
263
453
  context.file.imports.set("Microsoft.AspNetCore.Mvc", ["Microsoft.AspNetCore.Mvc"]);
454
+ if (this.#hasMultipartOperation(iface)) {
455
+ context.file.imports.set("Microsoft.AspNetCore.WebUtilities", [
456
+ "Microsoft.AspNetCore.WebUtilities",
457
+ ]);
458
+ }
264
459
  context.file.imports.set(modelNamespace, [modelNamespace]);
265
460
  return context;
266
461
  }
@@ -269,6 +464,16 @@ export async function $onEmit(context) {
269
464
  const builder = new StringBuilder();
270
465
  const metadata = new HttpMetadata();
271
466
  const context = this.emitter.getContext();
467
+ const name = `${ensureCSharpIdentifier(this.emitter.getProgram(), iface, iface.name, NameCasingType.Class)}`;
468
+ const ifaceNamespace = this.#getOrSetBaseNamespace(iface);
469
+ const namespace = `${ifaceNamespace}`;
470
+ const mock = {
471
+ className: name,
472
+ interfaceName: `I${name}`,
473
+ methods: [],
474
+ namespace: namespace,
475
+ usings: [`${ifaceNamespace}.Models`],
476
+ };
272
477
  for (const [name, operation] of iface.operations) {
273
478
  const doc = getDoc(this.emitter.getProgram(), operation);
274
479
  const returnTypes = [];
@@ -279,10 +484,35 @@ export async function $onEmit(context) {
279
484
  const returnInfo = coalesceTypes(this.emitter.getProgram(), returnTypes, context.namespace);
280
485
  const returnType = returnInfo?.type || UnknownType;
281
486
  const opName = ensureCSharpIdentifier(this.emitter.getProgram(), operation, name, NameCasingType.Method);
282
- const opDecl = this.emitter.result.declaration(opName, code `${doc ? `${formatComment(doc)}\n` : ""}${returnType.name === "void" ? "Task" : `Task<${returnType.getTypeReference(context.scope)}>`} ${opName}Async( ${this.#emitInterfaceOperationParameters(operation, opName, "")});`);
487
+ let opDecl;
488
+ let opImpl;
489
+ if (this.#isMultipartRequest(httpOp)) {
490
+ opImpl = {
491
+ methodName: `${opName}Async`,
492
+ methodParams: `${this.#emitInterfaceOperationParameters(operation, "MultipartReader reader")}`,
493
+ returnType: `${returnType.name === "void" ? "Task" : `Task<${returnType.getTypeReference(context.scope)}>`}`,
494
+ instantiatedReturnType: returnType.name === "void"
495
+ ? undefined
496
+ : `${returnType.getTypeReference(context.scope)}`,
497
+ };
498
+ opDecl = this.emitter.result.declaration(opName, code `${doc ? `${formatComment(doc)}\n` : ""}${opImpl.returnType} ${opImpl.methodName}( ${opImpl.methodParams});`);
499
+ }
500
+ else {
501
+ opImpl = {
502
+ methodName: `${opName}Async`,
503
+ methodParams: `${this.#emitInterfaceOperationParameters(operation)}`,
504
+ returnType: `${returnType.name === "void" ? "Task" : `Task<${returnType.getTypeReference(context.scope)}>`}`,
505
+ instantiatedReturnType: returnType.name === "void"
506
+ ? undefined
507
+ : `${returnType.getTypeReference(context.scope)}`,
508
+ };
509
+ opDecl = this.emitter.result.declaration(opName, code `${doc ? `${formatComment(doc)}\n` : ""}${opImpl.returnType} ${opImpl.methodName}( ${opImpl.methodParams});`);
510
+ }
511
+ mock.methods.push(opImpl);
283
512
  builder.push(code `${opDecl.value}\n`);
284
513
  this.emitter.emitInterfaceOperation(operation);
285
514
  }
515
+ this.#mockRegistrations.set(mock.interfaceName, mock);
286
516
  return builder.reduce();
287
517
  }
288
518
  interfaceOperationDeclarationContext(operation) {
@@ -298,21 +528,32 @@ export async function $onEmit(context) {
298
528
  const operationName = ensureCSharpIdentifier(this.emitter.getProgram(), operation, name, NameCasingType.Method);
299
529
  const doc = getDoc(this.emitter.getProgram(), operation);
300
530
  const [httpOperation, _] = getHttpOperation(this.emitter.getProgram(), operation);
301
- const declParams = this.#emitHttpOperationParameters(httpOperation);
302
- const responseInfo = this.#getOperationResponse(httpOperation);
303
- let status = "200";
304
- let response = new CSharpType({
305
- name: "void",
306
- namespace: "System",
307
- isBuiltIn: true,
308
- isValueType: false,
309
- });
310
- if (responseInfo !== undefined) {
311
- [status, response] = responseInfo;
531
+ const multipart = this.#isMultipartRequest(httpOperation);
532
+ const declParams = !multipart
533
+ ? this.#emitHttpOperationParameters(httpOperation)
534
+ : this.#emitHttpOperationParameters(httpOperation, true);
535
+ if (multipart) {
536
+ const context = this.emitter.getContext();
537
+ context.file.imports.set("Microsoft.AspNetCore.WebUtilities", [
538
+ "Microsoft.AspNetCore.WebUtilities",
539
+ ]);
540
+ context.file.imports.set("Microsoft.AspNetCore.Http.Extensions", [
541
+ "Microsoft.AspNetCore.Http.Extensions",
542
+ ]);
312
543
  }
544
+ const responseInfo = this.#getOperationResponse(httpOperation);
545
+ const status = responseInfo?.statusCode ?? 200;
546
+ const response = responseInfo?.resultType ??
547
+ new CSharpType({
548
+ name: "void",
549
+ namespace: "System",
550
+ isBuiltIn: true,
551
+ isValueType: false,
552
+ });
313
553
  const hasResponseValue = response.name !== "void";
314
- const resultString = `${status === "204" ? "NoContent" : "Ok"}`;
315
- return this.emitter.result.declaration(operation.name, code `
554
+ const resultString = `${status === 204 ? "NoContent" : "Ok"}`;
555
+ if (!this.#isMultipartRequest(httpOperation)) {
556
+ return this.emitter.result.declaration(operation.name, code `
316
557
  ${doc ? `${formatComment(doc)}` : ""}
317
558
  [${getOperationVerbDecorator(httpOperation)}]
318
559
  [Route("${httpOperation.path}")]
@@ -320,11 +561,36 @@ export async function $onEmit(context) {
320
561
  public virtual async Task<IActionResult> ${operationName}(${declParams})
321
562
  {
322
563
  ${hasResponseValue
323
- ? `var result = await ${this.emitter.getContext().resourceName}Impl.${operationName}Async(${this.#emitOperationCallParameters(httpOperation)});
564
+ ? `var result = await ${this.emitter.getContext().resourceName}Impl.${operationName}Async(${this.#emitOperationCallParameters(httpOperation)});
324
565
  return ${resultString}(result);`
325
- : `await ${this.emitter.getContext().resourceName}Impl.${operationName}Async(${this.#emitOperationCallParameters(httpOperation)});
566
+ : `await ${this.emitter.getContext().resourceName}Impl.${operationName}Async(${this.#emitOperationCallParameters(httpOperation)});
326
567
  return ${resultString}();`}
327
568
  }`);
569
+ }
570
+ else {
571
+ return this.emitter.result.declaration(operation.name, code `
572
+ ${doc ? `${formatComment(doc)}` : ""}
573
+ [${getOperationVerbDecorator(httpOperation)}]
574
+ [Route("${httpOperation.path}")]
575
+ [Consumes("multipart/form-data")]
576
+ ${this.emitter.emitOperationReturnType(operation)}
577
+ public virtual async Task<IActionResult> ${operationName}(${declParams})
578
+ {
579
+ var boundary = Request.GetMultipartBoundary();
580
+ if (boundary == null)
581
+ {
582
+ return BadRequest("Request missing multipart boundary");
583
+ }
584
+
585
+
586
+ var reader = new MultipartReader(boundary, Request.Body);
587
+ ${hasResponseValue
588
+ ? `var result = await ${this.emitter.getContext().resourceName}Impl.${operationName}Async(${this.#emitOperationCallParameters(httpOperation, "reader")});
589
+ return ${resultString}(result);`
590
+ : `await ${this.emitter.getContext().resourceName}Impl.${operationName}Async(${this.#emitOperationCallParameters(httpOperation, "reader")});
591
+ return ${resultString}();`}
592
+ }`);
593
+ }
328
594
  }
329
595
  operationDeclarationContext(operation, name) {
330
596
  const resource = getResourceOperation(this.emitter.getProgram(), operation);
@@ -339,18 +605,16 @@ export async function $onEmit(context) {
339
605
  const [httpOperation, _] = getHttpOperation(this.emitter.getProgram(), operation);
340
606
  const declParams = this.#emitHttpOperationParameters(httpOperation);
341
607
  const responseInfo = this.#getOperationResponse(httpOperation);
342
- let status = "200";
343
- let response = new CSharpType({
344
- name: "void",
345
- namespace: "System",
346
- isBuiltIn: true,
347
- isValueType: false,
348
- });
349
- if (responseInfo !== undefined) {
350
- [status, response] = responseInfo;
351
- }
608
+ const status = responseInfo?.statusCode ?? 200;
609
+ const response = responseInfo?.resultType ??
610
+ new CSharpType({
611
+ name: "void",
612
+ namespace: "System",
613
+ isBuiltIn: true,
614
+ isValueType: false,
615
+ });
352
616
  const hasResponseValue = response.name !== "void";
353
- const resultString = `${status === "204" ? "NoContent" : "Ok"}`;
617
+ const resultString = `${status === 204 ? "NoContent" : "Ok"}`;
354
618
  return this.emitter.result.declaration(operation.name, code `
355
619
  ${doc ? `${formatComment(doc)}` : ""}
356
620
  [${getOperationVerbDecorator(httpOperation)}]
@@ -369,20 +633,27 @@ export async function $onEmit(context) {
369
633
  const [httpOperation, _] = getHttpOperation(this.emitter.getProgram(), operation);
370
634
  return this.#emitOperationResponses(httpOperation);
371
635
  }
636
+ stringTemplate(stringTemplate) {
637
+ return this.emitter.result.rawCode(stringTemplate.stringValue || "");
638
+ }
372
639
  #getOperationResponse(operation) {
373
640
  const validResponses = operation.responses.filter((r) => !isErrorModel(this.emitter.getProgram(), r.type) &&
374
641
  getCSharpStatusCode(r.statusCodes) !== undefined);
375
642
  if (validResponses.length < 1)
376
643
  return undefined;
377
644
  const response = validResponses[0];
378
- const statusCode = getCSharpStatusCode(response.statusCodes);
379
- if (statusCode === undefined)
645
+ const csharpStatusCode = getCSharpStatusCode(response.statusCodes);
646
+ if (csharpStatusCode === undefined)
380
647
  return undefined;
381
648
  const responseType = new HttpMetadata().resolveLogicalResponseType(this.emitter.getProgram(), response);
382
649
  const context = this.emitter.getContext();
383
650
  const result = getCSharpType(this.emitter.getProgram(), responseType, context.namespace);
384
651
  const resultType = result?.type || UnknownType;
385
- return [statusCode, resultType];
652
+ return {
653
+ csharpStatusCode,
654
+ resultType,
655
+ statusCode: response.statusCodes,
656
+ };
386
657
  }
387
658
  #emitOperationResponses(operation) {
388
659
  const builder = new StringBuilder();
@@ -397,7 +668,8 @@ export async function $onEmit(context) {
397
668
  }
398
669
  }
399
670
  for (const response of operation.responses) {
400
- this.emitter.emitType(response.type);
671
+ if (!isEmptyResponseModel(this.emitter.getProgram(), response.type))
672
+ this.emitter.emitType(response.type);
401
673
  }
402
674
  return builder.reduce();
403
675
  }
@@ -411,40 +683,63 @@ export async function $onEmit(context) {
411
683
  const resultType = result?.type || UnknownType;
412
684
  return resultType.getTypeReference(context.scope);
413
685
  }
414
- #emitInterfaceOperationParameters(operation, operationName, resourceName) {
686
+ #emitInterfaceOperationParameters(operation, bodyParam) {
415
687
  const signature = new StringBuilder();
416
688
  const requiredParams = [];
417
689
  const optionalParams = [];
418
690
  let totalParams = 0;
419
- for (const [_, parameter] of operation.parameters.properties) {
420
- if (parameter.optional)
421
- optionalParams.push(parameter);
422
- else
423
- requiredParams.push(parameter);
691
+ if (bodyParam !== undefined)
424
692
  totalParams++;
693
+ const validParams = [...operation.parameters.properties.entries()].filter(([_, p]) => isValidParameter(this.emitter.getProgram(), p));
694
+ for (const [_, parameter] of validParams) {
695
+ if (!isContentTypeHeader(this.emitter.getProgram(), parameter) &&
696
+ (bodyParam === undefined || isHttpMetadata(this.emitter.getProgram(), parameter))) {
697
+ if (parameter.optional || parameter.defaultValue)
698
+ optionalParams.push(parameter);
699
+ else
700
+ requiredParams.push(parameter);
701
+ totalParams++;
702
+ }
425
703
  }
426
704
  let i = 1;
427
705
  for (const requiredParam of requiredParams) {
428
- signature.push(code `${this.emitter.emitTypeReference(requiredParam.type)} ${ensureCSharpIdentifier(this.emitter.getProgram(), requiredParam, requiredParam.name, NameCasingType.Parameter)}${i++ < totalParams ? ", " : ""}`);
706
+ const [paramType, _, __] = this.#findPropertyType(requiredParam);
707
+ signature.push(code `${paramType} ${ensureCSharpIdentifier(this.emitter.getProgram(), requiredParam, requiredParam.name, NameCasingType.Parameter)}${i++ < totalParams ? ", " : ""}`);
708
+ }
709
+ if (bodyParam) {
710
+ signature.push(bodyParam);
711
+ if (i++ < totalParams)
712
+ signature.push(", ");
429
713
  }
430
714
  for (const optionalParam of optionalParams) {
431
- signature.push(code `${this.emitter.emitTypeReference(optionalParam.type)}? ${ensureCSharpIdentifier(this.emitter.getProgram(), optionalParam, optionalParam.name, NameCasingType.Parameter)}${i++ < totalParams ? ", " : ""}`);
715
+ const [paramType, _, __] = this.#findPropertyType(optionalParam);
716
+ signature.push(code `${paramType}? ${ensureCSharpIdentifier(this.emitter.getProgram(), optionalParam, optionalParam.name, NameCasingType.Parameter)}${i++ < totalParams ? ", " : ""}`);
432
717
  }
433
718
  return signature.reduce();
434
719
  }
435
- #emitHttpOperationParameters(operation) {
720
+ #emitHttpOperationParameters(operation, bodyParameter) {
436
721
  const signature = new StringBuilder();
437
722
  const bodyParam = operation.parameters.body;
438
723
  let i = 0;
439
724
  //const pathParameters = operation.parameters.parameters.filter((p) => p.type === "path");
440
- for (const parameter of operation.parameters.parameters) {
441
- i++;
442
- if (parameter.param.type.kind !== "Intrinsic" || parameter.param.type.name !== "never") {
443
- signature.push(code `${this.#emitOperationSignatureParameter(operation, parameter)}${i < operation.parameters.parameters.length || bodyParam !== undefined ? ", " : ""}`);
725
+ const validParams = operation.parameters.parameters.filter((p) => isValidParameter(this.emitter.getProgram(), p.param));
726
+ const requiredParams = validParams.filter((p) => p.type === "path" || (!p.param.optional && p.param.defaultValue === undefined));
727
+ const optionalParams = validParams.filter((p) => p.type !== "path" && (p.param.optional || p.param.defaultValue !== undefined));
728
+ for (const parameter of requiredParams) {
729
+ signature.push(code `${this.#emitOperationSignatureParameter(operation, parameter)}${++i < requiredParams.length ? ", " : ""}`);
730
+ }
731
+ if (requiredParams.length > 0 &&
732
+ (optionalParams.length > 0 || (bodyParameter === undefined && bodyParam !== undefined))) {
733
+ signature.push(code `, `);
734
+ }
735
+ if (bodyParameter === undefined) {
736
+ if (bodyParam !== undefined) {
737
+ signature.push(code `${this.emitter.emitTypeReference(this.#metaInfo.getEffectivePayloadType(bodyParam.type, Visibility.Create || Visibility.Update))} body${requiredParams.length > 0 && optionalParams.length > 0 ? ", " : ""}`);
444
738
  }
445
739
  }
446
- if (bodyParam !== undefined) {
447
- signature.push(code `${this.emitter.emitTypeReference(this.#metaInfo.getEffectivePayloadType(bodyParam.type, Visibility.Create & Visibility.Update))} body`);
740
+ i = 0;
741
+ for (const parameter of optionalParams) {
742
+ signature.push(code `${this.#emitOperationSignatureParameter(operation, parameter)}${++i < optionalParams.length ? ", " : ""}`);
448
743
  }
449
744
  return signature.reduce();
450
745
  }
@@ -457,8 +752,7 @@ export async function $onEmit(context) {
457
752
  const doc = getDoc(this.emitter.getProgram(), union);
458
753
  const attributes = getModelAttributes(program, union, unionName);
459
754
  this.#metadateMap.set(union, new CSharpType({ name: unionName, namespace: namespace }));
460
- return this.emitter.result.declaration(unionName, code `${this.#licenseHeader}
461
- // <auto-generated />
755
+ return this.emitter.result.declaration(unionName, code `${this.#generatedFileHeader}
462
756
 
463
757
  ${this.#emitUsings()}
464
758
 
@@ -521,29 +815,66 @@ export async function $onEmit(context) {
521
815
  const name = httpParam.param.name;
522
816
  const parameter = httpParam.param;
523
817
  const emittedName = ensureCSharpIdentifier(this.emitter.getProgram(), parameter, name, NameCasingType.Parameter);
524
- const [emittedType, emittedDefault] = this.#findPropertyType(parameter);
525
- // eslint-disable-next-line deprecation/deprecation
818
+ let [emittedType, emittedDefault, _] = this.#findPropertyType(parameter);
819
+ if (emittedType.toString().endsWith("[]"))
820
+ emittedDefault = undefined;
821
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
526
822
  const defaultValue = parameter.default
527
- ? // eslint-disable-next-line deprecation/deprecation
823
+ ? // eslint-disable-next-line @typescript-eslint/no-deprecated
528
824
  code `${this.emitter.emitType(parameter.default)}`
529
825
  : emittedDefault;
530
826
  return this.emitter.result.rawCode(code `${httpParam.type !== "path" ? this.#emitParameterAttribute(httpParam) : ""}${emittedType} ${emittedName}${defaultValue === undefined ? "" : ` = ${defaultValue}`}`);
531
827
  }
532
- #emitOperationCallParameters(operation) {
533
- const signature = new StringBuilder();
828
+ #getBodyParameters(operation) {
534
829
  const bodyParam = operation.parameters.body;
830
+ if (bodyParam === undefined)
831
+ return undefined;
832
+ if (bodyParam.property !== undefined)
833
+ return [bodyParam.property];
834
+ if (bodyParam.type.kind !== "Model" || bodyParam.type.properties.size < 1)
835
+ return undefined;
836
+ return [...bodyParam.type.properties.values()];
837
+ }
838
+ #emitOperationCallParameters(operation, bodyParameter = "body") {
839
+ const signature = new StringBuilder();
535
840
  let i = 0;
841
+ const bodyParameters = this.#getBodyParameters(operation);
536
842
  //const pathParameters = operation.parameters.parameters.filter((p) => p.type === "path");
537
- for (const parameter of operation.parameters.parameters) {
843
+ const valid = operation.parameters.parameters.filter((p) => isValidParameter(this.emitter.getProgram(), p.param));
844
+ const required = valid.filter((p) => p.type === "path" || (!p.param.optional && p.param.defaultValue === undefined));
845
+ const optional = valid.filter((p) => p.type !== "path" && (p.param.optional || p.param.defaultValue !== undefined));
846
+ for (const parameter of required) {
847
+ const contentType = isContentTypeHeader(this.emitter.getProgram(), parameter.param);
538
848
  i++;
539
849
  if (!isNeverType(parameter.param.type) &&
540
850
  !isNullType(parameter.param.type) &&
541
- !isVoidType(parameter.param.type)) {
542
- signature.push(code `${this.#emitOperationCallParameter(operation, parameter)}${i < operation.parameters.parameters.length || bodyParam !== undefined ? ", " : ""}`);
851
+ !isVoidType(parameter.param.type) &&
852
+ !contentType) {
853
+ signature.push(code `${this.#emitOperationCallParameter(operation, parameter)}${i < valid.length || bodyParameters !== undefined ? ", " : ""}`);
854
+ }
855
+ }
856
+ if (bodyParameters !== undefined) {
857
+ if (bodyParameters.length === 1) {
858
+ signature.push(code `${bodyParameter}`);
859
+ }
860
+ else {
861
+ let j = 0;
862
+ for (const parameter of bodyParameters) {
863
+ j++;
864
+ const propertyName = ensureCSharpIdentifier(this.emitter.getProgram(), parameter, parameter.name, NameCasingType.Property);
865
+ signature.push(code `${bodyParameter}?.${propertyName}${j < bodyParameters.length || i < valid.length ? ", " : ""}`);
866
+ }
543
867
  }
544
868
  }
545
- if (bodyParam !== undefined) {
546
- signature.push(code `body`);
869
+ for (const parameter of optional) {
870
+ const contentType = isContentTypeHeader(this.emitter.getProgram(), parameter.param);
871
+ i++;
872
+ if (!isNeverType(parameter.param.type) &&
873
+ !isNullType(parameter.param.type) &&
874
+ !isVoidType(parameter.param.type) &&
875
+ !contentType) {
876
+ signature.push(code `${this.#emitOperationCallParameter(operation, parameter)}${i < valid.length ? ", " : ""}`);
877
+ }
547
878
  }
548
879
  return signature.reduce();
549
880
  }
@@ -563,9 +894,10 @@ export async function $onEmit(context) {
563
894
  return "";
564
895
  }
565
896
  }
566
- #createModelContext(namespace, file) {
897
+ #createModelContext(namespace, file, name) {
567
898
  const context = {
568
899
  namespace: namespace,
900
+ name: name,
569
901
  file: file,
570
902
  scope: file.globalScope,
571
903
  };
@@ -580,7 +912,7 @@ export async function $onEmit(context) {
580
912
  let context = controllers.get(name);
581
913
  if (context !== undefined)
582
914
  return context;
583
- const sourceFile = this.emitter.createSourceFile(`controllers/${name}ControllerBase.cs`);
915
+ const sourceFile = this.emitter.createSourceFile(`controllers/${name}Controller.cs`);
584
916
  const namespace = this.#getOrSetBaseNamespace(operation);
585
917
  const modelNamespace = `${namespace}.Models`;
586
918
  sourceFile.meta[this.#sourceTypeKey] = CSharpSourceType.Controller;
@@ -607,6 +939,7 @@ export async function $onEmit(context) {
607
939
  controllers.set(name, context);
608
940
  return context;
609
941
  }
942
+ // eslint-disable-next-line no-unused-private-class-members
610
943
  #getNamespaceFullName(namespace) {
611
944
  return namespace
612
945
  ? ensureCSharpIdentifier(this.emitter.getProgram(), namespace, getNamespaceFullName(namespace))
@@ -621,8 +954,6 @@ export async function $onEmit(context) {
621
954
  return scalarType.getTypeReference(this.emitter.getContext().scope);
622
955
  }
623
956
  scalarDeclaration(scalar, name) {
624
- const foo = new Placeholder();
625
- foo.setValue;
626
957
  const scalarType = getCSharpTypeForScalar(this.emitter.getProgram(), scalar);
627
958
  return scalarType.getTypeReference(this.emitter.getContext().scope);
628
959
  }
@@ -631,6 +962,18 @@ export async function $onEmit(context) {
631
962
  if (sourceFile === libFile.source)
632
963
  return libFile.emitted;
633
964
  }
965
+ if (this.#emitMocks === "all") {
966
+ for (const helper of this.#mockHelpers) {
967
+ if (sourceFile === helper.source)
968
+ return helper.emitted;
969
+ }
970
+ }
971
+ if (this.#mockFiles.length > 0) {
972
+ for (const mock of this.#mockFiles) {
973
+ if (sourceFile === mock.source)
974
+ return mock.emitted;
975
+ }
976
+ }
634
977
  const emittedSourceFile = {
635
978
  path: sourceFile.path,
636
979
  contents: "",
@@ -655,17 +998,21 @@ export async function $onEmit(context) {
655
998
  #emitControllerContents(file) {
656
999
  const namespace = file.meta.namespace;
657
1000
  const contents = new StringBuilder();
658
- contents.push(`${this.#licenseHeader}\n`);
659
- contents.push("// <auto-generated />\n\n");
1001
+ contents.push(`${this.#generatedFileHeader}\n\n`);
660
1002
  contents.push(code `${this.#emitUsings(file)}\n`);
661
1003
  contents.push("\n");
662
1004
  contents.push(`namespace ${namespace}.Controllers\n`);
663
1005
  contents.push("{\n");
664
1006
  contents.push("[ApiController]\n");
665
- contents.push(`public abstract partial class ${file.meta["resource"]}Base: ControllerBase\n`);
1007
+ contents.push(`public partial class ${file.meta["resource"]}: ControllerBase\n`);
1008
+ contents.push("{\n");
1009
+ contents.push("\n");
1010
+ contents.push(`public ${file.meta["resource"]}(I${file.meta.resourceName} operations)\n`);
666
1011
  contents.push("{\n");
1012
+ contents.push(` ${file.meta.resourceName}Impl = operations;\n`);
1013
+ contents.push("}");
667
1014
  contents.push("\n");
668
- contents.push(code `internal abstract I${file.meta.resourceName} ${file.meta.resourceName}Impl { get;}\n`);
1015
+ contents.push(code `internal virtual I${file.meta.resourceName} ${file.meta.resourceName}Impl { get;}\n`);
669
1016
  for (const decl of file.globalScope.declarations) {
670
1017
  contents.push(decl.value + "\n");
671
1018
  }
@@ -695,6 +1042,8 @@ export async function $onEmit(context) {
695
1042
  }
696
1043
  return current;
697
1044
  }
1045
+ // TODO: remove?
1046
+ // eslint-disable-next-line no-unused-private-class-members
698
1047
  #getTemplateParameters(model) {
699
1048
  if (!model.templateMapper)
700
1049
  return "";
@@ -715,40 +1064,91 @@ export async function $onEmit(context) {
715
1064
  return "";
716
1065
  }
717
1066
  #coalesceUnionTypes(union) {
718
- const defaultValue = new CSharpType({
719
- name: "object",
720
- namespace: "System",
721
- isValueType: false,
722
- });
1067
+ const [result, _] = this.#coalesceTypes([...union.variants.values()].flatMap((v) => v.type));
1068
+ return result;
1069
+ }
1070
+ #getNonNullableTsType(union) {
1071
+ const types = [...union.variants.values()];
1072
+ const nulls = types.flatMap((v) => v.type).filter((t) => isNullType(t));
1073
+ const nonNulls = types.flatMap((v) => v.type).filter((t) => !isNullType(t));
1074
+ if (nonNulls.length === 1)
1075
+ return { type: nonNulls[0], nullable: nulls.length > 0 };
1076
+ return undefined;
1077
+ }
1078
+ #coalesceTypes(types) {
1079
+ const defaultValue = [
1080
+ new CSharpType({
1081
+ name: "object",
1082
+ namespace: "System",
1083
+ isValueType: false,
1084
+ }),
1085
+ true,
1086
+ ];
723
1087
  let current = undefined;
724
- for (const [_, variant] of union.variants.entries()) {
725
- let candidate;
726
- switch (variant.type.kind) {
1088
+ let nullable = false;
1089
+ for (const type of types) {
1090
+ let candidate = undefined;
1091
+ switch (type.kind) {
727
1092
  case "Boolean":
728
1093
  candidate = new CSharpType({ name: "bool", namespace: "System", isValueType: true });
729
1094
  break;
1095
+ case "StringTemplate":
730
1096
  case "String":
731
1097
  candidate = new CSharpType({ name: "string", namespace: "System", isValueType: false });
732
1098
  break;
1099
+ case "Number":
1100
+ const stringValue = type.valueAsString;
1101
+ if (stringValue.includes(".") || stringValue.includes("e")) {
1102
+ candidate = new CSharpType({
1103
+ name: "double",
1104
+ namespace: "System",
1105
+ isValueType: true,
1106
+ });
1107
+ }
1108
+ else {
1109
+ candidate = new CSharpType({ name: "int", namespace: "System", isValueType: true });
1110
+ }
1111
+ break;
733
1112
  case "Union":
734
- candidate = this.#coalesceUnionTypes(variant.type);
1113
+ candidate = this.#coalesceUnionTypes(type);
735
1114
  break;
736
1115
  case "Scalar":
737
- candidate = getCSharpTypeForScalar(this.emitter.getProgram(), variant.type);
1116
+ candidate = getCSharpTypeForScalar(this.emitter.getProgram(), type);
1117
+ break;
1118
+ case "Intrinsic":
1119
+ if (isNullType(type)) {
1120
+ nullable = true;
1121
+ candidate = current;
1122
+ }
1123
+ else {
1124
+ return defaultValue;
1125
+ }
738
1126
  break;
739
1127
  default:
740
1128
  return defaultValue;
741
1129
  }
742
1130
  current = current ?? candidate;
743
- if (current === undefined || !candidate.equals(current))
1131
+ if (current === undefined || (candidate !== undefined && !candidate.equals(current)))
744
1132
  return defaultValue;
745
1133
  }
746
- return current ?? defaultValue;
1134
+ if (current !== undefined && nullable)
1135
+ current.isNullable = true;
1136
+ return current === undefined ? defaultValue : [current, false];
747
1137
  }
748
1138
  writeOutput(sourceFiles) {
749
1139
  for (const source of this.#libraryFiles) {
750
1140
  sourceFiles.push(source.source);
751
1141
  }
1142
+ if (this.#emitMocks === "all") {
1143
+ for (const helper of this.#mockHelpers) {
1144
+ sourceFiles.push(helper.source);
1145
+ }
1146
+ if (this.#mockRegistrations.size > 0) {
1147
+ const mocks = getBusinessLogicImplementations(this.emitter, this.#mockRegistrations);
1148
+ this.#mockFiles.push(...mocks);
1149
+ sourceFiles.push(...mocks.flatMap((l) => l.source));
1150
+ }
1151
+ }
752
1152
  const emittedSourceFiles = [];
753
1153
  for (const source of sourceFiles) {
754
1154
  switch (this.#emitterOutputType) {
@@ -785,11 +1185,34 @@ export async function $onEmit(context) {
785
1185
  return this.#baseNamespace;
786
1186
  }
787
1187
  }
788
- function processNameSpace(program, target) {
789
- const service = getService(program, target);
1188
+ function isEmptyResponseModel(program, model) {
1189
+ if (model.kind !== "Model")
1190
+ return false;
1191
+ return model.properties.size === 1 && isStatusCode(program, [...model.properties.values()][0]);
1192
+ }
1193
+ function isContentTypeHeader(program, parameter) {
1194
+ return (isHeader(program, parameter) &&
1195
+ (parameter.name === "contentType" ||
1196
+ getHeaderFieldName(program, parameter) === "Content-type"));
1197
+ }
1198
+ function isValidParameter(program, parameter) {
1199
+ return (!isContentTypeHeader(program, parameter) &&
1200
+ (parameter.type.kind !== "Intrinsic" || parameter.type.name !== "never"));
1201
+ }
1202
+ /** Determine whether the given parameter is http metadata */
1203
+ function isHttpMetadata(program, property) {
1204
+ return (isPathParam(program, property) ||
1205
+ isHeader(program, property) ||
1206
+ isQueryParam(program, property));
1207
+ }
1208
+ function processNameSpace(program, target, service) {
1209
+ if (!service)
1210
+ service = getService(program, target);
790
1211
  if (service) {
791
1212
  for (const [_, model] of target.models) {
792
- emitter.emitType(model);
1213
+ if (!isTemplateDeclaration(model) && !isEmptyResponseModel(program, model)) {
1214
+ emitter.emitType(model);
1215
+ }
793
1216
  }
794
1217
  for (const [_, en] of target.enums) {
795
1218
  emitter.emitType(en);
@@ -798,10 +1221,44 @@ export async function $onEmit(context) {
798
1221
  emitter.emitType(sc);
799
1222
  }
800
1223
  for (const [_, iface] of target.interfaces) {
801
- emitter.emitType(iface);
1224
+ if (!isTemplateDeclaration(iface)) {
1225
+ emitter.emitType(iface);
1226
+ }
1227
+ }
1228
+ if (target.operations.size > 0) {
1229
+ // Collect interface operations for a business logic interface and controller
1230
+ const nsOps = [];
1231
+ for (const [name, op] of target.operations) {
1232
+ if (!isTemplateDeclaration(op)) {
1233
+ nsOps.push([name, op]);
1234
+ }
1235
+ }
1236
+ const iface = program.checker.createAndFinishType({
1237
+ node: undefined,
1238
+ sourceInterfaces: [],
1239
+ decorators: [],
1240
+ operations: createRekeyableMap(nsOps),
1241
+ kind: "Interface",
1242
+ name: `${target.name}Operations`,
1243
+ namespace: target,
1244
+ entityKind: "Type",
1245
+ isFinished: true,
1246
+ });
1247
+ try {
1248
+ for (const [_, op] of nsOps) {
1249
+ op.interface = iface;
1250
+ }
1251
+ emitter.emitType(iface);
1252
+ }
1253
+ finally {
1254
+ for (const [_, op] of nsOps) {
1255
+ op.interface = undefined;
1256
+ }
1257
+ target.interfaces.delete(iface.name);
1258
+ }
802
1259
  }
803
- for (const [_, op] of target.operations) {
804
- emitter.emitType(op);
1260
+ for (const [_, sub] of target.namespaces) {
1261
+ processNameSpace(program, sub, service);
805
1262
  }
806
1263
  }
807
1264
  else {
@@ -814,16 +1271,18 @@ export async function $onEmit(context) {
814
1271
  const ns = context.program.checker.getGlobalNamespaceType();
815
1272
  const options = emitter.getOptions();
816
1273
  processNameSpace(context.program, ns);
817
- await ensureCleanDirectory(context.program, options.emitterOutputDir);
818
- await emitter.writeOutput();
819
- if (options["skip-format"] === undefined || options["skip-format"] === false) {
820
- await execFile("dotnet", [
821
- "format",
822
- "whitespace",
823
- emitter.getOptions().emitterOutputDir,
824
- "--include-generated",
825
- "--folder",
826
- ]);
1274
+ if (!doNotEmit) {
1275
+ await ensureCleanDirectory(context.program, options.emitterOutputDir);
1276
+ await emitter.writeOutput();
1277
+ if (options["skip-format"] === undefined || options["skip-format"] === false) {
1278
+ await execFile("dotnet", [
1279
+ "format",
1280
+ "whitespace",
1281
+ emitter.getOptions().emitterOutputDir,
1282
+ "--include-generated",
1283
+ "--folder",
1284
+ ]);
1285
+ }
827
1286
  }
828
1287
  }
829
1288
  //# sourceMappingURL=service.js.map