@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.
- package/cmd/hscs.js +2 -0
- package/dist/src/cli/cli.d.ts +2 -0
- package/dist/src/cli/cli.d.ts.map +1 -0
- package/dist/src/cli/cli.js +124 -0
- package/dist/src/cli/cli.js.map +1 -0
- package/dist/src/{attributes.d.ts → lib/attributes.d.ts} +1 -1
- package/dist/src/lib/attributes.d.ts.map +1 -0
- package/dist/src/{attributes.js → lib/attributes.js} +48 -4
- package/dist/src/{attributes.js.map → lib/attributes.js.map} +1 -1
- package/dist/src/{boilerplate.d.ts → lib/boilerplate.d.ts} +2 -0
- package/dist/src/lib/boilerplate.d.ts.map +1 -0
- package/dist/src/{boilerplate.js → lib/boilerplate.js} +101 -36
- package/dist/src/lib/boilerplate.js.map +1 -0
- package/dist/src/lib/index.d.ts.map +1 -0
- package/dist/src/lib/index.js.map +1 -0
- package/dist/src/{interfaces.d.ts → lib/interfaces.d.ts} +10 -0
- package/dist/src/lib/interfaces.d.ts.map +1 -0
- package/dist/src/{interfaces.js → lib/interfaces.js} +7 -3
- package/dist/src/lib/interfaces.js.map +1 -0
- package/dist/src/{lib.d.ts → lib/lib.d.ts} +25 -1
- package/dist/src/lib/lib.d.ts.map +1 -0
- package/dist/src/{lib.js → lib/lib.js} +31 -0
- package/dist/src/lib/lib.js.map +1 -0
- package/dist/src/lib/scaffolding.d.ts +20 -0
- package/dist/src/lib/scaffolding.d.ts.map +1 -0
- package/dist/src/lib/scaffolding.js +388 -0
- package/dist/src/lib/scaffolding.js.map +1 -0
- package/dist/src/lib/service.d.ts.map +1 -0
- package/dist/src/{service.js → lib/service.js} +584 -125
- package/dist/src/lib/service.js.map +1 -0
- package/dist/src/lib/testing/index.d.ts.map +1 -0
- package/dist/src/{testing → lib/testing}/index.js +1 -0
- package/dist/src/lib/testing/index.js.map +1 -0
- package/dist/src/lib/type-helpers.d.ts.map +1 -0
- package/dist/src/lib/type-helpers.js.map +1 -0
- package/dist/src/{utils.d.ts → lib/utils.d.ts} +3 -3
- package/dist/src/lib/utils.d.ts.map +1 -0
- package/dist/src/{utils.js → lib/utils.js} +67 -50
- package/dist/src/lib/utils.js.map +1 -0
- package/package.json +32 -23
- package/dist/src/attributes.d.ts.map +0 -1
- package/dist/src/boilerplate.d.ts.map +0 -1
- package/dist/src/boilerplate.js.map +0 -1
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js.map +0 -1
- package/dist/src/interfaces.d.ts.map +0 -1
- package/dist/src/interfaces.js.map +0 -1
- package/dist/src/lib.d.ts.map +0 -1
- package/dist/src/lib.js.map +0 -1
- package/dist/src/service.d.ts.map +0 -1
- package/dist/src/service.js.map +0 -1
- package/dist/src/testing/index.d.ts.map +0 -1
- package/dist/src/testing/index.js.map +0 -1
- package/dist/src/type-helpers.d.ts.map +0 -1
- package/dist/src/type-helpers.js.map +0 -1
- package/dist/src/utils.d.ts.map +0 -1
- package/dist/src/utils.js.map +0 -1
- /package/dist/src/{index.d.ts → lib/index.d.ts} +0 -0
- /package/dist/src/{index.js → lib/index.js} +0 -0
- /package/dist/src/{service.d.ts → lib/service.d.ts} +0 -0
- /package/dist/src/{testing → lib/testing}/index.d.ts +0 -0
- /package/dist/src/{type-helpers.d.ts → lib/type-helpers.d.ts} +0 -0
- /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,
|
|
3
|
-
import {
|
|
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
|
-
#
|
|
18
|
-
|
|
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.#
|
|
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.#
|
|
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
|
|
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
|
|
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 ${
|
|
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
|
-
|
|
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`, `"${
|
|
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`, `${
|
|
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(
|
|
215
|
-
return [code `JsonObject`, undefined];
|
|
380
|
+
if (this.#isRecord(tsType)) {
|
|
381
|
+
return [code `JsonObject`, undefined, false];
|
|
216
382
|
}
|
|
217
|
-
return [code `${
|
|
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 `${
|
|
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.#
|
|
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
|
-
|
|
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
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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 ===
|
|
315
|
-
|
|
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
|
-
|
|
564
|
+
? `var result = await ${this.emitter.getContext().resourceName}Impl.${operationName}Async(${this.#emitOperationCallParameters(httpOperation)});
|
|
324
565
|
return ${resultString}(result);`
|
|
325
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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 ===
|
|
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
|
|
379
|
-
if (
|
|
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
|
|
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.
|
|
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,
|
|
686
|
+
#emitInterfaceOperationParameters(operation, bodyParam) {
|
|
415
687
|
const signature = new StringBuilder();
|
|
416
688
|
const requiredParams = [];
|
|
417
689
|
const optionalParams = [];
|
|
418
690
|
let totalParams = 0;
|
|
419
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
447
|
-
|
|
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.#
|
|
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
|
-
|
|
525
|
-
|
|
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
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
546
|
-
|
|
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}
|
|
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.#
|
|
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
|
|
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
|
|
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
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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
|
-
|
|
725
|
-
|
|
726
|
-
|
|
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(
|
|
1113
|
+
candidate = this.#coalesceUnionTypes(type);
|
|
735
1114
|
break;
|
|
736
1115
|
case "Scalar":
|
|
737
|
-
candidate = getCSharpTypeForScalar(this.emitter.getProgram(),
|
|
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
|
-
|
|
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
|
|
789
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 [_,
|
|
804
|
-
|
|
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
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
"
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
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
|