From cb4ef2dd8c13f38bd5af79ee25f69de54a66f890 Mon Sep 17 00:00:00 2001 From: Alitzel Mendez Bustillo Date: Wed, 15 Jan 2025 11:45:20 -0800 Subject: [PATCH] Reapply "x-ms-identifiers alternatives (#1983)" (#2069) This reverts commit 3e47f62902813c1f3e49402d8584819ce606fac3. --- .../fake-change-log-2025-0-13-14-58-27.md | 8 - ...ver-x-ms-identifiers-2025-0-13-14-58-27.md | 8 - ...ifiers-alternatives-2024-11-12-14-15-30.md | 8 + packages/typespec-autorest/src/openapi.ts | 16 +- .../test/openapi-output.test.ts | 174 ++++++++++++++++++ .../typespec-azure-resource-manager/README.md | 29 +++ .../generated-defs/Azure.ResourceManager.ts | 10 +- .../lib/decorators.tsp | 16 ++ .../src/lib.ts | 4 + .../src/resource.ts | 99 ++++++++++ .../src/rules/missing-x-ms-identifiers.ts | 25 ++- .../src/state.ts | 1 + .../src/tsp-index.ts | 2 + .../test/resource.test.ts | 73 ++++++++ .../rules/missing-x-ms-identifiers.test.ts | 57 +++++- .../reference/decorators.md | 28 +++ .../reference/index.mdx | 1 + 17 files changed, 532 insertions(+), 27 deletions(-) delete mode 100644 .chronus/changes/fake-change-log-2025-0-13-14-58-27.md delete mode 100644 .chronus/changes/rever-x-ms-identifiers-2025-0-13-14-58-27.md create mode 100644 .chronus/changes/x-ms-identifiers-alternatives-2024-11-12-14-15-30.md diff --git a/.chronus/changes/fake-change-log-2025-0-13-14-58-27.md b/.chronus/changes/fake-change-log-2025-0-13-14-58-27.md deleted file mode 100644 index 745c3f3260..0000000000 --- a/.chronus/changes/fake-change-log-2025-0-13-14-58-27.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -changeKind: internal -packages: - - "@azure-tools/typespec-autorest" - - "@azure-tools/typespec-azure-resource-manager" ---- - -Feedback: create a change long to force publishing the dev version diff --git a/.chronus/changes/rever-x-ms-identifiers-2025-0-13-14-58-27.md b/.chronus/changes/rever-x-ms-identifiers-2025-0-13-14-58-27.md deleted file mode 100644 index 517e61e845..0000000000 --- a/.chronus/changes/rever-x-ms-identifiers-2025-0-13-14-58-27.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -changeKind: internal -packages: - - "@azure-tools/typespec-autorest" - - "@azure-tools/typespec-azure-resource-manager" ---- - -"Reverting change created by pull request #1983" diff --git a/.chronus/changes/x-ms-identifiers-alternatives-2024-11-12-14-15-30.md b/.chronus/changes/x-ms-identifiers-alternatives-2024-11-12-14-15-30.md new file mode 100644 index 0000000000..c2beccae88 --- /dev/null +++ b/.chronus/changes/x-ms-identifiers-alternatives-2024-11-12-14-15-30.md @@ -0,0 +1,8 @@ +--- +changeKind: feature +packages: + - "@azure-tools/typespec-autorest" + - "@azure-tools/typespec-azure-resource-manager" +--- + +Use the @identifiers decorator to identify and utilize identifiers for x-ms-identifiers. Additionally, use the @key decorator to identify identifiers. diff --git a/packages/typespec-autorest/src/openapi.ts b/packages/typespec-autorest/src/openapi.ts index 3ef3f624bf..eb0f9336cc 100644 --- a/packages/typespec-autorest/src/openapi.ts +++ b/packages/typespec-autorest/src/openapi.ts @@ -12,6 +12,7 @@ import { } from "@azure-tools/typespec-azure-core"; import { getArmCommonTypeOpenAPIRef, + getArmIdentifiers, isArmCommonType, isAzureResource, isConditionallyFlattened, @@ -1833,6 +1834,10 @@ export async function getOpenAPIForService( ); } + function ifArmIdentifiersDefault(armIdentifiers: string[]) { + return armIdentifiers.every((identifier) => identifier === "id" || identifier === "name"); + } + function getSchemaForUnionVariant( variant: UnionVariant, schemaContext: SchemaContext, @@ -2373,9 +2378,18 @@ export async function getOpenAPIForService( visibility: context.visibility | Visibility.Item, }), }; - if (!ifArrayItemContainsIdentifier(program, typespecType as any)) { + + const armIdentifiers = getArmIdentifiers(program, typespecType); + if ( + armIdentifiers !== undefined && + armIdentifiers.length > 0 && + !ifArmIdentifiersDefault(armIdentifiers) + ) { + array["x-ms-identifiers"] = armIdentifiers; + } else if (!ifArrayItemContainsIdentifier(program, typespecType as any)) { array["x-ms-identifiers"] = []; } + return applyIntrinsicDecorators(typespecType, array); } return undefined; diff --git a/packages/typespec-autorest/test/openapi-output.test.ts b/packages/typespec-autorest/test/openapi-output.test.ts index 727f95f758..81d13e7aff 100644 --- a/packages/typespec-autorest/test/openapi-output.test.ts +++ b/packages/typespec-autorest/test/openapi-output.test.ts @@ -823,6 +823,180 @@ describe("typespec-autorest: extension decorator", () => { }); }); +describe("typespec-azure: identifiers decorator", () => { + it("ignores name/id keys for x-ms-identifiers", async () => { + const oapi = await openApiFor( + ` + model Pet { + @key + name: string; + @key + id: int32; + } + model PetList { + value: Pet[] + } + @route("/Pets") + @get op list(): PetList; + `, + ); + ok(oapi.paths["/Pets"].get); + deepStrictEqual(oapi.definitions.PetList.properties.value["x-ms-identifiers"], undefined); + }); + it("uses identifiers decorator for properties", async () => { + const oapi = await openApiFor( + ` + model Pet { + name: string; + age: int32; + } + model PetList { + @identifiers(["age"]) + value: Pet[] + } + @route("/Pets") + @get op list(): PetList; + `, + ); + ok(oapi.paths["/Pets"].get); + deepStrictEqual(oapi.definitions.PetList.properties.value["x-ms-identifiers"], ["age"]); + }); + it("identifies keys correctly as x-ms-identifiers", async () => { + const oapi = await openApiFor( + ` + model Pet { + name: string; + @key + age: int32; + } + model PetList { + value: Pet[] + } + @route("/Pets") + @get op list(): PetList; + `, + ); + ok(oapi.paths["/Pets"].get); + deepStrictEqual(oapi.definitions.PetList.properties.value["x-ms-identifiers"], ["age"]); + }); + it("prioritizes identifiers decorator over keys", async () => { + const oapi = await openApiFor( + ` + model Pet { + name: string; + @key + age: int32; + } + model PetList { + @identifiers([]) + value: Pet[] + } + @route("/Pets") + @get op list(): PetList; + `, + ); + ok(oapi.paths["/Pets"].get); + deepStrictEqual(oapi.definitions.PetList.properties.value["x-ms-identifiers"], []); + }); + it("supports multiple identifiers", async () => { + const oapi = await openApiFor( + ` + model Pet { + name: string; + age: int32; + } + model PetList { + @identifiers(["name", "age"]) + value: Pet[] + } + @route("/Pets") + @get op list(): PetList; + `, + ); + ok(oapi.paths["/Pets"].get); + deepStrictEqual(oapi.definitions.PetList.properties.value["x-ms-identifiers"], ["name", "age"]); + }); + it("supports inner properties in identifiers decorator", async () => { + const oapi = await openApiFor( + ` + model Pet { + dogs: Dog; + } + + model Dog { + breed: string; + } + + model PetList { + @identifiers(["dogs/breed"]) + pets: Pet[] + } + @route("/Pets") + @get op list(): PetList; + `, + ); + ok(oapi.paths["/Pets"].get); + deepStrictEqual(oapi.definitions.PetList.properties.pets["x-ms-identifiers"], ["dogs/breed"]); + }); + it("supports inner properties for keys", async () => { + const oapi = await openApiFor( + ` + model Pet { + dogs: Dog; + cats: Cat; + } + + model Dog { + @key + breed: string; + } + + model Cat + { + features: Features; + } + + model Features { + @key + color:string; + size:int32; + } + + model PetList { + pets: Pet[] + } + + @route("/Pets") + @get op list(): PetList; + `, + ); + ok(oapi.paths["/Pets"].get); + deepStrictEqual(oapi.definitions.PetList.properties.pets["x-ms-identifiers"], [ + "dogs/breed", + "cats/features/color", + ]); + }); + it("supports multiple keys", async () => { + const oapi = await openApiFor( + ` + model Pet { + @key + name: string; + @key + age: int32; + } + model PetList { + pets: Pet[] + } + @route("/Pets") + @get op list(): PetList; + `, + ); + ok(oapi.paths["/Pets"].get); + deepStrictEqual(oapi.definitions.PetList.properties.pets["x-ms-identifiers"], ["name", "age"]); + }); +}); + describe("typespec-autorest: multipart formData", () => { it("expands model into formData parameters", async () => { const oapi = await openApiFor(` diff --git a/packages/typespec-azure-resource-manager/README.md b/packages/typespec-azure-resource-manager/README.md index 83f2165fc5..e78cf6d03b 100644 --- a/packages/typespec-azure-resource-manager/README.md +++ b/packages/typespec-azure-resource-manager/README.md @@ -79,6 +79,7 @@ Available ruleSets: - [`@armResourceUpdate`](#@armresourceupdate) - [`@armVirtualResource`](#@armvirtualresource) - [`@extensionResource`](#@extensionresource) +- [`@identifiers`](#@identifiers) - [`@locationResource`](#@locationresource) - [`@resourceBaseType`](#@resourcebasetype) - [`@resourceGroupResource`](#@resourcegroupresource) @@ -353,6 +354,34 @@ See more details on [different Azure Resource Manager resource type here.](https None +#### `@identifiers` + +This decorator is used to indicate the identifying properties of objects in the array, e.g. size +The properties that are used as identifiers for the object needs to be provided as a list of strings. + +```typespec +@Azure.ResourceManager.identifiers(properties: string[]) +``` + +##### Target + +`ModelProperty` + +##### Parameters + +| Name | Type | Description | +| ---------- | ---------- | ------------------------------------------------------------------------------------------------------------------- | +| properties | `string[]` | The list of properties that are used as identifiers for the object. This needs to be provided as a list of strings. | + +##### Examples + +```typespec +model Pet { + @identifiers(["size"]) + dog: Dog; +} +``` + #### `@locationResource` `@locationResource` marks an Azure Resource Manager resource model as a location based resource. diff --git a/packages/typespec-azure-resource-manager/generated-defs/Azure.ResourceManager.ts b/packages/typespec-azure-resource-manager/generated-defs/Azure.ResourceManager.ts index e956b75325..0b6b4d39aa 100644 --- a/packages/typespec-azure-resource-manager/generated-defs/Azure.ResourceManager.ts +++ b/packages/typespec-azure-resource-manager/generated-defs/Azure.ResourceManager.ts @@ -1,9 +1,10 @@ -import type { +import { DecoratorContext, EnumMember, EnumValue, Interface, Model, + ModelProperty, Namespace, Operation, Type, @@ -266,6 +267,12 @@ export type ResourceBaseTypeDecorator = ( baseType: Type, ) => void; +export type IdentifiersDecorator = ( + context: DecoratorContext, + target: ModelProperty, + properties: string[], +) => void; + export type AzureResourceManagerDecorators = { armResourceCollectionAction: ArmResourceCollectionActionDecorator; armProviderNameValue: ArmProviderNameValueDecorator; @@ -288,6 +295,7 @@ export type AzureResourceManagerDecorators = { armCommonTypesVersion: ArmCommonTypesVersionDecorator; armVirtualResource: ArmVirtualResourceDecorator; resourceBaseType: ResourceBaseTypeDecorator; + identifiers: IdentifiersDecorator; }; export type AzureResourceManagerLegacyDecorators = { diff --git a/packages/typespec-azure-resource-manager/lib/decorators.tsp b/packages/typespec-azure-resource-manager/lib/decorators.tsp index 2b99ea3b1f..d56bc315b0 100644 --- a/packages/typespec-azure-resource-manager/lib/decorators.tsp +++ b/packages/typespec-azure-resource-manager/lib/decorators.tsp @@ -191,3 +191,19 @@ extern dec resourceBaseType( target: Model, baseType: "Tenant" | "Subscription" | "ResourceGroup" | "Location" | "Extension" ); + +/** + * This decorator is used to indicate the identifying properties of objects in the array, e.g. size + * The properties that are used as identifiers for the object needs to be provided as a list of strings. + * + * @param properties The list of properties that are used as identifiers for the object. This needs to be provided as a list of strings. + * + * @example + * ```typespec + * model Pet { + * @identifiers(["size"]) + * dog: Dog; + * } + * ``` + */ +extern dec identifiers(entity: ModelProperty, properties: string[]); diff --git a/packages/typespec-azure-resource-manager/src/lib.ts b/packages/typespec-azure-resource-manager/src/lib.ts index 11dd6eca9c..e823b7116c 100644 --- a/packages/typespec-azure-resource-manager/src/lib.ts +++ b/packages/typespec-azure-resource-manager/src/lib.ts @@ -14,6 +14,10 @@ export const $lib = createTypeSpecLibrary({ messages: { armUpdateProviderNamespace: "The parameter to @armUpdateProviderNamespace must be an operation with a 'provider' parameter.", + armIdentifiersIncorrectEntity: + "The @identifiers decorator must be applied to a property that is an array of objects", + armIdentifiersProperties: + "The @identifiers decorator expects a parameter that is an array of strings or an empty array.", }, }, "arm-resource-circular-ancestry": { diff --git a/packages/typespec-azure-resource-manager/src/resource.ts b/packages/typespec-azure-resource-manager/src/resource.ts index ecfc16d292..aee7ce9753 100644 --- a/packages/typespec-azure-resource-manager/src/resource.ts +++ b/packages/typespec-azure-resource-manager/src/resource.ts @@ -1,10 +1,12 @@ import { getAllProperties } from "@azure-tools/typespec-azure-core"; import { $tag, + ArrayModelType, DecoratorContext, getKeyName, getTags, Interface, + isArrayModelType, isGlobalNamespace, isNeverType, isTemplateDeclaration, @@ -22,6 +24,7 @@ import { ArmVirtualResourceDecorator, CustomAzureResourceDecorator, ExtensionResourceDecorator, + IdentifiersDecorator, LocationResourceDecorator, ResourceBaseTypeDecorator, ResourceGroupResourceDecorator, @@ -373,6 +376,102 @@ export const $armProviderNameValue: ArmProviderNameValueDecorator = ( } }; +export const $identifiers: IdentifiersDecorator = ( + context: DecoratorContext, + entity: ModelProperty, + properties: string[], +) => { + const { program } = context; + const { type } = entity; + + if ( + type.kind !== "Model" || + !isArrayModelType(program, type) || + type.indexer.value.kind !== "Model" + ) { + reportDiagnostic(program, { + code: "decorator-param-wrong-type", + messageId: "armIdentifiersIncorrectEntity", + target: entity, + }); + return; + } + + const propertiesValues = properties.values; + if (!Array.isArray(propertiesValues)) { + reportDiagnostic(program, { + code: "decorator-param-wrong-type", + messageId: "armIdentifiersProperties", + target: entity, + }); + return; + } + + context.program.stateMap(ArmStateKeys.armIdentifiers).set( + type.indexer.value, + propertiesValues.map((property) => property.value), + ); +}; + +/** + * This function returns all arm identifiers for the given array model type + * This includes the identifiers specified using the @identifiers decorator + * and the identifiers using the @key decorator. + * + * @param program The program to process. + * @param entity The array model type to check. + * @returns returns list of arm identifiers for the given array model type if any or undefined. + */ +export function getArmIdentifiers(program: Program, entity: ArrayModelType): string[] | undefined { + const value = entity.indexer.value; + + const getIdentifiers = program.stateMap(ArmStateKeys.armIdentifiers).get(value); + if (getIdentifiers !== undefined) { + return getIdentifiers; + } + + const result: string[] = []; + if (value.kind === "Model") { + for (const property of value.properties.values()) { + const pathToKey = getPathToKey(program, property); + if (pathToKey !== undefined) { + result.push(property.name + pathToKey); + } else if (getKeyName(program, property)) { + result.push(property.name); + } + } + } + + return result.length > 0 ? result : undefined; +} + +function getPathToKey( + program: Program, + entity: ModelProperty, + visited = new Set(), +): string | undefined { + if (entity.type.kind !== "Model") { + return undefined; + } + if (visited.has(entity)) { + return undefined; + } + visited.add(entity); + + for (const property of entity.type.properties.values()) { + if (property.type.kind !== "Model" && getKeyName(program, property)) { + return "/" + property.name; + } + if (property.type.kind === "Model") { + const path = getPathToKey(program, property, visited); + if (path !== undefined) { + return "/" + property.name + path; + } + } + } + return undefined; +} + function getServiceNamespace(program: Program, type: Type | undefined): string | undefined { if (type === undefined) return undefined; switch (type.kind) { diff --git a/packages/typespec-azure-resource-manager/src/rules/missing-x-ms-identifiers.ts b/packages/typespec-azure-resource-manager/src/rules/missing-x-ms-identifiers.ts index 586047cb9d..0ec879469f 100644 --- a/packages/typespec-azure-resource-manager/src/rules/missing-x-ms-identifiers.ts +++ b/packages/typespec-azure-resource-manager/src/rules/missing-x-ms-identifiers.ts @@ -9,6 +9,7 @@ import { } from "@typespec/compiler"; import { getExtensions } from "@typespec/openapi"; import { isArmCommonType } from "../common-types.js"; +import { getArmIdentifiers } from "../resource.js"; export const missingXmsIdentifiersRule = createRule({ name: "missing-x-ms-identifiers", @@ -48,21 +49,32 @@ export const missingXmsIdentifiersRule = createRule({ return false; } - if (getProperty(elementType, "id")) { + if (getProperty(elementType, "id") || getProperty(elementType, "name")) { return false; } const xmsIdentifiers = getExtensions(program, property ?? array).get("x-ms-identifiers"); - if (xmsIdentifiers === undefined) { + const armIdentifiers = getArmIdentifiers(program, array); + if (xmsIdentifiers === undefined && armIdentifiers === undefined) { return true; } - if (Array.isArray(xmsIdentifiers)) { - for (const propIdentifier of xmsIdentifiers) { + const identifiers = armIdentifiers ?? xmsIdentifiers; + + if (Array.isArray(identifiers)) { + for (const propIdentifier of identifiers) { if (typeof propIdentifier === "string") { const props = propIdentifier.replace(/^\//, "").split("/"); let element = elementType; for (const prop of props) { + if (element === undefined || element.kind !== "Model") { + context.reportDiagnostic({ + messageId: "missingProperty", + format: { propertyName: prop, targetModelName: element?.name }, + target: property, + }); + return false; + } const propertyValue = getProperty(element, prop); if (propertyValue === undefined) { context.reportDiagnostic({ @@ -72,10 +84,7 @@ export const missingXmsIdentifiersRule = createRule({ }); } - const propertyType = propertyValue?.type as ArrayModelType; - if (propertyType !== undefined && propertyType.kind === "Model") { - element = propertyType; - } + element = propertyValue?.type as ArrayModelType; } } else { context.reportDiagnostic({ diff --git a/packages/typespec-azure-resource-manager/src/state.ts b/packages/typespec-azure-resource-manager/src/state.ts index 7323a184d3..88f3167687 100644 --- a/packages/typespec-azure-resource-manager/src/state.ts +++ b/packages/typespec-azure-resource-manager/src/state.ts @@ -14,6 +14,7 @@ export const ArmStateKeys = { armLibraryNamespaces: azureResourceManagerCreateStateSymbol("armLibraryNamespaces"), usesArmLibraryNamespaces: azureResourceManagerCreateStateSymbol("usesArmLibraryNamespaces"), armCommonTypesVersion: azureResourceManagerCreateStateSymbol("armCommonTypesVersion"), + armIdentifiers: azureResourceManagerCreateStateSymbol("armIdentifiers"), // resource.ts armResourcesCached: azureResourceManagerCreateStateSymbol("armResourcesCached"), diff --git a/packages/typespec-azure-resource-manager/src/tsp-index.ts b/packages/typespec-azure-resource-manager/src/tsp-index.ts index 2d0a27ea1b..1acc6a211f 100644 --- a/packages/typespec-azure-resource-manager/src/tsp-index.ts +++ b/packages/typespec-azure-resource-manager/src/tsp-index.ts @@ -20,6 +20,7 @@ import { $armVirtualResource, $customAzureResource, $extensionResource, + $identifiers, $locationResource, $resourceBaseType, $resourceGroupResource, @@ -54,6 +55,7 @@ export const $decorators = { armCommonTypesVersion: $armCommonTypesVersion, armVirtualResource: $armVirtualResource, resourceBaseType: $resourceBaseType, + identifiers: $identifiers, } satisfies AzureResourceManagerDecorators, "Azure.ResourceManager.Legacy": { customAzureResource: $customAzureResource, diff --git a/packages/typespec-azure-resource-manager/test/resource.test.ts b/packages/typespec-azure-resource-manager/test/resource.test.ts index 2577afb9d8..101889576a 100644 --- a/packages/typespec-azure-resource-manager/test/resource.test.ts +++ b/packages/typespec-azure-resource-manager/test/resource.test.ts @@ -883,3 +883,76 @@ it("recognizes resource with customResource identifier", async () => { `); expectDiagnosticEmpty(diagnostics); }); + +describe("typespec-azure-resource-manager: identifiers decorator", () => { + it("allows multiple model properties in identifiers decorator", async () => { + const { diagnostics } = await checkFor(` + @armProviderNamespace + @useDependency(Azure.ResourceManager.Versions.v1_0_Preview_1) + namespace Microsoft.Contoso; + + model Dog { + name: string; + age: int32; + } + + model Pets + { + @identifiers(["name", "age"]) + dogs: Dog[]; + } +`); + + expectDiagnosticEmpty(diagnostics); + }); + + it("allows inner model properties in identifiers decorator", async () => { + const { diagnostics } = await checkFor(` + @armProviderNamespace + @useDependency(Azure.ResourceManager.Versions.v1_0_Preview_1) + namespace Microsoft.Contoso; + + model Dog { + breed: Breed; + } + + model Breed { + type: string; + } + + model Pets + { + @identifiers(["breed/type"]) + dogs: Dog[]; + } +`); + + expectDiagnosticEmpty(diagnostics); + }); + + it("emits diagnostic when identifiers is not of a model property object array", async () => { + const { diagnostics } = await checkFor(` + @armProviderNamespace + @useDependency(Azure.ResourceManager.Versions.v1_0_Preview_1) + namespace Microsoft.Contoso; + + model Dog { + name: string; + } + + model Pets + { + @identifiers(["age"]) + dogs: Dog; + } +`); + + expectDiagnostics(diagnostics, [ + { + code: "@azure-tools/typespec-azure-resource-manager/decorator-param-wrong-type", + message: + "The @identifiers decorator must be applied to a property that is an array of objects", + }, + ]); + }); +}); diff --git a/packages/typespec-azure-resource-manager/test/rules/missing-x-ms-identifiers.test.ts b/packages/typespec-azure-resource-manager/test/rules/missing-x-ms-identifiers.test.ts index 4c529814b0..0b84b96d79 100644 --- a/packages/typespec-azure-resource-manager/test/rules/missing-x-ms-identifiers.test.ts +++ b/packages/typespec-azure-resource-manager/test/rules/missing-x-ms-identifiers.test.ts @@ -177,6 +177,60 @@ describe("typespec-azure-core: no-enum rule", () => { .toBeValid(); }); + it("allow x-ms-identifiers from keys", async () => { + await tester + .expect( + ` + model Pet { + pet: Dog[]; + } + + model Dog { + food: Food; + } + + model Food { + @key + brand: string; + } + `, + ) + .toBeValid(); + }); + + it("allow x-ms-identifiers from keys on default identifiers", async () => { + await tester + .expect( + ` + model Pet { + pet: Dog[]; + } + + model Dog { + name: string; + } + `, + ) + .toBeValid(); + }); + + it("allow x-ms-identifiers from identifiers decorator", async () => { + await tester + .expect( + ` + model Pet { + @identifiers(["name"]) + pet: Dog[]; + } + + model Dog { + name: string; + } + `, + ) + .toBeValid(); + }); + it("emit diagnostic if a section is not found", async () => { await tester .expect( @@ -188,12 +242,13 @@ describe("typespec-azure-core: no-enum rule", () => { model Dog { food: string; + brand: string; } `, ) .toEmitDiagnostics({ code: "@azure-tools/typespec-azure-resource-manager/missing-x-ms-identifiers", - message: `Property "brand" is not found in "Dog". Make sure value of x-ms-identifiers extension are valid property name of the array element.`, + message: `Property "brand" is not found in "string". Make sure value of x-ms-identifiers extension are valid property name of the array element.`, }); }); }); 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 9b2cc18146..8f07c32af2 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 @@ -272,6 +272,34 @@ See more details on [different Azure Resource Manager resource type here.](https None +### `@identifiers` {#@Azure.ResourceManager.identifiers} + +This decorator is used to indicate the identifying properties of objects in the array, e.g. size +The properties that are used as identifiers for the object needs to be provided as a list of strings. + +```typespec +@Azure.ResourceManager.identifiers(properties: string[]) +``` + +#### Target + +`ModelProperty` + +#### Parameters + +| Name | Type | Description | +| ---------- | ---------- | ------------------------------------------------------------------------------------------------------------------- | +| properties | `string[]` | The list of properties that are used as identifiers for the object. This needs to be provided as a list of strings. | + +#### Examples + +```typespec +model Pet { + @identifiers(["size"]) + dog: Dog; +} +``` + ### `@locationResource` {#@Azure.ResourceManager.locationResource} `@locationResource` marks an Azure Resource Manager resource model as a location based resource. 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 88fbc6bd48..6a10dbf9ac 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 @@ -48,6 +48,7 @@ npm install --save-peer @azure-tools/typespec-azure-resource-manager - [`@armResourceUpdate`](./decorators.md#@Azure.ResourceManager.armResourceUpdate) - [`@armVirtualResource`](./decorators.md#@Azure.ResourceManager.armVirtualResource) - [`@extensionResource`](./decorators.md#@Azure.ResourceManager.extensionResource) +- [`@identifiers`](./decorators.md#@Azure.ResourceManager.identifiers) - [`@locationResource`](./decorators.md#@Azure.ResourceManager.locationResource) - [`@resourceBaseType`](./decorators.md#@Azure.ResourceManager.resourceBaseType) - [`@resourceGroupResource`](./decorators.md#@Azure.ResourceManager.resourceGroupResource)