diff --git a/.chronus/changes/uptake-body-consitency-2024-3-16-19-18-52.md b/.chronus/changes/uptake-body-consitency-2024-3-16-19-18-52.md new file mode 100644 index 0000000000..af68c7673a --- /dev/null +++ b/.chronus/changes/uptake-body-consitency-2024-3-16-19-18-52.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: fix +packages: + - "@azure-tools/typespec-autorest" +--- + +Add support for new `@body` `@bodyRoot` and `@bodyIgnore` decorators diff --git a/.chronus/changes/uptake-body-consitency-2024-3-16-19-18-53.md b/.chronus/changes/uptake-body-consitency-2024-3-16-19-18-53.md new file mode 100644 index 0000000000..40e2bcc6d3 --- /dev/null +++ b/.chronus/changes/uptake-body-consitency-2024-3-16-19-18-53.md @@ -0,0 +1,9 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: fix +packages: + - "@azure-tools/typespec-azure-core" + - "@azure-tools/typespec-azure-resource-manager" +--- + +Update to support new meaning of `@body` diff --git a/.chronus/changes/uptake-body-consitency-2024-3-16-20-19-22.md b/.chronus/changes/uptake-body-consitency-2024-3-16-20-19-22.md new file mode 100644 index 0000000000..a4b9be13b1 --- /dev/null +++ b/.chronus/changes/uptake-body-consitency-2024-3-16-20-19-22.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: fix +packages: + - "@azure-tools/typespec-autorest-canonical" +--- + +Add support for new `@body`, `@bodyRoot` and `@bodyIgnore` diff --git a/core b/core index 083ba8d39d..1265330298 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 083ba8d39d0e6e3b2ec0f4e8b635c0955a78c81d +Subproject commit 1265330298d86b648d06bb755e823f2fd383c5e9 diff --git a/docs/libraries/azure-resource-manager/reference/data-types.md b/docs/libraries/azure-resource-manager/reference/data-types.md index 1dade9c2bd..ab990671e0 100644 --- a/docs/libraries/azure-resource-manager/reference/data-types.md +++ b/docs/libraries/azure-resource-manager/reference/data-types.md @@ -308,10 +308,10 @@ model Azure.ResourceManager.ArmResourceCreatedSyncResponse #### Properties -| Name | Type | Description | -| ---------- | ---------- | --------------------------------------------------- | -| body | `Resource` | The body type of the operation request or response. | -| statusCode | `201` | The status code. | +| Name | Type | Description | +| ---------- | ---------- | ---------------- | +| statusCode | `201` | The status code. | +| body | `Resource` | | ### `ArmResourceExistsResponse` {#Azure.ResourceManager.ArmResourceExistsResponse} @@ -372,10 +372,10 @@ model Azure.ResourceManager.ArmResponse #### Properties -| Name | Type | Description | -| ---------- | -------------- | --------------------------------------------------- | -| statusCode | `200` | The status code. | -| body | `ResponseBody` | The body type of the operation request or response. | +| Name | Type | Description | +| ---------- | -------------- | ---------------- | +| statusCode | `200` | The status code. | +| body | `ResponseBody` | | ### `CustomerManagedKeyEncryption` {#Azure.ResourceManager.CustomerManagedKeyEncryption} diff --git a/packages/samples/specs/data-plane/widget-manager/main.tsp b/packages/samples/specs/data-plane/widget-manager/main.tsp index 5e34cbb7ed..e39be4c2bd 100644 --- a/packages/samples/specs/data-plane/widget-manager/main.tsp +++ b/packages/samples/specs/data-plane/widget-manager/main.tsp @@ -236,7 +236,7 @@ interface WidgetParts { reorderParts is Operations.LongRunningResourceCollectionAction< WidgetPart, WidgetPartReorderRequest, - TypeSpec.Http.AcceptedResponse + never >; } diff --git a/packages/samples/specs/resource-manager/liftr.confluent/confluent.tsp b/packages/samples/specs/resource-manager/liftr.confluent/confluent.tsp index 219bf0b1b6..0c636bbcd8 100644 --- a/packages/samples/specs/resource-manager/liftr.confluent/confluent.tsp +++ b/packages/samples/specs/resource-manager/liftr.confluent/confluent.tsp @@ -168,7 +168,7 @@ interface MarketplaceAgreements extends ResourceListBySubscription, @doc("The agreement details.") - @body + @bodyRoot agreement: ConfluentAgreementResource, ): ArmResponse | ErrorResponse; } diff --git a/packages/typespec-autorest-canonical/src/openapi.ts b/packages/typespec-autorest-canonical/src/openapi.ts index 41ce9918fe..b0483d10c7 100644 --- a/packages/typespec-autorest-canonical/src/openapi.ts +++ b/packages/typespec-autorest-canonical/src/openapi.ts @@ -89,6 +89,7 @@ import { HttpOperation, HttpOperationParameters, HttpOperationResponse, + HttpOperationResponseBody, HttpStatusCodeRange, HttpStatusCodesEntry, MetadataInfo, @@ -146,6 +147,11 @@ import { } from "./types.js"; import { AutorestCanonicalEmitterContext, resolveOperationId } from "./utils.js"; +interface SchemaContext { + readonly visibility: Visibility; + readonly ignoreMetadataAnnotations: boolean; +} + const defaultOptions = { "output-file": "{azure-resource-provider-folder}/{service-name}/{version}/openapi.json", "new-line": "lf", @@ -418,7 +424,10 @@ function createOAPIEmitter( } const parameters: OpenAPI2PathParameter[] = []; for (const prop of server.parameters.values()) { - const param = getOpenAPI2Parameter(prop, "path", Visibility.Read); + const param = getOpenAPI2Parameter(prop, "path", { + visibility: Visibility.Read, + ignoreMetadataAnnotations: false, + }); if ( prop.type.kind === "Scalar" && ignoreDiagnostics( @@ -785,7 +794,7 @@ function createOAPIEmitter( openapiResponse["x-ms-error-response"] = true; } const contentTypes: string[] = []; - let body: Type | undefined; + let body: HttpOperationResponseBody | undefined; for (const data of response.responses) { if (data.headers && Object.keys(data.headers).length > 0) { openapiResponse.headers ??= {}; @@ -795,20 +804,25 @@ function createOAPIEmitter( } if (data.body) { - if (body && body !== data.body.type) { + if (body && body.type !== data.body.type) { reportDiagnostic(program, { code: "duplicate-body-types", target: response.type, }); } - body = data.body.type; + body = data.body; contentTypes.push(...data.body.contentTypes); } } if (body) { - const isBinary = contentTypes.every((t) => isBinaryPayload(body!, t)); - openapiResponse.schema = isBinary ? { type: "file" } : getSchemaOrRef(body, Visibility.Read); + const isBinary = contentTypes.every((t) => isBinaryPayload(body!.type, t)); + openapiResponse.schema = isBinary + ? { type: "file" } + : getSchemaOrRef(body.type, { + visibility: Visibility.Read, + ignoreMetadataAnnotations: body.isExplicit && body.containsMetadataAnnotations, + }); } for (const contentType of contentTypes) { @@ -820,14 +834,17 @@ function createOAPIEmitter( function getResponseHeader(prop: ModelProperty): OpenAPI2HeaderDefinition { const header: any = {}; - populateParameter(header, prop, "header", Visibility.Read); + populateParameter(header, prop, "header", { + visibility: Visibility.Read, + ignoreMetadataAnnotations: false, + }); delete header.in; delete header.name; delete header.required; return header; } - function getSchemaOrRef(type: Type, visibility: Visibility): any { + function getSchemaOrRef(type: Type, schemaContext: SchemaContext): any { if (type.kind === "Scalar" && program.checker.isStdType(type)) { return getSchemaForScalar(type); } @@ -854,14 +871,14 @@ function createOAPIEmitter( } if (type.kind === "ModelProperty") { - return resolveProperty(type, visibility); + return resolveProperty(type, schemaContext); } - type = metadataInfo.getEffectivePayloadType(type, visibility); + type = metadataInfo.getEffectivePayloadType(type, schemaContext.visibility); const name = getOpenAPITypeName(program, type, typeNameOptions); if (shouldInline(program, type)) { - const schema = getSchemaForInlineType(type, name, visibility); + const schema = getSchemaForInlineType(type, name, schemaContext); if (schema === undefined && isErrorType(type)) { // Exit early so that syntax errors are exposed. This error will @@ -876,18 +893,18 @@ function createOAPIEmitter( return schema; } else { // Use shared schema when type is not transformed by visibility from the canonical read visibility. - if (!metadataInfo.isTransformed(type, visibility)) { - visibility = Visibility.Read; + if (!metadataInfo.isTransformed(type, schemaContext.visibility)) { + schemaContext = { ...schemaContext, visibility: Visibility.Read }; } - const pending = pendingSchemas.getOrAdd(type, visibility, () => ({ + const pending = pendingSchemas.getOrAdd(type, schemaContext.visibility, () => ({ type, - visibility, - ref: refs.getOrAdd(type, visibility, () => new Ref()), + visibility: schemaContext.visibility, + ref: refs.getOrAdd(type, schemaContext.visibility, () => new Ref()), })); return { $ref: pending.ref }; } } - function getSchemaForInlineType(type: Type, name: string, visibility: Visibility) { + function getSchemaForInlineType(type: Type, name: string, context: SchemaContext) { if (inProgressInlineTypes.has(type)) { reportDiagnostic(program, { code: "inline-cycle", @@ -897,7 +914,7 @@ function createOAPIEmitter( return {}; } inProgressInlineTypes.add(type); - const schema = getSchemaForType(type, visibility); + const schema = getSchemaForType(type, context); inProgressInlineTypes.delete(type); return schema; } @@ -958,7 +975,12 @@ function createOAPIEmitter( if (httpOpParam.type === "header" && isContentTypeHeader(program, httpOpParam.param)) { continue; } - emitParameter(httpOpParam.param, httpOpParam.type, visibility, httpOpParam.name); + emitParameter( + httpOpParam.param, + httpOpParam.type, + { visibility, ignoreMetadataAnnotations: false }, + httpOpParam.name + ); } if (consumes.length === 0 && methodParams.body) { @@ -974,9 +996,14 @@ function createOAPIEmitter( if (methodParams.body && !isVoidType(methodParams.body.type)) { const isBinary = isBinaryPayload(methodParams.body.type, consumes); + const schemaContext = { + visibility, + ignoreMetadataAnnotations: + methodParams.body.isExplicit && methodParams.body.containsMetadataAnnotations, + }; const schema = isBinary ? { type: "string", format: "binary" } - : getSchemaOrRef(methodParams.body.type, visibility); + : getSchemaOrRef(methodParams.body.type, schemaContext); if (currentConsumes.has("multipart/form-data")) { const bodyModelType = methodParams.body.type; @@ -984,14 +1011,14 @@ function createOAPIEmitter( compilerAssert(bodyModelType.kind === "Model", "Body should always be a Model."); if (bodyModelType) { for (const param of bodyModelType.properties.values()) { - emitParameter(param, "formData", visibility, getJsonName(param)); + emitParameter(param, "formData", schemaContext, getJsonName(param)); } } } else if (methodParams.body.parameter) { emitParameter( methodParams.body.parameter, "body", - visibility, + { visibility, ignoreMetadataAnnotations: false }, getJsonName(methodParams.body.parameter), schema ); @@ -1025,7 +1052,7 @@ function createOAPIEmitter( function emitParameter( param: ModelProperty, kind: OpenAPI2ParameterType, - visibility: Visibility, + schemaContext: SchemaContext, name?: string, typeOverride?: any ) { @@ -1038,17 +1065,17 @@ function createOAPIEmitter( // If the parameter already has a $ref, don't bother populating it if (!("$ref" in ph)) { - populateParameter(ph, param, kind, visibility, name, typeOverride); + populateParameter(ph, param, kind, schemaContext, name, typeOverride); } } function getSchemaForPrimitiveItems( type: Type, - visibility: Visibility, + schemaContext: SchemaContext, paramName: string, multipart?: boolean ): PrimitiveItems | undefined { - const fullSchema = getSchemaForType(type, visibility); + const fullSchema = getSchemaForType(type, schemaContext); if (fullSchema === undefined) { return undefined; } @@ -1066,7 +1093,7 @@ function createOAPIEmitter( function getFormDataSchema( type: Type, - visibility: Visibility, + schemaContext: SchemaContext, paramName: string ): Omit | undefined { if (isBytes(type)) { @@ -1074,7 +1101,7 @@ function createOAPIEmitter( } if (type.kind === "Model" && isArrayModelType(program, type)) { - const schema = getSchemaForPrimitiveItems(type.indexer.value, visibility, paramName, true); + const schema = getSchemaForPrimitiveItems(type.indexer.value, schemaContext, paramName, true); if (schema === undefined) { return undefined; } @@ -1086,7 +1113,7 @@ function createOAPIEmitter( items: schema, }; } else { - const schema = getSchemaForPrimitiveItems(type, visibility, paramName, true); + const schema = getSchemaForPrimitiveItems(type, schemaContext, paramName, true); if (schema === undefined) { return undefined; @@ -1099,7 +1126,7 @@ function createOAPIEmitter( function getOpenAPI2Parameter( param: ModelProperty, kind: T, - visibility: Visibility, + schemaContext: SchemaContext, name?: string, bodySchema?: any ): OpenAPI2Parameter & { in: T } { @@ -1120,7 +1147,7 @@ function createOAPIEmitter( compilerAssert(bodySchema, "bodySchema argument is required to populate body parameter"); ph.schema = bodySchema; } else if (ph.in === "formData") { - Object.assign(ph, getFormDataSchema(param.type, visibility, ph.name)); + Object.assign(ph, getFormDataSchema(param.type, schemaContext, ph.name)); } else { const collectionFormat = ( kind === "query" @@ -1139,12 +1166,12 @@ function createOAPIEmitter( if (param.type.kind === "Model" && isArrayModelType(program, param.type)) { ph.type = "array"; const schema = { - ...getSchemaForPrimitiveItems(param.type.indexer.value, visibility, ph.name), + ...getSchemaForPrimitiveItems(param.type.indexer.value, schemaContext, ph.name), }; delete (schema as any).description; ph.items = schema; } else { - Object.assign(ph, getSchemaForPrimitiveItems(param.type, visibility, ph.name)); + Object.assign(ph, getSchemaForPrimitiveItems(param.type, schemaContext, ph.name)); } } @@ -1162,11 +1189,11 @@ function createOAPIEmitter( ph: OpenAPI2Parameter, param: ModelProperty, kind: OpenAPI2ParameterType, - visibility: Visibility, + schemaContext: SchemaContext, name?: string, bodySchema?: any ) { - Object.assign(ph, getOpenAPI2Parameter(param, kind, visibility, name, bodySchema)); + Object.assign(ph, getOpenAPI2Parameter(param, kind, schemaContext, name, bodySchema)); } function emitParameters() { @@ -1223,7 +1250,10 @@ function createOAPIEmitter( for (const [visibility, pending] of group) { processedSchemas.getOrAdd(type, visibility, () => ({ ...pending, - schema: getSchemaForType(type, visibility), + schema: getSchemaForType(type, { + visibility: visibility, + ignoreMetadataAnnotations: false, + }), })); } pendingSchemas.delete(type); @@ -1234,7 +1264,7 @@ function createOAPIEmitter( function processUnreferencedSchemas() { const addSchema = (type: Type) => { if (!processedSchemas.has(type) && !paramModels.has(type) && !shouldInline(program, type)) { - getSchemaOrRef(type, Visibility.Read); + getSchemaOrRef(type, { visibility: Visibility.Read, ignoreMetadataAnnotations: false }); } }; const skipSubNamespaces = isGlobalNamespace(program, serviceNamespace); @@ -1258,7 +1288,7 @@ function createOAPIEmitter( } } - function getSchemaForType(type: Type, visibility: Visibility): OpenAPI2Schema | undefined { + function getSchemaForType(type: Type, schemaContext: SchemaContext): OpenAPI2Schema | undefined { const builtinType = getSchemaForLiterals(type); if (builtinType !== undefined) { return builtinType; @@ -1268,15 +1298,15 @@ function createOAPIEmitter( case "Intrinsic": return getSchemaForIntrinsicType(type); case "Model": - return getSchemaForModel(type, visibility); + return getSchemaForModel(type, schemaContext); case "ModelProperty": - return getSchemaForType(type.type, visibility); + return getSchemaForType(type.type, schemaContext); case "Scalar": return getSchemaForScalar(type); case "Union": - return getSchemaForUnion(type, visibility); + return getSchemaForUnion(type, schemaContext); case "UnionVariant": - return getSchemaForUnionVariant(type, visibility); + return getSchemaForUnionVariant(type, schemaContext); case "Enum": return getSchemaForEnum(type); case "Tuple": @@ -1377,7 +1407,7 @@ function createOAPIEmitter( return applyIntrinsicDecorators(union, schema); } - function getSchemaForUnion(union: Union, visibility: Visibility): OpenAPI2Schema { + function getSchemaForUnion(union: Union, schemaContext: SchemaContext): OpenAPI2Schema { const nonNullOptions = [...union.variants.values()] .map((x) => x.type) .filter((t) => !isNullType(t)); @@ -1391,7 +1421,7 @@ function createOAPIEmitter( const type = nonNullOptions[0]; // Get the schema for the model type - const schema = getSchemaOrRef(type, visibility); + const schema = getSchemaOrRef(type, schemaContext); if (schema.$ref) { if (type.kind === "Model") { return { type: "object", allOf: [schema], "x-nullable": nullable }; @@ -1425,8 +1455,11 @@ function createOAPIEmitter( ); } - function getSchemaForUnionVariant(variant: UnionVariant, visibility: Visibility): OpenAPI2Schema { - return getSchemaForType(variant.type, visibility)!; + function getSchemaForUnionVariant( + variant: UnionVariant, + schemaContext: SchemaContext + ): OpenAPI2Schema { + return getSchemaForType(variant.type, schemaContext)!; } function getDefaultValue(type: Type): any { @@ -1477,8 +1510,8 @@ function createOAPIEmitter( }); } - function getSchemaForModel(model: Model, visibility: Visibility) { - const array = getArrayType(model, visibility); + function getSchemaForModel(model: Model, schemaContext: SchemaContext) { + const array = getArrayType(model, schemaContext); if (array) { return array; } @@ -1507,14 +1540,14 @@ function createOAPIEmitter( const properties: OpenAPI2Schema["properties"] = {}; if (isRecordModelType(program, model)) { - modelSchema.additionalProperties = getSchemaOrRef(model.indexer.value, visibility); + modelSchema.additionalProperties = getSchemaOrRef(model.indexer.value, schemaContext); } const derivedModels = model.derivedModels.filter(includeDerivedModel); // getSchemaOrRef on all children to push them into components.schemas for (const child of derivedModels) { - getSchemaOrRef(child, visibility); + getSchemaOrRef(child, schemaContext); } const discriminator = getDiscriminator(program, model); @@ -1543,7 +1576,13 @@ function createOAPIEmitter( reportDisallowedDecorator(UnsupportedVersioningDecorators.TypeChangedFrom, prop.type); } - if (!metadataInfo.isPayloadProperty(prop, visibility)) { + if ( + !metadataInfo.isPayloadProperty( + prop, + schemaContext.visibility, + schemaContext.ignoreMetadataAnnotations + ) + ) { continue; } @@ -1565,7 +1604,10 @@ function createOAPIEmitter( } } - if (!metadataInfo.isOptional(prop, visibility) || prop.name === discriminator?.propertyName) { + if ( + !metadataInfo.isOptional(prop, schemaContext.visibility) || + prop.name === discriminator?.propertyName + ) { if (!modelSchema.required) { modelSchema.required = []; } @@ -1573,7 +1615,7 @@ function createOAPIEmitter( } // Apply decorators on the property to the type's schema - properties[jsonName] = resolveProperty(prop, visibility); + properties[jsonName] = resolveProperty(prop, schemaContext); const property: OpenAPI2SchemaProperty = properties[jsonName]; if (jsonName !== clientName) { property["x-ms-client-name"] = clientName; @@ -1623,10 +1665,10 @@ function createOAPIEmitter( ) { // Take the base model schema but carry across the documentation property // that we set before - const baseSchema = getSchemaForType(model.baseModel, visibility); + const baseSchema = getSchemaForType(model.baseModel, schemaContext); Object.assign(modelSchema, baseSchema, { description: modelSchema.description }); } else if (model.baseModel) { - const baseSchema = getSchemaOrRef(model.baseModel, visibility); + const baseSchema = getSchemaOrRef(model.baseModel, schemaContext); modelSchema.allOf = [baseSchema]; } @@ -1652,7 +1694,10 @@ function createOAPIEmitter( return true; } - function resolveProperty(prop: ModelProperty, visibility: Visibility): OpenAPI2SchemaProperty { + function resolveProperty( + prop: ModelProperty, + schemaContext: SchemaContext + ): OpenAPI2SchemaProperty { let propSchema; if (prop.type.kind === "Enum" && prop.default) { propSchema = getSchemaForEnum(prop.type); @@ -1661,10 +1706,10 @@ function createOAPIEmitter( if (asEnum) { propSchema = getSchemaForUnionEnum(prop.type, asEnum); } else { - propSchema = getSchemaOrRef(prop.type, visibility); + propSchema = getSchemaOrRef(prop.type, schemaContext); } } else { - propSchema = getSchemaOrRef(prop.type, visibility); + propSchema = getSchemaOrRef(prop.type, schemaContext); } return applyIntrinsicDecorators(prop, propSchema); @@ -1936,11 +1981,14 @@ function createOAPIEmitter( /** * If the model is an array model return the OpenAPI2Schema for the array type. */ - function getArrayType(typespecType: Model, visibility: Visibility): OpenAPI2Schema | undefined { + function getArrayType(typespecType: Model, context: SchemaContext): OpenAPI2Schema | undefined { if (isArrayModelType(program, typespecType)) { const array: OpenAPI2Schema = { type: "array", - items: getSchemaOrRef(typespecType.indexer.value!, visibility | Visibility.Item), + items: getSchemaOrRef(typespecType.indexer.value!, { + ...context, + visibility: context.visibility | Visibility.Item, + }), }; if (!ifArrayItemContainsIdentifier(program, typespecType as any)) { array["x-ms-identifiers"] = []; diff --git a/packages/typespec-autorest-canonical/test/metadata.test.ts b/packages/typespec-autorest-canonical/test/metadata.test.ts index 9ed6a62078..5ccca863f5 100644 --- a/packages/typespec-autorest-canonical/test/metadata.test.ts +++ b/packages/typespec-autorest-canonical/test/metadata.test.ts @@ -332,7 +332,7 @@ it("puts inapplicable metadata in schema", async () => { @header h: string; } @route("/single") @get op single(...Parameters): string; - @route("/batch") @get op batch(...Body): string; + @route("/batch") @get op batch(@bodyRoot body: Parameters[]): string; ` ); deepStrictEqual(res.paths, { @@ -363,7 +363,6 @@ it("puts inapplicable metadata in schema", async () => { }, parameters: [ { - description: "The body type of the operation request or response.", in: "body", name: "body", required: true, @@ -532,11 +531,11 @@ it("handles cycle in transformed model", async () => { }); }); -it("supports nested metadata and removes emptied properties", async () => { +it("supports nested metadata and removes properties with @bodyIgnore", async () => { const res = await openApiFor( ` model Pet { - headers: { + @bodyIgnore headers: { @header h1: string; moreHeaders: { @header h2: string; diff --git a/packages/typespec-autorest-canonical/test/produces-consumes.test.ts b/packages/typespec-autorest-canonical/test/produces-consumes.test.ts index 14c9f4a1fd..c92024ca12 100644 --- a/packages/typespec-autorest-canonical/test/produces-consumes.test.ts +++ b/packages/typespec-autorest-canonical/test/produces-consumes.test.ts @@ -126,7 +126,7 @@ function createAdlFromConfig(configuration: ProducesConsumesOperation[]): string configuration.forEach((config) => { const opString = config.type === "consumes" - ? `@delete op remove(@body payload : ${config.modelName}) : NoContentResponse;` + ? `@delete op remove(@bodyRoot payload : ${config.modelName}) : NoContentResponse;` : `@get op read() : ${config.modelName};`; const doc = ` ${config.modelDef} diff --git a/packages/typespec-autorest-canonical/test/return-types.test.ts b/packages/typespec-autorest-canonical/test/return-types.test.ts index 9a4a3f6325..60cb421a9b 100644 --- a/packages/typespec-autorest-canonical/test/return-types.test.ts +++ b/packages/typespec-autorest-canonical/test/return-types.test.ts @@ -1,12 +1,30 @@ import { expectDiagnostics } from "@typespec/compiler/testing"; import { deepStrictEqual, ok, strictEqual } from "assert"; -import { describe, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { createAutorestCanonicalTestRunner, ignoreUseStandardOps, openApiFor, } from "./test-host.js"; +it("model used with @body and without shouldn't conflict if it contains no metadata", async () => { + const res = await openApiFor( + ` + model Foo { + name: string; + } + @route("c1") op c1(): Foo; + @route("c2") op c2(): {@body _: Foo}; + ` + ); + deepStrictEqual(res.paths["/c1"].get.responses["200"].schema, { + $ref: "#/definitions/Foo", + }); + deepStrictEqual(res.paths["/c2"].get.responses["200"].schema, { + $ref: "#/definitions/Foo", + }); +}); + it("defines responses with response headers", async () => { const res = await openApiFor( ` @@ -257,34 +275,6 @@ it("defines responses with top-level array type", async () => { strictEqual(res.paths["/"].get.responses["200"].schema.type, "array"); }); -it("return type with no properties should be 200 with empty object as type", async () => { - const res = await openApiFor( - ` - @get op test(): {}; - ` - ); - - const responses = res.paths["/"].get.responses; - ok(responses["200"]); - deepStrictEqual(responses["200"].schema, { - type: "object", - }); -}); - -it("{} return type should produce 200 ", async () => { - const res = await openApiFor( - ` - @get op test(): {}; - ` - ); - - const responses = res.paths["/"].get.responses; - ok(responses["200"]); - deepStrictEqual(responses["200"].schema, { - type: "object", - }); -}); - it("produce additionalProperties schema if response is Record", async () => { const res = await openApiFor( ` @@ -302,7 +292,7 @@ it("produce additionalProperties schema if response is Record", async () => { }); }); -it("return type with only response metadata should be 204 response w/ no content", async () => { +it("return type with only response metadata should be 200 response w/ no content", async () => { const res = await openApiFor( ` @get op delete(): {@header date: string}; @@ -310,23 +300,9 @@ it("return type with only response metadata should be 204 response w/ no content ); const responses = res.paths["/"].get.responses; - ok(responses["204"]); - ok(responses["204"].schema === undefined, "response should have no content"); - ok(responses["200"] === undefined); -}); - -it("defaults status code to 204 when implicit body has no content", async () => { - const res = await openApiFor( - ` - @delete - op delete(): { @header date: string }; - ` - ); - const responses = res.paths["/"].delete.responses; - ok(responses["200"] === undefined); - ok(responses["204"]); - ok(responses["204"].headers["date"]); - ok(responses["204"].schema === undefined); + ok(responses["200"]); + ok(responses["200"].schema === undefined, "response should have no content"); + ok(responses["204"] === undefined); }); it("defaults status code to default when model has @error decorator", async () => { @@ -494,7 +470,73 @@ it("defaults to 204 no content with void response type", async () => { it("defaults to 204 no content with void @body", async () => { const res = await openApiFor(`@get op read(): {@body body: void};`); - ok(res.paths["/"].get.responses["204"]); + ok(res.paths["/"].get.responses["200"]); +}); +it("using @body ignore any metadata property underneath", async () => { + const res = await openApiFor(`@get op read(): { + @body body: { + #suppress "@typespec/http/metadata-ignored" + @header header: string, + #suppress "@typespec/http/metadata-ignored" + @query query: string, + #suppress "@typespec/http/metadata-ignored" + @statusCode code: 201, + } + };`); + expect(res.paths["/"].get.responses["200"].schema).toEqual({ + type: "object", + properties: { + header: { type: "string" }, + query: { type: "string" }, + code: { type: "number", enum: [201] }, + }, + required: ["header", "query", "code"], + }); +}); + +describe("response model resolving to no property in the body produce no body", () => { + it.each(["{}", "{@header prop: string}", `{@visibility("none") prop: string}`])( + "%s", + async (body) => { + const res = await openApiFor(`op test(): ${body};`); + strictEqual(res.paths["/"].get.responses["200"].schema, undefined); + } + ); +}); + +it("property in body with only metadata properties should still be included", async () => { + const res = await openApiFor(`op read(): { + headers: { + @header header1: string; + @header header2: string; + }; + name: string; + };`); + expect(res.paths["/"].get.responses["200"].schema).toEqual({ + type: "object", + properties: { + headers: { type: "object" }, + name: { type: "string" }, + }, + required: ["headers", "name"], + }); +}); + +it("property in body with only metadata properties and @bodyIgnore should not be included", async () => { + const res = await openApiFor(`op read(): { + @bodyIgnore headers: { + @header header1: string; + @header header2: string; + }; + name: string; + };`); + expect(res.paths["/"].get.responses["200"].schema).toEqual({ + type: "object", + properties: { + name: { type: "string" }, + }, + required: ["name"], + }); }); describe("binary responses", () => { diff --git a/packages/typespec-autorest/src/openapi.ts b/packages/typespec-autorest/src/openapi.ts index ed616d2145..8e0b963484 100644 --- a/packages/typespec-autorest/src/openapi.ts +++ b/packages/typespec-autorest/src/openapi.ts @@ -96,6 +96,7 @@ import { HttpOperation, HttpOperationParameters, HttpOperationResponse, + HttpOperationResponseBody, HttpStatusCodeRange, HttpStatusCodesEntry, MetadataInfo, @@ -149,6 +150,10 @@ import { } from "./types.js"; import { AutorestEmitterContext, resolveOperationId } from "./utils.js"; +interface SchemaContext { + readonly visibility: Visibility; + readonly ignoreMetadataAnnotations: boolean; +} const defaultOptions = { "output-file": "{azure-resource-provider-folder}/{service-name}/{version-status}/{version}/openapi.json", @@ -438,7 +443,10 @@ function createOAPIEmitter( } const parameters: OpenAPI2PathParameter[] = []; for (const prop of server.parameters.values()) { - const param = getOpenAPI2Parameter(prop, "path", Visibility.Read); + const param = getOpenAPI2Parameter(prop, "path", { + visibility: Visibility.Read, + ignoreMetadataAnnotations: false, + }); if ( prop.type.kind === "Scalar" && ignoreDiagnostics( @@ -840,7 +848,7 @@ function createOAPIEmitter( openapiResponse["x-ms-error-response"] = true; } const contentTypes: string[] = []; - let body: Type | undefined; + let body: HttpOperationResponseBody | undefined; for (const data of response.responses) { if (data.headers && Object.keys(data.headers).length > 0) { openapiResponse.headers ??= {}; @@ -850,20 +858,25 @@ function createOAPIEmitter( } if (data.body) { - if (body && body !== data.body.type) { + if (body && body.type !== data.body.type) { reportDiagnostic(program, { code: "duplicate-body-types", target: response.type, }); } - body = data.body.type; + body = data.body; contentTypes.push(...data.body.contentTypes); } } if (body) { - const isBinary = contentTypes.every((t) => isBinaryPayload(body!, t)); - openapiResponse.schema = isBinary ? { type: "file" } : getSchemaOrRef(body, Visibility.Read); + const isBinary = contentTypes.every((t) => isBinaryPayload(body!.type, t)); + openapiResponse.schema = isBinary + ? { type: "file" } + : getSchemaOrRef(body.type, { + visibility: Visibility.Read, + ignoreMetadataAnnotations: body.isExplicit && body.containsMetadataAnnotations, + }); } for (const contentType of contentTypes) { @@ -875,7 +888,10 @@ function createOAPIEmitter( function getResponseHeader(prop: ModelProperty): OpenAPI2HeaderDefinition { const header: any = {}; - populateParameter(header, prop, "header", Visibility.Read); + populateParameter(header, prop, "header", { + visibility: Visibility.Read, + ignoreMetadataAnnotations: false, + }); delete header.in; delete header.name; delete header.required; @@ -893,7 +909,7 @@ function createOAPIEmitter( return getRelativePathFromDirectory(getDirectoryPath(outputFile), absoluteRef, false); } - function getSchemaOrRef(type: Type, visibility: Visibility): any { + function getSchemaOrRef(type: Type, schemaContext: SchemaContext): any { const refUrl = getRef(program, type, { version: context.version, service: context.service }); if (refUrl) { return { @@ -927,14 +943,14 @@ function createOAPIEmitter( } if (type.kind === "ModelProperty") { - return resolveProperty(type, visibility); + return resolveProperty(type, schemaContext); } - type = metadataInfo.getEffectivePayloadType(type, visibility); + type = metadataInfo.getEffectivePayloadType(type, schemaContext.visibility); const name = getOpenAPITypeName(program, type, typeNameOptions); if (shouldInline(program, type)) { - const schema = getSchemaForInlineType(type, name, visibility); + const schema = getSchemaForInlineType(type, name, schemaContext); if (schema === undefined && isErrorType(type)) { // Exit early so that syntax errors are exposed. This error will @@ -949,18 +965,18 @@ function createOAPIEmitter( return schema; } else { // Use shared schema when type is not transformed by visibility from the canonical read visibility. - if (!metadataInfo.isTransformed(type, visibility)) { - visibility = Visibility.Read; + if (!metadataInfo.isTransformed(type, schemaContext.visibility)) { + schemaContext = { ...schemaContext, visibility: Visibility.Read }; } - const pending = pendingSchemas.getOrAdd(type, visibility, () => ({ + const pending = pendingSchemas.getOrAdd(type, schemaContext.visibility, () => ({ type, - visibility, - ref: refs.getOrAdd(type, visibility, () => new Ref()), + visibility: schemaContext.visibility, + ref: refs.getOrAdd(type, schemaContext.visibility, () => new Ref()), })); return { $ref: pending.ref }; } } - function getSchemaForInlineType(type: Type, name: string, visibility: Visibility) { + function getSchemaForInlineType(type: Type, name: string, context: SchemaContext) { if (inProgressInlineTypes.has(type)) { reportDiagnostic(program, { code: "inline-cycle", @@ -970,7 +986,7 @@ function createOAPIEmitter( return {}; } inProgressInlineTypes.add(type); - const schema = getSchemaForType(type, visibility); + const schema = getSchemaForType(type, context); inProgressInlineTypes.delete(type); return schema; } @@ -1041,7 +1057,12 @@ function createOAPIEmitter( if (httpOpParam.type === "header" && isContentTypeHeader(program, httpOpParam.param)) { continue; } - emitParameter(httpOpParam.param, httpOpParam.type, visibility, httpOpParam.name); + emitParameter( + httpOpParam.param, + httpOpParam.type, + { visibility, ignoreMetadataAnnotations: false }, + httpOpParam.name + ); } if (consumes.length === 0 && methodParams.body) { @@ -1057,9 +1078,14 @@ function createOAPIEmitter( if (methodParams.body && !isVoidType(methodParams.body.type)) { const isBinary = isBinaryPayload(methodParams.body.type, consumes); + const schemaContext = { + visibility, + ignoreMetadataAnnotations: + methodParams.body.isExplicit && methodParams.body.containsMetadataAnnotations, + }; const schema = isBinary ? { type: "string", format: "binary" } - : getSchemaOrRef(methodParams.body.type, visibility); + : getSchemaOrRef(methodParams.body.type, schemaContext); if (currentConsumes.has("multipart/form-data")) { const bodyModelType = methodParams.body.type; @@ -1067,14 +1093,14 @@ function createOAPIEmitter( compilerAssert(bodyModelType.kind === "Model", "Body should always be a Model."); if (bodyModelType) { for (const param of bodyModelType.properties.values()) { - emitParameter(param, "formData", visibility, getJsonName(param)); + emitParameter(param, "formData", schemaContext, getJsonName(param)); } } } else if (methodParams.body.parameter) { emitParameter( methodParams.body.parameter, "body", - visibility, + { visibility, ignoreMetadataAnnotations: false }, getJsonName(methodParams.body.parameter), schema ); @@ -1108,7 +1134,7 @@ function createOAPIEmitter( function emitParameter( param: ModelProperty, kind: OpenAPI2ParameterType, - visibility: Visibility, + schemaContext: SchemaContext, name?: string, typeOverride?: any ) { @@ -1121,17 +1147,17 @@ function createOAPIEmitter( // If the parameter already has a $ref, don't bother populating it if (!("$ref" in ph)) { - populateParameter(ph, param, kind, visibility, name, typeOverride); + populateParameter(ph, param, kind, schemaContext, name, typeOverride); } } function getSchemaForPrimitiveItems( type: Type, - visibility: Visibility, + schemaContext: SchemaContext, paramName: string, multipart?: boolean ): PrimitiveItems | undefined { - const fullSchema = getSchemaForType(type, visibility); + const fullSchema = getSchemaForType(type, schemaContext); if (fullSchema === undefined) { return undefined; } @@ -1149,7 +1175,7 @@ function createOAPIEmitter( function getFormDataSchema( type: Type, - visibility: Visibility, + schemaContext: SchemaContext, paramName: string ): Omit | undefined { if (isBytes(type)) { @@ -1161,7 +1187,7 @@ function createOAPIEmitter( if (isBytes(elementType)) { return { type: "array", items: { type: "string", format: "binary" } }; } - const schema = getSchemaForPrimitiveItems(elementType, visibility, paramName, true); + const schema = getSchemaForPrimitiveItems(elementType, schemaContext, paramName, true); if (schema === undefined) { return undefined; } @@ -1173,7 +1199,7 @@ function createOAPIEmitter( items: schema, }; } else { - const schema = getSchemaForPrimitiveItems(type, visibility, paramName, true); + const schema = getSchemaForPrimitiveItems(type, schemaContext, paramName, true); if (schema === undefined) { return undefined; @@ -1186,7 +1212,7 @@ function createOAPIEmitter( function getOpenAPI2Parameter( param: ModelProperty, kind: T, - visibility: Visibility, + schemaContext: SchemaContext, name?: string, bodySchema?: any ): OpenAPI2Parameter & { in: T } { @@ -1207,7 +1233,7 @@ function createOAPIEmitter( compilerAssert(bodySchema, "bodySchema argument is required to populate body parameter"); ph.schema = bodySchema; } else if (ph.in === "formData") { - Object.assign(ph, getFormDataSchema(param.type, visibility, ph.name)); + Object.assign(ph, getFormDataSchema(param.type, schemaContext, ph.name)); } else { const collectionFormat = ( kind === "query" @@ -1226,12 +1252,12 @@ function createOAPIEmitter( if (param.type.kind === "Model" && isArrayModelType(program, param.type)) { ph.type = "array"; const schema = { - ...getSchemaForPrimitiveItems(param.type.indexer.value, visibility, ph.name), + ...getSchemaForPrimitiveItems(param.type.indexer.value, schemaContext, ph.name), }; delete (schema as any).description; ph.items = schema; } else { - Object.assign(ph, getSchemaForPrimitiveItems(param.type, visibility, ph.name)); + Object.assign(ph, getSchemaForPrimitiveItems(param.type, schemaContext, ph.name)); } } @@ -1249,11 +1275,11 @@ function createOAPIEmitter( ph: OpenAPI2Parameter, param: ModelProperty, kind: OpenAPI2ParameterType, - visibility: Visibility, + schemaContext: SchemaContext, name?: string, bodySchema?: any ) { - Object.assign(ph, getOpenAPI2Parameter(param, kind, visibility, name, bodySchema)); + Object.assign(ph, getOpenAPI2Parameter(param, kind, schemaContext, name, bodySchema)); } function emitParameters() { @@ -1310,7 +1336,10 @@ function createOAPIEmitter( for (const [visibility, pending] of group) { processedSchemas.getOrAdd(type, visibility, () => ({ ...pending, - schema: getSchemaForType(type, visibility), + schema: getSchemaForType(type, { + visibility: visibility, + ignoreMetadataAnnotations: false, + }), })); } pendingSchemas.delete(type); @@ -1321,7 +1350,7 @@ function createOAPIEmitter( function processUnreferencedSchemas() { const addSchema = (type: Type) => { if (!processedSchemas.has(type) && !paramModels.has(type) && !shouldInline(program, type)) { - getSchemaOrRef(type, Visibility.Read); + getSchemaOrRef(type, { visibility: Visibility.Read, ignoreMetadataAnnotations: false }); } }; const skipSubNamespaces = isGlobalNamespace(program, serviceNamespace); @@ -1406,7 +1435,7 @@ function createOAPIEmitter( } } - function getSchemaForType(type: Type, visibility: Visibility): OpenAPI2Schema | undefined { + function getSchemaForType(type: Type, schemaContext: SchemaContext): OpenAPI2Schema | undefined { const builtinType = getSchemaForLiterals(type); if (builtinType !== undefined) { return builtinType; @@ -1416,15 +1445,15 @@ function createOAPIEmitter( case "Intrinsic": return getSchemaForIntrinsicType(type); case "Model": - return getSchemaForModel(type, visibility); + return getSchemaForModel(type, schemaContext); case "ModelProperty": - return getSchemaForType(type.type, visibility); + return getSchemaForType(type.type, schemaContext); case "Scalar": return getSchemaForScalar(type); case "Union": - return getSchemaForUnion(type, visibility); + return getSchemaForUnion(type, schemaContext); case "UnionVariant": - return getSchemaForUnionVariant(type, visibility); + return getSchemaForUnionVariant(type, schemaContext); case "Enum": return getSchemaForEnum(type); case "Tuple": @@ -1538,7 +1567,7 @@ function createOAPIEmitter( return applyIntrinsicDecorators(union, schema); } - function getSchemaForUnion(union: Union, visibility: Visibility): OpenAPI2Schema { + function getSchemaForUnion(union: Union, schemaContext: SchemaContext): OpenAPI2Schema { const nonNullOptions = [...union.variants.values()] .map((x) => x.type) .filter((t) => !isNullType(t)); @@ -1552,7 +1581,7 @@ function createOAPIEmitter( const type = nonNullOptions[0]; // Get the schema for the model type - const schema = getSchemaOrRef(type, visibility); + const schema = getSchemaOrRef(type, schemaContext); if (schema.$ref) { if (type.kind === "Model") { return { type: "object", allOf: [schema], "x-nullable": nullable }; @@ -1586,8 +1615,11 @@ function createOAPIEmitter( ); } - function getSchemaForUnionVariant(variant: UnionVariant, visibility: Visibility): OpenAPI2Schema { - return getSchemaForType(variant.type, visibility)!; + function getSchemaForUnionVariant( + variant: UnionVariant, + schemaContext: SchemaContext + ): OpenAPI2Schema { + return getSchemaForType(variant.type, schemaContext)!; } function getDefaultValue(type: Type): any { @@ -1653,8 +1685,8 @@ function createOAPIEmitter( return undefined; } - function getSchemaForModel(model: Model, visibility: Visibility) { - const array = getArrayType(model, visibility); + function getSchemaForModel(model: Model, schemaContext: SchemaContext) { + const array = getArrayType(model, schemaContext); if (array) { return array; } @@ -1677,14 +1709,14 @@ function createOAPIEmitter( const properties: OpenAPI2Schema["properties"] = {}; if (isRecordModelType(program, model)) { - modelSchema.additionalProperties = getSchemaOrRef(model.indexer.value, visibility); + modelSchema.additionalProperties = getSchemaOrRef(model.indexer.value, schemaContext); } const derivedModels = model.derivedModels.filter(includeDerivedModel); // getSchemaOrRef on all children to push them into components.schemas for (const child of derivedModels) { - getSchemaOrRef(child, visibility); + getSchemaOrRef(child, schemaContext); } const discriminator = getDiscriminator(program, model); @@ -1705,7 +1737,13 @@ function createOAPIEmitter( applyExternalDocs(model, modelSchema); for (const prop of model.properties.values()) { - if (!metadataInfo.isPayloadProperty(prop, visibility)) { + if ( + !metadataInfo.isPayloadProperty( + prop, + schemaContext.visibility, + schemaContext.ignoreMetadataAnnotations + ) + ) { continue; } @@ -1727,7 +1765,10 @@ function createOAPIEmitter( } } - if (!metadataInfo.isOptional(prop, visibility) || prop.name === discriminator?.propertyName) { + if ( + !metadataInfo.isOptional(prop, schemaContext.visibility) || + prop.name === discriminator?.propertyName + ) { if (!modelSchema.required) { modelSchema.required = []; } @@ -1735,7 +1776,7 @@ function createOAPIEmitter( } // Apply decorators on the property to the type's schema - properties[jsonName] = resolveProperty(prop, visibility); + properties[jsonName] = resolveProperty(prop, schemaContext); const property: OpenAPI2SchemaProperty = properties[jsonName]; if (jsonName !== clientName) { property["x-ms-client-name"] = clientName; @@ -1785,10 +1826,10 @@ function createOAPIEmitter( ) { // Take the base model schema but carry across the documentation property // that we set before - const baseSchema = getSchemaForType(model.baseModel, visibility); + const baseSchema = getSchemaForType(model.baseModel, schemaContext); Object.assign(modelSchema, baseSchema, { description: modelSchema.description }); } else if (model.baseModel) { - const baseSchema = getSchemaOrRef(model.baseModel, visibility); + const baseSchema = getSchemaOrRef(model.baseModel, schemaContext); modelSchema.allOf = [baseSchema]; } @@ -1814,7 +1855,7 @@ function createOAPIEmitter( return true; } - function resolveProperty(prop: ModelProperty, visibility: Visibility): OpenAPI2SchemaProperty { + function resolveProperty(prop: ModelProperty, context: SchemaContext): OpenAPI2SchemaProperty { let propSchema; if (prop.type.kind === "Enum" && prop.default) { propSchema = getSchemaForEnum(prop.type); @@ -1823,10 +1864,10 @@ function createOAPIEmitter( if (asEnum) { propSchema = getSchemaForUnionEnum(prop.type, asEnum); } else { - propSchema = getSchemaOrRef(prop.type, visibility); + propSchema = getSchemaOrRef(prop.type, context); } } else { - propSchema = getSchemaOrRef(prop.type, visibility); + propSchema = getSchemaOrRef(prop.type, context); } return applyIntrinsicDecorators(prop, propSchema); @@ -2104,11 +2145,14 @@ function createOAPIEmitter( /** * If the model is an array model return the OpenAPI2Schema for the array type. */ - function getArrayType(typespecType: Model, visibility: Visibility): OpenAPI2Schema | undefined { + function getArrayType(typespecType: Model, context: SchemaContext): OpenAPI2Schema | undefined { if (isArrayModelType(program, typespecType)) { const array: OpenAPI2Schema = { type: "array", - items: getSchemaOrRef(typespecType.indexer.value!, visibility | Visibility.Item), + items: getSchemaOrRef(typespecType.indexer.value!, { + ...context, + visibility: context.visibility | Visibility.Item, + }), }; if (!ifArrayItemContainsIdentifier(program, typespecType as any)) { array["x-ms-identifiers"] = []; diff --git a/packages/typespec-autorest/test/metadata.test.ts b/packages/typespec-autorest/test/metadata.test.ts index d226006210..89a31560ba 100644 --- a/packages/typespec-autorest/test/metadata.test.ts +++ b/packages/typespec-autorest/test/metadata.test.ts @@ -334,7 +334,7 @@ describe("typespec-autorest: metadata", () => { @header h: string; } @route("/single") @get op single(...Parameters): string; - @route("/batch") @get op batch(...Body): string; + @route("/batch") @get op batch(@bodyRoot body: Parameters[]): string; ` ); deepStrictEqual(res.paths, { @@ -365,7 +365,6 @@ describe("typespec-autorest: metadata", () => { }, parameters: [ { - description: "The body type of the operation request or response.", in: "body", name: "body", required: true, @@ -534,11 +533,11 @@ describe("typespec-autorest: metadata", () => { }); }); - it("supports nested metadata and removes emptied properties", async () => { + it("supports nested metadata and removes properties with @bodyIgnore ", async () => { const res = await openApiFor( ` model Pet { - headers: { + @bodyIgnore headers: { @header h1: string; moreHeaders: { @header h2: string; diff --git a/packages/typespec-autorest/test/parameters.test.ts b/packages/typespec-autorest/test/parameters.test.ts index 424bca71fa..a82481a156 100644 --- a/packages/typespec-autorest/test/parameters.test.ts +++ b/packages/typespec-autorest/test/parameters.test.ts @@ -1,6 +1,6 @@ import { expectDiagnostics } from "@typespec/compiler/testing"; import { deepStrictEqual, ok, strictEqual } from "assert"; -import { describe, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { OpenAPI2HeaderParameter, OpenAPI2QueryParameter } from "../src/types.js"; import { diagnoseOpenApiFor, ignoreUseStandardOps, openApiFor } from "./test-host.js"; @@ -222,6 +222,73 @@ describe("typespec-autorest: parameters", () => { deepStrictEqual(res.paths["/"].post.parameters, []); }); + it("using @body ignore any metadata property underneath", async () => { + const res = await openApiFor(`@get op read( + @body body: { + #suppress "@typespec/http/metadata-ignored" + @header header: string, + #suppress "@typespec/http/metadata-ignored" + @query query: string, + #suppress "@typespec/http/metadata-ignored" + @statusCode code: 201, + } + ): void;`); + expect(res.paths["/"].get.parameters[0].schema).toEqual({ + type: "object", + properties: { + header: { type: "string" }, + query: { type: "string" }, + code: { type: "number", enum: [201] }, + }, + required: ["header", "query", "code"], + }); + }); + + describe("request parameters resolving to no property in the body produce no body", () => { + it.each(["()", "(@header prop: string)", `(@visibility("none") prop: string)`])( + "%s", + async (params) => { + const res = await openApiFor(`op test${params}: void;`); + strictEqual(res.paths["/"].get.requestBody, undefined); + } + ); + }); + + it("property in body with only metadata properties should still be included", async () => { + const res = await openApiFor(`op read( + headers: { + @header header1: string; + @header header2: string; + }; + name: string; + ): void;`); + expect(res.paths["/"].post.parameters[2].schema).toEqual({ + type: "object", + properties: { + headers: { type: "object" }, + name: { type: "string" }, + }, + required: ["headers", "name"], + }); + }); + + it("property in body with only metadata properties and @bodyIgnore should not be included", async () => { + const res = await openApiFor(`op read( + @bodyIgnore headers: { + @header header1: string; + @header header2: string; + }; + name: string; + ): void;`); + expect(res.paths["/"].post.parameters[2].schema).toEqual({ + type: "object", + properties: { + name: { type: "string" }, + }, + required: ["name"], + }); + }); + describe("content type parameter", () => { it("header named with 'Content-Type' gets resolved as content type for operation.", async () => { const res = await openApiFor( diff --git a/packages/typespec-autorest/test/produces-consumes.test.ts b/packages/typespec-autorest/test/produces-consumes.test.ts index 7e868ffbfa..332c13c807 100644 --- a/packages/typespec-autorest/test/produces-consumes.test.ts +++ b/packages/typespec-autorest/test/produces-consumes.test.ts @@ -128,7 +128,7 @@ function createAdlFromConfig(configuration: ProducesConsumesOperation[]): string configuration.forEach((config) => { const opString = config.type === "consumes" - ? `@delete op remove(@body payload : ${config.modelName}) : NoContentResponse;` + ? `@delete op remove(@bodyRoot payload : ${config.modelName}) : NoContentResponse;` : `@get op read() : ${config.modelName};`; const doc = ` ${config.modelDef} diff --git a/packages/typespec-autorest/test/return-types.test.ts b/packages/typespec-autorest/test/return-types.test.ts index 32305317c6..543dfd9b63 100644 --- a/packages/typespec-autorest/test/return-types.test.ts +++ b/packages/typespec-autorest/test/return-types.test.ts @@ -1,9 +1,27 @@ import { expectDiagnostics } from "@typespec/compiler/testing"; import { deepStrictEqual, ok, strictEqual } from "assert"; -import { describe, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { createAutorestTestRunner, ignoreUseStandardOps, openApiFor } from "./test-host.js"; describe("typespec-autorest: return types", () => { + it("model used with @body and without shouldn't conflict if it contains no metadata", async () => { + const res = await openApiFor( + ` + model Foo { + name: string; + } + @route("c1") op c1(): Foo; + @route("c2") op c2(): {@body _: Foo}; + ` + ); + deepStrictEqual(res.paths["/c1"].get.responses["200"].schema, { + $ref: "#/definitions/Foo", + }); + deepStrictEqual(res.paths["/c2"].get.responses["200"].schema, { + $ref: "#/definitions/Foo", + }); + }); + it("defines responses with response headers", async () => { const res = await openApiFor( ` @@ -254,34 +272,6 @@ describe("typespec-autorest: return types", () => { strictEqual(res.paths["/"].get.responses["200"].schema.type, "array"); }); - it("return type with no properties should be 200 with empty object as type", async () => { - const res = await openApiFor( - ` - @get op test(): {}; - ` - ); - - const responses = res.paths["/"].get.responses; - ok(responses["200"]); - deepStrictEqual(responses["200"].schema, { - type: "object", - }); - }); - - it("{} return type should produce 200 ", async () => { - const res = await openApiFor( - ` - @get op test(): {}; - ` - ); - - const responses = res.paths["/"].get.responses; - ok(responses["200"]); - deepStrictEqual(responses["200"].schema, { - type: "object", - }); - }); - it("produce additionalProperties schema if response is Record", async () => { const res = await openApiFor( ` @@ -299,7 +289,7 @@ describe("typespec-autorest: return types", () => { }); }); - it("return type with only response metadata should be 204 response w/ no content", async () => { + it("return type with only response metadata should be 200 response w/ no content", async () => { const res = await openApiFor( ` @get op delete(): {@header date: string}; @@ -307,23 +297,9 @@ describe("typespec-autorest: return types", () => { ); const responses = res.paths["/"].get.responses; - ok(responses["204"]); - ok(responses["204"].schema === undefined, "response should have no content"); - ok(responses["200"] === undefined); - }); - - it("defaults status code to 204 when implicit body has no content", async () => { - const res = await openApiFor( - ` - @delete - op delete(): { @header date: string }; - ` - ); - const responses = res.paths["/"].delete.responses; - ok(responses["200"] === undefined); - ok(responses["204"]); - ok(responses["204"].headers["date"]); - ok(responses["204"].schema === undefined); + ok(responses["200"]); + ok(responses["200"].schema === undefined, "response should have no content"); + ok(responses["204"] === undefined); }); it("defaults status code to default when model has @error decorator", async () => { @@ -491,7 +467,74 @@ describe("typespec-autorest: return types", () => { it("defaults to 204 no content with void @body", async () => { const res = await openApiFor(`@get op read(): {@body body: void};`); - ok(res.paths["/"].get.responses["204"]); + ok(res.paths["/"].get.responses["200"]); + }); + + it("using @body ignore any metadata property underneath", async () => { + const res = await openApiFor(`@get op read(): { + @body body: { + #suppress "@typespec/http/metadata-ignored" + @header header: string, + #suppress "@typespec/http/metadata-ignored" + @query query: string, + #suppress "@typespec/http/metadata-ignored" + @statusCode code: 201, + } + };`); + expect(res.paths["/"].get.responses["200"].schema).toEqual({ + type: "object", + properties: { + header: { type: "string" }, + query: { type: "string" }, + code: { type: "number", enum: [201] }, + }, + required: ["header", "query", "code"], + }); + }); + + describe("response model resolving to no property in the body produce no body", () => { + it.each(["{}", "{@header prop: string}", `{@visibility("none") prop: string}`])( + "%s", + async (body) => { + const res = await openApiFor(`op test(): ${body};`); + strictEqual(res.paths["/"].get.responses["200"].schema, undefined); + } + ); + }); + + it("property in body with only metadata properties should still be included", async () => { + const res = await openApiFor(`op read(): { + headers: { + @header header1: string; + @header header2: string; + }; + name: string; + };`); + expect(res.paths["/"].get.responses["200"].schema).toEqual({ + type: "object", + properties: { + headers: { type: "object" }, + name: { type: "string" }, + }, + required: ["headers", "name"], + }); + }); + + it("property in body with only metadata properties and @bodyIgnore should not be included", async () => { + const res = await openApiFor(`op read(): { + @bodyIgnore headers: { + @header header1: string; + @header header2: string; + }; + name: string; + };`); + expect(res.paths["/"].get.responses["200"].schema).toEqual({ + type: "object", + properties: { + name: { type: "string" }, + }, + required: ["name"], + }); }); describe("binary responses", () => { diff --git a/packages/typespec-azure-core/src/lro-helpers.ts b/packages/typespec-azure-core/src/lro-helpers.ts index 4a4b245120..e3f5ccbd2c 100644 --- a/packages/typespec-azure-core/src/lro-helpers.ts +++ b/packages/typespec-azure-core/src/lro-helpers.ts @@ -24,6 +24,7 @@ import { getOperationVerb, HttpOperation, isBody, + isBodyRoot, isHeader, } from "@typespec/http"; import { @@ -453,7 +454,7 @@ function createLroMetadata( function createOperationLink(program: Program, modelProperty: ModelProperty): OperationLink { let location: "ResponseBody" | "ResponseHeader" | "Self" = "ResponseBody"; if (isHeader(program, modelProperty)) location = "ResponseHeader"; - if (isBody(program, modelProperty)) location = "Self"; + if (isBody(program, modelProperty) || isBodyRoot(program, modelProperty)) location = "Self"; return { kind: "link", location: location, @@ -527,7 +528,10 @@ function ensureContext( } function getBodyType(program: Program, model: Model): Model | undefined { - const bodyProps = filterModelProperties(model, (p) => isBody(program, p)); + const bodyProps = filterModelProperties( + model, + (p) => isBody(program, p) || isBodyRoot(program, p) + ); if (bodyProps.length === 1 && bodyProps[0].type.kind === "Model") return bodyProps[0].type; return undefined; } @@ -834,7 +838,7 @@ function getStatusMonitorLinksFromModel( if (pollingLinks === undefined) return undefined; // favor status monitor links over stepwise polling if (pollingLinks.length > 1) { - pollingLinks = pollingLinks.filter((p) => !isBody(program, p)); + pollingLinks = pollingLinks.filter((p) => !isBody(program, p) && !isBodyRoot(program, p)); } const pollingProperty = pollingLinks[0]; pollingData = getPollingLocationInfo(program, pollingProperty); diff --git a/packages/typespec-azure-core/src/rules/request-body-array.ts b/packages/typespec-azure-core/src/rules/request-body-array.ts index 52d2d21012..4d768ce5d1 100644 --- a/packages/typespec-azure-core/src/rules/request-body-array.ts +++ b/packages/typespec-azure-core/src/rules/request-body-array.ts @@ -1,5 +1,5 @@ import { Operation, createRule } from "@typespec/compiler"; -import { isBody } from "@typespec/http"; +import { isBody, isBodyRoot } from "@typespec/http"; export const bodyArrayRule = createRule({ name: "request-body-problem", @@ -14,7 +14,7 @@ export const bodyArrayRule = createRule({ operation: (op: Operation) => { for (const prop of op.parameters.properties.values()) { if ( - isBody(context.program, prop) && + (isBody(context.program, prop) || isBodyRoot(context.program, prop)) && prop.type.kind === "Model" && prop.type.name === "Array" ) { diff --git a/packages/typespec-azure-core/test/operations.test.ts b/packages/typespec-azure-core/test/operations.test.ts index 48432a9153..64d0a21004 100644 --- a/packages/typespec-azure-core/test/operations.test.ts +++ b/packages/typespec-azure-core/test/operations.test.ts @@ -1356,7 +1356,7 @@ describe("typespec-azure-core: operation templates", () => { @header("operation-id") operate: string, @finalLocation @header("Location") location: ResourceLocation, @pollingLocation @header("Operation-Location") opLink: string, - @lroResult @body body?: SimpleWidget + @lroResult @bodyRoot body?: SimpleWidget }; `, "createWidget" @@ -1412,7 +1412,7 @@ describe("typespec-azure-core: operation templates", () => { @pollingLocation(StatusMonitorPollingOptions) @header("Azure-AsyncOperation") opLink: string, @finalLocation(SimpleWidget) @header location: string; - @body body?: SimpleWidget + @bodyRoot body?: SimpleWidget }; `, "createWidget" @@ -1466,7 +1466,7 @@ describe("typespec-azure-core: operation templates", () => { { @statusCode statusCode: 201; @pollingLocation(StatusMonitorPollingOptions) @header("Azure-AsyncOperation") opLink: string, - @body body?: SimpleWidget + @bodyRoot body?: SimpleWidget }; `, "createWidget" @@ -1513,7 +1513,7 @@ describe("typespec-azure-core: operation templates", () => { { @statusCode statusCode: 201; @pollingLocation(StatusMonitorPollingOptions) @header("location") opLink: string, - @body body?: SimpleWidget + @bodyRoot body?: SimpleWidget }; `, "createWidget" @@ -1565,7 +1565,7 @@ describe("typespec-azure-core: operation templates", () => { @pollingLocation(StatusMonitorPollingOptions) @header("Azure-AsyncOperation") opLink: string, @finalLocation(SimpleWidget) @header location: string; - @body body?: SimpleWidget + @bodyRoot body?: SimpleWidget }; `, "createWidget" @@ -1621,7 +1621,7 @@ describe("typespec-azure-core: operation templates", () => { { @statusCode statusCode: 201; @pollingLocation(StatusMonitorPollingOptions) @header("Azure-AsyncOperation") opLink: string, - @body body?: SimpleWidget + @bodyRoot body?: SimpleWidget }; `, "createWidget" @@ -1671,7 +1671,7 @@ describe("typespec-azure-core: operation templates", () => { { @statusCode statusCode: 201; @finalLocation(SimpleWidget) @pollingLocation(StatusMonitorPollingOptions) @header("location") opLink: string, - @body body?: SimpleWidget + @bodyRoot body?: SimpleWidget }; `, "createWidget" @@ -1728,7 +1728,7 @@ describe("typespec-azure-core: operation templates", () => { @pollingLocation(StatusMonitorPollingOptions) @header("Azure-AsyncOperation") opLink: string, @finalLocation(SimpleWidget) @header location: string; - @body body?: SimpleWidget + @bodyRoot body?: SimpleWidget }; `, "createWidget" @@ -1782,7 +1782,7 @@ describe("typespec-azure-core: operation templates", () => { { @statusCode statusCode: 202; @pollingLocation(StatusMonitorPollingOptions) @header("Azure-AsyncOperation") opLink: string, - @body body?: SimpleWidget + @bodyRoot body?: SimpleWidget }; `, "createWidget" @@ -1830,7 +1830,7 @@ describe("typespec-azure-core: operation templates", () => { { @statusCode statusCode: 202; @finalLocation(SimpleWidget) @pollingLocation(StatusMonitorPollingOptions) @header("location") opLink: string, - @body body?: SimpleWidget + @bodyRoot body?: SimpleWidget }; `, "createWidget" @@ -1973,7 +1973,7 @@ describe("typespec-azure-core: operation templates", () => { @header id: string, @header("operation-id") operate: string, @pollingLocation @header("Operation-Location") opLink: string, - @lroResult @body body?: SimpleWidget + @lroResult @bodyRoot body?: SimpleWidget }; `, "createWidget" @@ -2032,7 +2032,7 @@ describe("typespec-azure-core: operation templates", () => { @header id: string, @header("operation-id") operate: string, @pollingLocation @header("Operation-Location") opLink: string, - @lroResult @body body?: SimpleWidget + @lroResult @bodyRoot body?: SimpleWidget }; `, "createWidget" @@ -2093,7 +2093,7 @@ describe("typespec-azure-core: operation templates", () => { @header id: string; @header("operation-id") operate: string; @pollingLocation @header("Operation-Location") opLink: string; - @lroResult @body body?: SimpleWidget; + @lroResult @bodyRoot body?: SimpleWidget; }; `, "mungeWidget" diff --git a/packages/typespec-azure-resource-manager/lib/arm.foundations.tsp b/packages/typespec-azure-resource-manager/lib/arm.foundations.tsp index 0b0d274edc..0e87d4608e 100644 --- a/packages/typespec-azure-resource-manager/lib/arm.foundations.tsp +++ b/packages/typespec-azure-resource-manager/lib/arm.foundations.tsp @@ -794,7 +794,7 @@ op ArmCreateOperation< ErrorResponse extends {} >( ...HttpParameters, - @doc("Resource create parameters.") @body resource: BodyParameter, + @doc("Resource create parameters.") @bodyRoot resource: BodyParameter, ): Response | ErrorResponse; /** @@ -811,5 +811,5 @@ op ArmUpdateOperation< ErrorResponse extends {} >( ...HttpParameters, - @doc("The resource properties to be updated.") @body properties: BodyParameter, + @doc("The resource properties to be updated.") @bodyRoot properties: BodyParameter, ): Response | ErrorResponse; diff --git a/packages/typespec-azure-resource-manager/lib/responses.tsp b/packages/typespec-azure-resource-manager/lib/responses.tsp index b326199466..1ce2a3f67f 100644 --- a/packages/typespec-azure-resource-manager/lib/responses.tsp +++ b/packages/typespec-azure-resource-manager/lib/responses.tsp @@ -13,7 +13,10 @@ namespace Azure.ResourceManager; @doc("Azure operation completed successfully.") model ArmResponse { ...OkResponse; - ...Body; + + @doc("The body type of the operation request or response.") + @bodyRoot + body: ResponseBody; } /** @@ -219,7 +222,7 @@ model ArmResourceCreatedResponse< @Azure.Core.pollingLocation(Azure.Core.StatusMonitorPollingOptions) @doc("The resource body") - @body + @bodyRoot body: Resource; } @@ -228,9 +231,12 @@ model ArmResourceCreatedResponse< * @template Resource The resource being updated */ @doc("Resource '{name}' create operation succeeded", Resource) -model ArmResourceCreatedSyncResponse - is Body { +model ArmResourceCreatedSyncResponse { ...CreatedResponse; + + @doc("The body type of the operation request or response.") + @bodyRoot + body: Resource; } /** diff --git a/packages/typespec-azure-resource-manager/src/rules/arm-resource-patch.ts b/packages/typespec-azure-resource-manager/src/rules/arm-resource-patch.ts index bb2455adbd..f7023782d6 100644 --- a/packages/typespec-azure-resource-manager/src/rules/arm-resource-patch.ts +++ b/packages/typespec-azure-resource-manager/src/rules/arm-resource-patch.ts @@ -11,7 +11,14 @@ import { isErrorType, paramMessage, } from "@typespec/compiler"; -import { getOperationVerb, isBody, isHeader, isPathParam, isQueryParam } from "@typespec/http"; +import { + getOperationVerb, + isBody, + isBodyRoot, + isHeader, + isPathParam, + isQueryParam, +} from "@typespec/http"; import { getArmResource } from "../resource.js"; import { getSourceModel, isInternalTypeSpec } from "./utils.js"; @@ -121,7 +128,11 @@ function getPatchModel(program: Program, operation: Operation): ModelProperty[] isPathParam(program, property) ) continue; - if (isBody(program, property) && property.type.kind === "Scalar") return undefined; + if ( + (isBody(program, property) || isBodyRoot(program, property)) && + property.type.kind === "Scalar" + ) + return undefined; bodyProperties.push(property); } diff --git a/packages/typespec-azure-resource-manager/test/resource.test.ts b/packages/typespec-azure-resource-manager/test/resource.test.ts index bbcc09307c..dd4b72b386 100644 --- a/packages/typespec-azure-resource-manager/test/resource.test.ts +++ b/packages/typespec-azure-resource-manager/test/resource.test.ts @@ -841,7 +841,7 @@ describe("typespec-azure-resource-manager: ARM resource model", () => { ${versionEnum} @Azure.ResourceManager.Private.armCommonDefinition("Foo", Azure.ResourceManager.CommonTypes.Versions.v3, "foo.json") - model Foo {} + model Foo {prop: string} model FooParam { @path @@ -851,7 +851,7 @@ describe("typespec-azure-resource-manager: ARM resource model", () => { @Azure.ResourceManager.Private.armCommonDefinition("Bar", { version: Azure.ResourceManager.CommonTypes.Versions.v4, isDefault: true }, "bar.json") @Azure.ResourceManager.Private.armCommonDefinition("Bar", Azure.ResourceManager.CommonTypes.Versions.v5, "bar-v5.json") - model Bar {} + model Bar {prop: string} model BarParam { @path @@ -861,7 +861,7 @@ describe("typespec-azure-resource-manager: ARM resource model", () => { } @Azure.ResourceManager.Private.armCommonDefinition("Baz", Azure.ResourceManager.CommonTypes.Versions.v5, "baz.json") - model Baz {} + model Baz {prop: string} model BazParam { @path diff --git a/packages/typespec-azure-resource-manager/test/rules/arm-post-response-codes.test.ts b/packages/typespec-azure-resource-manager/test/rules/arm-post-response-codes.test.ts index aa5ca857b6..1651b020eb 100644 --- a/packages/typespec-azure-resource-manager/test/rules/arm-post-response-codes.test.ts +++ b/packages/typespec-azure-resource-manager/test/rules/arm-post-response-codes.test.ts @@ -146,7 +146,7 @@ it("Does not emit a warning for a long-running post operation that satisfies the @pollingOperation(Widgets.getStatus, {widgetName: RequestParameter<"name">, statusId: ResponseProperty<"operationId">}) @finalOperation(Widgets.getWidget, {widgetName: RequestParameter<"name">}) @armResourceAction(Widget) - @post create(...KeysOf, @body body: Widget): { + @post create(...KeysOf, @bodyRoot body: Widget): { @statusCode code: "202"; @header("x-ms-operation-id") operationId: string; } | { @@ -192,10 +192,10 @@ it("Emits a warning for a long-running post operation that has a 202 response wi @pollingOperation(Widgets.getStatus, {widgetName: RequestParameter<"name">, statusId: ResponseProperty<"operationId">}) @finalOperation(Widgets.getWidget, {widgetName: RequestParameter<"name">}) @armResourceAction(Widget) - @post create(...KeysOf, @body body: Widget): { + @post create(...KeysOf, @bodyRoot body: Widget): { @statusCode code: "202"; @header("x-ms-operation-id") operationId: string; - @body body: Widget; + @bodyRoot body: Widget; } | ErrorResponse; }` ) @@ -239,7 +239,7 @@ it("Emits a warning for a long-running post operation that has a 200 response wi @pollingOperation(Widgets.getStatus, {widgetName: RequestParameter<"name">, statusId: ResponseProperty<"operationId">}) @finalOperation(Widgets.getWidget, {widgetName: RequestParameter<"name">}) @armResourceAction(Widget) - @post create(...KeysOf, @body body: Widget): { + @post create(...KeysOf, @bodyRoot body: Widget): { @statusCode code: "202"; @header("x-ms-operation-id") operationId: string; } | { @@ -288,7 +288,7 @@ it("Emits a warning for a long-running post operation that has invalid response @pollingOperation(Widgets.getStatus, {widgetName: RequestParameter<"name">, statusId: ResponseProperty<"operationId">}) @finalOperation(Widgets.getWidget, {widgetName: RequestParameter<"name">}) @armResourceAction(Widget) - @post create(...KeysOf, @body body: Widget): { + @post create(...KeysOf, @bodyRoot body: Widget): { @statusCode code: "203"; @header("x-ms-operation-id") operationId: string } | ErrorResponse; diff --git a/packages/typespec-azure-resource-manager/test/rules/arm-resource-operations.test.ts b/packages/typespec-azure-resource-manager/test/rules/arm-resource-operations.test.ts index 793a16b148..cb23c81ae6 100644 --- a/packages/typespec-azure-resource-manager/test/rules/arm-resource-operations.test.ts +++ b/packages/typespec-azure-resource-manager/test/rules/arm-resource-operations.test.ts @@ -44,7 +44,7 @@ describe("typespec-azure-resource-manager: arm resource operations rule", () => @armResourceOperations interface FooResources { @get @armResourceRead(FooResource) get(@key("foo") name: string): ArmResponse | ErrorResponse; - @put @armResourceCreateOrUpdate(FooResource) create(...ResourceInstanceParameters, @body resource: FooResource): ArmResponse | ArmCreatedResponse | ErrorResponse; + @put @armResourceCreateOrUpdate(FooResource) create(...ResourceInstanceParameters, @bodyRoot resource: FooResource): ArmResponse | ArmCreatedResponse | ErrorResponse; @get @armResourceList(FooResource) listBySubscription(...SubscriptionScope): ArmResponse> | ErrorResponse; } ` @@ -75,7 +75,7 @@ describe("typespec-azure-resource-manager: arm resource operations rule", () => @armResourceOperations interface FooResources { - @put @armResourceCreateOrUpdate(FooResource) create(...ResourceInstanceParameters, @body resource: FooResource): ArmResponse | ArmCreatedResponse | ErrorResponse; + @put @armResourceCreateOrUpdate(FooResource) create(...ResourceInstanceParameters, @bodyRoot resource: FooResource): ArmResponse | ArmCreatedResponse | ErrorResponse; } ` ) diff --git a/packages/typespec-azure-resource-manager/test/rules/core-operations.test.ts b/packages/typespec-azure-resource-manager/test/rules/core-operations.test.ts index 4fc7699f25..0e3974a92b 100644 --- a/packages/typespec-azure-resource-manager/test/rules/core-operations.test.ts +++ b/packages/typespec-azure-resource-manager/test/rules/core-operations.test.ts @@ -38,9 +38,9 @@ describe("typespec-azure-resource-manager: core operations rule", () => { @armResourceOperations interface FooResources extends ResourceCollectionOperations { - @put createOrUpdate( ...ResourceInstanceParameters, @body resource: FooResource): ArmResponse | ArmCreatedResponse | ErrorResponse; + @put createOrUpdate( ...ResourceInstanceParameters, @bodyRoot resource: FooResource): ArmResponse | ArmCreatedResponse | ErrorResponse; @get get(...ResourceInstanceParameters): ArmResponse | ErrorResponse; - @patch update(...ResourceInstanceParameters, @body properties: ResourceUpdateModel): ArmResponse | ErrorResponse; + @patch update(...ResourceInstanceParameters, @bodyRoot properties: ResourceUpdateModel): ArmResponse | ErrorResponse; @delete delete(...ResourceInstanceParameters): | ArmDeletedResponse | ArmDeleteAcceptedResponse | ArmDeletedNoContentResponse | ErrorResponse; @post action(...ResourceInstanceParameters) : ArmResponse | ErrorResponse; } diff --git a/packages/typespec-azure-resource-manager/test/rules/no-response-body.test.ts b/packages/typespec-azure-resource-manager/test/rules/no-response-body.test.ts index bff70c72d1..833bc656ea 100644 --- a/packages/typespec-azure-resource-manager/test/rules/no-response-body.test.ts +++ b/packages/typespec-azure-resource-manager/test/rules/no-response-body.test.ts @@ -41,7 +41,7 @@ describe("typespec-azure-resource-manager: no response body rule", () => { ` model TestAcceptedResponse { @statusCode statusCode: 202; - @body body: string; + @bodyRoot body: string; } op walk(): TestAcceptedResponse; ` diff --git a/packages/typespec-azure-resource-manager/test/rules/patch-envelope.test.ts b/packages/typespec-azure-resource-manager/test/rules/patch-envelope.test.ts index 10f0349251..9e84a7250b 100644 --- a/packages/typespec-azure-resource-manager/test/rules/patch-envelope.test.ts +++ b/packages/typespec-azure-resource-manager/test/rules/patch-envelope.test.ts @@ -75,7 +75,7 @@ describe("typespec-azure-resource-manager: patch identity should be present in t @patch op update(...ResourceInstanceParameters, @doc("The resource properties to be updated.") - @body + @bodyRoot properties: MyPatchModel):TrackedResource | ErrorResponse; } diff --git a/packages/typespec-azure-resource-manager/test/rules/patch-operations.test.ts b/packages/typespec-azure-resource-manager/test/rules/patch-operations.test.ts index 68fac4812c..6656d269f3 100644 --- a/packages/typespec-azure-resource-manager/test/rules/patch-operations.test.ts +++ b/packages/typespec-azure-resource-manager/test/rules/patch-operations.test.ts @@ -67,7 +67,7 @@ describe("typespec-azure-resource-manager: core operations rule", () => { extends ResourceRead, ResourceCreate, ResourceDelete { @doc("Updates my Foos") @armResourceUpdate(FooResource) - @patch myFooUpdate(...ResourceInstanceParameters, @doc("The body") @body body: MyBadPatch) : ArmResponse | ErrorResponse; + @patch myFooUpdate(...ResourceInstanceParameters, @doc("The body") @bodyRoot body: MyBadPatch) : ArmResponse | ErrorResponse; } @doc("The state of the resource") diff --git a/packages/typespec-client-generator-core/test/decorators.test.ts b/packages/typespec-client-generator-core/test/decorators.test.ts index 2af4ea6b3a..a4eb83ab5b 100644 --- a/packages/typespec-client-generator-core/test/decorators.test.ts +++ b/packages/typespec-client-generator-core/test/decorators.test.ts @@ -2109,16 +2109,16 @@ describe("typespec-client-generator-core: decorators", () => { @service({}) @test namespace MyService { @test - model Model1{} + model Model1{ prop: string } @test - model Model2{} + model Model2{ prop: string } @test - model Model3{} + model Model3{ prop: string } @test - model Model4{} + model Model4 { prop: string } @test @route("/func1") @@ -2163,18 +2163,18 @@ describe("typespec-client-generator-core: decorators", () => { @test @usage(Usage.input | Usage.output) @access(Access.public) - model Model1{} + model Model1{ prop: string } @test - model Model4{} + model Model4{ prop: string } @test @usage(Usage.output) - model Model2{} + model Model2{ prop: string } @test @usage(Usage.input) - model Model3{} + model Model3{ prop: string } @test @route("/func1") diff --git a/tsconfig.ws.json b/tsconfig.ws.json index 3997f662c2..392a571ce7 100644 --- a/tsconfig.ws.json +++ b/tsconfig.ws.json @@ -2,6 +2,7 @@ "references": [ { "path": "core/tsconfig.ws.json" }, { "path": "packages/typespec-autorest/tsconfig.json" }, + { "path": "packages/typespec-autorest-canonical/tsconfig.json" }, { "path": "packages/typespec-azure-core/tsconfig.json" }, { "path": "packages/typespec-azure-resource-manager/tsconfig.json" }, { "path": "packages/typespec-client-generator-core/tsconfig.json" },