diff --git a/.chronus/changes/add-lro-metadata-spector-2026-10-15.md b/.chronus/changes/add-lro-metadata-spector-2026-10-15.md new file mode 100644 index 0000000000..3e56b5208c --- /dev/null +++ b/.chronus/changes/add-lro-metadata-spector-2026-10-15.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@azure-tools/azure-http-specs" +--- + +Add service-shaped Azure HTTP scenarios for LRO operation ID extraction and nested typed status metadata. diff --git a/packages/azure-http-specs/spec-summary.md b/packages/azure-http-specs/spec-summary.md index f3560ed8e1..a1436cb54d 100644 --- a/packages/azure-http-specs/spec-summary.md +++ b/packages/azure-http-specs/spec-summary.md @@ -1268,6 +1268,40 @@ Expected response body: } ``` +### Azure_ClientGenerator_Core_Lro_Metadata_analyze + +- Endpoint: `post /azure/client-generator-core/lro/metadata/documentModels/{modelId}:analyze` + +Declares a Document Intelligence-shaped flow for emitter tests of +`@lroOperationId` on the status-monitor operation ID path parameter. + +The service returns the complete opaque status-monitor URI, including its API-version query. +Polling must continue with that URI. Emitter-specific client tests can separately assert that +the server-issued ID `analysis-123` is exposed from the final non-empty path segment. + +Expected calls: + +- POST `/documentModels/document-1:analyze` returns 202 with + `Operation-Location: {endpoint}/documentModels/document-1/analyzeResults/analysis-123?api-version=2022-12-01-preview` +- GET that exact status-monitor URI returns a running status +- GET that exact status-monitor URI returns a succeeded status with the analysis result + +### Azure_ClientGenerator_Core_Lro_Metadata_analyzeWithUsage + +- Endpoint: `post /azure/client-generator-core/lro/metadata/analyzers/{analyzerId}:analyze` + +Declares a Content Understanding-shaped flow for emitter tests of property-level +`@lroStatusMetadata`. + +The optional `usage` value is a typed nested model with optional numeric fields and a token +dictionary. Emitter-specific client tests can assert the generated public type and runtime value. + +Expected calls: + +- POST `/analyzers/analyzer-1:analyze` returns 202 with a status-monitor URI +- GET the status-monitor URI returns a running status with nested `usage` +- GET the status-monitor URI returns a succeeded status with nested `usage` and the result + ### Azure_ClientGenerator_Core_NextLinkVerb_listItems - Endpoint: `post /azure/client-generator-core/next-link-verb/items` diff --git a/packages/azure-http-specs/specs/azure/client-generator-core/lro/metadata/main.tsp b/packages/azure-http-specs/specs/azure/client-generator-core/lro/metadata/main.tsp new file mode 100644 index 0000000000..49b4483afc --- /dev/null +++ b/packages/azure-http-specs/specs/azure/client-generator-core/lro/metadata/main.tsp @@ -0,0 +1,125 @@ +import "@azure-tools/typespec-azure-core"; +import "@azure-tools/typespec-client-generator-core"; +import "@typespec/spector"; +import "@typespec/versioning"; + +using Azure.ClientGenerator.Core; +using Azure.Core; +using Spector; +using TypeSpec.Http; +using TypeSpec.Versioning; + +@doc("Defines service-shaped HTTP flows for testing client LRO metadata.") +@scenarioService( + "/azure/client-generator-core/lro/metadata", + { + versioned: Versions, + } +) +@global.Azure.ClientGenerator.Core.clientNamespace( + "azure.clientgenerator.core.lro.metadata", + "java" +) +@global.Azure.ClientGenerator.Core.clientNamespace( + "specs.azure.clientgenerator.core.lro.metadata", + "python" +) +namespace _Specs_.Azure.ClientGenerator.Core.Lro.Metadata; + +enum Versions { + v2022_12_01_preview: "2022-12-01-preview", +} + +model DocumentAnalysisStatus { + id: string; + status: global.Azure.Core.Foundations.OperationState; + result?: DocumentAnalysisResult; +} + +model DocumentAnalysisResult { + summary: string; +} + +@route("/documentModels/{modelId}/analyzeResults/{resultId}") +@get +op getDocumentAnalysisStatus( + @path modelId: string, + + @lroOperationId + @path + resultId: string, + + ...global.Azure.Core.Foundations.ApiVersionParameter, +): DocumentAnalysisStatus; + +@scenario +@scenarioDoc(""" + Declares a Document Intelligence-shaped flow for emitter tests of + `@lroOperationId` on the status-monitor operation ID path parameter. + + The service returns the complete opaque status-monitor URI, including its API-version query. + Polling must continue with that URI. Emitter-specific client tests can separately assert that + the server-issued ID `analysis-123` is exposed from the final non-empty path segment. + + Expected calls: + - POST `/documentModels/document-1:analyze` returns 202 with + `Operation-Location: {endpoint}/documentModels/document-1/analyzeResults/analysis-123?api-version=2022-12-01-preview` + - GET that exact status-monitor URI returns a running status + - GET that exact status-monitor URI returns a succeeded status with the analysis result + """) +@route("/documentModels/{modelId}:analyze") +@post +@pollingOperation(getDocumentAnalysisStatus) +op analyze( + @path modelId: string, + ...global.Azure.Core.Foundations.ApiVersionParameter, +): global.Azure.Core.Foundations.AcceptedResponse & + global.Azure.Core.Foundations.LongRunningStatusLocation; + +model UsageDetails { + documentPagesBasic?: int32; + tokens?: Record; +} + +model ContentAnalysisStatus { + id: string; + status: global.Azure.Core.Foundations.OperationState; + + @lroStatusMetadata + usage?: UsageDetails; + + result?: ContentAnalysisResult; +} + +model ContentAnalysisResult { + contents: string; +} + +@route("/analyzerResults/{operationId}") +@get +op getContentAnalysisStatus( + @path operationId: string, + ...global.Azure.Core.Foundations.ApiVersionParameter, +): ContentAnalysisStatus; + +@scenario +@scenarioDoc(""" + Declares a Content Understanding-shaped flow for emitter tests of property-level + `@lroStatusMetadata`. + + The optional `usage` value is a typed nested model with optional numeric fields and a token + dictionary. Emitter-specific client tests can assert the generated public type and runtime value. + + Expected calls: + - POST `/analyzers/analyzer-1:analyze` returns 202 with a status-monitor URI + - GET the status-monitor URI returns a running status with nested `usage` + - GET the status-monitor URI returns a succeeded status with nested `usage` and the result + """) +@route("/analyzers/{analyzerId}:analyze") +@post +@pollingOperation(getContentAnalysisStatus) +op analyzeWithUsage( + @path analyzerId: string, + ...global.Azure.Core.Foundations.ApiVersionParameter, +): global.Azure.Core.Foundations.AcceptedResponse & + global.Azure.Core.Foundations.LongRunningStatusLocation; diff --git a/packages/azure-http-specs/specs/azure/client-generator-core/lro/metadata/mockapi.ts b/packages/azure-http-specs/specs/azure/client-generator-core/lro/metadata/mockapi.ts new file mode 100644 index 0000000000..a43b4aaefa --- /dev/null +++ b/packages/azure-http-specs/specs/azure/client-generator-core/lro/metadata/mockapi.ts @@ -0,0 +1,214 @@ +import { + dyn, + dynItem, + json, + MockRequest, + passOnSuccess, + type ScenarioMockApi, +} from "@typespec/spec-api"; + +export const Scenarios: Record = {}; + +const apiVersion = "2022-12-01-preview"; +let analyzePollCount = 0; +let analyzeWithUsagePollCount = 0; + +Scenarios.Azure_ClientGenerator_Core_Lro_Metadata_analyze = passOnSuccess([ + { + uri: "/azure/client-generator-core/lro/metadata/documentModels/document-1:analyze", + method: "post", + request: { + query: { + "api-version": apiVersion, + }, + }, + response: { + status: 202, + headers: { + "operation-location": dyn`${dynItem("baseUrl")}/azure/client-generator-core/lro/metadata/documentModels/document-1/analyzeResults/analysis-123?api-version=2022-12-01-preview`, + }, + }, + handler: (req: MockRequest) => { + req.expect.containsQueryParam("api-version", apiVersion); + analyzePollCount = 0; + return { + status: 202, + headers: { + "operation-location": `${req.baseUrl}/azure/client-generator-core/lro/metadata/documentModels/document-1/analyzeResults/analysis-123?api-version=${apiVersion}`, + }, + }; + }, + kind: "MockApiDefinition", + }, + { + uri: "/azure/client-generator-core/lro/metadata/documentModels/document-1/analyzeResults/analysis-123", + method: "get", + request: { + query: { + "api-version": apiVersion, + }, + }, + response: { + status: 200, + body: json({ + id: "analysis-123", + status: "Running", + }), + }, + handler: analyzeLroHandler, + kind: "MockApiDefinition", + }, + { + uri: "/azure/client-generator-core/lro/metadata/documentModels/document-1/analyzeResults/analysis-123", + method: "get", + request: { + query: { + "api-version": apiVersion, + }, + }, + response: { + status: 200, + body: json({ + id: "analysis-123", + status: "Succeeded", + result: { + summary: "The document contains three sections.", + }, + }), + }, + handler: analyzeLroHandler, + kind: "MockApiDefinition", + }, +]); + +Scenarios.Azure_ClientGenerator_Core_Lro_Metadata_analyzeWithUsage = passOnSuccess([ + { + uri: "/azure/client-generator-core/lro/metadata/analyzers/analyzer-1:analyze", + method: "post", + request: { + query: { + "api-version": apiVersion, + }, + }, + response: { + status: 202, + headers: { + "operation-location": dyn`${dynItem("baseUrl")}/azure/client-generator-core/lro/metadata/analyzerResults/usage-123?api-version=2022-12-01-preview`, + }, + }, + handler: (req: MockRequest) => { + req.expect.containsQueryParam("api-version", apiVersion); + analyzeWithUsagePollCount = 0; + return { + status: 202, + headers: { + "operation-location": `${req.baseUrl}/azure/client-generator-core/lro/metadata/analyzerResults/usage-123?api-version=${apiVersion}`, + }, + }; + }, + kind: "MockApiDefinition", + }, + { + uri: "/azure/client-generator-core/lro/metadata/analyzerResults/usage-123", + method: "get", + request: { + query: { + "api-version": apiVersion, + }, + }, + response: { + status: 200, + body: json({ + id: "usage-123", + status: "Running", + usage: { + documentPagesBasic: 1, + tokens: { + input: 10, + }, + }, + }), + }, + handler: analyzeWithUsageLroHandler, + kind: "MockApiDefinition", + }, + { + uri: "/azure/client-generator-core/lro/metadata/analyzerResults/usage-123", + method: "get", + request: { + query: { + "api-version": apiVersion, + }, + }, + response: { + status: 200, + body: json({ + id: "usage-123", + status: "Succeeded", + usage: { + documentPagesBasic: 1, + tokens: { + input: 10, + output: 4, + }, + }, + result: { + contents: "The document contains three sections.", + }, + }), + }, + handler: analyzeWithUsageLroHandler, + kind: "MockApiDefinition", + }, +]); + +function analyzeLroHandler(req: MockRequest) { + req.expect.containsQueryParam("api-version", apiVersion); + const response = + analyzePollCount > 0 + ? { + id: "analysis-123", + status: "Succeeded", + result: { + summary: "The document contains three sections.", + }, + } + : { + id: "analysis-123", + status: "Running", + }; + analyzePollCount += 1; + return { status: 200, body: json(response) }; +} + +function analyzeWithUsageLroHandler(req: MockRequest) { + req.expect.containsQueryParam("api-version", apiVersion); + const response = + analyzeWithUsagePollCount > 0 + ? { + id: "usage-123", + status: "Succeeded", + usage: { + documentPagesBasic: 1, + tokens: { + input: 10, + output: 4, + }, + }, + result: { + contents: "The document contains three sections.", + }, + } + : { + id: "usage-123", + status: "Running", + usage: { + documentPagesBasic: 1, + tokens: { + input: 10, + }, + }, + }; + analyzeWithUsagePollCount += 1; + return { status: 200, body: json(response) }; +}