Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .chronus/changes/ext-cut-2025-7-11-17-12-14.md
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions .chronus/changes/ext-cut-2025-7-11-17-44-13.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@azure-tools/typespec-autorest"
---

Add support for x-ms-azure-resource extension for custom resources
11 changes: 10 additions & 1 deletion packages/typespec-autorest/src/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ import {
getArmCommonTypeOpenAPIRef,
getArmIdentifiers,
getArmKeyIdentifiers,
getCustomResourceOptions,
getExternalTypeRef,
isArmCommonType,
isArmExternalType,
isArmProviderNamespace,
isAzureResource,
isConditionallyFlattened,
Expand Down Expand Up @@ -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) {
Expand Down
58 changes: 57 additions & 1 deletion packages/typespec-autorest/test/arm/resources.test.ts
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand Down Expand Up @@ -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(
`
Expand Down
24 changes: 22 additions & 2 deletions packages/typespec-azure-resource-manager/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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`

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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.
Expand All @@ -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;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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);
34 changes: 30 additions & 4 deletions packages/typespec-azure-resource-manager/src/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -80,6 +85,19 @@ export interface ArmResourceDetailsBase {
typespecType: Model;
}

export const [isArmExternalType, setArmExternalType] = useStateMap<Model, boolean>(
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 */
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -245,14 +264,21 @@ export function getArmVirtualResourceDetails(
return undefined;
}

const [getCustomResourceOptions, setCustomResource] = useStateMap<Model, CustomResourceOptions>(
ArmStateKeys.customAzureResource,
);

export { getCustomResourceOptions };

/**
* Determine if the given model is a custom resource.
* @param program The program to process.
* @param target The model to check.
* @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;
}
Expand Down
1 change: 1 addition & 0 deletions packages/typespec-azure-resource-manager/src/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,5 @@ export const ArmStateKeys = {
armCommonParameters: azureResourceManagerCreateStateSymbol("armCommonParameters"),
armCommonTypesVersions: azureResourceManagerCreateStateSymbol("armCommonTypesVersions"),
armResourceRoute: azureResourceManagerCreateStateSymbol("armResourceRoute"),
armExternalType: azureResourceManagerCreateStateSymbol("armExternalType"),
};
2 changes: 2 additions & 0 deletions packages/typespec-azure-resource-manager/src/tsp-index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
$armResourceUpdate,
} from "./operations.js";
import {
$armExternalType,
$armProviderNameValue,
$armResourceOperations,
$armVirtualResource,
Expand Down Expand Up @@ -60,6 +61,7 @@ export const $decorators = {
customAzureResource: $customAzureResource,
externalTypeRef: $externalTypeRef,
armOperationRoute: $armOperationRoute,
armExternalType: $armExternalType,
} satisfies AzureResourceManagerLegacyDecorators,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading