Skip to content

Commit 9cb77e6

Browse files
committed
Support anonymous response body and other fixes
1 parent d2d8cde commit 9cb77e6

File tree

5 files changed

+254
-41
lines changed

5 files changed

+254
-41
lines changed

cspell.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ words:
146146
- nanos
147147
- nexted
148148
- nihao
149+
- nint
149150
- NODEFS
150151
- noformat
151152
- noopener
@@ -156,6 +157,7 @@ words:
156157
- npath
157158
- npmjs
158159
- nspkg
160+
- nuint
159161
- nupkg
160162
- oapi
161163
- ODATA

packages/http-server-csharp/src/lib/service.ts

+66-29
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import {
6363
} from "./boilerplate.js";
6464
import { getProjectDocs } from "./doc.js";
6565
import {
66+
Attribute,
6667
CSharpSourceType,
6768
CSharpType,
6869
CSharpTypeMetadata,
@@ -554,7 +555,12 @@ export async function $onEmit(context: EmitContext<CSharpServiceEmitterOptions>)
554555
nullableType: nullable,
555556
} = this.#findPropertyType(property);
556557
const doc = getDoc(this.emitter.getProgram(), property);
557-
const attributes = getModelAttributes(this.emitter.getProgram(), property, propertyName);
558+
const attributes = new Map<string, Attribute>(
559+
getModelAttributes(this.emitter.getProgram(), property, propertyName).map((a) => [
560+
a.type.name,
561+
a,
562+
]),
563+
);
558564
const modelName: string | undefined = this.emitter.getContext()["name"];
559565
if (
560566
modelName === propertyName ||
@@ -563,15 +569,15 @@ export async function $onEmit(context: EmitContext<CSharpServiceEmitterOptions>)
563569
isErrorModel(this.emitter.getProgram(), property.model))
564570
) {
565571
propertyName = `${propertyName}Prop`;
566-
attributes.push(
567-
getEncodedNameAttribute(this.emitter.getProgram(), property, propertyName)!,
568-
);
572+
const attr = getEncodedNameAttribute(this.emitter.getProgram(), property, propertyName)!;
573+
if (!attributes.has(attr.type.name)) attributes.set(attr.type.name, attr);
569574
}
570575
const defaultValue = property.defaultValue
571576
? code`${JSON.stringify(serializeValueAsJson(this.emitter.getProgram(), property.defaultValue, property))}`
572577
: typeDefault;
578+
const attributeList = [...attributes.values()];
573579
return this.emitter.result
574-
.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}${
580+
.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}${
575581
isValueType(this.emitter.getProgram(), property.type) && (property.optional || nullable)
576582
? "?"
577583
: ""
@@ -658,7 +664,6 @@ export async function $onEmit(context: EmitContext<CSharpServiceEmitterOptions>)
658664
interfaceDeclarationOperations(iface: Interface): EmitterOutput<string> {
659665
// add in operations
660666
const builder: StringBuilder = new StringBuilder();
661-
const metadata = new HttpMetadata();
662667
const context = this.emitter.getContext();
663668
const name = `${ensureCSharpIdentifier(
664669
this.emitter.getProgram(),
@@ -682,9 +687,8 @@ export async function $onEmit(context: EmitContext<CSharpServiceEmitterOptions>)
682687
for (const response of httpOp.responses.filter(
683688
(r) => !isErrorModel(this.emitter.getProgram(), r.type),
684689
)) {
685-
returnTypes.push(
686-
metadata.resolveLogicalResponseType(this.emitter.getProgram(), response),
687-
);
690+
const [_, responseType] = this.#resolveOperationResponse(response, httpOp.operation);
691+
returnTypes.push(responseType);
688692
}
689693
const returnInfo = coalesceTypes(this.emitter.getProgram(), returnTypes, namespace);
690694
const returnType: CSharpType = returnInfo?.type || UnknownType;
@@ -878,6 +882,32 @@ export async function $onEmit(context: EmitContext<CSharpServiceEmitterOptions>)
878882
return this.emitter.result.rawCode(stringTemplate.stringValue || "");
879883
}
880884

885+
#resolveOperationResponse(
886+
response: HttpOperationResponse,
887+
operation: Operation,
888+
): [CSharpType, Type] {
889+
function getName(sourceType: Model, part: string): string {
890+
return ensureCSharpIdentifier(emitter.getProgram(), sourceType, part, NameCasingType.Class);
891+
}
892+
let responseType = new HttpMetadata().resolveLogicalResponseType(
893+
this.emitter.getProgram(),
894+
response,
895+
);
896+
897+
if (responseType.kind === "Model" && !responseType.name) {
898+
const modelName = `${getName(responseType, operation.interface!.name)}${getName(responseType, operation.name)}Response}`;
899+
const returnedType = this.emitter
900+
.getProgram()
901+
.checker.cloneType(responseType, { name: modelName });
902+
responseType = returnedType;
903+
}
904+
this.emitter.emitType(responseType);
905+
906+
const context = this.emitter.getContext();
907+
const result = getCSharpType(this.emitter.getProgram(), responseType, context.namespace);
908+
return [result?.type || UnknownType, responseType];
909+
}
910+
881911
#getOperationResponse(operation: HttpOperation): ResponseInfo | undefined {
882912
const validResponses = operation.responses.filter(
883913
(r) =>
@@ -902,46 +932,53 @@ export async function $onEmit(context: EmitContext<CSharpServiceEmitterOptions>)
902932
};
903933
}
904934
#emitOperationResponses(operation: HttpOperation): EmitterOutput<string> {
935+
function isValid(program: Program, response: HttpOperationResponse) {
936+
return (
937+
!isErrorModel(program, response.type) &&
938+
getCSharpStatusCode(response.statusCodes) !== undefined
939+
);
940+
}
905941
const builder: StringBuilder = new StringBuilder();
906942
let i = 0;
907943
const validResponses = operation.responses.filter(
908944
(r) =>
909945
!isErrorModel(this.emitter.getProgram(), r.type) &&
910946
getCSharpStatusCode(r.statusCodes) !== undefined,
911947
);
912-
for (const response of validResponses) {
913-
i++;
914-
builder.push(code`${this.#emitOperationResponseDecorator(response)}`);
915-
if (i < validResponses.length) {
916-
builder.pushLiteralSegment("\n");
917-
}
918-
}
919-
920948
for (const response of operation.responses) {
921-
if (!isEmptyResponseModel(this.emitter.getProgram(), response.type))
922-
this.emitter.emitType(response.type);
949+
const [responseType, resolvedResponse] = this.#resolveOperationResponse(
950+
response,
951+
operation.operation,
952+
);
953+
if (isValid(this.emitter.getProgram(), response)) {
954+
i++;
955+
builder.push(
956+
code`${this.#emitOperationResponseDecorator(response, resolvedResponse, responseType)}`,
957+
);
958+
if (i < validResponses.length) {
959+
builder.pushLiteralSegment("\n");
960+
}
961+
}
923962
}
924963

925964
return builder.reduce();
926965
}
927966

928-
#emitOperationResponseDecorator(response: HttpOperationResponse) {
929-
const responseType = new HttpMetadata().resolveLogicalResponseType(
930-
this.emitter.getProgram(),
931-
response,
932-
);
967+
#emitOperationResponseDecorator(
968+
response: HttpOperationResponse,
969+
responseType: Type,
970+
result: CSharpType,
971+
) {
933972
return this.emitter.result.rawCode(
934973
code`[ProducesResponseType((int)${getCSharpStatusCode(
935974
response.statusCodes,
936-
)!}, Type = typeof(${this.#emitResponseType(responseType)}))]`,
975+
)!}, Type = typeof(${this.#emitResponseType(result)}))]`,
937976
);
938977
}
939978

940-
#emitResponseType(type: Type) {
979+
#emitResponseType(type: CSharpType) {
941980
const context = this.emitter.getContext();
942-
const result = getCSharpType(this.emitter.getProgram(), type, context.namespace);
943-
const resultType = result?.type || UnknownType;
944-
return resultType.getTypeReference(context.scope);
981+
return type.getTypeReference(context.scope);
945982
}
946983

947984
unionDeclaration(union: Union, name: string): EmitterOutput<string> {

packages/http-server-csharp/src/lib/utils.ts

+141-7
Original file line numberDiff line numberDiff line change
@@ -802,13 +802,139 @@ export function isValidCSharpIdentifier(identifier: string, isNamespace: boolean
802802
}
803803

804804
export function replaceCSharpReservedWord(identifier: string, context?: NameCasingType): string {
805-
const reserved: Map<string, string> = new Map<string, string>([
806-
["string", "StringName"],
807-
["type", "TypeName"],
808-
["boolean", "BooleanName"],
809-
["decimal", "DecimalName"],
810-
["enum", "EnumName"],
811-
]);
805+
function generateReplacement(input: string): [string, string] {
806+
return [input, `${pascalCase(input)}Name`];
807+
}
808+
const contextualWords: string[] = [
809+
"add",
810+
"allows",
811+
"alias",
812+
"and",
813+
"ascending",
814+
"args",
815+
"async",
816+
"await",
817+
"by",
818+
"descending",
819+
"dynamic",
820+
"equals",
821+
"field",
822+
"file",
823+
"from",
824+
"get",
825+
"global",
826+
"group",
827+
"init",
828+
"into",
829+
"join",
830+
"let",
831+
"managed",
832+
"nameof",
833+
"nint",
834+
"not",
835+
"notnull",
836+
"nuint",
837+
"on",
838+
"or",
839+
"orderby",
840+
"partial",
841+
"record",
842+
"remove",
843+
"required",
844+
"scoped",
845+
"select",
846+
"set",
847+
"unmanaged",
848+
"value",
849+
"var",
850+
"when",
851+
"where",
852+
"with",
853+
"yield",
854+
];
855+
const reservedWords: string[] = [
856+
"abstract",
857+
"as",
858+
"base",
859+
"bool",
860+
"boolean",
861+
"break",
862+
"byte",
863+
"case",
864+
"catch",
865+
"char",
866+
"checked",
867+
"class",
868+
"const",
869+
"continue",
870+
"decimal",
871+
"default",
872+
"do",
873+
"double",
874+
"else",
875+
"enum",
876+
"event",
877+
"explicit",
878+
"extern",
879+
"false",
880+
"finally",
881+
"fixed",
882+
"float",
883+
"for",
884+
"foreach",
885+
"goto",
886+
"if",
887+
"implicit",
888+
"in",
889+
"int",
890+
"interface",
891+
"internal",
892+
"is",
893+
"lock",
894+
"long",
895+
"namespace",
896+
"new",
897+
"null",
898+
"object",
899+
"operator",
900+
"out",
901+
"override",
902+
"params",
903+
"private",
904+
"protected",
905+
"public",
906+
"readonly",
907+
"ref",
908+
"return",
909+
"sbyte",
910+
"sealed",
911+
"short",
912+
"sizeof",
913+
"stackalloc",
914+
"static",
915+
"string",
916+
"struct",
917+
"switch",
918+
"this",
919+
"throw",
920+
"true",
921+
"try",
922+
"type",
923+
"typeof",
924+
"uint",
925+
"ulong",
926+
"unchecked",
927+
"unsafe",
928+
"ushort",
929+
"using",
930+
"virtual",
931+
"void",
932+
"volatile",
933+
"while",
934+
];
935+
const reserved: Map<string, string> = new Map<string, string>(
936+
reservedWords.concat(contextualWords).map((w) => generateReplacement(w)),
937+
);
812938
const check = reserved.get(identifier.toLowerCase());
813939
if (check !== undefined) {
814940
return getCSharpIdentifier(check, context, false);
@@ -1000,6 +1126,14 @@ export class CSharpOperationHelpers {
10001126
}
10011127
emitter: AssetEmitter<string, CSharpServiceEmitterOptions>;
10021128
#opCache: Map<Operation, CSharpOperationParameter[]>;
1129+
getResponse(program: Program, operation: HttpOperation): CSharpType {
1130+
return new CSharpType({
1131+
name: "void",
1132+
namespace: "System",
1133+
isBuiltIn: true,
1134+
isValueType: true,
1135+
});
1136+
}
10031137
getParameters(program: Program, operation: HttpOperation): CSharpOperationParameter[] {
10041138
function safeConcat(...names: (string | undefined)[]): string {
10051139
return names

0 commit comments

Comments
 (0)