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 7be75d1a28..0516e4d9a9 100644 --- a/packages/typespec-autorest/src/openapi.ts +++ b/packages/typespec-autorest/src/openapi.ts @@ -12,8 +12,10 @@ import { } from "@azure-tools/typespec-azure-core"; import { getArmCommonTypeOpenAPIRef, + getArmIdentifiers, getExternalTypeRef, isArmCommonType, + isArmProviderNamespace, isAzureResource, isConditionallyFlattened, } from "@azure-tools/typespec-azure-resource-manager"; @@ -963,7 +965,7 @@ export async function getOpenAPIForService( } return undefined; } - function getSchemaOrRef(type: Type, schemaContext: SchemaContext): any { + function getSchemaOrRef(type: Type, schemaContext: SchemaContext, namespace?: Namespace): any { let schemaNameOverride: ((name: string, visibility: Visibility) => string) | undefined = undefined; const ref = resolveExternalRef(type); @@ -1013,7 +1015,7 @@ export async function getOpenAPIForService( const name = getOpenAPITypeName(program, type, typeNameOptions); if (shouldInline(program, type)) { - const schema = getSchemaForInlineType(type, name, schemaContext); + const schema = getSchemaForInlineType(type, name, schemaContext, namespace); if (schema === undefined && isErrorType(type)) { // Exit early so that syntax errors are exposed. This error will @@ -1040,7 +1042,12 @@ export async function getOpenAPIForService( return { $ref: pending.ref }; } } - function getSchemaForInlineType(type: Type, name: string, context: SchemaContext) { + function getSchemaForInlineType( + type: Type, + name: string, + context: SchemaContext, + namespace?: Namespace, + ) { if (inProgressInlineTypes.has(type)) { reportDiagnostic(program, { code: "inline-cycle", @@ -1050,7 +1057,7 @@ export async function getOpenAPIForService( return {}; } inProgressInlineTypes.add(type); - const schema = getSchemaForType(type, context); + const schema = getSchemaForType(type, context, namespace); inProgressInlineTypes.delete(type); return schema; } @@ -1627,17 +1634,20 @@ export async function getOpenAPIForService( } } - function getSchemaForType(type: Type, schemaContext: SchemaContext): OpenAPI2Schema | undefined { + function getSchemaForType( + type: Type, + schemaContext: SchemaContext, + namespace?: Namespace, + ): OpenAPI2Schema | undefined { const builtinType = getSchemaForLiterals(type); if (builtinType !== undefined) { return builtinType; } - switch (type.kind) { case "Intrinsic": return getSchemaForIntrinsicType(type); case "Model": - return getSchemaForModel(type, schemaContext); + return getSchemaForModel(type, schemaContext, namespace); case "ModelProperty": return getSchemaForType(type.type, schemaContext); case "Scalar": @@ -1841,6 +1851,10 @@ export async function getOpenAPIForService( ); } + function ifArmIdentifiersDefault(armIdentifiers: string[]) { + return armIdentifiers.every((identifier) => identifier === "id" || identifier === "name"); + } + function getSchemaForUnionVariant( variant: UnionVariant, schemaContext: SchemaContext, @@ -1885,8 +1899,8 @@ export async function getOpenAPIForService( return undefined; } - function getSchemaForModel(model: Model, schemaContext: SchemaContext) { - const array = getArrayType(model, schemaContext); + function getSchemaForModel(model: Model, schemaContext: SchemaContext, namespace?: Namespace) { + const array = getArrayType(model, schemaContext, namespace); if (array) { return array; } @@ -2071,7 +2085,7 @@ export async function getOpenAPIForService( propSchema = getSchemaOrRef(prop.type, context); } } else { - propSchema = getSchemaOrRef(prop.type, context); + propSchema = getSchemaOrRef(prop.type, context, prop.model?.namespace); } if (options.armResourceFlattening && isConditionallyFlattened(program, prop)) { @@ -2372,7 +2386,11 @@ export async function getOpenAPIForService( /** * If the model is an array model return the OpenAPI2Schema for the array type. */ - function getArrayType(typespecType: Model, context: SchemaContext): OpenAPI2Schema | undefined { + function getArrayType( + typespecType: Model, + context: SchemaContext, + namespace?: Namespace, + ): OpenAPI2Schema | undefined { if (isArrayModelType(program, typespecType)) { const array: OpenAPI2Schema = { type: "array", @@ -2381,14 +2399,28 @@ export async function getOpenAPIForService( visibility: context.visibility | Visibility.Item, }), }; - if (!ifArrayItemContainsIdentifier(program, typespecType as any)) { + + const indexer = typespecType.indexer.value as Model; + const armIdentifiers = getArmIdentifiers(program, typespecType); + if (isArmProviderNamespace(program, namespace) && hasValidArmIdentifiers(armIdentifiers)) { + array["x-ms-identifiers"] = armIdentifiers; + } else if (!ifArrayItemContainsIdentifier(program, typespecType as any)) { array["x-ms-identifiers"] = []; } + return applyIntrinsicDecorators(typespecType, array); } return undefined; } + function hasValidArmIdentifiers(armIdentifiers: string[] | undefined) { + return ( + armIdentifiers !== undefined && + armIdentifiers.length > 0 && + !ifArmIdentifiersDefault(armIdentifiers) + ); + } + function getSchemaForScalar(scalar: Scalar): OpenAPI2Schema { let result: OpenAPI2Schema = {}; const isStd = program.checker.isStdType(scalar); diff --git a/packages/typespec-autorest/test/openapi-output.test.ts b/packages/typespec-autorest/test/openapi-output.test.ts index 727f95f758..bbe504d5d3 100644 --- a/packages/typespec-autorest/test/openapi-output.test.ts +++ b/packages/typespec-autorest/test/openapi-output.test.ts @@ -823,6 +823,228 @@ 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( + ` + @armProviderNamespace + @useDependency(Azure.ResourceManager.Versions.v1_0_Preview_1) + namespace Microsoft.Test; + + 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( + ` + @armProviderNamespace + @useDependency(Azure.ResourceManager.Versions.v1_0_Preview_1) + namespace Microsoft.Test; + + 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("x-ms-identifiers ignores keys for non armProviderNamespace", 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"], []); + }); + + 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( + ` + @armProviderNamespace + @useDependency(Azure.ResourceManager.Versions.v1_0_Preview_1) + namespace Microsoft.Test; + + 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( + ` + @armProviderNamespace + @useDependency(Azure.ResourceManager.Versions.v1_0_Preview_1) + namespace Microsoft.Test; + + 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("support inner models in different namespace but route models should be on armProviderNamespace", async () => { + const oapi = await openApiFor( + ` + @armProviderNamespace + @useDependency(Azure.ResourceManager.Versions.v1_0_Preview_1) + namespace Microsoft.Test + { + + @route("/Pets") + @get op list(): PetList; + + model PetList { + @identifiers(["age"]) + pets: Microsoft.Modeling.Pet[] + } + } + + namespace Microsoft.Modeling + { + model Pet { + age: int32; + } + } + `, + ); + ok(oapi.paths["/Pets"].get); + deepStrictEqual(oapi.definitions.PetList.properties.pets["x-ms-identifiers"], ["age"]); + }); + it("supports inner properties for keys", async () => { + const oapi = await openApiFor( + ` + @armProviderNamespace + @useDependency(Azure.ResourceManager.Versions.v1_0_Preview_1) + namespace Microsoft.Test; + + 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", + ]); + }); +}); + 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 badb581b40..78b456f85d 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 0955d90f24..c425dff850 100644 --- a/packages/typespec-azure-resource-manager/generated-defs/Azure.ResourceManager.ts +++ b/packages/typespec-azure-resource-manager/generated-defs/Azure.ResourceManager.ts @@ -267,6 +267,12 @@ export type ResourceBaseTypeDecorator = ( baseType: Type, ) => void; +export type IdentifiersDecorator = ( + context: DecoratorContext, + target: ModelProperty, + properties: string[], +) => void; + /** * * Specify an external reference that should be used when emitting this type. * * @param jsonRef - External reference(e.g. "../../common.json#/definitions/Foo") @@ -300,6 +306,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/namespace.ts b/packages/typespec-azure-resource-manager/src/namespace.ts index c7e8c3c108..09df0dbed9 100644 --- a/packages/typespec-azure-resource-manager/src/namespace.ts +++ b/packages/typespec-azure-resource-manager/src/namespace.ts @@ -74,6 +74,15 @@ export function isArmLibraryNamespace(program: Program, namespace: Namespace): b return program.stateMap(ArmStateKeys.armLibraryNamespaces).get(namespace) === true; } +export function isArmProviderNamespace( + program: Program, + namespace: Namespace | undefined, +): boolean { + return ( + namespace !== undefined && program.stateMap(ArmStateKeys.armProviderNamespaces).has(namespace) + ); +} + function isArmNamespaceOverride(program: Program, entity: Namespace): boolean { return ( program.stateMap(ArmStateKeys.armProviderNamespaces).size === 1 && 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 6ef6716671..4ae43e9d64 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"), externalTypeRef: azureResourceManagerCreateStateSymbol("externalTypeRef"), // resource.ts diff --git a/packages/typespec-azure-resource-manager/src/tsp-index.ts b/packages/typespec-azure-resource-manager/src/tsp-index.ts index ea04cf6ff9..ab9991abaa 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 b5b1520c55..6242a1b37c 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 7c3821bb4d..55da9b7af0 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)