diff --git a/.chronus/changes/ext-cut-2025-7-11-17-12-14.md b/.chronus/changes/ext-cut-2025-7-11-17-12-14.md new file mode 100644 index 0000000000..e8a90f73ba --- /dev/null +++ b/.chronus/changes/ext-cut-2025-7-11-17-12-14.md @@ -0,0 +1,8 @@ +--- +changeKind: fix +packages: + - "@azure-tools/typespec-autorest" + - "@azure-tools/typespec-azure-resource-manager" +--- + +Add support for x-ms-external through armExternalResource decorator \ No newline at end of file diff --git a/.chronus/changes/ext-cut-2025-7-11-17-44-13.md b/.chronus/changes/ext-cut-2025-7-11-17-44-13.md new file mode 100644 index 0000000000..2a326200b8 --- /dev/null +++ b/.chronus/changes/ext-cut-2025-7-11-17-44-13.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@azure-tools/typespec-autorest" +--- + +Add support for x-ms-azure-resource extension for custom resources \ No newline at end of file diff --git a/packages/typespec-autorest/src/openapi.ts b/packages/typespec-autorest/src/openapi.ts index cced691284..cad4b021c6 100644 --- a/packages/typespec-autorest/src/openapi.ts +++ b/packages/typespec-autorest/src/openapi.ts @@ -15,8 +15,10 @@ import { getArmCommonTypeOpenAPIRef, getArmIdentifiers, getArmKeyIdentifiers, + getCustomResourceOptions, getExternalTypeRef, isArmCommonType, + isArmExternalType, isArmProviderNamespace, isAzureResource, isConditionallyFlattened, @@ -2128,12 +2130,19 @@ export async function getOpenAPIForService( function attachExtensions(type: Type, emitObject: any) { // Attach any OpenAPI extensions const extensions = getExtensions(program, type); - if (isAzureResource(program, type as Model)) { + if ( + type.kind === "Model" && + (isAzureResource(program, type) || + getCustomResourceOptions(program, type)?.isAzureResource === true) + ) { emitObject["x-ms-azure-resource"] = true; } if (getAsEmbeddingVector(program, type as Model) !== undefined) { emitObject["x-ms-embedding-vector"] = true; } + if (type.kind === "Model" && isArmExternalType(program, type) === true) { + emitObject["x-ms-external"] = true; + } if (type.kind === "Scalar") { const ext = getArmResourceIdentifierConfig(program, type); if (ext) { diff --git a/packages/typespec-autorest/test/arm/resources.test.ts b/packages/typespec-autorest/test/arm/resources.test.ts index e5c134a88b..249cabe007 100644 --- a/packages/typespec-autorest/test/arm/resources.test.ts +++ b/packages/typespec-autorest/test/arm/resources.test.ts @@ -1,5 +1,5 @@ import { deepEqual, deepStrictEqual, ok, strictEqual } from "assert"; -import { it } from "vitest"; +import { expect, it } from "vitest"; import { compileOpenAPI } from "../test-host.js"; it("emits correct paths for tenant resources", async () => { @@ -338,6 +338,62 @@ it("emits x-ms-azure-resource for resource with @azureResourceBase", async () => ok(openApi.definitions?.Widget["x-ms-azure-resource"]); }); +it("emits x-ms-external for resource with @armExternalType", async () => { + const openApi = await compileOpenAPI( + ` + @armProviderNamespace + @useDependency(Azure.ResourceManager.Versions.v1_0_Preview_1) + namespace Microsoft.Contoso; + + #suppress "@azure-tools/typespec-azure-core/no-legacy-usage" "legacy test" + @doc("Widget resource") + @Azure.ResourceManager.Legacy.armExternalType + model Widget { + name: string; + } +`, + { preset: "azure" }, + ); + ok(openApi.definitions?.Widget["x-ms-external"]); +}); + +it("emits x-ms-azure-resource for resource with @customAzureResource and options", async () => { + const openApi = await compileOpenAPI( + ` + @armProviderNamespace + @useDependency(Azure.ResourceManager.Versions.v1_0_Preview_1) + namespace Microsoft.Contoso; + + #suppress "@azure-tools/typespec-azure-core/no-legacy-usage" "legacy test" + @doc("Widget resource") + @Azure.ResourceManager.Legacy.customAzureResource(#{isAzureResource: true}) + model Widget { + name: string; + } +`, + { preset: "azure" }, + ); + ok(openApi.definitions?.Widget["x-ms-azure-resource"]); +}); +it("does not emit x-ms-azure-resource for resource with @customAzureResource", async () => { + const openApi = await compileOpenAPI( + ` + @armProviderNamespace + @useDependency(Azure.ResourceManager.Versions.v1_0_Preview_1) + namespace Microsoft.Contoso; + + #suppress "@azure-tools/typespec-azure-core/no-legacy-usage" "legacy test" + @doc("Widget resource") + @Azure.ResourceManager.Legacy.customAzureResource + model Widget { + name: string; + } +`, + { preset: "azure" }, + ); + expect(openApi.definitions?.Widget["x-ms-azure-resource"]).toBeUndefined(); +}); + it("excludes properties marked @invisible from the resource payload", async () => { const openApi = await compileOpenAPI( ` diff --git a/packages/typespec-azure-resource-manager/README.md b/packages/typespec-azure-resource-manager/README.md index 72e20817a3..5e1e9e0a52 100644 --- a/packages/typespec-azure-resource-manager/README.md +++ b/packages/typespec-azure-resource-manager/README.md @@ -541,10 +541,28 @@ This allows sharing Azure Resource Manager resource types across specifications ### Azure.ResourceManager.Legacy +- [`@armExternalType`](#@armexternaltype) - [`@armOperationRoute`](#@armoperationroute) - [`@customAzureResource`](#@customazureresource) - [`@externalTypeRef`](#@externaltyperef) +#### `@armExternalType` + +Signifies that a Resource is represented using a library type in generated SDKs. + +```typespec +@Azure.ResourceManager.Legacy.armExternalType +``` + +##### Target + +The model to that is an external resource +`Model` + +##### Parameters + +None + #### `@armOperationRoute` Signifies that an operation is an Azure Resource Manager operation @@ -571,7 +589,7 @@ This decorator is used on resources that do not satisfy the definition of a reso but need to be identified as such. ```typespec -@Azure.ResourceManager.Legacy.customAzureResource +@Azure.ResourceManager.Legacy.customAzureResource(options?: valueof Azure.ResourceManager.Legacy.CustomResourceOptions) ``` ##### Target @@ -580,7 +598,9 @@ but need to be identified as such. ##### Parameters -None +| Name | Type | Description | +| ------- | --------------------------------------------------------- | ---------------------------------------------------- | +| options | [valueof `CustomResourceOptions`](#customresourceoptions) | Options for customizing the behavior of the resource | #### `@externalTypeRef` diff --git a/packages/typespec-azure-resource-manager/generated-defs/Azure.ResourceManager.Legacy.ts b/packages/typespec-azure-resource-manager/generated-defs/Azure.ResourceManager.Legacy.ts index e6f45ca7ac..8b21cbd241 100644 --- a/packages/typespec-azure-resource-manager/generated-defs/Azure.ResourceManager.Legacy.ts +++ b/packages/typespec-azure-resource-manager/generated-defs/Azure.ResourceManager.Legacy.ts @@ -1,5 +1,9 @@ import type { DecoratorContext, Model, ModelProperty, Operation } from "@typespec/compiler"; +export interface CustomResourceOptions { + readonly isAzureResource?: boolean; +} + export interface ArmOperationOptions { readonly useStaticRoute?: boolean; readonly route?: string; @@ -8,8 +12,14 @@ export interface ArmOperationOptions { /** * This decorator is used on resources that do not satisfy the definition of a resource * but need to be identified as such. + * + * @param options Options for customizing the behavior of the resource */ -export type CustomAzureResourceDecorator = (context: DecoratorContext, target: Model) => void; +export type CustomAzureResourceDecorator = ( + context: DecoratorContext, + target: Model, + options?: CustomResourceOptions, +) => void; /** * Specify an external reference that should be used when emitting this type. @@ -35,8 +45,16 @@ export type ArmOperationRouteDecorator = ( route?: ArmOperationOptions, ) => void; +/** + * Signifies that a Resource is represented using a library type in generated SDKs. + * + * @param target The model to that is an external resource + */ +export type ArmExternalTypeDecorator = (context: DecoratorContext, target: Model) => void; + export type AzureResourceManagerLegacyDecorators = { customAzureResource: CustomAzureResourceDecorator; externalTypeRef: ExternalTypeRefDecorator; armOperationRoute: ArmOperationRouteDecorator; + armExternalType: ArmExternalTypeDecorator; }; diff --git a/packages/typespec-azure-resource-manager/lib/Legacy/decorator.tsp b/packages/typespec-azure-resource-manager/lib/Legacy/decorator.tsp index 58bc68a2fe..1aefbe526f 100644 --- a/packages/typespec-azure-resource-manager/lib/Legacy/decorator.tsp +++ b/packages/typespec-azure-resource-manager/lib/Legacy/decorator.tsp @@ -12,11 +12,19 @@ model ArmOperationOptions { /** The status route for operations to use */ route?: string; } + +/** Options for customizing the behavior of a custom azure resource */ +model CustomResourceOptions { + /** Should the resource be marked as an Azure resource */ + isAzureResource?: boolean; +} + /** * This decorator is used on resources that do not satisfy the definition of a resource * but need to be identified as such. + * @param options Options for customizing the behavior of the resource */ -extern dec customAzureResource(target: Model); +extern dec customAzureResource(target: Model, options?: valueof CustomResourceOptions); /** * Specify an external reference that should be used when emitting this type. @@ -31,3 +39,9 @@ extern dec externalTypeRef(entity: Model | ModelProperty, jsonRef: valueof strin * @param route Optional route to associate with the operation */ extern dec armOperationRoute(target: Operation, route?: valueof ArmOperationOptions); + +/** + * Signifies that a Resource is represented using a library type in generated SDKs. + * @param target The model to that is an external resource + */ +extern dec armExternalType(target: Model); diff --git a/packages/typespec-azure-resource-manager/src/resource.ts b/packages/typespec-azure-resource-manager/src/resource.ts index db39050de8..3ab93e9e07 100644 --- a/packages/typespec-azure-resource-manager/src/resource.ts +++ b/packages/typespec-azure-resource-manager/src/resource.ts @@ -23,6 +23,7 @@ import { import { useStateMap } from "@typespec/compiler/utils"; import { getHttpOperation, isPathParam } from "@typespec/http"; import { $autoRoute, getParentResource, getSegment } from "@typespec/rest"; + import { ArmProviderNameValueDecorator, ArmResourceOperationsDecorator, @@ -37,7 +38,11 @@ import { SubscriptionResourceDecorator, TenantResourceDecorator, } from "../generated-defs/Azure.ResourceManager.js"; -import { CustomAzureResourceDecorator } from "../generated-defs/Azure.ResourceManager.Legacy.js"; +import { + ArmExternalTypeDecorator, + CustomAzureResourceDecorator, + CustomResourceOptions, +} from "../generated-defs/Azure.ResourceManager.Legacy.js"; import { reportDiagnostic } from "./lib.js"; import { getArmProviderNamespace, @@ -80,6 +85,19 @@ export interface ArmResourceDetailsBase { typespecType: Model; } +export const [isArmExternalType, setArmExternalType] = useStateMap( + ArmStateKeys.armExternalType, +); + +export const $armExternalType: ArmExternalTypeDecorator = ( + context: DecoratorContext, + entity: Model, +) => { + const { program } = context; + if (isTemplateDeclaration(entity)) return; + setArmExternalType(program, entity, true); +}; + /** Details for RP resources */ export interface ArmResourceDetails extends ArmResourceDetailsBase { /** The set of lifecycle operations and actions for the resource */ @@ -188,11 +206,12 @@ export const $armVirtualResource: ArmVirtualResourceDecorator = ( export const $customAzureResource: CustomAzureResourceDecorator = ( context: DecoratorContext, entity: Model, + options?: CustomResourceOptions, ) => { const { program } = context; + const optionsValue = options ?? { isAzureResource: false }; if (isTemplateDeclaration(entity)) return; - - program.stateMap(ArmStateKeys.customAzureResource).set(entity, "Custom"); + setCustomResource(program, entity, optionsValue); }; function getProperty( @@ -245,6 +264,12 @@ export function getArmVirtualResourceDetails( return undefined; } +const [getCustomResourceOptions, setCustomResource] = useStateMap( + ArmStateKeys.customAzureResource, +); + +export { getCustomResourceOptions }; + /** * Determine if the given model is a custom resource. * @param program The program to process. @@ -252,7 +277,8 @@ export function getArmVirtualResourceDetails( * @returns true if the model or any model it extends is marked as a resource, otherwise false. */ export function isCustomAzureResource(program: Program, target: Model): boolean { - if (program.stateMap(ArmStateKeys.customAzureResource).has(target)) return true; + const resourceOptions = getCustomResourceOptions(program, target); + if (resourceOptions) return true; if (target.baseModel) return isCustomAzureResource(program, target.baseModel); return false; } diff --git a/packages/typespec-azure-resource-manager/src/state.ts b/packages/typespec-azure-resource-manager/src/state.ts index 9f65e0cd5f..4bde376e7e 100644 --- a/packages/typespec-azure-resource-manager/src/state.ts +++ b/packages/typespec-azure-resource-manager/src/state.ts @@ -37,4 +37,5 @@ export const ArmStateKeys = { armCommonParameters: azureResourceManagerCreateStateSymbol("armCommonParameters"), armCommonTypesVersions: azureResourceManagerCreateStateSymbol("armCommonTypesVersions"), armResourceRoute: azureResourceManagerCreateStateSymbol("armResourceRoute"), + armExternalType: azureResourceManagerCreateStateSymbol("armExternalType"), }; diff --git a/packages/typespec-azure-resource-manager/src/tsp-index.ts b/packages/typespec-azure-resource-manager/src/tsp-index.ts index 14cc4b9ee8..eae6bb932f 100644 --- a/packages/typespec-azure-resource-manager/src/tsp-index.ts +++ b/packages/typespec-azure-resource-manager/src/tsp-index.ts @@ -14,6 +14,7 @@ import { $armResourceUpdate, } from "./operations.js"; import { + $armExternalType, $armProviderNameValue, $armResourceOperations, $armVirtualResource, @@ -60,6 +61,7 @@ export const $decorators = { customAzureResource: $customAzureResource, externalTypeRef: $externalTypeRef, armOperationRoute: $armOperationRoute, + armExternalType: $armExternalType, } satisfies AzureResourceManagerLegacyDecorators, }; diff --git a/website/src/content/docs/docs/libraries/azure-resource-manager/reference/data-types.md b/website/src/content/docs/docs/libraries/azure-resource-manager/reference/data-types.md index f43af2c21f..1383bda2c3 100644 --- a/website/src/content/docs/docs/libraries/azure-resource-manager/reference/data-types.md +++ b/website/src/content/docs/docs/libraries/azure-resource-manager/reference/data-types.md @@ -3370,6 +3370,20 @@ model Azure.ResourceManager.Legacy.ArmOperationOptions | useStaticRoute? | `boolean` | Should a static route be used | | route? | `string` | The status route for operations to use | +### `CustomResourceOptions` {#Azure.ResourceManager.Legacy.CustomResourceOptions} + +Options for customizing the behavior of a custom azure resource + +```typespec +model Azure.ResourceManager.Legacy.CustomResourceOptions +``` + +#### Properties + +| Name | Type | Description | +| ---------------- | --------- | -------------------------------------------------- | +| isAzureResource? | `boolean` | Should the resource be marked as an Azure resource | + ### `LegacyTrackedResource` {#Azure.ResourceManager.Legacy.LegacyTrackedResource} A tracked resource with the 'location' property optional diff --git a/website/src/content/docs/docs/libraries/azure-resource-manager/reference/decorators.md b/website/src/content/docs/docs/libraries/azure-resource-manager/reference/decorators.md index 9da665a3b7..9a14cc3b11 100644 --- a/website/src/content/docs/docs/libraries/azure-resource-manager/reference/decorators.md +++ b/website/src/content/docs/docs/libraries/azure-resource-manager/reference/decorators.md @@ -458,6 +458,23 @@ This allows sharing Azure Resource Manager resource types across specifications ## Azure.ResourceManager.Legacy +### `@armExternalType` {#@Azure.ResourceManager.Legacy.armExternalType} + +Signifies that a Resource is represented using a library type in generated SDKs. + +```typespec +@Azure.ResourceManager.Legacy.armExternalType +``` + +#### Target + +The model to that is an external resource +`Model` + +#### Parameters + +None + ### `@armOperationRoute` {#@Azure.ResourceManager.Legacy.armOperationRoute} Signifies that an operation is an Azure Resource Manager operation @@ -484,7 +501,7 @@ This decorator is used on resources that do not satisfy the definition of a reso but need to be identified as such. ```typespec -@Azure.ResourceManager.Legacy.customAzureResource +@Azure.ResourceManager.Legacy.customAzureResource(options?: valueof Azure.ResourceManager.Legacy.CustomResourceOptions) ``` #### Target @@ -493,7 +510,9 @@ but need to be identified as such. #### Parameters -None +| Name | Type | Description | +| ------- | ----------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | +| options | [valueof `CustomResourceOptions`](./data-types.md#Azure.ResourceManager.Legacy.CustomResourceOptions) | Options for customizing the behavior of the resource | ### `@externalTypeRef` {#@Azure.ResourceManager.Legacy.externalTypeRef} diff --git a/website/src/content/docs/docs/libraries/azure-resource-manager/reference/index.mdx b/website/src/content/docs/docs/libraries/azure-resource-manager/reference/index.mdx index bdecd231fe..9ac14cd8a1 100644 --- a/website/src/content/docs/docs/libraries/azure-resource-manager/reference/index.mdx +++ b/website/src/content/docs/docs/libraries/azure-resource-manager/reference/index.mdx @@ -314,6 +314,7 @@ npm install --save-peer @azure-tools/typespec-azure-resource-manager ### Decorators +- [`@armExternalType`](./decorators.md#@Azure.ResourceManager.Legacy.armExternalType) - [`@armOperationRoute`](./decorators.md#@Azure.ResourceManager.Legacy.armOperationRoute) - [`@customAzureResource`](./decorators.md#@Azure.ResourceManager.Legacy.customAzureResource) - [`@externalTypeRef`](./decorators.md#@Azure.ResourceManager.Legacy.externalTypeRef) @@ -338,6 +339,7 @@ npm install --save-peer @azure-tools/typespec-azure-resource-manager ### Models - [`ArmOperationOptions`](./data-types.md#Azure.ResourceManager.Legacy.ArmOperationOptions) +- [`CustomResourceOptions`](./data-types.md#Azure.ResourceManager.Legacy.CustomResourceOptions) - [`LegacyTrackedResource`](./data-types.md#Azure.ResourceManager.Legacy.LegacyTrackedResource) - [`ManagedServiceIdentityV4`](./data-types.md#Azure.ResourceManager.Legacy.ManagedServiceIdentityV4) - [`ManagedServiceIdentityV4Property`](./data-types.md#Azure.ResourceManager.Legacy.ManagedServiceIdentityV4Property)