diff --git a/packages/typespec-ts/src/lib.ts b/packages/typespec-ts/src/lib.ts index 50137725b0..6e694bb081 100644 --- a/packages/typespec-ts/src/lib.ts +++ b/packages/typespec-ts/src/lib.ts @@ -152,7 +152,7 @@ const libDef = { "invalid-schema": { severity: "error", messages: { - default: paramMessage`Couldn't get schema for type ${"type"}` + default: paramMessage`Couldn't get schema for type ${"type"} with property ${"property"}` } }, "union-null": { diff --git a/packages/typespec-ts/src/modular/buildCodeModel.ts b/packages/typespec-ts/src/modular/buildCodeModel.ts index fe950c028d..737fbb1c60 100644 --- a/packages/typespec-ts/src/modular/buildCodeModel.ts +++ b/packages/typespec-ts/src/modular/buildCodeModel.ts @@ -37,7 +37,8 @@ import { isNullType, getEncode, isTemplateDeclarationOrInstance, - UsageFlags + UsageFlags, + isVoidType } from "@typespec/compiler"; import { getAuthentication, @@ -633,9 +634,11 @@ function emitResponse( ? undefined : getType(context, metadata.finalResult); } else { - type = getType(context, innerResponse.body.type, { - usage: UsageFlags.Output - }); + type = isVoidType(innerResponse.body.type) + ? undefined + : getType(context, innerResponse.body.type, { + usage: UsageFlags.Output + }); } } const statusCodes: (number | "default")[] = []; @@ -845,7 +848,10 @@ function emitBasicOperation( } let bodyParameter: any | undefined; - if (httpOperation.parameters.body === undefined) { + if ( + httpOperation.parameters.body === undefined || + isVoidType(httpOperation.parameters.body.type) + ) { bodyParameter = undefined; } else { bodyParameter = emitBodyParameter(context, httpOperation); diff --git a/packages/typespec-ts/src/transform/transformParameters.ts b/packages/typespec-ts/src/transform/transformParameters.ts index d9354261ad..fd0a8fa282 100644 --- a/packages/typespec-ts/src/transform/transformParameters.ts +++ b/packages/typespec-ts/src/transform/transformParameters.ts @@ -11,7 +11,7 @@ import { SchemaContext, ApiVersionInfo } from "@azure-tools/rlc-common"; -import { ignoreDiagnostics, Type } from "@typespec/compiler"; +import { ignoreDiagnostics, isVoidType, Type } from "@typespec/compiler"; import { getHttpOperation, HttpOperation, @@ -259,7 +259,7 @@ function transformBodyParameters( (parameters.bodyType ?? parameters.bodyParameter?.type) && inputBodyType ? inputBodyType : parameters.bodyType ?? parameters.bodyParameter?.type; - if (!bodyType) { + if (!bodyType || isVoidType(bodyType)) { return; } return transformRequestBody( diff --git a/packages/typespec-ts/src/transform/transformResponses.ts b/packages/typespec-ts/src/transform/transformResponses.ts index 5e0f000270..cfdee14a53 100644 --- a/packages/typespec-ts/src/transform/transformResponses.ts +++ b/packages/typespec-ts/src/transform/transformResponses.ts @@ -15,7 +15,7 @@ import { getLroLogicalResponseName, Imports } from "@azure-tools/rlc-common"; -import { getDoc, ignoreDiagnostics } from "@typespec/compiler"; +import { getDoc, ignoreDiagnostics, isVoidType } from "@typespec/compiler"; import { getHttpOperation, HttpOperation, @@ -174,7 +174,7 @@ function transformBody( let fromCore = false; for (const data of response.responses) { const body = data?.body; - if (!body) { + if (!body || isVoidType(body.type)) { continue; } const hasBinaryContent = body.contentTypes.some((contentType) => diff --git a/packages/typespec-ts/src/utils/modelUtils.ts b/packages/typespec-ts/src/utils/modelUtils.ts index d743c8bdcf..51468bb50d 100644 --- a/packages/typespec-ts/src/utils/modelUtils.ts +++ b/packages/typespec-ts/src/utils/modelUtils.ts @@ -231,7 +231,10 @@ export function getSchemaForType( } reportDiagnostic(program, { code: "invalid-schema", - format: { type: type.kind }, + format: { + type: type.kind, + property: options?.relevantProperty?.name ?? "" + }, target: type }); return undefined; diff --git a/packages/typespec-ts/test/modularUnit/operations.spec.ts b/packages/typespec-ts/test/modularUnit/operations.spec.ts index 933a7fb5f3..da3e58aa7e 100644 --- a/packages/typespec-ts/test/modularUnit/operations.spec.ts +++ b/packages/typespec-ts/test/modularUnit/operations.spec.ts @@ -4,6 +4,96 @@ import { assertEqualContent } from "../util/testUtil.js"; import { Diagnostic } from "@typespec/compiler"; describe("operations", () => { + describe("void parameter/return type", () => { + it("void request body should be omitted", async () => { + const tspContent = ` + op read(@body param: void): void; + `; + + const operationFiles = + await emitModularOperationsFromTypeSpec(tspContent); + assert.ok(operationFiles); + assert.equal(operationFiles?.length, 1); + await assertEqualContent( + operationFiles?.[0]?.getFullText()!, + ` + import { TestingContext as Client } from "../rest/index.js"; + import { StreamableMethod, operationOptionsToRequestParameters, createRestError } from "@azure-rest/core-client"; + + export function _readSend(context: Client, options: ReadOptionalParams = { requestOptions: {} }): StreamableMethod { + return context.path("/", ).post({...operationOptionsToRequestParameters(options)}) ; + } + + export async function _readDeserialize(result: Read204Response): Promise { + if(result.status !== "204"){ + throw createRestError(result); + } + + return; + } + + export async function read(context: Client, options: ReadOptionalParams = { requestOptions: {} }): Promise { + const result = await _readSend(context, options); + return _readDeserialize(result); + }` + ); + }); + + it("void response body should be omitted", async () => { + const tspContent = ` + op read(): { @body _: void;}; + `; + + const operationFiles = + await emitModularOperationsFromTypeSpec(tspContent); + assert.ok(operationFiles); + assert.equal(operationFiles?.length, 1); + await assertEqualContent( + operationFiles?.[0]?.getFullText()!, + ` + import { TestingContext as Client } from "../rest/index.js"; + import { StreamableMethod, operationOptionsToRequestParameters, createRestError } from "@azure-rest/core-client"; + + export function _readSend(context: Client, options: ReadOptionalParams = { requestOptions: {} }): StreamableMethod { + return context.path("/", ).get({...operationOptionsToRequestParameters(options)}) ; + } + + export async function _readDeserialize(result: Read204Response): Promise { + if(result.status !== "204"){ + throw createRestError(result); + } + + return; + } + + export async function read(context: Client, options: ReadOptionalParams = { requestOptions: {} }): Promise { + const result = await _readSend(context, options); + return _readDeserialize(result); + }` + ); + }); + + it("should throw exception if property type as void", async () => { + try { + const tspContent = ` + model Foo { + param: void; + } + op read(...Foo): {}; + `; + + await emitModularOperationsFromTypeSpec(tspContent); + assert.fail("Should throw diagnostic errors"); + } catch (e: any) { + assert.equal(e[0]?.code, "@azure-tools/typespec-ts/invalid-schema"); + assert.equal( + e[0]?.message, + "Couldn't get schema for type Intrinsic with property param" + ); + assert.equal(e[0]?.target?.name, "void"); + } + }); + }); describe("nullable header", () => { it("required & optional & nullable headers", async () => { const tspContent = ` diff --git a/packages/typespec-ts/test/unit/modelsGenerator.spec.ts b/packages/typespec-ts/test/unit/modelsGenerator.spec.ts index 2ed244b1e8..ec690a5dbc 100644 --- a/packages/typespec-ts/test/unit/modelsGenerator.spec.ts +++ b/packages/typespec-ts/test/unit/modelsGenerator.spec.ts @@ -87,6 +87,23 @@ describe("Input/output model type", () => { ); } + describe("void generation", async () => { + it("should throw exception for property with void type", async () => { + try { + const tspType = "void"; + const typeScriptType = "void"; + await verifyPropertyType(tspType, typeScriptType); + assert.fail("Should throw exception"); + } catch (err: any) { + assert.equal( + err[0].message, + "Couldn't get schema for type Intrinsic with property prop" + ); + assert.equal(err[0]?.target?.name, "void"); + } + }); + }); + describe("null generation", async () => { it("should generate null only", async () => { const tspType = "null"; diff --git a/packages/typespec-ts/test/unit/parametersGenerator.spec.ts b/packages/typespec-ts/test/unit/parametersGenerator.spec.ts index ba737f3d16..b8ab8b4106 100644 --- a/packages/typespec-ts/test/unit/parametersGenerator.spec.ts +++ b/packages/typespec-ts/test/unit/parametersGenerator.spec.ts @@ -593,4 +593,20 @@ describe("Parameters.ts", () => { ); }); }); + + describe("void as request body", () => { + it("void request body should be emitted", async () => { + const parameters = await emitParameterFromTypeSpec(` + op read(@body param: void): void;`); + assert.ok(parameters); + await assertEqualContent( + parameters?.content!, + ` + import { RequestParameters } from "@azure-rest/core-client"; + + export type ReadParameters = RequestParameters; + ` + ); + }); + }); }); diff --git a/packages/typespec-ts/test/unit/responsesGenerator.spec.ts b/packages/typespec-ts/test/unit/responsesGenerator.spec.ts index 081a1ef917..953a908593 100644 --- a/packages/typespec-ts/test/unit/responsesGenerator.spec.ts +++ b/packages/typespec-ts/test/unit/responsesGenerator.spec.ts @@ -107,6 +107,24 @@ describe("Responses.ts", () => { }); describe("body generation", () => { + it("void as response body should be omitted", async () => { + const parameters = await emitResponsesFromTypeSpec(` + @post op read(): {@body body: void; @statusCode _: 204; }; + `); + assert.ok(parameters); + await assertEqualContent( + parameters?.content!, + ` + import { HttpResponse } from "@azure-rest/core-client"; + + /** There is no content to send for this request, but the headers may be useful. */ + export interface Read204Response extends HttpResponse { + status: "204"; + } + ` + ); + }); + it("unknown array response generation", async () => { const parameters = await emitResponsesFromTypeSpec(` @post op read(): unknown[];