From 6940e1a9c804ab9c708c3a88b21cf724957a1d5c Mon Sep 17 00:00:00 2001 From: Arcturus Zhang Date: Mon, 1 Dec 2025 13:53:01 +0800 Subject: [PATCH 1/7] add a test case --- core | 2 +- .../test/resource-resolution.test.ts | 68 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/core b/core index fecf4f2300..f1fa49ad1d 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit fecf4f2300b88dc663f27dcb3f37bff78313a17b +Subproject commit f1fa49ad1dfdc5585ba44e2ad60091916cb2b061 diff --git a/packages/typespec-azure-resource-manager/test/resource-resolution.test.ts b/packages/typespec-azure-resource-manager/test/resource-resolution.test.ts index bfa7f63403..afffccd2eb 100644 --- a/packages/typespec-azure-resource-manager/test/resource-resolution.test.ts +++ b/packages/typespec-azure-resource-manager/test/resource-resolution.test.ts @@ -3546,4 +3546,72 @@ model DependentProperties { expect(ResB.resourceName).toEqual("ResB"); expect(ResB.providerNamespace).toEqual("Microsoft.ServiceB"); }); + + it("supports multiple singleton resources", async () => { + const { program } = await Tester.compile(` +using Azure.Core; + +@armProviderNamespace +namespace Microsoft.ContosoProviderHub; + +interface Operations extends Azure.ResourceManager.Operations {} + +@singleton +model Employee is TrackedResource { + ...ResourceNameParameter; +} + +model EmployeeProperties { + age?: int32; + + @visibility(Lifecycle.Read) + provisioningState?: ProvisioningState; +} + +@singleton +model Building is TrackedResource { + ...ResourceNameParameter; +} + +model BuildingProperties { + address?: string; + + @visibility(Lifecycle.Read) + provisioningState?: ProvisioningState; +} + +@armResourceOperations +interface Employees { + get is ArmResourceRead; + createOrUpdate is ArmResourceCreateOrReplaceAsync; + update is ArmCustomPatchSync< + Employee, + Azure.ResourceManager.Foundations.ResourceUpdateModel + >; +} + +@armResourceOperations +interface Buildings { + get is ArmResourceRead; + createOrUpdate is ArmResourceCreateOrReplaceAsync; + update is ArmCustomPatchSync< + Building, + Azure.ResourceManager.Foundations.ResourceUpdateModel + >; +} +`); + const provider = resolveArmResources(program); + expect(provider).toBeDefined(); + expect(provider.resources).toBeDefined(); + expect(provider.resources).toHaveLength(2); + const employee = provider.resources![0]; + ok(employee); + expect(employee).toMatchObject({ + kind: "Tracked", + providerNamespace: "Microsoft.ContosoProviderHub", + type: expect.anything(), + scope: "ResourceGroup", + parent: undefined, + }); + }); }); From c4d12670a3dc74bf81fbb68f3b1bdeae964c26ff Mon Sep 17 00:00:00 2001 From: Arcturus Zhang Date: Mon, 1 Dec 2025 16:00:25 +0800 Subject: [PATCH 2/7] fix the issue --- .../src/resource.ts | 13 +- .../test/resource-resolution.test.ts | 139 ++++++++++++++++++ 2 files changed, 145 insertions(+), 7 deletions(-) diff --git a/packages/typespec-azure-resource-manager/src/resource.ts b/packages/typespec-azure-resource-manager/src/resource.ts index 348625bbe6..dd34888b06 100644 --- a/packages/typespec-azure-resource-manager/src/resource.ts +++ b/packages/typespec-azure-resource-manager/src/resource.ts @@ -622,7 +622,7 @@ function getResourceScope( } function isVariableSegment(segment: string): boolean { - return (segment.startsWith("{") && segment.endsWith("}")) || segment === "default"; + return (segment.startsWith("{") && segment.endsWith("}")); } function getResourceInfo( @@ -652,11 +652,7 @@ export function getResourcePathElements( break; } - if (i + 1 < segments.length && isVariableSegment(segments[i + 1])) { - typeSegments.push(segments[i]); - instanceSegments.push(segments[i]); - instanceSegments.push(segments[i + 1]); - } else if (i + 1 === segments.length) { + if (i + 1 === segments.length) { switch (kind) { case "list": typeSegments.push(segments[i]); @@ -666,7 +662,10 @@ export function getResourcePathElements( default: break; } - break; + } else { + typeSegments.push(segments[i]); + instanceSegments.push(segments[i]); + instanceSegments.push(segments[i + 1]); } } if (provider !== undefined && typeSegments.length > 0) { diff --git a/packages/typespec-azure-resource-manager/test/resource-resolution.test.ts b/packages/typespec-azure-resource-manager/test/resource-resolution.test.ts index afffccd2eb..d6de6cd01f 100644 --- a/packages/typespec-azure-resource-manager/test/resource-resolution.test.ts +++ b/packages/typespec-azure-resource-manager/test/resource-resolution.test.ts @@ -3599,6 +3599,21 @@ interface Buildings { Azure.ResourceManager.Foundations.ResourceUpdateModel >; } + +@lroStatus +union ProvisioningState { + ResourceProvisioningState, + + Provisioning: "Provisioning", + + Updating: "Updating", + + Deleting: "Deleting", + + Accepted: "Accepted", + + string, +} `); const provider = resolveArmResources(program); expect(provider).toBeDefined(); @@ -3610,6 +3625,130 @@ interface Buildings { kind: "Tracked", providerNamespace: "Microsoft.ContosoProviderHub", type: expect.anything(), + resourceType: { + provider: "Microsoft.ContosoProviderHub", + types: [ + "employees", + ], + }, + scope: "ResourceGroup", + parent: undefined, + }); + const building = provider.resources![1]; + ok(building); + expect(building).toMatchObject({ + kind: "Tracked", + providerNamespace: "Microsoft.ContosoProviderHub", + type: expect.anything(), + resourceType: { + provider: "Microsoft.ContosoProviderHub", + types: [ + "buildings", + ], + }, + scope: "ResourceGroup", + parent: undefined, + }); + }); + it("supports multiple singleton resources with different names", async () => { + const { program } = await Tester.compile(` +using Azure.Core; + +@armProviderNamespace +namespace Microsoft.ContosoProviderHub; + +interface Operations extends Azure.ResourceManager.Operations {} + +@singleton +model Employee is TrackedResource { + ...ResourceNameParameter; +} + +model EmployeeProperties { + age?: int32; + + @visibility(Lifecycle.Read) + provisioningState?: ProvisioningState; +} + +@singleton("current") +model Building is TrackedResource { + ...ResourceNameParameter; +} + +model BuildingProperties { + address?: string; + + @visibility(Lifecycle.Read) + provisioningState?: ProvisioningState; +} + +@armResourceOperations +interface Employees { + get is ArmResourceRead; + createOrUpdate is ArmResourceCreateOrReplaceAsync; + update is ArmCustomPatchSync< + Employee, + Azure.ResourceManager.Foundations.ResourceUpdateModel + >; +} + +@armResourceOperations +interface Buildings { + get is ArmResourceRead; + createOrUpdate is ArmResourceCreateOrReplaceAsync; + update is ArmCustomPatchSync< + Building, + Azure.ResourceManager.Foundations.ResourceUpdateModel + >; +} + +@lroStatus +union ProvisioningState { + ResourceProvisioningState, + + Provisioning: "Provisioning", + + Updating: "Updating", + + Deleting: "Deleting", + + Accepted: "Accepted", + + string, +} +`); + const provider = resolveArmResources(program); + expect(provider).toBeDefined(); + expect(provider.resources).toBeDefined(); + expect(provider.resources).toHaveLength(2); + const employee = provider.resources![0]; + ok(employee); + expect(employee).toMatchObject({ + kind: "Tracked", + providerNamespace: "Microsoft.ContosoProviderHub", + type: expect.anything(), + resourceType: { + provider: "Microsoft.ContosoProviderHub", + types: [ + "employees", + ], + }, + scope: "ResourceGroup", + parent: undefined, + }); + const building = provider.resources![1]; + ok(building); + expect(building).toMatchObject({ + kind: "Tracked", + providerNamespace: "Microsoft.ContosoProviderHub", + type: expect.anything(), + resourceType: { + provider: "Microsoft.ContosoProviderHub", + types: [ + "buildings", + ], + }, scope: "ResourceGroup", parent: undefined, }); From b9a7446a86c921706285fe4f33065bbadb3735e5 Mon Sep 17 00:00:00 2001 From: Arcturus Zhang Date: Mon, 1 Dec 2025 16:05:04 +0800 Subject: [PATCH 3/7] add tests for one singleton resource --- .../test/resource-resolution.test.ts | 144 ++++++++++++++++-- 1 file changed, 132 insertions(+), 12 deletions(-) diff --git a/packages/typespec-azure-resource-manager/test/resource-resolution.test.ts b/packages/typespec-azure-resource-manager/test/resource-resolution.test.ts index d6de6cd01f..e2590a0de2 100644 --- a/packages/typespec-azure-resource-manager/test/resource-resolution.test.ts +++ b/packages/typespec-azure-resource-manager/test/resource-resolution.test.ts @@ -3547,6 +3547,134 @@ model DependentProperties { expect(ResB.providerNamespace).toEqual("Microsoft.ServiceB"); }); + it("supports singleton resource", async () => { + const { program } = await Tester.compile(` +using Azure.Core; + +@armProviderNamespace +namespace Microsoft.ContosoProviderHub; + +interface Operations extends Azure.ResourceManager.Operations {} + +@singleton +model Employee is TrackedResource { + ...ResourceNameParameter; +} + +model EmployeeProperties { + age?: int32; + + @visibility(Lifecycle.Read) + provisioningState?: ProvisioningState; +} + +@lroStatus +union ProvisioningState { + ResourceProvisioningState, + + Provisioning: "Provisioning", + + Updating: "Updating", + + Deleting: "Deleting", + + Accepted: "Accepted", + + string, +} + +@armResourceOperations +interface Employees { + get is ArmResourceRead; + createOrUpdate is ArmResourceCreateOrReplaceAsync; + update is ArmCustomPatchSync< + Employee, + Azure.ResourceManager.Foundations.ResourceUpdateModel + >; +} +`); + const provider = resolveArmResources(program); + expect(provider).toBeDefined(); + expect(provider.resources).toBeDefined(); + expect(provider.resources).toHaveLength(1); + const employee = provider.resources![0]; + ok(employee); + expect(employee).toMatchObject({ + kind: "Tracked", + providerNamespace: "Microsoft.ContosoProviderHub", + type: expect.anything(), + resourceType: { + provider: "Microsoft.ContosoProviderHub", + types: ["employees"], + }, + scope: "ResourceGroup", + parent: undefined, + }); + }); + it("supports singleton resource with customized name", async () => { + const { program } = await Tester.compile(` +using Azure.Core; + +@armProviderNamespace +namespace Microsoft.ContosoProviderHub; + +interface Operations extends Azure.ResourceManager.Operations {} + +@singleton("current") +model Employee is TrackedResource { + ...ResourceNameParameter; +} + +model EmployeeProperties { + age?: int32; + + @visibility(Lifecycle.Read) + provisioningState?: ProvisioningState; +} + +@lroStatus +union ProvisioningState { + ResourceProvisioningState, + + Provisioning: "Provisioning", + + Updating: "Updating", + + Deleting: "Deleting", + + Accepted: "Accepted", + + string, +} + +@armResourceOperations +interface Employees { + get is ArmResourceRead; + createOrUpdate is ArmResourceCreateOrReplaceAsync; + update is ArmCustomPatchSync< + Employee, + Azure.ResourceManager.Foundations.ResourceUpdateModel + >; +} +`); + const provider = resolveArmResources(program); + expect(provider).toBeDefined(); + expect(provider.resources).toBeDefined(); + expect(provider.resources).toHaveLength(1); + const employee = provider.resources![0]; + ok(employee); + expect(employee).toMatchObject({ + kind: "Tracked", + providerNamespace: "Microsoft.ContosoProviderHub", + type: expect.anything(), + resourceType: { + provider: "Microsoft.ContosoProviderHub", + types: ["employees"], + }, + scope: "ResourceGroup", + parent: undefined, + }); + }); it("supports multiple singleton resources", async () => { const { program } = await Tester.compile(` using Azure.Core; @@ -3627,9 +3755,7 @@ union ProvisioningState { type: expect.anything(), resourceType: { provider: "Microsoft.ContosoProviderHub", - types: [ - "employees", - ], + types: ["employees"], }, scope: "ResourceGroup", parent: undefined, @@ -3642,9 +3768,7 @@ union ProvisioningState { type: expect.anything(), resourceType: { provider: "Microsoft.ContosoProviderHub", - types: [ - "buildings", - ], + types: ["buildings"], }, scope: "ResourceGroup", parent: undefined, @@ -3730,9 +3854,7 @@ union ProvisioningState { type: expect.anything(), resourceType: { provider: "Microsoft.ContosoProviderHub", - types: [ - "employees", - ], + types: ["employees"], }, scope: "ResourceGroup", parent: undefined, @@ -3745,9 +3867,7 @@ union ProvisioningState { type: expect.anything(), resourceType: { provider: "Microsoft.ContosoProviderHub", - types: [ - "buildings", - ], + types: ["buildings"], }, scope: "ResourceGroup", parent: undefined, From 451d7b9ab6c5e4168a17ca35e3addef510c321f0 Mon Sep 17 00:00:00 2001 From: Arcturus Zhang Date: Tue, 2 Dec 2025 10:40:40 +0800 Subject: [PATCH 4/7] patch up some tests --- .../test/resource-resolution.test.ts | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/typespec-azure-resource-manager/test/resource-resolution.test.ts b/packages/typespec-azure-resource-manager/test/resource-resolution.test.ts index e2590a0de2..05640f1cf6 100644 --- a/packages/typespec-azure-resource-manager/test/resource-resolution.test.ts +++ b/packages/typespec-azure-resource-manager/test/resource-resolution.test.ts @@ -208,10 +208,10 @@ describe("unit tests for resource manager helpers", () => { expected: { resourceType: { provider: "Microsoft.Bar", - types: ["bars", "basses"], + types: ["bars", "basses", "actionName"], // 'actionName' is treated as a type here because this action has even segments }, resourceInstancePath: - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Test/foos/{fooName}/providers/Microsoft.Bar/bars/{barName}/basses/{baseName}", + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Test/foos/{fooName}/providers/Microsoft.Bar/bars/{barName}/basses/{baseName}/actionName/doSomething", }, }, { @@ -234,10 +234,10 @@ describe("unit tests for resource manager helpers", () => { expected: { resourceType: { provider: "Microsoft.Bar", - types: ["bars", "basses"], + types: ["bars", "basses", "actionName", "doSomethingElse"], }, resourceInstancePath: - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{providerName}/{resourceType}/{resourceName}/{childResourceType}/{childResourceName}/providers/Microsoft.Bar/bars/{barName}/basses/{name}", + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{providerName}/{resourceType}/{resourceName}/{childResourceType}/{childResourceName}/providers/Microsoft.Bar/bars/{barName}/basses/{name}/actionName/doSomething/doSomethingElse/andAnotherThing", }, }, { @@ -247,10 +247,10 @@ describe("unit tests for resource manager helpers", () => { expected: { resourceType: { provider: "Microsoft.Bar", - types: ["bars"], + types: ["bars", "basses", "actionName", "doSomethingElse"], }, resourceInstancePath: - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{providerName}/{resourceType}/{resourceName}/{childResourceType}/{childResourceName}/providers/Microsoft.Bar/bars/{barName}", + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{providerName}/{resourceType}/{resourceName}/{childResourceType}/{childResourceName}/providers/Microsoft.Bar/bars/{barName}/basses/drums/actionName/doSomething/doSomethingElse/andAnotherThing", }, }, { @@ -260,10 +260,10 @@ describe("unit tests for resource manager helpers", () => { expected: { resourceType: { provider: "Microsoft.Bar", - types: ["bars", "basses"], + types: ["bars", "basses", "actionName", "doSomethingElse"], }, resourceInstancePath: - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{providerName}/{resourceType}/{resourceName}/{childResourceType}/{childResourceName}/providers/Microsoft.Bar/bars/{barName}/basses/default", + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{providerName}/{resourceType}/{resourceName}/{childResourceType}/{childResourceName}/providers/Microsoft.Bar/bars/{barName}/basses/default/actionName/doSomething/doSomethingElse/andAnotherThing", }, }, { @@ -273,10 +273,10 @@ describe("unit tests for resource manager helpers", () => { expected: { resourceType: { provider: "Microsoft.Bar", - types: ["bars", "basses"], + types: ["bars", "basses", "actionName", "doSomethingElse"], }, resourceInstancePath: - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{providerName}/{resourceType}/{resourceName}/{childResourceType}/{childResourceName}/providers/Microsoft.Bar/bars/{barName}/basses/default", + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{providerName}/{resourceType}/{resourceName}/{childResourceType}/{childResourceName}/providers/Microsoft.Bar/bars/{barName}/basses/default/actionName/doSomething/doSomethingElse/andAnotherThing", }, }, { @@ -286,10 +286,10 @@ describe("unit tests for resource manager helpers", () => { expected: { resourceType: { provider: "Microsoft.Bar", - types: ["bars", "basses"], + types: ["bars", "basses", "actionName", "doSomethingElse"], }, resourceInstancePath: - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{providerName}/{resourceType}/{resourceName}/{childResourceType}/{childResourceName}/providers/Microsoft.Bar/bars/{barName}/basses/default", + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{providerName}/{resourceType}/{resourceName}/{childResourceType}/{childResourceName}/providers/Microsoft.Bar/bars/{barName}/basses/default/actionName/doSomething/doSomethingElse/andAnotherThing", }, }, { @@ -299,10 +299,10 @@ describe("unit tests for resource manager helpers", () => { expected: { resourceType: { provider: "Microsoft.Bar", - types: ["bars"], + types: ["bars", "basses"], }, resourceInstancePath: - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{providerName}/{resourceType}/{resourceName}/{childResourceType}/{childResourceName}/providers/Microsoft.Bar/bars/{barName}", + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{providerName}/{resourceType}/{resourceName}/{childResourceType}/{childResourceName}/providers/Microsoft.Bar/bars/{barName}/basses/drums", }, }, { @@ -312,10 +312,10 @@ describe("unit tests for resource manager helpers", () => { expected: { resourceType: { provider: "Microsoft.Bar", - types: ["bars"], + types: ["bars", "basses"], }, resourceInstancePath: - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{providerName}/{resourceType}/{resourceName}/{childResourceType}/{childResourceName}/providers/Microsoft.Bar/bars/{barName}", + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{providerName}/{resourceType}/{resourceName}/{childResourceType}/{childResourceName}/providers/Microsoft.Bar/bars/{barName}/basses/drums", }, }, ]; From 290dd82662e09a056531df9a13b05d6e8be16b66 Mon Sep 17 00:00:00 2001 From: Arcturus Zhang Date: Fri, 5 Dec 2025 14:44:58 +0800 Subject: [PATCH 5/7] update --- .../src/resource.ts | 32 ++++++++++----- .../test/resource-resolution.test.ts | 40 ++++++++++--------- .../test/resource.test.ts | 2 +- 3 files changed, 46 insertions(+), 28 deletions(-) diff --git a/packages/typespec-azure-resource-manager/src/resource.ts b/packages/typespec-azure-resource-manager/src/resource.ts index dd34888b06..c17eb29d4e 100644 --- a/packages/typespec-azure-resource-manager/src/resource.ts +++ b/packages/typespec-azure-resource-manager/src/resource.ts @@ -167,6 +167,8 @@ interface ResolvedResourceOperations { parent?: ResolvedResource; /** The scope of this resource */ scope?: string; + /** For singleton resources, the names that can be used to reference the resource */ + singletonResourceNames?: string[]; } /** Resolved operations, including operations for non-arm resources */ export interface ResolvedResource { @@ -190,6 +192,8 @@ export interface ResolvedResource { parent?: ResolvedResource; /** The scope of this resource */ scope?: string | ResolvedResource; + /** For singleton resources, the names that can be used to reference the resource */ + singletonResourceNames?: string[]; } /** Description of the resource type */ @@ -621,15 +625,16 @@ function getResourceScope( return undefined; } -function isVariableSegment(segment: string): boolean { - return (segment.startsWith("{") && segment.endsWith("}")); +function isVariableSegment(segment: string, singletonResourceName?: string): boolean { + return (segment.startsWith("{") && segment.endsWith("}")) || segment === singletonResourceName; } function getResourceInfo( program: Program, operation: ArmResourceOperation, + singletonResourceName: string | undefined ): ResolvedResourceInfo | undefined { - const pathInfo = getResourcePathElements(operation.httpOperation.path, operation.kind); + const pathInfo = getResourcePathElements(operation.httpOperation.path, operation.kind, singletonResourceName); if (pathInfo === undefined) return undefined; return { ...pathInfo, @@ -640,6 +645,7 @@ function getResourceInfo( export function getResourcePathElements( path: string, kind: ArmOperationKind, + singletonResourceName?: string, ): ResourcePathInfo | undefined { const segments = path.split("/").filter((s) => s.length > 0); const providerIndex = segments.findLastIndex((s) => s === "providers"); @@ -652,7 +658,16 @@ export function getResourcePathElements( break; } - if (i + 1 === segments.length) { + // if the next segment is the last segment + if (i + 1 === segments.length - 1 && isVariableSegment(segments[i + 1], singletonResourceName)) { + typeSegments.push(segments[i]); + instanceSegments.push(segments[i]); + instanceSegments.push(segments[i + 1]); + } else if (i + 1 < segments.length && isVariableSegment(segments[i + 1])) { + typeSegments.push(segments[i]); + instanceSegments.push(segments[i]); + instanceSegments.push(segments[i + 1]); + } else if (i + 1 === segments.length) { switch (kind) { case "list": typeSegments.push(segments[i]); @@ -662,10 +677,7 @@ export function getResourcePathElements( default: break; } - } else { - typeSegments.push(segments[i]); - instanceSegments.push(segments[i]); - instanceSegments.push(segments[i + 1]); + break; } } if (provider !== undefined && typeSegments.length > 0) { @@ -861,9 +873,10 @@ export function resolveArmResourceOperations( if (armOperation === undefined) continue; armOperation.kind = operation.kind; + const singletonResourceName = getSingletonResourceKey(program, resourceType) armOperation.resourceModelName = operation.resource?.name ?? resourceType.name; - const resourceInfo = getResourceInfo(program, armOperation); + const resourceInfo = getResourceInfo(program, armOperation, singletonResourceName); if (resourceInfo === undefined) continue; armOperation.name = operation.name; armOperation.resourceKind = operation.resourceKind; @@ -892,6 +905,7 @@ export function resolveArmResourceOperations( resourceType: resourceInfo.resourceType, resourceInstancePath: resourceInfo.resourceInstancePath, resourceName: resourceInfo.resourceName, + singletonResourceNames: singletonResourceName !== undefined ? [singletonResourceName] : undefined, operations: { lifecycle: { read: undefined, diff --git a/packages/typespec-azure-resource-manager/test/resource-resolution.test.ts b/packages/typespec-azure-resource-manager/test/resource-resolution.test.ts index 05640f1cf6..15d472f797 100644 --- a/packages/typespec-azure-resource-manager/test/resource-resolution.test.ts +++ b/packages/typespec-azure-resource-manager/test/resource-resolution.test.ts @@ -122,6 +122,7 @@ describe("unit tests for resource manager helpers", () => { title: string; path: string; kind: ArmOperationKind; + singletonResourceName?: string; expected: ResourcePathInfo; }[] = [ { @@ -208,10 +209,10 @@ describe("unit tests for resource manager helpers", () => { expected: { resourceType: { provider: "Microsoft.Bar", - types: ["bars", "basses", "actionName"], // 'actionName' is treated as a type here because this action has even segments + types: ["bars", "basses"], }, resourceInstancePath: - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Test/foos/{fooName}/providers/Microsoft.Bar/bars/{barName}/basses/{baseName}/actionName/doSomething", + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Test/foos/{fooName}/providers/Microsoft.Bar/bars/{barName}/basses/{baseName}", }, }, { @@ -234,10 +235,10 @@ describe("unit tests for resource manager helpers", () => { expected: { resourceType: { provider: "Microsoft.Bar", - types: ["bars", "basses", "actionName", "doSomethingElse"], + types: ["bars", "basses"], }, resourceInstancePath: - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{providerName}/{resourceType}/{resourceName}/{childResourceType}/{childResourceName}/providers/Microsoft.Bar/bars/{barName}/basses/{name}/actionName/doSomething/doSomethingElse/andAnotherThing", + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{providerName}/{resourceType}/{resourceName}/{childResourceType}/{childResourceName}/providers/Microsoft.Bar/bars/{barName}/basses/{name}", }, }, { @@ -247,49 +248,52 @@ describe("unit tests for resource manager helpers", () => { expected: { resourceType: { provider: "Microsoft.Bar", - types: ["bars", "basses", "actionName", "doSomethingElse"], + types: ["bars"], }, resourceInstancePath: - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{providerName}/{resourceType}/{resourceName}/{childResourceType}/{childResourceName}/providers/Microsoft.Bar/bars/{barName}/basses/drums/actionName/doSomething/doSomethingElse/andAnotherThing", + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{providerName}/{resourceType}/{resourceName}/{childResourceType}/{childResourceName}/providers/Microsoft.Bar/bars/{barName}", }, }, { title: "generic extension resource weird read path with default", path: "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{providerName}/{resourceType}/{resourceName}/{childResourceType}/{childResourceName}/providers/Microsoft.Bar/bars/{barName}/basses/default/actionName/doSomething/doSomethingElse/andAnotherThing", kind: "read", + singletonResourceName: "default", expected: { resourceType: { provider: "Microsoft.Bar", - types: ["bars", "basses", "actionName", "doSomethingElse"], + types: ["bars"], }, resourceInstancePath: - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{providerName}/{resourceType}/{resourceName}/{childResourceType}/{childResourceName}/providers/Microsoft.Bar/bars/{barName}/basses/default/actionName/doSomething/doSomethingElse/andAnotherThing", + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{providerName}/{resourceType}/{resourceName}/{childResourceType}/{childResourceName}/providers/Microsoft.Bar/bars/{barName}", }, }, { title: "handles paths with leading and trailing slashes", path: "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{providerName}/{resourceType}/{resourceName}/{childResourceType}/{childResourceName}/providers/Microsoft.Bar/bars/{barName}/basses/default/actionName/doSomething/doSomethingElse/andAnotherThing/", kind: "read", + singletonResourceName: "default", expected: { resourceType: { provider: "Microsoft.Bar", - types: ["bars", "basses", "actionName", "doSomethingElse"], + types: ["bars"], }, resourceInstancePath: - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{providerName}/{resourceType}/{resourceName}/{childResourceType}/{childResourceName}/providers/Microsoft.Bar/bars/{barName}/basses/default/actionName/doSomething/doSomethingElse/andAnotherThing", + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{providerName}/{resourceType}/{resourceName}/{childResourceType}/{childResourceName}/providers/Microsoft.Bar/bars/{barName}", }, }, { title: "handles paths without leading and trailing slashes", path: "subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{providerName}/{resourceType}/{resourceName}/{childResourceType}/{childResourceName}/providers/Microsoft.Bar/bars/{barName}/basses/default/actionName/doSomething/doSomethingElse/andAnotherThing", kind: "read", + singletonResourceName: "default", expected: { resourceType: { provider: "Microsoft.Bar", - types: ["bars", "basses", "actionName", "doSomethingElse"], + types: ["bars"], }, resourceInstancePath: - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{providerName}/{resourceType}/{resourceName}/{childResourceType}/{childResourceName}/providers/Microsoft.Bar/bars/{barName}/basses/default/actionName/doSomething/doSomethingElse/andAnotherThing", + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{providerName}/{resourceType}/{resourceName}/{childResourceType}/{childResourceName}/providers/Microsoft.Bar/bars/{barName}", }, }, { @@ -299,10 +303,10 @@ describe("unit tests for resource manager helpers", () => { expected: { resourceType: { provider: "Microsoft.Bar", - types: ["bars", "basses"], + types: ["bars"], }, resourceInstancePath: - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{providerName}/{resourceType}/{resourceName}/{childResourceType}/{childResourceName}/providers/Microsoft.Bar/bars/{barName}/basses/drums", + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{providerName}/{resourceType}/{resourceName}/{childResourceType}/{childResourceName}/providers/Microsoft.Bar/bars/{barName}", }, }, { @@ -312,16 +316,16 @@ describe("unit tests for resource manager helpers", () => { expected: { resourceType: { provider: "Microsoft.Bar", - types: ["bars", "basses"], + types: ["bars"], }, resourceInstancePath: - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{providerName}/{resourceType}/{resourceName}/{childResourceType}/{childResourceName}/providers/Microsoft.Bar/bars/{barName}/basses/drums", + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{providerName}/{resourceType}/{resourceName}/{childResourceType}/{childResourceName}/providers/Microsoft.Bar/bars/{barName}", }, }, ]; - for (const { title, path, kind, expected } of cases) { + for (const { title, path, kind, singletonResourceName, expected } of cases) { it(`parses path for ${title} operations correctly`, () => { - const result = getResourcePathElements(path, kind); + const result = getResourcePathElements(path, kind, singletonResourceName); expect(result).toEqual(expected); }); } diff --git a/packages/typespec-azure-resource-manager/test/resource.test.ts b/packages/typespec-azure-resource-manager/test/resource.test.ts index 145e503f79..eab35f9e24 100644 --- a/packages/typespec-azure-resource-manager/test/resource.test.ts +++ b/packages/typespec-azure-resource-manager/test/resource.test.ts @@ -244,7 +244,7 @@ describe("ARM resource model:", () => { model BarResourceProperties { iAmBar: string; - provisioningState: ResourceState; + provisioningState: ResourceState; } @singleton From f5f59106de2e350dccd7d447e8009ea5afd3f1a1 Mon Sep 17 00:00:00 2001 From: Dapeng Zhang Date: Fri, 5 Dec 2025 14:57:25 +0800 Subject: [PATCH 6/7] update changelog --- .../changes/introduce-tests-for-bug-2025-11-2-2-42-19.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .chronus/changes/introduce-tests-for-bug-2025-11-2-2-42-19.md diff --git a/.chronus/changes/introduce-tests-for-bug-2025-11-2-2-42-19.md b/.chronus/changes/introduce-tests-for-bug-2025-11-2-2-42-19.md new file mode 100644 index 0000000000..be8150d8e1 --- /dev/null +++ b/.chronus/changes/introduce-tests-for-bug-2025-11-2-2-42-19.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: fix +packages: + - "@azure-tools/typespec-azure-resource-manager" +--- + +Fix issues in `resolveArmResources` when we have singleton resource with a customized name From 155ce306bb781e8b46c2f29a2beec8c77bf42f61 Mon Sep 17 00:00:00 2001 From: Arcturus Zhang Date: Fri, 5 Dec 2025 15:21:41 +0800 Subject: [PATCH 7/7] run prettier --- .../src/resource.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/typespec-azure-resource-manager/src/resource.ts b/packages/typespec-azure-resource-manager/src/resource.ts index c17eb29d4e..6b3f5304ab 100644 --- a/packages/typespec-azure-resource-manager/src/resource.ts +++ b/packages/typespec-azure-resource-manager/src/resource.ts @@ -632,9 +632,13 @@ function isVariableSegment(segment: string, singletonResourceName?: string): boo function getResourceInfo( program: Program, operation: ArmResourceOperation, - singletonResourceName: string | undefined + singletonResourceName: string | undefined, ): ResolvedResourceInfo | undefined { - const pathInfo = getResourcePathElements(operation.httpOperation.path, operation.kind, singletonResourceName); + const pathInfo = getResourcePathElements( + operation.httpOperation.path, + operation.kind, + singletonResourceName, + ); if (pathInfo === undefined) return undefined; return { ...pathInfo, @@ -659,7 +663,10 @@ export function getResourcePathElements( } // if the next segment is the last segment - if (i + 1 === segments.length - 1 && isVariableSegment(segments[i + 1], singletonResourceName)) { + if ( + i + 1 === segments.length - 1 && + isVariableSegment(segments[i + 1], singletonResourceName) + ) { typeSegments.push(segments[i]); instanceSegments.push(segments[i]); instanceSegments.push(segments[i + 1]); @@ -873,7 +880,7 @@ export function resolveArmResourceOperations( if (armOperation === undefined) continue; armOperation.kind = operation.kind; - const singletonResourceName = getSingletonResourceKey(program, resourceType) + const singletonResourceName = getSingletonResourceKey(program, resourceType); armOperation.resourceModelName = operation.resource?.name ?? resourceType.name; const resourceInfo = getResourceInfo(program, armOperation, singletonResourceName); @@ -905,7 +912,8 @@ export function resolveArmResourceOperations( resourceType: resourceInfo.resourceType, resourceInstancePath: resourceInfo.resourceInstancePath, resourceName: resourceInfo.resourceName, - singletonResourceNames: singletonResourceName !== undefined ? [singletonResourceName] : undefined, + singletonResourceNames: + singletonResourceName !== undefined ? [singletonResourceName] : undefined, operations: { lifecycle: { read: undefined,