@typespec/http-server-csharp 0.58.0-alpha.6-dev.1 → 0.58.0-alpha.6-dev.3

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.
@@ -1,5 +1,6 @@
1
- import { getDoc, getNamespaceFullName, getService, isErrorModel, isNeverType, isNullType, isVoidType, } from "@typespec/compiler";
1
+ import { getDoc, getNamespaceFullName, getService, isErrorModel, isNeverType, isNullType, isTemplateDeclaration, isVoidType, } from "@typespec/compiler";
2
2
  import { CodeTypeEmitter, StringBuilder, code, createAssetEmitter, } from "@typespec/compiler/emitter-framework";
3
+ import { createRekeyableMap } from "@typespec/compiler/utils";
3
4
  import { Visibility, createMetadataInfo, getHttpOperation, isStatusCode, } from "@typespec/http";
4
5
  import { getResourceOperation } from "@typespec/rest";
5
6
  import { execFile } from "child_process";
@@ -7,7 +8,7 @@ import { getSerializationSourceFiles } from "./boilerplate.js";
7
8
  import { CSharpSourceType, CSharpType, NameCasingType, } from "./interfaces.js";
8
9
  import { reportDiagnostic } from "./lib.js";
9
10
  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";
11
+ import { HttpMetadata, UnknownType, coalesceTypes, ensureCSharpIdentifier, ensureCleanDirectory, formatComment, getCSharpIdentifier, getCSharpStatusCode, getCSharpType, getCSharpTypeForIntrinsic, getCSharpTypeForScalar, getModelAttributes, getModelInstantiationName, getOperationVerbDecorator, isValueType, } from "./utils.js";
11
12
  export async function $onEmit(context) {
12
13
  let _unionCounter = 0;
13
14
  const controllers = new Map();
@@ -40,8 +41,17 @@ export async function $onEmit(context) {
40
41
  declarationName(declarationType) {
41
42
  switch (declarationType.kind) {
42
43
  case "Enum":
44
+ if (!declarationType.name)
45
+ return `Enum${_unionCounter++}`;
46
+ return getCSharpIdentifier(declarationType.name, NameCasingType.Class);
43
47
  case "Interface":
48
+ if (!declarationType.name)
49
+ return `Interface${_unionCounter++}`;
50
+ return getCSharpIdentifier(declarationType.name, NameCasingType.Class);
44
51
  case "Model":
52
+ if (!declarationType.name)
53
+ return `Model${_unionCounter++}`;
54
+ return getCSharpIdentifier(declarationType.name, NameCasingType.Class);
45
55
  case "Operation":
46
56
  return getCSharpIdentifier(declarationType.name, NameCasingType.Class);
47
57
  case "Union":
@@ -108,13 +118,15 @@ export async function $onEmit(context) {
108
118
  return this.emitter.result.rawCode(code `System.Text.Json.Nodes.JsonNode`);
109
119
  case "ErrorType":
110
120
  case "never":
111
- case "void":
112
121
  reportDiagnostic(this.emitter.getProgram(), {
113
122
  code: "invalid-intrinsic",
114
123
  target: intrinsic,
115
124
  format: { typeName: intrinsic.name },
116
125
  });
117
126
  return "";
127
+ case "void":
128
+ const type = getCSharpTypeForIntrinsic(this.emitter.getProgram(), intrinsic);
129
+ return this.emitter.result.rawCode(`${type?.type.getTypeReference()}`);
118
130
  }
119
131
  }
120
132
  #emitUsings(file) {
@@ -150,14 +162,14 @@ export async function $onEmit(context) {
150
162
  const modelFile = this.emitter.createSourceFile(`models/${modelName}.cs`);
151
163
  modelFile.meta[this.#sourceTypeKey] = CSharpSourceType.Model;
152
164
  const modelNamespace = `${this.#getOrSetBaseNamespace(model)}.Models`;
153
- return this.#createModelContext(modelNamespace, modelFile);
165
+ return this.#createModelContext(modelNamespace, modelFile, modelName);
154
166
  }
155
167
  modelInstantiationContext(model) {
156
168
  const modelName = getModelInstantiationName(this.emitter.getProgram(), model, model.name);
157
169
  const sourceFile = this.emitter.createSourceFile(`models/${modelName}.cs`);
158
170
  sourceFile.meta[this.#sourceTypeKey] = CSharpSourceType.Model;
159
171
  const modelNamespace = `${this.#getOrSetBaseNamespace(model)}.Models`;
160
- const context = this.#createModelContext(modelNamespace, sourceFile);
172
+ const context = this.#createModelContext(modelNamespace, sourceFile, model.name);
161
173
  context.instantiationName = modelName;
162
174
  return context;
163
175
  }
@@ -177,48 +189,178 @@ export async function $onEmit(context) {
177
189
  if (!isVoidType(prop.type) &&
178
190
  !isNeverType(prop.type) &&
179
191
  !isNullType(prop.type) &&
180
- !isErrorModel(this.emitter.getProgram(), prop.type))
192
+ !isErrorModel(this.emitter.getProgram(), prop.type)) {
181
193
  result.push(code `${this.emitter.emitModelProperty(prop)}`);
194
+ }
182
195
  }
183
196
  return result.reduce();
184
197
  }
198
+ modelLiteralContext(model) {
199
+ const name = this.emitter.emitDeclarationName(model) || "";
200
+ return this.modelDeclarationContext(model, name);
201
+ }
202
+ modelLiteral(model) {
203
+ const modelName = this.emitter.getContext().name;
204
+ reportDiagnostic(this.emitter.getProgram(), {
205
+ code: "anonymous-model",
206
+ target: model,
207
+ format: { emittedName: modelName },
208
+ });
209
+ return this.modelDeclaration(model, modelName);
210
+ }
185
211
  #isRecord(type) {
186
212
  return type.kind === "Model" && type.name === "Record" && type.indexer !== undefined;
187
213
  }
214
+ #isInheritedProperty(property) {
215
+ const visited = [];
216
+ function isInherited(model, propertyName) {
217
+ if (visited.includes(model))
218
+ return false;
219
+ visited.push(model);
220
+ if (model.properties.has(propertyName))
221
+ return true;
222
+ if (model.baseModel === undefined)
223
+ return false;
224
+ return isInherited(model.baseModel, propertyName);
225
+ }
226
+ const model = property.model;
227
+ if (model === undefined || model.baseModel === undefined)
228
+ return false;
229
+ return isInherited(model.baseModel, property.name);
230
+ }
188
231
  modelPropertyLiteral(property) {
189
232
  if (isStatusCode(this.emitter.getProgram(), property))
190
233
  return "";
191
234
  const propertyName = ensureCSharpIdentifier(this.emitter.getProgram(), property, property.name);
192
- const [typeName, typeDefault] = this.#findPropertyType(property);
235
+ const [typeName, typeDefault, nullable] = this.#findPropertyType(property);
193
236
  const doc = getDoc(this.emitter.getProgram(), property);
194
- const attributes = getModelAttributes(this.emitter.getProgram(), property);
237
+ const attributes = getModelAttributes(this.emitter.getProgram(), property, propertyName);
195
238
  // eslint-disable-next-line @typescript-eslint/no-deprecated
196
239
  const defaultValue = property.default
197
240
  ? // eslint-disable-next-line @typescript-eslint/no-deprecated
198
241
  code `${this.emitter.emitType(property.default)}`
199
242
  : typeDefault;
200
243
  return this.emitter.result
201
- .rawCode(code `${doc ? `${formatComment(doc)}\n` : ""}${`${attributes.map((attribute) => attribute.getApplicationString(this.emitter.getContext().scope)).join("\n")}${attributes?.length > 0 ? "\n" : ""}`}public ${typeName}${property.optional && isValueType(this.emitter.getProgram(), property.type) ? "?" : ""} ${propertyName} { get; ${typeDefault ? "}" : "set; }"}${defaultValue ? ` = ${defaultValue};\n` : "\n"}
244
+ .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)
245
+ ? "?"
246
+ : ""} ${propertyName} { get; ${typeDefault ? "}" : "set; }"}${defaultValue ? ` = ${defaultValue};\n` : "\n"}
202
247
  `);
203
248
  }
204
249
  #findPropertyType(property) {
205
- switch (property.type.kind) {
250
+ return this.#getTypeInfoForTsType(property.type);
251
+ }
252
+ #getTypeInfoForUnion(union) {
253
+ const propResult = this.#getNonNullableTsType(union);
254
+ if (propResult === undefined) {
255
+ return [
256
+ code `${emitter.emitTypeReference(union)}`,
257
+ undefined,
258
+ [...union.variants.values()].filter((v) => isNullType(v.type)).length > 0,
259
+ ];
260
+ }
261
+ const [typeName, typeDefault, _] = this.#getTypeInfoForTsType(propResult.type);
262
+ return [typeName, typeDefault, propResult.nullable];
263
+ }
264
+ #getTypeInfoForTsType(tsType) {
265
+ function extractStringValue(type, span) {
266
+ switch (type.kind) {
267
+ case "String":
268
+ return type.value;
269
+ case "Boolean":
270
+ return `${type.value}`;
271
+ case "Number":
272
+ return type.valueAsString;
273
+ case "StringTemplateSpan":
274
+ if (type.isInterpolated) {
275
+ return extractStringValue(type.type, span);
276
+ }
277
+ else {
278
+ return type.type.value;
279
+ }
280
+ case "ModelProperty":
281
+ return extractStringValue(type.type, span);
282
+ case "EnumMember":
283
+ if (type.value === undefined)
284
+ return type.name;
285
+ if (typeof type.value === "string")
286
+ return type.value;
287
+ if (typeof type.value === "number")
288
+ return `${type.value}`;
289
+ }
290
+ reportDiagnostic(emitter.getProgram(), {
291
+ code: "invalid-interpolation",
292
+ target: span,
293
+ format: {},
294
+ });
295
+ return "";
296
+ }
297
+ switch (tsType.kind) {
206
298
  case "String":
207
- return [code `string`, `"${property.type.value}"`];
299
+ return [code `string`, `"${tsType.value}"`, false];
300
+ case "StringTemplate":
301
+ const template = tsType;
302
+ if (template.stringValue !== undefined)
303
+ return [code `string`, `"${template.stringValue}"`, false];
304
+ const spanResults = [];
305
+ for (const span of template.spans) {
306
+ spanResults.push(extractStringValue(span, span));
307
+ }
308
+ return [code `string`, `"${spanResults.join("")}"`, false];
208
309
  case "Boolean":
209
- return [code `bool`, `${property.type.value === true ? true : false}`];
310
+ return [code `bool`, `${tsType.value === true ? true : false}`, false];
210
311
  case "Number":
312
+ const [type, value] = this.#findNumericType(tsType);
313
+ return [code `${type}`, `${value}`, false];
314
+ case "Tuple":
315
+ const defaults = [];
316
+ const [csharpType, isObject] = this.#coalesceTypes(tsType.values);
317
+ if (isObject)
318
+ return ["object[]", undefined, false];
319
+ for (const value of tsType.values) {
320
+ const [_, itemDefault] = this.#getTypeInfoForTsType(value);
321
+ defaults.push(itemDefault);
322
+ }
323
+ return [
324
+ code `${csharpType.getTypeReference()}[]`,
325
+ `[${defaults.join(", ")}]`,
326
+ csharpType.isNullable,
327
+ ];
211
328
  case "Object":
212
- return [code `object`, undefined];
329
+ return [code `object`, undefined, false];
213
330
  case "Model":
214
- if (this.#isRecord(property.type)) {
215
- return [code `JsonObject`, undefined];
331
+ if (this.#isRecord(tsType)) {
332
+ return [code `JsonObject`, undefined, false];
333
+ }
334
+ return [code `${emitter.emitTypeReference(tsType)}`, undefined, false];
335
+ case "ModelProperty":
336
+ return this.#getTypeInfoForTsType(tsType.type);
337
+ case "Enum":
338
+ return [code `${emitter.emitTypeReference(tsType)}`, undefined, false];
339
+ case "EnumMember":
340
+ if (typeof tsType.value === "number") {
341
+ const stringValue = tsType.value.toString();
342
+ if (stringValue.includes(".") || stringValue.includes("e"))
343
+ return ["double", stringValue, false];
344
+ return ["int", stringValue, false];
216
345
  }
217
- return [code `${this.emitter.emitTypeReference(property.type)}`, undefined];
346
+ if (typeof tsType.value === "string") {
347
+ return ["string", tsType.value, false];
348
+ }
349
+ return [code `object`, undefined, false];
350
+ case "Union":
351
+ return this.#getTypeInfoForUnion(tsType);
352
+ case "UnionVariant":
353
+ return this.#getTypeInfoForTsType(tsType.type);
218
354
  default:
219
- return [code `${this.emitter.emitTypeReference(property.type)}`, undefined];
355
+ return [code `${emitter.emitTypeReference(tsType)}`, undefined, false];
220
356
  }
221
357
  }
358
+ #findNumericType(type) {
359
+ const stringValue = type.valueAsString;
360
+ if (stringValue.includes(".") || stringValue.includes("e"))
361
+ return ["double", stringValue];
362
+ return ["int", stringValue];
363
+ }
222
364
  modelPropertyReference(property) {
223
365
  return code `${this.emitter.emitTypeReference(property.type)}`;
224
366
  }
@@ -252,7 +394,7 @@ export async function $onEmit(context) {
252
394
  sourceFile.meta[this.#sourceTypeKey] = CSharpSourceType.Interface;
253
395
  const ifaceNamespace = this.#getOrSetBaseNamespace(iface);
254
396
  const modelNamespace = `${ifaceNamespace}.Models`;
255
- const context = this.#createModelContext(ifaceNamespace, sourceFile);
397
+ const context = this.#createModelContext(ifaceNamespace, sourceFile, ifaceName);
256
398
  context.file.imports.set("System", ["System"]);
257
399
  context.file.imports.set("System.Net", ["System.Net"]);
258
400
  context.file.imports.set("System.Text.Json", ["System.Text.Json"]);
@@ -369,6 +511,9 @@ export async function $onEmit(context) {
369
511
  const [httpOperation, _] = getHttpOperation(this.emitter.getProgram(), operation);
370
512
  return this.#emitOperationResponses(httpOperation);
371
513
  }
514
+ stringTemplate(stringTemplate) {
515
+ return this.emitter.result.rawCode(stringTemplate.stringValue || "");
516
+ }
372
517
  #getOperationResponse(operation) {
373
518
  const validResponses = operation.responses.filter((r) => !isErrorModel(this.emitter.getProgram(), r.type) &&
374
519
  getCSharpStatusCode(r.statusCodes) !== undefined);
@@ -397,7 +542,8 @@ export async function $onEmit(context) {
397
542
  }
398
543
  }
399
544
  for (const response of operation.responses) {
400
- this.emitter.emitType(response.type);
545
+ if (!isEmptyResponseModel(this.emitter.getProgram(), response.type))
546
+ this.emitter.emitType(response.type);
401
547
  }
402
548
  return builder.reduce();
403
549
  }
@@ -425,10 +571,12 @@ export async function $onEmit(context) {
425
571
  }
426
572
  let i = 1;
427
573
  for (const requiredParam of requiredParams) {
428
- signature.push(code `${this.emitter.emitTypeReference(requiredParam.type)} ${ensureCSharpIdentifier(this.emitter.getProgram(), requiredParam, requiredParam.name, NameCasingType.Parameter)}${i++ < totalParams ? ", " : ""}`);
574
+ const [paramType, _, __] = this.#findPropertyType(requiredParam);
575
+ signature.push(code `${paramType} ${ensureCSharpIdentifier(this.emitter.getProgram(), requiredParam, requiredParam.name, NameCasingType.Parameter)}${i++ < totalParams ? ", " : ""}`);
429
576
  }
430
577
  for (const optionalParam of optionalParams) {
431
- signature.push(code `${this.emitter.emitTypeReference(optionalParam.type)}? ${ensureCSharpIdentifier(this.emitter.getProgram(), optionalParam, optionalParam.name, NameCasingType.Parameter)}${i++ < totalParams ? ", " : ""}`);
578
+ const [paramType, _, __] = this.#findPropertyType(optionalParam);
579
+ signature.push(code `${paramType}? ${ensureCSharpIdentifier(this.emitter.getProgram(), optionalParam, optionalParam.name, NameCasingType.Parameter)}${i++ < totalParams ? ", " : ""}`);
432
580
  }
433
581
  return signature.reduce();
434
582
  }
@@ -521,7 +669,9 @@ export async function $onEmit(context) {
521
669
  const name = httpParam.param.name;
522
670
  const parameter = httpParam.param;
523
671
  const emittedName = ensureCSharpIdentifier(this.emitter.getProgram(), parameter, name, NameCasingType.Parameter);
524
- const [emittedType, emittedDefault] = this.#findPropertyType(parameter);
672
+ let [emittedType, emittedDefault, _] = this.#findPropertyType(parameter);
673
+ if (emittedType.toString().endsWith("[]"))
674
+ emittedDefault = undefined;
525
675
  // eslint-disable-next-line @typescript-eslint/no-deprecated
526
676
  const defaultValue = parameter.default
527
677
  ? // eslint-disable-next-line @typescript-eslint/no-deprecated
@@ -529,21 +679,41 @@ export async function $onEmit(context) {
529
679
  : emittedDefault;
530
680
  return this.emitter.result.rawCode(code `${httpParam.type !== "path" ? this.#emitParameterAttribute(httpParam) : ""}${emittedType} ${emittedName}${defaultValue === undefined ? "" : ` = ${defaultValue}`}`);
531
681
  }
682
+ #getBodyParameters(operation) {
683
+ const bodyParam = operation.parameters.body;
684
+ if (bodyParam === undefined)
685
+ return undefined;
686
+ if (bodyParam.property !== undefined)
687
+ return [bodyParam.property];
688
+ if (bodyParam.type.kind !== "Model" || bodyParam.type.properties.size < 1)
689
+ return undefined;
690
+ return [...bodyParam.type.properties.values()];
691
+ }
532
692
  #emitOperationCallParameters(operation) {
533
693
  const signature = new StringBuilder();
534
- const bodyParam = operation.parameters.body;
535
694
  let i = 0;
695
+ const bodyParameters = this.#getBodyParameters(operation);
536
696
  //const pathParameters = operation.parameters.parameters.filter((p) => p.type === "path");
537
697
  for (const parameter of operation.parameters.parameters) {
538
698
  i++;
539
699
  if (!isNeverType(parameter.param.type) &&
540
700
  !isNullType(parameter.param.type) &&
541
701
  !isVoidType(parameter.param.type)) {
542
- signature.push(code `${this.#emitOperationCallParameter(operation, parameter)}${i < operation.parameters.parameters.length || bodyParam !== undefined ? ", " : ""}`);
702
+ signature.push(code `${this.#emitOperationCallParameter(operation, parameter)}${i < operation.parameters.parameters.length || bodyParameters !== undefined ? ", " : ""}`);
543
703
  }
544
704
  }
545
- if (bodyParam !== undefined) {
546
- signature.push(code `body`);
705
+ if (bodyParameters !== undefined) {
706
+ if (bodyParameters.length === 1) {
707
+ signature.push(code `body`);
708
+ }
709
+ else {
710
+ let j = 0;
711
+ for (const parameter of bodyParameters) {
712
+ j++;
713
+ const propertyName = ensureCSharpIdentifier(this.emitter.getProgram(), parameter, parameter.name, NameCasingType.Property);
714
+ signature.push(code `body?.${propertyName}${j < bodyParameters.length ? ", " : ""}`);
715
+ }
716
+ }
547
717
  }
548
718
  return signature.reduce();
549
719
  }
@@ -563,9 +733,10 @@ export async function $onEmit(context) {
563
733
  return "";
564
734
  }
565
735
  }
566
- #createModelContext(namespace, file) {
736
+ #createModelContext(namespace, file, name) {
567
737
  const context = {
568
738
  namespace: namespace,
739
+ name: name,
569
740
  file: file,
570
741
  scope: file.globalScope,
571
742
  };
@@ -716,35 +887,76 @@ export async function $onEmit(context) {
716
887
  return "";
717
888
  }
718
889
  #coalesceUnionTypes(union) {
719
- const defaultValue = new CSharpType({
720
- name: "object",
721
- namespace: "System",
722
- isValueType: false,
723
- });
890
+ const [result, _] = this.#coalesceTypes([...union.variants.values()].flatMap((v) => v.type));
891
+ return result;
892
+ }
893
+ #getNonNullableTsType(union) {
894
+ const types = [...union.variants.values()];
895
+ const nulls = types.flatMap((v) => v.type).filter((t) => isNullType(t));
896
+ const nonNulls = types.flatMap((v) => v.type).filter((t) => !isNullType(t));
897
+ if (nonNulls.length === 1)
898
+ return { type: nonNulls[0], nullable: nulls.length > 0 };
899
+ return undefined;
900
+ }
901
+ #coalesceTypes(types) {
902
+ const defaultValue = [
903
+ new CSharpType({
904
+ name: "object",
905
+ namespace: "System",
906
+ isValueType: false,
907
+ }),
908
+ true,
909
+ ];
724
910
  let current = undefined;
725
- for (const [_, variant] of union.variants.entries()) {
726
- let candidate;
727
- switch (variant.type.kind) {
911
+ let nullable = false;
912
+ for (const type of types) {
913
+ let candidate = undefined;
914
+ switch (type.kind) {
728
915
  case "Boolean":
729
916
  candidate = new CSharpType({ name: "bool", namespace: "System", isValueType: true });
730
917
  break;
918
+ case "StringTemplate":
731
919
  case "String":
732
920
  candidate = new CSharpType({ name: "string", namespace: "System", isValueType: false });
733
921
  break;
922
+ case "Number":
923
+ const stringValue = type.valueAsString;
924
+ if (stringValue.includes(".") || stringValue.includes("e")) {
925
+ candidate = new CSharpType({
926
+ name: "double",
927
+ namespace: "System",
928
+ isValueType: true,
929
+ });
930
+ }
931
+ else {
932
+ candidate = new CSharpType({ name: "int", namespace: "System", isValueType: true });
933
+ }
934
+ break;
734
935
  case "Union":
735
- candidate = this.#coalesceUnionTypes(variant.type);
936
+ candidate = this.#coalesceUnionTypes(type);
736
937
  break;
737
938
  case "Scalar":
738
- candidate = getCSharpTypeForScalar(this.emitter.getProgram(), variant.type);
939
+ candidate = getCSharpTypeForScalar(this.emitter.getProgram(), type);
940
+ break;
941
+ case "Intrinsic":
942
+ if (isNullType(type)) {
943
+ nullable = true;
944
+ candidate = current;
945
+ }
946
+ else {
947
+ return defaultValue;
948
+ }
739
949
  break;
740
950
  default:
741
951
  return defaultValue;
742
952
  }
743
953
  current = current ?? candidate;
744
- if (current === undefined || !candidate.equals(current))
954
+ if (current === undefined || (candidate !== undefined && !candidate.equals(current)))
745
955
  return defaultValue;
746
956
  }
747
- return current ?? defaultValue;
957
+ if (current !== undefined && nullable)
958
+ current.isNullable = true;
959
+ return current === undefined ? defaultValue : [current, false];
748
960
  }
749
961
  writeOutput(sourceFiles) {
750
962
  for (const source of this.#libraryFiles) {
@@ -786,11 +998,19 @@ export async function $onEmit(context) {
786
998
  return this.#baseNamespace;
787
999
  }
788
1000
  }
789
- function processNameSpace(program, target) {
790
- const service = getService(program, target);
1001
+ function isEmptyResponseModel(program, model) {
1002
+ if (model.kind !== "Model")
1003
+ return false;
1004
+ return model.properties.size === 1 && isStatusCode(program, [...model.properties.values()][0]);
1005
+ }
1006
+ function processNameSpace(program, target, service) {
1007
+ if (!service)
1008
+ service = getService(program, target);
791
1009
  if (service) {
792
1010
  for (const [_, model] of target.models) {
793
- emitter.emitType(model);
1011
+ if (!isTemplateDeclaration(model) && !isEmptyResponseModel(program, model)) {
1012
+ emitter.emitType(model);
1013
+ }
794
1014
  }
795
1015
  for (const [_, en] of target.enums) {
796
1016
  emitter.emitType(en);
@@ -799,10 +1019,36 @@ export async function $onEmit(context) {
799
1019
  emitter.emitType(sc);
800
1020
  }
801
1021
  for (const [_, iface] of target.interfaces) {
1022
+ if (!isTemplateDeclaration(iface)) {
1023
+ emitter.emitType(iface);
1024
+ }
1025
+ }
1026
+ if (target.operations.size > 0) {
1027
+ // Collect interface operations for a business logic interface and controller
1028
+ const nsOps = [];
1029
+ for (const [name, op] of target.operations) {
1030
+ if (!isTemplateDeclaration(op)) {
1031
+ nsOps.push([name, op]);
1032
+ }
1033
+ }
1034
+ const iface = program.checker.createAndFinishType({
1035
+ node: undefined,
1036
+ sourceInterfaces: [],
1037
+ decorators: [],
1038
+ operations: createRekeyableMap(nsOps),
1039
+ kind: "Interface",
1040
+ name: `${target.name}Operations`,
1041
+ namespace: target,
1042
+ entityKind: "Type",
1043
+ isFinished: true,
1044
+ });
1045
+ for (const [_, op] of nsOps) {
1046
+ op.interface = iface;
1047
+ }
802
1048
  emitter.emitType(iface);
803
1049
  }
804
- for (const [_, op] of target.operations) {
805
- emitter.emitType(op);
1050
+ for (const [_, sub] of target.namespaces) {
1051
+ processNameSpace(program, sub, service);
806
1052
  }
807
1053
  }
808
1054
  else {