@typespec/http-server-csharp 0.58.0-alpha.9 → 0.58.0-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 (46) hide show
  1. package/README.md +69 -6
  2. package/dist/src/cli/cli.js +92 -43
  3. package/dist/src/cli/cli.js.map +1 -1
  4. package/dist/src/lib/attributes.d.ts.map +1 -1
  5. package/dist/src/lib/attributes.js +65 -31
  6. package/dist/src/lib/attributes.js.map +1 -1
  7. package/dist/src/lib/boilerplate.d.ts +1 -1
  8. package/dist/src/lib/boilerplate.d.ts.map +1 -1
  9. package/dist/src/lib/boilerplate.js +153 -31
  10. package/dist/src/lib/boilerplate.js.map +1 -1
  11. package/dist/src/lib/doc.d.ts +5 -0
  12. package/dist/src/lib/doc.d.ts.map +1 -0
  13. package/dist/src/lib/doc.js +237 -0
  14. package/dist/src/lib/doc.js.map +1 -0
  15. package/dist/src/lib/interfaces.d.ts +48 -4
  16. package/dist/src/lib/interfaces.d.ts.map +1 -1
  17. package/dist/src/lib/interfaces.js +89 -28
  18. package/dist/src/lib/interfaces.js.map +1 -1
  19. package/dist/src/lib/lib.d.ts +32 -3
  20. package/dist/src/lib/lib.d.ts.map +1 -1
  21. package/dist/src/lib/lib.js +57 -2
  22. package/dist/src/lib/lib.js.map +1 -1
  23. package/dist/src/lib/project.d.ts +5 -0
  24. package/dist/src/lib/project.d.ts.map +1 -0
  25. package/dist/src/lib/project.js +101 -0
  26. package/dist/src/lib/project.js.map +1 -0
  27. package/dist/src/lib/scaffolding.d.ts +7 -5
  28. package/dist/src/lib/scaffolding.d.ts.map +1 -1
  29. package/dist/src/lib/scaffolding.js +113 -40
  30. package/dist/src/lib/scaffolding.js.map +1 -1
  31. package/dist/src/lib/service.d.ts.map +1 -1
  32. package/dist/src/lib/service.js +496 -587
  33. package/dist/src/lib/service.js.map +1 -1
  34. package/dist/src/lib/type-helpers.d.ts +5 -1
  35. package/dist/src/lib/type-helpers.d.ts.map +1 -1
  36. package/dist/src/lib/type-helpers.js +32 -3
  37. package/dist/src/lib/type-helpers.js.map +1 -1
  38. package/dist/src/lib/utils.d.ts +79 -7
  39. package/dist/src/lib/utils.d.ts.map +1 -1
  40. package/dist/src/lib/utils.js +971 -36
  41. package/dist/src/lib/utils.js.map +1 -1
  42. package/package.json +39 -26
  43. package/dist/src/lib/testing/index.d.ts +0 -3
  44. package/dist/src/lib/testing/index.d.ts.map +0 -1
  45. package/dist/src/lib/testing/index.js +0 -7
  46. package/dist/src/lib/testing/index.js.map +0 -1
@@ -1,52 +1,138 @@
1
- import { getDoc, getNamespaceFullName, getService, isErrorModel, isNeverType, isNullType, isTemplateDeclaration, isVoidType, } from "@typespec/compiler";
2
- import { CodeTypeEmitter, StringBuilder, code, createAssetEmitter, } from "@typespec/compiler/emitter-framework";
1
+ import { CodeTypeEmitter, StringBuilder, code, createAssetEmitter, } from "@typespec/asset-emitter";
2
+ import { getDoc, getNamespaceFullName, getService, isErrorModel, isNeverType, isNullType, isNumericType, isTemplateDeclaration, isVoidType, resolvePath, serializeValueAsJson, } from "@typespec/compiler";
3
3
  import { createRekeyableMap } from "@typespec/compiler/utils";
4
- import { Visibility, createMetadataInfo, getHeaderFieldName, getHttpOperation, getHttpPart, isHeader, isPathParam, isQueryParam, isStatusCode, } from "@typespec/http";
4
+ import { getHeaderFieldName, getHttpOperation, getHttpPart, isHeader, isStatusCode, } from "@typespec/http";
5
+ import { getUniqueItems } from "@typespec/json-schema";
5
6
  import { getResourceOperation } from "@typespec/rest";
6
7
  import { execFile } from "child_process";
8
+ import path from "path";
9
+ import { getEncodedNameAttribute } from "./attributes.js";
7
10
  import { GeneratedFileHeader, GeneratedFileHeaderWithNullable, getSerializationSourceFiles, } from "./boilerplate.js";
8
- import { CSharpSourceType, CSharpType, NameCasingType, } from "./interfaces.js";
9
- import { reportDiagnostic } from "./lib.js";
11
+ import { getProjectDocs } from "./doc.js";
12
+ import { CSharpSourceType, CSharpType, CollectionType, NameCasingType, checkOrAddNamespaceToScope, } from "./interfaces.js";
13
+ import { CSharpServiceOptions, reportDiagnostic } from "./lib.js";
14
+ import { getProjectHelpers } from "./project.js";
10
15
  import { getBusinessLogicImplementations, getScaffoldingHelpers, } from "./scaffolding.js";
11
- import { getRecordType, isKnownReferenceType } from "./type-helpers.js";
12
- import { HttpMetadata, UnknownType, coalesceTypes, ensureCSharpIdentifier, ensureCleanDirectory, formatComment, getCSharpIdentifier, getCSharpStatusCode, getCSharpType, getCSharpTypeForIntrinsic, getCSharpTypeForScalar, getModelAttributes, getModelInstantiationName, getOperationVerbDecorator, isValueType, } from "./utils.js";
16
+ import { getEnumType, getRecordType, isKnownReferenceType } from "./type-helpers.js";
17
+ import { CSharpOperationHelpers, HttpMetadata, ModelInfo, UnknownType, coalesceTypes, coalesceUnionTypes, ensureCSharpIdentifier, ensureCleanDirectory, formatComment, getBusinessLogicCallParameters, getBusinessLogicDeclParameters, getCSharpIdentifier, getCSharpStatusCode, getCSharpType, getCSharpTypeForIntrinsic, getCSharpTypeForScalar, getControllerReturnStatement, getFreePort, getHttpDeclParameters, getImports, getModelAttributes, getModelDeclarationName, getModelInstantiationName, getOpenApiConfig, getOperationVerbDecorator, getStatusCode, isEmptyResponseModel, isRecord, isStringEnumType, isValueType, resolveReferenceFromScopes, } from "./utils.js";
13
18
  export async function $onEmit(context) {
14
19
  let _unionCounter = 0;
15
20
  const controllers = new Map();
16
21
  const NoResourceContext = "RPCOperations";
17
- const doNotEmit = context.program.compilerOptions.noEmit || false;
22
+ const doNotEmit = context.program.compilerOptions.dryRun || false;
23
+ CSharpServiceOptions.getInstance().initialize(context.options);
24
+ function getFileWriter(program) {
25
+ return async (path) => !!(await program.host.stat(resolvePath(path)).catch((_) => false));
26
+ }
18
27
  class CSharpCodeEmitter extends CodeTypeEmitter {
19
28
  #metadateMap = new Map();
20
29
  #generatedFileHeaderWithNullable = GeneratedFileHeaderWithNullable;
21
30
  #generatedFileHeader = GeneratedFileHeader;
22
31
  #sourceTypeKey = "sourceType";
23
- #libraryFiles = getSerializationSourceFiles(this.emitter);
24
- #baseNamespace = undefined;
32
+ #nsKey = "ResolvedNamespace";
33
+ #namespaces = new Map();
25
34
  #emitterOutputType = context.options["output-type"];
26
35
  #emitMocks = context.options["emit-mocks"];
27
36
  #useSwagger = context.options["use-swaggerui"] || false;
28
37
  #openapiPath = context.options["openapi-path"] || "openapi/openapi.yaml";
29
38
  #mockRegistrations = new Map();
30
- #mockHelpers = this.#emitMocks === "all"
31
- ? getScaffoldingHelpers(this.emitter, this.#useSwagger, this.#openapiPath)
32
- : [];
33
- #mockFiles = [];
34
- #metaInfo = createMetadataInfo(this.emitter.getProgram(), {
35
- canonicalVisibility: Visibility.Read,
36
- canShareProperty: (p) => true,
37
- });
39
+ #opHelpers = new CSharpOperationHelpers(this.emitter);
40
+ #fileExists = getFileWriter(this.emitter.getProgram());
41
+ #getOrAddNamespaceForType(type) {
42
+ const program = this.emitter.getProgram();
43
+ switch (type.kind) {
44
+ case "Boolean":
45
+ case "String":
46
+ case "StringTemplate":
47
+ case "Number":
48
+ case "Scalar":
49
+ case "Intrinsic":
50
+ case "FunctionParameter":
51
+ case "ScalarConstructor":
52
+ case "StringTemplateSpan":
53
+ case "TemplateParameter":
54
+ case "Tuple":
55
+ case "FunctionType":
56
+ return undefined;
57
+ case "EnumMember":
58
+ return this.#getOrAddNamespaceForType(type.enum);
59
+ case "ModelProperty":
60
+ case "UnionVariant":
61
+ return this.#getOrAddNamespaceForType(type.type);
62
+ case "Model":
63
+ if (isRecord(type) && type.indexer)
64
+ return this.#getOrAddNamespaceForType(type.indexer.value);
65
+ if (type.indexer !== undefined && isNumericType(program, type.indexer?.key))
66
+ return this.#getOrAddNamespaceForType(type.indexer.value);
67
+ return this.#getOrAddNamespace(type.namespace);
68
+ case "Union":
69
+ if (isStringEnumType(program, type))
70
+ return this.#getOrAddNamespace(type.namespace);
71
+ const unionType = this.#coalesceTsUnion(type);
72
+ if (unionType === undefined)
73
+ return undefined;
74
+ return this.#getOrAddNamespaceForType(unionType);
75
+ default:
76
+ return this.#getOrAddNamespace(type.namespace);
77
+ }
78
+ }
79
+ #coalesceTsUnion(union) {
80
+ let result = undefined;
81
+ for (const [_, variant] of union.variants) {
82
+ if (!isNullType(variant.type)) {
83
+ if (result !== undefined && result !== variant.type)
84
+ return undefined;
85
+ result = variant.type;
86
+ }
87
+ }
88
+ return result;
89
+ }
90
+ #getOrAddNamespace(typespecNamespace) {
91
+ if (!typespecNamespace?.name)
92
+ return this.#getDefaultNamespace();
93
+ let resolvedNamespace = this.#namespaces.get(typespecNamespace);
94
+ if (resolvedNamespace !== undefined)
95
+ return resolvedNamespace;
96
+ const nsName = getNamespaceFullName(typespecNamespace);
97
+ resolvedNamespace = ensureCSharpIdentifier(this.emitter.getProgram(), typespecNamespace, nsName, NameCasingType.Namespace);
98
+ this.#namespaces.set(typespecNamespace, resolvedNamespace);
99
+ return resolvedNamespace;
100
+ }
101
+ #getDefaultNamespace() {
102
+ return "TypeSpec.Service";
103
+ }
38
104
  arrayDeclaration(array, name, elementType) {
39
- return this.emitter.result.declaration(ensureCSharpIdentifier(this.emitter.getProgram(), array, name), code `${this.emitter.emitTypeReference(elementType)}[]`);
105
+ return this.collectionDeclaration(elementType, array);
40
106
  }
41
107
  arrayLiteral(array, elementType) {
42
- return this.emitter.result.rawCode(code `${this.emitter.emitTypeReference(elementType)}[]`);
108
+ this.emitter.emitType(elementType, this.emitter.getContext());
109
+ const csType = getCSharpType(this.emitter.getProgram(), array, this.#getOrAddNamespaceForType(elementType));
110
+ if (csType?.type) {
111
+ return code `${csType.type.getTypeReference(this.emitter.getContext()?.scope)}`;
112
+ }
113
+ return this.collectionDeclaration(elementType, array);
114
+ }
115
+ collectionDeclaration(elementType, array) {
116
+ const isUniqueItems = getUniqueItems(this.emitter.getProgram(), array);
117
+ const collectionType = isUniqueItems
118
+ ? CollectionType.ISet
119
+ : CSharpServiceOptions.getInstance().collectionType;
120
+ switch (collectionType) {
121
+ case CollectionType.ISet:
122
+ return code `ISet<${this.emitter.emitTypeReference(elementType, this.emitter.getContext())}>`;
123
+ case CollectionType.IEnumerable:
124
+ return code `IEnumerable<${this.emitter.emitTypeReference(elementType, this.emitter.getContext())}>`;
125
+ case CollectionType.Array:
126
+ default:
127
+ return code `${this.emitter.emitTypeReference(elementType, this.emitter.getContext())}[]`;
128
+ }
43
129
  }
44
130
  booleanLiteral(boolean) {
45
- return this.emitter.result.rawCode(code `${boolean.value === true ? "true" : "false"}`);
131
+ return code `${boolean.value === true ? "true" : "false"}`;
46
132
  }
47
133
  unionLiteral(union) {
48
- const csType = this.#coalesceUnionTypes(union);
49
- return this.emitter.result.rawCode(csType && csType.isBuiltIn ? csType.name : "object");
134
+ const csType = coalesceUnionTypes(this.emitter.getProgram(), union);
135
+ return this.emitter.result.rawCode(csType ? csType.getTypeReference(this.emitter.getContext()?.scope) : "object");
50
136
  }
51
137
  declarationName(declarationType) {
52
138
  switch (declarationType.kind) {
@@ -60,7 +146,7 @@ export async function $onEmit(context) {
60
146
  return getCSharpIdentifier(declarationType.name, NameCasingType.Class);
61
147
  case "Model":
62
148
  if (!declarationType.name)
63
- return `Model${_unionCounter++}`;
149
+ return getModelDeclarationName(this.emitter.getProgram(), declarationType, `${_unionCounter++}`);
64
150
  return getCSharpIdentifier(declarationType.name, NameCasingType.Class);
65
151
  case "Operation":
66
152
  return getCSharpIdentifier(declarationType.name, NameCasingType.Class);
@@ -74,16 +160,15 @@ export async function $onEmit(context) {
74
160
  }
75
161
  }
76
162
  enumDeclaration(en, name) {
163
+ if (getEnumType(en) === "double")
164
+ return "";
77
165
  const program = this.emitter.getProgram();
78
166
  const enumName = ensureCSharpIdentifier(program, en, name);
79
167
  const namespace = this.emitter.getContext().namespace;
80
168
  const doc = getDoc(this.emitter.getProgram(), en);
81
169
  const attributes = getModelAttributes(program, en, enumName);
82
170
  this.#metadateMap.set(en, new CSharpType({ name: enumName, namespace: namespace }));
83
- return this.emitter.result.declaration(enumName, code `${this.#generatedFileHeader}
84
-
85
- ${this.#emitUsings()}
86
-
171
+ return this.emitter.result.declaration(enumName, code `
87
172
  namespace ${namespace}
88
173
  {
89
174
 
@@ -91,40 +176,48 @@ export async function $onEmit(context) {
91
176
  ${attributes.map((attribute) => attribute.getApplicationString(this.emitter.getContext().scope)).join("\n")}
92
177
  public enum ${enumName}
93
178
  {
94
- ${this.emitter.emitEnumMembers(en)};
179
+ ${this.emitter.emitEnumMembers(en)}
95
180
  }
96
181
  } `);
97
182
  }
98
183
  enumDeclarationContext(en) {
184
+ if (getEnumType(en) === "double")
185
+ return this.emitter.getContext();
99
186
  const enumName = ensureCSharpIdentifier(this.emitter.getProgram(), en, en.name);
100
- const enumFile = this.emitter.createSourceFile(`models/${enumName}.cs`);
187
+ const enumFile = this.emitter.createSourceFile(`generated/models/${enumName}.cs`);
101
188
  enumFile.meta[this.#sourceTypeKey] = CSharpSourceType.Model;
102
- const enumNamespace = `${this.#getOrSetBaseNamespace(en)}.Models`;
103
- return {
104
- namespace: enumNamespace,
105
- file: enumFile,
106
- scope: enumFile.globalScope,
107
- };
189
+ const enumNamespace = this.#getOrAddNamespace(en.namespace);
190
+ return this.#createEnumContext(enumNamespace, enumFile, enumName);
108
191
  }
109
192
  enumMembers(en) {
193
+ const enumType = getEnumType(en);
110
194
  const result = new StringBuilder();
111
195
  let i = 0;
112
196
  for (const [name, member] of en.members) {
113
197
  i++;
114
198
  const memberName = ensureCSharpIdentifier(this.emitter.getProgram(), member, name);
115
199
  this.#metadateMap.set(member, { name: memberName });
116
- result.push(code `${ensureCSharpIdentifier(this.emitter.getProgram(), member, name)} = "${member.value ? member.value : name}"`);
117
- if (i < en.members.size)
118
- result.pushLiteralSegment(", ");
200
+ if (enumType === "string") {
201
+ result.push(code `
202
+ [JsonStringEnumMemberName("${member.value ? member.value : name}")]
203
+ ${ensureCSharpIdentifier(this.emitter.getProgram(), member, name)}`);
204
+ if (i < en.members.size)
205
+ result.pushLiteralSegment(",\n");
206
+ }
207
+ else if (member.value !== undefined) {
208
+ result.push(code `
209
+ ${ensureCSharpIdentifier(this.emitter.getProgram(), member, name)} = ${member.value.toString()}`);
210
+ if (i < en.members.size)
211
+ result.pushLiteralSegment(",\n");
212
+ }
119
213
  }
120
214
  return this.emitter.result.rawCode(result.reduce());
121
215
  }
122
216
  intrinsic(intrinsic, name) {
123
217
  switch (intrinsic.name) {
124
218
  case "unknown":
125
- return this.emitter.result.rawCode(code `System.Text.Json.Nodes.JsonNode`);
126
219
  case "null":
127
- return this.emitter.result.rawCode(code `System.Text.Json.Nodes.JsonNode`);
220
+ return this.emitter.result.rawCode(code `${UnknownType.getTypeReference(this.emitter.getContext().scope)}`);
128
221
  case "ErrorType":
129
222
  case "never":
130
223
  reportDiagnostic(this.emitter.getProgram(), {
@@ -135,7 +228,7 @@ export async function $onEmit(context) {
135
228
  return "";
136
229
  case "void":
137
230
  const type = getCSharpTypeForIntrinsic(this.emitter.getProgram(), intrinsic);
138
- return this.emitter.result.rawCode(`${type?.type.getTypeReference()}`);
231
+ return this.emitter.result.rawCode(`${type?.type.getTypeReference(this.emitter.getContext().scope)}`);
139
232
  }
140
233
  }
141
234
  #emitUsings(file) {
@@ -153,39 +246,143 @@ export async function $onEmit(context) {
153
246
  parts.forEach((p) => this.emitter.emitType(p));
154
247
  return "";
155
248
  }
156
- const className = ensureCSharpIdentifier(this.emitter.getProgram(), model, name);
249
+ const isErrorType = isErrorModel(this.emitter.getProgram(), model);
250
+ if (model.baseModel && model.baseModel.namespace !== model.namespace) {
251
+ const resolvedNs = this.#getOrAddNamespaceForType(model.baseModel);
252
+ if (resolvedNs)
253
+ checkOrAddNamespaceToScope(resolvedNs, this.emitter.getContext().scope);
254
+ }
255
+ const baseModelRef = model.baseModel
256
+ ? code `: ${this.emitter.emitTypeReference(model.baseModel, this.emitter.getContext())}`
257
+ : "";
258
+ const baseClass = baseModelRef || (isErrorType ? ": HttpServiceException" : "");
157
259
  const namespace = this.emitter.getContext().namespace;
260
+ const className = ensureCSharpIdentifier(this.emitter.getProgram(), model, name);
158
261
  const doc = getDoc(this.emitter.getProgram(), model);
159
262
  const attributes = getModelAttributes(this.emitter.getProgram(), model, className);
263
+ const exceptionConstructor = isErrorType
264
+ ? this.getModelExceptionConstructor(this.emitter.getProgram(), model, name, className)
265
+ : "";
160
266
  this.#metadateMap.set(model, new CSharpType({ name: className, namespace: namespace }));
161
- const decl = this.emitter.result.declaration(className, code `${this.#generatedFileHeader}
162
-
163
- ${this.#emitUsings()}
164
-
267
+ const decl = this.emitter.result.declaration(className, code `
165
268
  namespace ${namespace} {
166
269
 
167
- ${doc ? `${formatComment(doc)}\n` : ""}${`${attributes.map((attribute) => attribute.getApplicationString(this.emitter.getContext().scope)).join("\n")}${attributes?.length > 0 ? "\n" : ""}`}public partial class ${className} ${model.baseModel ? code `: ${this.emitter.emitTypeReference(model.baseModel)}` : ""} {
168
- ${this.emitter.emitModelProperties(model)}
270
+ ${doc ? `${formatComment(doc)}\n` : ""}${`${attributes.map((attribute) => attribute.getApplicationString(this.emitter.getContext().scope)).join("\n")}${attributes?.length > 0 ? "\n" : ""}`}public partial class ${className} ${baseClass} {
271
+ ${exceptionConstructor ? `${exceptionConstructor}\n` : ""}${this.emitter.emitModelProperties(model)}
169
272
  }
170
273
  } `);
171
274
  return decl;
172
275
  }
276
+ getModelExceptionConstructor(program, model, modelName, className) {
277
+ if (!isErrorModel(program, model))
278
+ return undefined;
279
+ const constructor = this.getExceptionConstructorData(program, model, modelName);
280
+ const isParent = !!model.derivedModels?.length;
281
+ return `public ${className}(${constructor.properties}) : base(${constructor.statusCode?.value ?? `400`}${constructor.header ? `, \n\t\t headers: new(){${constructor.header}}` : ""}${constructor.value ? `, \n\t\t value: new{${constructor.value}}` : ""})
282
+ { ${constructor.body ? `\n${constructor.body}` : ""}\n\t}${isParent ? `\npublic ${className}(int statusCode, object? value = null, Dictionary<string, string>? headers = default): base(statusCode, value, headers) {}\n` : ""}`;
283
+ }
284
+ isDuplicateExceptionName(name) {
285
+ const exceptionPropertyNames = [
286
+ "value",
287
+ "headers",
288
+ "stacktrace",
289
+ "source",
290
+ "message",
291
+ "innerexception",
292
+ "hresult",
293
+ "data",
294
+ "targetsite",
295
+ "helplink",
296
+ ];
297
+ return exceptionPropertyNames.includes(name.toLowerCase());
298
+ }
299
+ getExceptionConstructorData(program, model, modelName) {
300
+ const allProperties = new ModelInfo().getAllProperties(program, model) ?? [];
301
+ const propertiesWithDefaults = allProperties.map((prop) => {
302
+ const { defaultValue: typeDefault } = this.#findPropertyType(prop);
303
+ const defaultValue = prop.defaultValue
304
+ ? code `${JSON.stringify(serializeValueAsJson(program, prop.defaultValue, prop))}`
305
+ : typeDefault;
306
+ return { prop, defaultValue };
307
+ });
308
+ const sortedProperties = propertiesWithDefaults
309
+ .filter(({ prop }) => !isStatusCode(program, prop))
310
+ .sort(({ prop: a, defaultValue: aDefault }, { prop: b, defaultValue: bDefault }) => {
311
+ if (!a.optional && !aDefault && (b.optional || bDefault))
312
+ return -1;
313
+ if (!b.optional && !bDefault && (a.optional || aDefault))
314
+ return 1;
315
+ return 0;
316
+ });
317
+ const properties = [];
318
+ const body = [];
319
+ const header = [];
320
+ const value = [];
321
+ const statusCode = getStatusCode(program, model);
322
+ if (statusCode?.requiresConstructorArgument) {
323
+ properties.push(`int ${statusCode.value}`);
324
+ }
325
+ for (const { prop, defaultValue } of sortedProperties) {
326
+ let propertyName = ensureCSharpIdentifier(program, prop, prop.name);
327
+ if (modelName === propertyName || this.isDuplicateExceptionName(propertyName)) {
328
+ propertyName = `${propertyName}Prop`;
329
+ }
330
+ const type = getCSharpType(program, prop.type);
331
+ properties.push(`${type?.type.name} ${prop.name}${defaultValue ? ` = ${defaultValue}` : `${prop.optional ? " = default" : ""}`}`);
332
+ body.push(`\t\t${propertyName} = ${prop.name};`);
333
+ if (isHeader(program, prop)) {
334
+ const headerName = getHeaderFieldName(program, prop);
335
+ header.push(`{"${headerName}", ${prop.name}}`);
336
+ }
337
+ else {
338
+ value.push(`${prop.name} = ${prop.name}`);
339
+ }
340
+ }
341
+ return {
342
+ properties: properties.join(", "),
343
+ body: body.join("\n"),
344
+ header: header.join(", "),
345
+ value: value.join(","),
346
+ statusCode: statusCode,
347
+ };
348
+ }
173
349
  modelDeclarationContext(model, name) {
350
+ function getSourceModels(source, visited = new Set()) {
351
+ const result = [];
352
+ if (visited.has(source))
353
+ return [];
354
+ result.push(source);
355
+ visited.add(source);
356
+ for (const candidate of source.sourceModels) {
357
+ result.push(...getSourceModels(candidate.model, visited));
358
+ }
359
+ return result;
360
+ }
361
+ function getModelNamespace(model) {
362
+ const sourceModels = getSourceModels(model);
363
+ if (sourceModels.length === 0)
364
+ return undefined;
365
+ return sourceModels
366
+ .filter((m) => m.namespace !== undefined)
367
+ .flatMap((s) => s.namespace)
368
+ .reduce((c, n) => (c === n ? c : undefined));
369
+ }
174
370
  if (this.#isMultipartModel(model))
175
- return {};
371
+ return this.emitter.getContext();
176
372
  const modelName = ensureCSharpIdentifier(this.emitter.getProgram(), model, name);
177
- const modelFile = this.emitter.createSourceFile(`models/${modelName}.cs`);
373
+ const modelFile = this.emitter.createSourceFile(`generated/models/${modelName}.cs`);
178
374
  modelFile.meta[this.#sourceTypeKey] = CSharpSourceType.Model;
179
- const modelNamespace = `${this.#getOrSetBaseNamespace(model)}.Models`;
375
+ const ns = model.namespace ?? getModelNamespace(model);
376
+ const modelNamespace = this.#getOrAddNamespace(ns);
180
377
  return this.#createModelContext(modelNamespace, modelFile, modelName);
181
378
  }
182
379
  modelInstantiationContext(model) {
183
380
  if (this.#isMultipartModel(model))
184
- return {};
381
+ return this.emitter.getContext();
185
382
  const modelName = getModelInstantiationName(this.emitter.getProgram(), model, model.name);
186
- const sourceFile = this.emitter.createSourceFile(`models/${modelName}.cs`);
383
+ const sourceFile = this.emitter.createSourceFile(`generated/models/${modelName}.cs`);
187
384
  sourceFile.meta[this.#sourceTypeKey] = CSharpSourceType.Model;
188
- const modelNamespace = `${this.#getOrSetBaseNamespace(model)}.Models`;
385
+ const modelNamespace = this.#getOrAddNamespace(model.namespace);
189
386
  const context = this.#createModelContext(modelNamespace, sourceFile, model.name);
190
387
  context.instantiationName = modelName;
191
388
  return context;
@@ -199,12 +396,33 @@ export async function $onEmit(context) {
199
396
  const program = this.emitter.getProgram();
200
397
  const recordType = getRecordType(program, model);
201
398
  if (recordType !== undefined) {
202
- return code `Dictionary<string, ${this.emitter.emitTypeReference(recordType)}>`;
399
+ const valueType = this.#getSimpleType(recordType);
400
+ return code `Dictionary<string, ${valueType ? valueType?.getTypeReference() : this.emitter.emitTypeReference(recordType)}>`;
203
401
  }
204
402
  const context = this.emitter.getContext();
205
403
  const className = context.instantiationName ?? name;
206
404
  return this.modelDeclaration(model, className);
207
405
  }
406
+ #getSimpleType(type) {
407
+ switch (type.kind) {
408
+ case "Model":
409
+ if (!isRecord(type))
410
+ return undefined;
411
+ return getCSharpType(this.emitter.getProgram(), type)?.type;
412
+ case "Boolean":
413
+ case "Intrinsic":
414
+ case "ModelProperty":
415
+ case "Number":
416
+ case "Scalar":
417
+ case "String":
418
+ case "StringTemplate":
419
+ case "StringTemplateSpan":
420
+ case "Tuple":
421
+ return getCSharpType(this.emitter.getProgram(), type)?.type;
422
+ default:
423
+ return undefined;
424
+ }
425
+ }
208
426
  #getMultipartParts(model) {
209
427
  const parts = [...model.properties.values()]
210
428
  .flatMap((p) => getHttpPart(this.emitter.getProgram(), p.type)?.type)
@@ -243,9 +461,6 @@ export async function $onEmit(context) {
243
461
  });
244
462
  return this.modelDeclaration(model, modelName);
245
463
  }
246
- #isRecord(type) {
247
- return type.kind === "Model" && type.name === "Record" && type.indexer !== undefined;
248
- }
249
464
  #isInheritedProperty(property) {
250
465
  const visited = [];
251
466
  function isInherited(model, propertyName) {
@@ -266,23 +481,33 @@ export async function $onEmit(context) {
266
481
  modelPropertyLiteral(property) {
267
482
  if (isStatusCode(this.emitter.getProgram(), property))
268
483
  return "";
269
- const propertyName = ensureCSharpIdentifier(this.emitter.getProgram(), property, property.name);
270
- const [typeName, typeDefault, nullable] = this.#findPropertyType(property);
484
+ let propertyName = ensureCSharpIdentifier(this.emitter.getProgram(), property, property.name);
485
+ const { typeReference: typeName, defaultValue: typeDefault, nullableType: nullable, } = this.#findPropertyType(property);
271
486
  const doc = getDoc(this.emitter.getProgram(), property);
272
- const attributes = getModelAttributes(this.emitter.getProgram(), property, propertyName);
273
- // eslint-disable-next-line @typescript-eslint/no-deprecated
274
- const defaultValue = property.default
275
- ? // eslint-disable-next-line @typescript-eslint/no-deprecated
276
- code `${this.emitter.emitType(property.default)}`
277
- : typeDefault;
487
+ const attributes = new Map(getModelAttributes(this.emitter.getProgram(), property, propertyName).map((a) => [
488
+ a.type.name,
489
+ a,
490
+ ]));
491
+ const modelName = this.emitter.getContext()["name"];
492
+ if (modelName === propertyName ||
493
+ (this.isDuplicateExceptionName(propertyName) &&
494
+ property.model &&
495
+ isErrorModel(this.emitter.getProgram(), property.model))) {
496
+ propertyName = `${propertyName}Prop`;
497
+ const attr = getEncodedNameAttribute(this.emitter.getProgram(), property, propertyName);
498
+ if (!attributes.has(attr.type.name))
499
+ attributes.set(attr.type.name, attr);
500
+ }
501
+ const defaultValue = this.#opHelpers.getDefaultValue(this.emitter.getProgram(), property.type, property.defaultValue) ?? typeDefault;
502
+ const attributeList = [...attributes.values()];
278
503
  return this.emitter.result
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)
504
+ .rawCode(code `${doc ? `${formatComment(doc)}\n` : ""}${`${attributeList.map((attribute) => attribute.getApplicationString(this.emitter.getContext().scope)).join("\n")}${attributeList?.length > 0 ? "\n" : ""}`}public ${this.#isInheritedProperty(property) ? "new " : ""}${typeName}${isValueType(this.emitter.getProgram(), property.type) && (property.optional || nullable)
280
505
  ? "?"
281
506
  : ""} ${propertyName} { get; ${typeDefault ? "}" : "set; }"}${defaultValue ? ` = ${defaultValue};\n` : "\n"}
282
507
  `);
283
508
  }
284
509
  #findPropertyType(property) {
285
- return this.#getTypeInfoForTsType(property.type);
510
+ return this.#opHelpers.getTypeInfo(this.emitter.getProgram(), property.type, property);
286
511
  }
287
512
  #isMultipartRequest(operation) {
288
513
  const body = operation.parameters.body;
@@ -298,118 +523,6 @@ export async function $onEmit(context) {
298
523
  }
299
524
  return false;
300
525
  }
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) {
347
- case "String":
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];
358
- case "Boolean":
359
- return [code `bool`, `${tsType.value === true ? true : false}`, false];
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
- ];
377
- case "Object":
378
- return [code `object`, undefined, false];
379
- case "Model":
380
- if (this.#isRecord(tsType)) {
381
- return [code `JsonObject`, undefined, false];
382
- }
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);
403
- default:
404
- return [code `${emitter.emitTypeReference(tsType)}`, undefined, false];
405
- }
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
- }
413
526
  modelPropertyReference(property) {
414
527
  return code `${this.emitter.emitTypeReference(property.type)}`;
415
528
  }
@@ -423,10 +536,7 @@ export async function $onEmit(context) {
423
536
  const doc = getDoc(this.emitter.getProgram(), iface);
424
537
  const attributes = getModelAttributes(this.emitter.getProgram(), iface, ifaceName);
425
538
  this.#metadateMap.set(iface, new CSharpType({ name: ifaceName, namespace: namespace }));
426
- const decl = this.emitter.result.declaration(ifaceName, code `${this.#generatedFileHeaderWithNullable}
427
-
428
- ${this.#emitUsings()}
429
-
539
+ const decl = this.emitter.result.declaration(ifaceName, code `
430
540
  namespace ${namespace} {
431
541
 
432
542
  ${doc ? `${formatComment(doc)}\n` : ""}${`${attributes.map((attribute) => attribute.getApplicationString(this.emitter.getContext().scope)).join("\n")}${attributes?.length > 0 ? "\n" : ""}`}public interface ${ifaceName} {
@@ -438,80 +548,66 @@ export async function $onEmit(context) {
438
548
  interfaceDeclarationContext(iface, name) {
439
549
  // set up interfaces file for declaration
440
550
  const ifaceName = `I${ensureCSharpIdentifier(this.emitter.getProgram(), iface, name, NameCasingType.Class)}`;
441
- const sourceFile = this.emitter.createSourceFile(`operations/${ifaceName}.cs`);
551
+ const sourceFile = this.emitter.createSourceFile(`generated/operations/${ifaceName}.cs`);
442
552
  sourceFile.meta[this.#sourceTypeKey] = CSharpSourceType.Interface;
443
- const ifaceNamespace = this.#getOrSetBaseNamespace(iface);
444
- const modelNamespace = `${ifaceNamespace}.Models`;
445
- const context = this.#createModelContext(ifaceNamespace, sourceFile, ifaceName);
446
- context.file.imports.set("System", ["System"]);
447
- context.file.imports.set("System.Net", ["System.Net"]);
448
- context.file.imports.set("System.Text.Json", ["System.Text.Json"]);
449
- context.file.imports.set("System.Text.Json.Serialization", [
450
- "System.Text.Json.Serialization",
451
- ]);
452
- context.file.imports.set("System.Threading.Tasks", ["System.Threading.Tasks"]);
453
- context.file.imports.set("Microsoft.AspNetCore.Mvc", ["Microsoft.AspNetCore.Mvc"]);
553
+ sourceFile.meta["nullable"] = true;
554
+ const ifaceNamespace = this.#getOrAddNamespace(iface.namespace);
555
+ sourceFile.imports.set("System", ["System"]);
556
+ sourceFile.imports.set("System.Net", ["System.Net"]);
557
+ sourceFile.imports.set("System.Text.Json", ["System.Text.Json"]);
558
+ sourceFile.imports.set("System.Text.Json.Serialization", ["System.Text.Json.Serialization"]);
559
+ sourceFile.imports.set("System.Threading.Tasks", ["System.Threading.Tasks"]);
560
+ sourceFile.imports.set("Microsoft.AspNetCore.Mvc", ["Microsoft.AspNetCore.Mvc"]);
454
561
  if (this.#hasMultipartOperation(iface)) {
455
- context.file.imports.set("Microsoft.AspNetCore.WebUtilities", [
562
+ sourceFile.imports.set("Microsoft.AspNetCore.WebUtilities", [
456
563
  "Microsoft.AspNetCore.WebUtilities",
457
564
  ]);
458
565
  }
459
- context.file.imports.set(modelNamespace, [modelNamespace]);
460
- return context;
566
+ return this.#createModelContext(ifaceNamespace, sourceFile, ifaceName);
461
567
  }
462
568
  interfaceDeclarationOperations(iface) {
463
569
  // add in operations
464
570
  const builder = new StringBuilder();
465
- const metadata = new HttpMetadata();
466
571
  const context = this.emitter.getContext();
467
572
  const name = `${ensureCSharpIdentifier(this.emitter.getProgram(), iface, iface.name, NameCasingType.Class)}`;
468
- const ifaceNamespace = this.#getOrSetBaseNamespace(iface);
469
- const namespace = `${ifaceNamespace}`;
573
+ const ifaceNamespace = this.#getOrAddNamespace(iface.namespace);
574
+ const namespace = ifaceNamespace;
470
575
  const mock = {
471
576
  className: name,
472
577
  interfaceName: `I${name}`,
473
578
  methods: [],
474
579
  namespace: namespace,
475
- usings: [`${ifaceNamespace}.Models`],
580
+ usings: [],
476
581
  };
477
582
  for (const [name, operation] of iface.operations) {
583
+ if (isTemplateDeclaration(operation))
584
+ continue;
478
585
  const doc = getDoc(this.emitter.getProgram(), operation);
479
586
  const returnTypes = [];
480
587
  const [httpOp, _] = getHttpOperation(this.emitter.getProgram(), operation);
481
588
  for (const response of httpOp.responses.filter((r) => !isErrorModel(this.emitter.getProgram(), r.type))) {
482
- returnTypes.push(metadata.resolveLogicalResponseType(this.emitter.getProgram(), response));
589
+ const [_, responseType] = this.#resolveOperationResponse(response, httpOp.operation);
590
+ returnTypes.push(responseType);
483
591
  }
484
- const returnInfo = coalesceTypes(this.emitter.getProgram(), returnTypes, context.namespace);
592
+ const returnInfo = coalesceTypes(this.emitter.getProgram(), returnTypes, namespace);
485
593
  const returnType = returnInfo?.type || UnknownType;
486
594
  const opName = ensureCSharpIdentifier(this.emitter.getProgram(), operation, name, NameCasingType.Method);
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
- }
595
+ const parameters = this.#opHelpers.getParameters(this.emitter.getProgram(), httpOp);
596
+ const opImpl = {
597
+ methodName: `${opName}Async`,
598
+ methodParams: `${getBusinessLogicDeclParameters(parameters)}`,
599
+ returnType: returnType,
600
+ returnTypeName: `${returnType.name === "void" ? "Task" : `Task<${returnType.getTypeReference(context.scope)}>`}`,
601
+ instantiatedReturnType: returnType.name === "void"
602
+ ? undefined
603
+ : `${returnType.getTypeReference(context.scope)}`,
604
+ };
605
+ const opDecl = this.emitter.result.declaration(opName, code `${doc ? `${formatComment(doc)}\n` : ""}${opImpl.returnTypeName} ${opImpl.methodName}( ${opImpl.methodParams});`);
511
606
  mock.methods.push(opImpl);
512
607
  builder.push(code `${opDecl.value}\n`);
513
608
  this.emitter.emitInterfaceOperation(operation);
514
609
  }
610
+ mock.usings.push(...getImports(context.scope));
515
611
  this.#mockRegistrations.set(mock.interfaceName, mock);
516
612
  return builder.reduce();
517
613
  }
@@ -529,9 +625,8 @@ export async function $onEmit(context) {
529
625
  const doc = getDoc(this.emitter.getProgram(), operation);
530
626
  const [httpOperation, _] = getHttpOperation(this.emitter.getProgram(), operation);
531
627
  const multipart = this.#isMultipartRequest(httpOperation);
532
- const declParams = !multipart
533
- ? this.#emitHttpOperationParameters(httpOperation)
534
- : this.#emitHttpOperationParameters(httpOperation, true);
628
+ const parameters = this.#opHelpers.getParameters(this.emitter.getProgram(), httpOperation);
629
+ const declParams = getHttpDeclParameters(parameters);
535
630
  if (multipart) {
536
631
  const context = this.emitter.getContext();
537
632
  context.file.imports.set("Microsoft.AspNetCore.WebUtilities", [
@@ -551,7 +646,7 @@ export async function $onEmit(context) {
551
646
  isValueType: false,
552
647
  });
553
648
  const hasResponseValue = response.name !== "void";
554
- const resultString = `${status === 204 ? "NoContent" : "Ok"}`;
649
+ const returnStatement = getControllerReturnStatement(status, hasResponseValue);
555
650
  if (!this.#isMultipartRequest(httpOperation)) {
556
651
  return this.emitter.result.declaration(operation.name, code `
557
652
  ${doc ? `${formatComment(doc)}` : ""}
@@ -561,10 +656,10 @@ export async function $onEmit(context) {
561
656
  public virtual async Task<IActionResult> ${operationName}(${declParams})
562
657
  {
563
658
  ${hasResponseValue
564
- ? `var result = await ${this.emitter.getContext().resourceName}Impl.${operationName}Async(${this.#emitOperationCallParameters(httpOperation)});
565
- return ${resultString}(result);`
566
- : `await ${this.emitter.getContext().resourceName}Impl.${operationName}Async(${this.#emitOperationCallParameters(httpOperation)});
567
- return ${resultString}();`}
659
+ ? `var result = await ${this.emitter.getContext().resourceName}Impl.${operationName}Async(${getBusinessLogicCallParameters(parameters)});
660
+ ${returnStatement}`
661
+ : `await ${this.emitter.getContext().resourceName}Impl.${operationName}Async(${getBusinessLogicCallParameters(parameters)});
662
+ ${returnStatement}`}
568
663
  }`);
569
664
  }
570
665
  else {
@@ -585,10 +680,10 @@ export async function $onEmit(context) {
585
680
 
586
681
  var reader = new MultipartReader(boundary, Request.Body);
587
682
  ${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}();`}
683
+ ? `var result = await ${this.emitter.getContext().resourceName}Impl.${operationName}Async(${getBusinessLogicCallParameters(parameters)});
684
+ ${returnStatement}`
685
+ : `await ${this.emitter.getContext().resourceName}Impl.${operationName}Async(${getBusinessLogicCallParameters(parameters)});
686
+ ${returnStatement}`}
592
687
  }`);
593
688
  }
594
689
  }
@@ -603,7 +698,8 @@ export async function $onEmit(context) {
603
698
  const operationName = ensureCSharpIdentifier(this.emitter.getProgram(), operation, name, NameCasingType.Method);
604
699
  const doc = getDoc(this.emitter.getProgram(), operation);
605
700
  const [httpOperation, _] = getHttpOperation(this.emitter.getProgram(), operation);
606
- const declParams = this.#emitHttpOperationParameters(httpOperation);
701
+ const parameters = this.#opHelpers.getParameters(this.emitter.getProgram(), httpOperation);
702
+ const declParams = getHttpDeclParameters(parameters);
607
703
  const responseInfo = this.#getOperationResponse(httpOperation);
608
704
  const status = responseInfo?.statusCode ?? 200;
609
705
  const response = responseInfo?.resultType ??
@@ -614,7 +710,7 @@ export async function $onEmit(context) {
614
710
  isValueType: false,
615
711
  });
616
712
  const hasResponseValue = response.name !== "void";
617
- const resultString = `${status === 204 ? "NoContent" : "Ok"}`;
713
+ const returnStatement = getControllerReturnStatement(status, hasResponseValue);
618
714
  return this.emitter.result.declaration(operation.name, code `
619
715
  ${doc ? `${formatComment(doc)}` : ""}
620
716
  [${getOperationVerbDecorator(httpOperation)}]
@@ -623,10 +719,10 @@ export async function $onEmit(context) {
623
719
  public virtual async Task<IActionResult> ${operationName}(${declParams})
624
720
  {
625
721
  ${hasResponseValue
626
- ? `var result = await ${this.emitter.getContext().resourceName}Impl.${operationName}Async(${this.#emitOperationCallParameters(httpOperation)});
627
- return ${resultString}(result);`
628
- : `await ${this.emitter.getContext().resourceName}Impl.${operationName}Async(${this.#emitOperationCallParameters(httpOperation)});
629
- return ${resultString}();`}
722
+ ? `var result = await ${this.emitter.getContext().resourceName}Impl.${operationName}Async(${getBusinessLogicCallParameters(parameters)});
723
+ ${returnStatement}`
724
+ : `await ${this.emitter.getContext().resourceName}Impl.${operationName}Async(${getBusinessLogicCallParameters(parameters)});
725
+ ${returnStatement}`}
630
726
  }`);
631
727
  }
632
728
  operationReturnType(operation, returnType) {
@@ -636,6 +732,28 @@ export async function $onEmit(context) {
636
732
  stringTemplate(stringTemplate) {
637
733
  return this.emitter.result.rawCode(stringTemplate.stringValue || "");
638
734
  }
735
+ #resolveOperationResponse(response, operation) {
736
+ function getName(sourceType, part) {
737
+ return ensureCSharpIdentifier(emitter.getProgram(), sourceType, part, NameCasingType.Class);
738
+ }
739
+ let responseType = new HttpMetadata().resolveLogicalResponseType(this.emitter.getProgram(), response);
740
+ if (responseType.kind === "Model" && !responseType.name) {
741
+ const modelName = `${getName(responseType, operation.interface.name)}${getName(responseType, operation.name)}Response}`;
742
+ const returnedType = this.emitter
743
+ .getProgram()
744
+ .checker.cloneType(responseType, { name: modelName });
745
+ responseType = returnedType;
746
+ }
747
+ // TemplateParameter types cannot be emitted (they are unresolved template placeholders).
748
+ // Template operations are filtered in interfaceDeclarationOperations, but this guard
749
+ // prevents crashes if a TemplateParameter response type is encountered via other paths.
750
+ if (responseType.kind !== "TemplateParameter") {
751
+ this.emitter.emitType(responseType);
752
+ }
753
+ const context = this.emitter.getContext();
754
+ const result = getCSharpType(this.emitter.getProgram(), responseType, context.namespace);
755
+ return [result?.type || UnknownType, responseType];
756
+ }
639
757
  #getOperationResponse(operation) {
640
758
  const validResponses = operation.responses.filter((r) => !isErrorModel(this.emitter.getProgram(), r.type) &&
641
759
  getCSharpStatusCode(r.statusCodes) !== undefined);
@@ -656,106 +774,36 @@ export async function $onEmit(context) {
656
774
  };
657
775
  }
658
776
  #emitOperationResponses(operation) {
659
- const builder = new StringBuilder();
660
- let i = 0;
661
- const validResponses = operation.responses.filter((r) => !isErrorModel(this.emitter.getProgram(), r.type) &&
662
- getCSharpStatusCode(r.statusCodes) !== undefined);
663
- for (const response of validResponses) {
664
- i++;
665
- builder.push(code `${this.#emitOperationResponseDecorator(response)}`);
666
- if (i < validResponses.length) {
667
- builder.pushLiteralSegment("\n");
668
- }
777
+ function isValid(program, response) {
778
+ return (!isErrorModel(program, response.type) &&
779
+ getCSharpStatusCode(response.statusCodes) !== undefined);
669
780
  }
781
+ const builder = new StringBuilder();
670
782
  for (const response of operation.responses) {
671
- if (!isEmptyResponseModel(this.emitter.getProgram(), response.type))
672
- this.emitter.emitType(response.type);
783
+ const [responseType, _] = this.#resolveOperationResponse(response, operation.operation);
784
+ if (isValid(this.emitter.getProgram(), response)) {
785
+ builder.push(code `${builder.segments.length > 0 ? "\n" : ""}${this.#emitOperationResponseDecorator(response, responseType)}`);
786
+ }
673
787
  }
674
788
  return builder.reduce();
675
789
  }
676
- #emitOperationResponseDecorator(response) {
677
- const responseType = new HttpMetadata().resolveLogicalResponseType(this.emitter.getProgram(), response);
678
- return this.emitter.result.rawCode(code `[ProducesResponseType((int)${getCSharpStatusCode(response.statusCodes)}, Type = typeof(${this.#emitResponseType(responseType)}))]`);
790
+ #emitOperationResponseDecorator(response, result) {
791
+ return this.emitter.result.rawCode(code `[ProducesResponseType((int)${getCSharpStatusCode(response.statusCodes)}, Type = typeof(${this.#emitResponseType(result)}))]`);
679
792
  }
680
793
  #emitResponseType(type) {
681
794
  const context = this.emitter.getContext();
682
- const result = getCSharpType(this.emitter.getProgram(), type, context.namespace);
683
- const resultType = result?.type || UnknownType;
684
- return resultType.getTypeReference(context.scope);
685
- }
686
- #emitInterfaceOperationParameters(operation, bodyParam) {
687
- const signature = new StringBuilder();
688
- const requiredParams = [];
689
- const optionalParams = [];
690
- let totalParams = 0;
691
- if (bodyParam !== undefined)
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
- }
703
- }
704
- let i = 1;
705
- for (const requiredParam of requiredParams) {
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(", ");
713
- }
714
- for (const optionalParam of optionalParams) {
715
- const [paramType, _, __] = this.#findPropertyType(optionalParam);
716
- signature.push(code `${paramType}? ${ensureCSharpIdentifier(this.emitter.getProgram(), optionalParam, optionalParam.name, NameCasingType.Parameter)}${i++ < totalParams ? ", " : ""}`);
717
- }
718
- return signature.reduce();
719
- }
720
- #emitHttpOperationParameters(operation, bodyParameter) {
721
- const signature = new StringBuilder();
722
- const bodyParam = operation.parameters.body;
723
- let i = 0;
724
- //const pathParameters = operation.parameters.parameters.filter((p) => p.type === "path");
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 ? ", " : ""}`);
738
- }
739
- }
740
- i = 0;
741
- for (const parameter of optionalParams) {
742
- signature.push(code `${this.#emitOperationSignatureParameter(operation, parameter)}${++i < optionalParams.length ? ", " : ""}`);
743
- }
744
- return signature.reduce();
795
+ return type.getTypeReference(context.scope);
745
796
  }
746
797
  unionDeclaration(union, name) {
747
- const baseType = this.#coalesceUnionTypes(union);
748
- if (baseType.isBuiltIn && baseType.name === "string") {
798
+ const baseType = coalesceUnionTypes(this.emitter.getProgram(), union);
799
+ if (isStringEnumType(this.emitter.getProgram(), union)) {
749
800
  const program = this.emitter.getProgram();
750
801
  const unionName = ensureCSharpIdentifier(program, union, name);
751
802
  const namespace = this.emitter.getContext().namespace;
752
803
  const doc = getDoc(this.emitter.getProgram(), union);
753
804
  const attributes = getModelAttributes(program, union, unionName);
754
805
  this.#metadateMap.set(union, new CSharpType({ name: unionName, namespace: namespace }));
755
- return this.emitter.result.declaration(unionName, code `${this.#generatedFileHeader}
756
-
757
- ${this.#emitUsings()}
758
-
806
+ return this.emitter.result.declaration(unionName, code `
759
807
  namespace ${namespace}
760
808
  {
761
809
 
@@ -763,24 +811,19 @@ export async function $onEmit(context) {
763
811
  ${attributes.map((attribute) => attribute.getApplicationString(this.emitter.getContext().scope)).join("\n")}
764
812
  public enum ${unionName}
765
813
  {
766
- ${this.emitter.emitUnionVariants(union)};
814
+ ${this.emitter.emitUnionVariants(union)}
767
815
  }
768
816
  } `);
769
817
  }
770
- return this.emitter.result.rawCode(code `${baseType.getTypeReference()}`);
818
+ return this.emitter.result.rawCode(code `${baseType.getTypeReference(this.emitter.getContext().scope)}`);
771
819
  }
772
820
  unionDeclarationContext(union) {
773
- const baseType = this.#coalesceUnionTypes(union);
774
- if (baseType.isBuiltIn && baseType.name === "string") {
821
+ if (isStringEnumType(this.emitter.getProgram(), union)) {
775
822
  const unionName = ensureCSharpIdentifier(this.emitter.getProgram(), union, union.name || "Union");
776
- const unionFile = this.emitter.createSourceFile(`models/${unionName}.cs`);
823
+ const unionFile = this.emitter.createSourceFile(`generated/models/${unionName}.cs`);
777
824
  unionFile.meta[this.#sourceTypeKey] = CSharpSourceType.Model;
778
- const unionNamespace = `${this.#getOrSetBaseNamespace(union)}.Models`;
779
- return {
780
- namespace: unionNamespace,
781
- file: unionFile,
782
- scope: unionFile.globalScope,
783
- };
825
+ const unionNamespace = this.#getOrAddNamespace(union.namespace);
826
+ return this.#createEnumContext(unionNamespace, unionFile, unionName);
784
827
  }
785
828
  else {
786
829
  return this.emitter.getContext();
@@ -801,7 +844,9 @@ export async function $onEmit(context) {
801
844
  const nameHint = name || variant.type.value;
802
845
  const memberName = ensureCSharpIdentifier(this.emitter.getProgram(), variant, nameHint);
803
846
  this.#metadateMap.set(variant, { name: memberName });
804
- result.push(code `${ensureCSharpIdentifier(this.emitter.getProgram(), variant, nameHint)} = "${variant.type.value}"`);
847
+ result.push(code `
848
+ [JsonStringEnumMemberName("${variant.type.value}")]
849
+ ${ensureCSharpIdentifier(this.emitter.getProgram(), variant, nameHint, NameCasingType.Property)}`);
805
850
  if (i < union.variants.size)
806
851
  result.pushLiteralSegment(", ");
807
852
  }
@@ -811,132 +856,59 @@ export async function $onEmit(context) {
811
856
  unionVariantContext(union) {
812
857
  return super.unionVariantContext(union);
813
858
  }
814
- #emitOperationSignatureParameter(operation, httpParam) {
815
- const name = httpParam.param.name;
816
- const parameter = httpParam.param;
817
- const emittedName = ensureCSharpIdentifier(this.emitter.getProgram(), parameter, name, NameCasingType.Parameter);
818
- let [emittedType, emittedDefault, _] = this.#findPropertyType(parameter);
819
- if (emittedType.toString().endsWith("[]"))
820
- emittedDefault = undefined;
821
- // eslint-disable-next-line @typescript-eslint/no-deprecated
822
- const defaultValue = parameter.default
823
- ? // eslint-disable-next-line @typescript-eslint/no-deprecated
824
- code `${this.emitter.emitType(parameter.default)}`
825
- : emittedDefault;
826
- return this.emitter.result.rawCode(code `${httpParam.type !== "path" ? this.#emitParameterAttribute(httpParam) : ""}${emittedType} ${emittedName}${defaultValue === undefined ? "" : ` = ${defaultValue}`}`);
827
- }
828
- #getBodyParameters(operation) {
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();
840
- let i = 0;
841
- const bodyParameters = this.#getBodyParameters(operation);
842
- //const pathParameters = operation.parameters.parameters.filter((p) => p.type === "path");
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);
848
- i++;
849
- if (!isNeverType(parameter.param.type) &&
850
- !isNullType(parameter.param.type) &&
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
- }
867
- }
868
- }
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
- }
878
- }
879
- return signature.reduce();
880
- }
881
- #emitOperationCallParameter(operation, httpParam) {
882
- const name = httpParam.param.name;
883
- const parameter = httpParam.param;
884
- const emittedName = ensureCSharpIdentifier(this.emitter.getProgram(), parameter, name, NameCasingType.Parameter);
885
- return this.emitter.result.rawCode(code `${emittedName}`);
886
- }
887
- #emitParameterAttribute(parameter) {
888
- switch (parameter.type) {
889
- case "header":
890
- return code `[FromHeader(Name="${parameter.name}")] `;
891
- case "query":
892
- return code `[FromQuery(Name="${parameter.name}")] `;
893
- default:
894
- return "";
895
- }
896
- }
897
859
  #createModelContext(namespace, file, name) {
860
+ file.imports.set("System", ["System"]);
861
+ file.imports.set("System.Text.Json", ["System.Text.Json"]);
862
+ file.imports.set("System.Text.Json.Serialization", ["System.Text.Json.Serialization"]);
863
+ file.imports.set("TypeSpec.Helpers.JsonConverters", ["TypeSpec.Helpers.JsonConverters"]);
864
+ file.imports.set("TypeSpec.Helpers", ["TypeSpec.Helpers"]);
865
+ file.meta[this.#nsKey] = namespace;
898
866
  const context = {
899
867
  namespace: namespace,
900
868
  name: name,
901
869
  file: file,
902
870
  scope: file.globalScope,
903
871
  };
904
- context.file.imports.set("System", ["System"]);
905
- context.file.imports.set("System.Text.Json", ["System.Text.Json"]);
906
- context.file.imports.set("System.Text.Json.Serialization", [
907
- "System.Text.Json.Serialization",
908
- ]);
909
872
  return context;
910
873
  }
874
+ #createEnumContext(namespace, file, name) {
875
+ file.imports.set("System.Text.Json", ["System.Text.Json"]);
876
+ file.imports.set("System.Text.Json.Serialization", ["System.Text.Json.Serialization"]);
877
+ file.meta[this.#nsKey] = namespace;
878
+ return {
879
+ namespace: namespace,
880
+ name: name,
881
+ file: file,
882
+ scope: file.globalScope,
883
+ };
884
+ }
911
885
  #createOrGetResourceContext(name, operation, resource) {
912
- let context = controllers.get(name);
886
+ name = ensureCSharpIdentifier(this.emitter.getProgram(), operation, name, NameCasingType.Class);
887
+ const namespace = this.#getOrAddNamespace(operation.namespace);
888
+ let context = controllers.get(`${namespace}.${name}`);
913
889
  if (context !== undefined)
914
890
  return context;
915
- const sourceFile = this.emitter.createSourceFile(`controllers/${name}Controller.cs`);
916
- const namespace = this.#getOrSetBaseNamespace(operation);
917
- const modelNamespace = `${namespace}.Models`;
891
+ const sourceFile = this.emitter.createSourceFile(`generated/controllers/${name}Controller.cs`);
918
892
  sourceFile.meta[this.#sourceTypeKey] = CSharpSourceType.Controller;
919
893
  sourceFile.meta["resourceName"] = name;
920
894
  sourceFile.meta["resource"] = `${name}Controller`;
921
895
  sourceFile.meta["namespace"] = namespace;
896
+ sourceFile.imports.set("System", ["System"]);
897
+ sourceFile.imports.set("System.Net", ["System.Net"]);
898
+ sourceFile.imports.set("System.Threading.Tasks", ["System.Threading.Tasks"]);
899
+ sourceFile.imports.set("System.Text.Json", ["System.Text.Json"]);
900
+ sourceFile.imports.set("System.Text.Json.Serialization", ["System.Text.Json.Serialization"]);
901
+ sourceFile.imports.set("Microsoft.AspNetCore.Mvc", ["Microsoft.AspNetCore.Mvc"]);
902
+ sourceFile.imports.set(namespace, [namespace]);
903
+ sourceFile.meta[this.#nsKey] = namespace;
922
904
  context = {
923
- namespace: sourceFile.meta["namespace"],
905
+ namespace: namespace,
924
906
  file: sourceFile,
925
907
  resourceName: name,
926
908
  scope: sourceFile.globalScope,
927
909
  resourceType: resource,
928
910
  };
929
- context.file.imports.set("System", ["System"]);
930
- context.file.imports.set("System.Net", ["System.Net"]);
931
- context.file.imports.set("System.Threading.Tasks", ["System.Threading.Tasks"]);
932
- context.file.imports.set("System.Text.Json", ["System.Text.Json"]);
933
- context.file.imports.set("System.Text.Json.Serialization", [
934
- "System.Text.Json.Serialization",
935
- ]);
936
- context.file.imports.set("Microsoft.AspNetCore.Mvc", ["Microsoft.AspNetCore.Mvc"]);
937
- context.file.imports.set(modelNamespace, [modelNamespace]);
938
- context.file.imports.set(namespace, [namespace]);
939
- controllers.set(name, context);
911
+ controllers.set(`${namespace}.${name}`, context);
940
912
  return context;
941
913
  }
942
914
  // eslint-disable-next-line no-unused-private-class-members
@@ -946,8 +918,8 @@ export async function $onEmit(context) {
946
918
  : "TypeSpec";
947
919
  }
948
920
  reference(targetDeclaration, pathUp, pathDown, commonScope) {
949
- //if (targetDeclaration.name) return targetDeclaration.name;
950
- return super.reference(targetDeclaration, pathUp, pathDown, commonScope);
921
+ const resolved = resolveReferenceFromScopes(targetDeclaration, pathDown, pathUp);
922
+ return resolved ?? super.reference(targetDeclaration, pathUp, pathDown, commonScope);
951
923
  }
952
924
  scalarInstantiation(scalar, name) {
953
925
  const scalarType = getCSharpTypeForScalar(this.emitter.getProgram(), scalar);
@@ -958,21 +930,8 @@ export async function $onEmit(context) {
958
930
  return scalarType.getTypeReference(this.emitter.getContext().scope);
959
931
  }
960
932
  sourceFile(sourceFile) {
961
- for (const libFile of this.#libraryFiles) {
962
- if (sourceFile === libFile.source)
963
- return libFile.emitted;
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
- }
933
+ if (sourceFile.meta.emitted) {
934
+ return sourceFile.meta.emitted;
976
935
  }
977
936
  const emittedSourceFile = {
978
937
  path: sourceFile.path,
@@ -990,15 +949,26 @@ export async function $onEmit(context) {
990
949
  }
991
950
  #emitCodeContents(file) {
992
951
  const contents = new StringBuilder();
952
+ contents.pushLiteralSegment(`${file.meta["nullable"] ? this.#generatedFileHeaderWithNullable : this.#generatedFileHeader}
953
+
954
+ ${this.#emitUsings(file)}
955
+ `);
993
956
  for (const decl of file.globalScope.declarations) {
994
957
  contents.push(decl.value);
995
958
  }
959
+ for (const child of file.globalScope.childScopes) {
960
+ if (child.declarations) {
961
+ for (const decl of child.declarations) {
962
+ contents.push(decl.value);
963
+ }
964
+ }
965
+ }
996
966
  return contents.segments.join("\n") + "\n";
997
967
  }
998
968
  #emitControllerContents(file) {
999
969
  const namespace = file.meta.namespace;
1000
970
  const contents = new StringBuilder();
1001
- contents.push(`${this.#generatedFileHeader}\n\n`);
971
+ contents.push(`${this.#generatedFileHeaderWithNullable}\n\n`);
1002
972
  contents.push(code `${this.#emitUsings(file)}\n`);
1003
973
  contents.push("\n");
1004
974
  contents.push(`namespace ${namespace}.Controllers\n`);
@@ -1063,92 +1033,18 @@ export async function $onEmit(context) {
1063
1033
  return params.reduce();
1064
1034
  return "";
1065
1035
  }
1066
- #coalesceUnionTypes(union) {
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
- ];
1087
- let current = undefined;
1088
- let nullable = false;
1089
- for (const type of types) {
1090
- let candidate = undefined;
1091
- switch (type.kind) {
1092
- case "Boolean":
1093
- candidate = new CSharpType({ name: "bool", namespace: "System", isValueType: true });
1094
- break;
1095
- case "StringTemplate":
1096
- case "String":
1097
- candidate = new CSharpType({ name: "string", namespace: "System", isValueType: false });
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;
1112
- case "Union":
1113
- candidate = this.#coalesceUnionTypes(type);
1114
- break;
1115
- case "Scalar":
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
- }
1126
- break;
1127
- default:
1128
- return defaultValue;
1129
- }
1130
- current = current ?? candidate;
1131
- if (current === undefined || (candidate !== undefined && !candidate.equals(current)))
1132
- return defaultValue;
1133
- }
1134
- if (current !== undefined && nullable)
1135
- current.isNullable = true;
1136
- return current === undefined ? defaultValue : [current, false];
1137
- }
1138
- writeOutput(sourceFiles) {
1139
- for (const source of this.#libraryFiles) {
1140
- sourceFiles.push(source.source);
1141
- }
1142
- if (this.#emitMocks === "all") {
1143
- for (const helper of this.#mockHelpers) {
1144
- sourceFiles.push(helper.source);
1145
- }
1036
+ async writeOutput(sourceFiles) {
1037
+ sourceFiles.push(...getSerializationSourceFiles(this.emitter).flatMap((l) => l.source));
1038
+ sourceFiles.push(...getProjectDocs(this.emitter, this.#useSwagger, this.#mockRegistrations).flatMap((l) => l.source));
1039
+ if (this.#emitMocks === "mocks-and-project-files" || this.#emitMocks === "mocks-only") {
1146
1040
  if (this.#mockRegistrations.size > 0) {
1147
- const mocks = getBusinessLogicImplementations(this.emitter, this.#mockRegistrations);
1148
- this.#mockFiles.push(...mocks);
1041
+ const mocks = getBusinessLogicImplementations(this.emitter, this.#mockRegistrations, this.#useSwagger, this.#openapiPath);
1149
1042
  sourceFiles.push(...mocks.flatMap((l) => l.source));
1150
1043
  }
1151
1044
  }
1045
+ async function shouldWrite(source, exists) {
1046
+ return (!source.meta.conditional || options.overwrite === true || !(await exists(source.path)));
1047
+ }
1152
1048
  const emittedSourceFiles = [];
1153
1049
  for (const source of sourceFiles) {
1154
1050
  switch (this.#emitterOutputType) {
@@ -1159,51 +1055,22 @@ export async function $onEmit(context) {
1159
1055
  // do nothing
1160
1056
  break;
1161
1057
  default:
1162
- emittedSourceFiles.push(source);
1058
+ if (await shouldWrite(source, this.#fileExists)) {
1059
+ emittedSourceFiles.push(source);
1060
+ }
1163
1061
  break;
1164
1062
  }
1165
1063
  }
1166
1064
  break;
1167
1065
  default:
1168
- emittedSourceFiles.push(source);
1066
+ if (await shouldWrite(source, this.#fileExists)) {
1067
+ emittedSourceFiles.push(source);
1068
+ }
1169
1069
  break;
1170
1070
  }
1171
1071
  }
1172
1072
  return super.writeOutput(emittedSourceFiles);
1173
1073
  }
1174
- #getOrSetBaseNamespace(type) {
1175
- if (this.#baseNamespace === undefined) {
1176
- if (type.namespace !== undefined) {
1177
- this.#baseNamespace = `${type.namespace
1178
- ? ensureCSharpIdentifier(this.emitter.getProgram(), type.namespace, getNamespaceFullName(type.namespace))
1179
- : "TypeSpec"}.Service`;
1180
- }
1181
- else {
1182
- this.#baseNamespace = "TypeSpec.Service";
1183
- }
1184
- }
1185
- return this.#baseNamespace;
1186
- }
1187
- }
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
1074
  }
1208
1075
  function processNameSpace(program, target, service) {
1209
1076
  if (!service)
@@ -1234,7 +1101,6 @@ export async function $onEmit(context) {
1234
1101
  }
1235
1102
  }
1236
1103
  const iface = program.checker.createAndFinishType({
1237
- node: undefined,
1238
1104
  sourceInterfaces: [],
1239
1105
  decorators: [],
1240
1106
  operations: createRekeyableMap(nsOps),
@@ -1268,17 +1134,60 @@ export async function $onEmit(context) {
1268
1134
  }
1269
1135
  }
1270
1136
  const emitter = createAssetEmitter(context.program, CSharpCodeEmitter, context);
1271
- const ns = context.program.checker.getGlobalNamespaceType();
1137
+ const ns = context.program.getGlobalNamespaceType();
1272
1138
  const options = emitter.getOptions();
1273
1139
  processNameSpace(context.program, ns);
1274
1140
  if (!doNotEmit) {
1275
- await ensureCleanDirectory(context.program, options.emitterOutputDir);
1141
+ const outputDir = options.emitterOutputDir;
1142
+ const generatedDir = path.join(outputDir, "generated");
1143
+ await ensureCleanDirectory(context.program, generatedDir);
1144
+ function normalizeSlashes(path) {
1145
+ return path.replaceAll("\\", "/");
1146
+ }
1147
+ async function getOpenApiPath() {
1148
+ if (options["openapi-path"])
1149
+ return options["openapi-path"];
1150
+ const openApiSettings = await getOpenApiConfig(context.program);
1151
+ const projectDir = resolvePath(context.program.projectRoot, outputDir);
1152
+ if (openApiSettings.outputDir) {
1153
+ const openApiPath = resolvePath(openApiSettings.outputDir, openApiSettings.fileName || "openapi.yaml");
1154
+ return normalizeSlashes(path.relative(projectDir, openApiPath));
1155
+ }
1156
+ if (openApiSettings.emitted) {
1157
+ const baseDir = context.program.compilerOptions.outputDir ||
1158
+ resolvePath(context.program.projectRoot, "tsp-output");
1159
+ const openApiPath = resolvePath(baseDir, "@typespec", "openapi3", openApiSettings.fileName || "openapi.yaml");
1160
+ return normalizeSlashes(path.relative(projectDir, openApiPath));
1161
+ }
1162
+ return "";
1163
+ }
1164
+ const openApiPath = await getOpenApiPath();
1165
+ const UseSwaggerUI = openApiPath !== "" && options["use-swaggerui"] === true;
1166
+ let httpPort;
1167
+ let httpsPort;
1168
+ if (options["emit-mocks"] !== "none") {
1169
+ getScaffoldingHelpers(emitter, UseSwaggerUI, openApiPath, true);
1170
+ }
1171
+ if (options["emit-mocks"] === "mocks-and-project-files") {
1172
+ httpPort = options["http-port"] || (await getFreePort(5000, 5999));
1173
+ httpsPort = options["https-port"] || (await getFreePort(7000, 7999));
1174
+ getProjectHelpers(emitter, options["project-name"] || "ServiceProject", options["use-swaggerui"] || false, httpPort, httpsPort);
1175
+ }
1276
1176
  await emitter.writeOutput();
1177
+ const projectDir = normalizeSlashes(path.relative(process.cwd(), resolvePath(outputDir)));
1178
+ function trace(message) {
1179
+ context.program.trace("http-server-csharp", `hscs-msg: ${message}`);
1180
+ }
1181
+ trace(`Your project was successfully created at "${projectDir}"`);
1182
+ trace(`You can build and start the project using 'dotnet run --project "${projectDir}"'`);
1183
+ if (options["use-swaggerui"] === true && httpsPort) {
1184
+ trace(`You can browse the swagger UI to test your service using 'start https://localhost:${httpsPort}/swagger/' `);
1185
+ }
1277
1186
  if (options["skip-format"] === undefined || options["skip-format"] === false) {
1278
1187
  await execFile("dotnet", [
1279
1188
  "format",
1280
1189
  "whitespace",
1281
- emitter.getOptions().emitterOutputDir,
1190
+ outputDir,
1282
1191
  "--include-generated",
1283
1192
  "--folder",
1284
1193
  ]);