diff --git a/packages/spec-parser/src/interfaces.ts b/packages/spec-parser/src/interfaces.ts index ed6f8e81eb..7c489b0665 100644 --- a/packages/spec-parser/src/interfaces.ts +++ b/packages/spec-parser/src/interfaces.ts @@ -161,17 +161,19 @@ export interface ImageElement { $when: string; } +export type AdaptiveCardBody = Array; + export interface ArrayElement { type: string; $data: string; - items: Array; + items: AdaptiveCardBody; } export interface AdaptiveCard { type: string; $schema: string; version: string; - body: Array; + body: AdaptiveCardBody; } export interface PreviewCardTemplate { diff --git a/packages/spec-parser/src/manifestUpdater.ts b/packages/spec-parser/src/manifestUpdater.ts index 85d4f5529b..d040fbb20d 100644 --- a/packages/spec-parser/src/manifestUpdater.ts +++ b/packages/spec-parser/src/manifestUpdater.ts @@ -186,7 +186,7 @@ export class ManifestUpdater { const [card, jsonPath] = AdaptiveCardGenerator.generateAdaptiveCard(operationItem); - card.body = card.body.slice(0, 5); + card.body = Utils.limitACBodyProperties(card.body, 5); const responseSemantic = wrapResponseSemantics(card, jsonPath); funcObj.capabilities = { response_semantics: responseSemantic, diff --git a/packages/spec-parser/src/utils.ts b/packages/spec-parser/src/utils.ts index f5b4516525..c6062aa3d1 100644 --- a/packages/spec-parser/src/utils.ts +++ b/packages/spec-parser/src/utils.ts @@ -4,7 +4,15 @@ import { OpenAPIV3 } from "openapi-types"; import { ConstantString } from "./constants"; -import { AuthInfo, AuthType, ErrorResult, ErrorType, ParseOptions } from "./interfaces"; +import { + AdaptiveCardBody, + ArrayElement, + AuthInfo, + AuthType, + ErrorResult, + ErrorType, + ParseOptions, +} from "./interfaces"; import { IMessagingExtensionCommand, IParameter } from "@microsoft/teams-manifest"; import { SpecParserError } from "./specParserError"; @@ -451,4 +459,33 @@ export class Utils { return serverUrl; } + + static limitACBodyProperties(body: AdaptiveCardBody, maxCount: number): AdaptiveCardBody { + const result: AdaptiveCardBody = []; + let currentCount = 0; + + for (const element of body) { + if (element.type === ConstantString.ContainerType) { + const items = this.limitACBodyProperties( + (element as ArrayElement).items, + maxCount - currentCount + ); + + result.push({ + type: ConstantString.ContainerType, + $data: (element as ArrayElement).$data, + items: items, + }); + + currentCount += items.length; + } else { + if (currentCount < maxCount) { + result.push(element); + currentCount++; + } + } + } + + return result; + } } diff --git a/packages/spec-parser/test/manifestUpdater.test.ts b/packages/spec-parser/test/manifestUpdater.test.ts index 9ea453fd35..ebee89d41f 100644 --- a/packages/spec-parser/test/manifestUpdater.test.ts +++ b/packages/spec-parser/test/manifestUpdater.test.ts @@ -590,6 +590,208 @@ describe("updateManifestWithAiPlugin", () => { expect(apiPlugin).to.deep.equal(expectedPlugins); expect(warnings).to.deep.equal([]); }); + + it("should keep at most 5 properties in response semantics for complex nested properties", async () => { + const spec: any = { + openapi: "3.0.2", + info: { + title: "My API", + description: "My API description", + }, + servers: [ + { + url: "/v3", + }, + ], + paths: { + "/pets": { + get: { + operationId: "getPets", + summary: "Get all pets", + description: "Returns all pets from the system that the user has access to", + parameters: [ + { + name: "limit", + description: "Maximum number of pets to return", + required: true, + schema: { + type: "integer", + }, + }, + ], + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { + type: "string", + }, + description: { + type: "array", + items: { + type: "object", + properties: { + title: { + type: "array", + items: { + type: "string", + }, + }, + url: { + type: "string", + }, + }, + }, + }, + imageUrl: { + type: "string", + }, + id: { + type: "string", + }, + age: { + type: "string", + }, + status: { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + const manifestPath = "/path/to/your/manifest.json"; + const outputSpecPath = "/path/to/your/spec/outputSpec.yaml"; + const pluginFilePath = "/path/to/your/ai-plugin.json"; + + const originalManifest = { + name: { short: "Original Name", full: "Original Full Name" }, + description: { short: "Original Short Description", full: "Original Full Description" }, + }; + const expectedManifest = { + name: { short: "Original Name", full: "Original Full Name" }, + description: { short: "My API", full: "My API description" }, + copilotExtensions: { + plugins: [ + { + file: "ai-plugin.json", + id: "plugin_1", + }, + ], + }, + }; + + const expectedPlugins: PluginManifestSchema = { + $schema: ConstantString.PluginManifestSchema, + schema_version: "v2.1", + name_for_human: "Original Name", + namespace: "originalname", + description_for_human: "My API description", + functions: [ + { + name: "getPets", + description: "Returns all pets from the system that the user has access to", + capabilities: { + response_semantics: { + data_path: "$", + properties: { + subtitle: "$.id", + title: "$.name", + url: "$.imageUrl", + }, + static_template: { + $schema: "http://adaptivecards.io/schemas/adaptive-card.json", + body: [ + { + text: "name: ${if(name, name, 'N/A')}", + type: "TextBlock", + wrap: true, + }, + { + $data: "${description}", + items: [ + { + $data: "${title}", + items: [ + { + text: "title: ${$data}", + type: "TextBlock", + wrap: true, + }, + ], + type: "Container", + }, + { + text: "description.url: ${if(url, url, 'N/A')}", + type: "TextBlock", + wrap: true, + }, + ], + type: "Container", + }, + { + $when: "${imageUrl != null}", + type: "Image", + url: "${imageUrl}", + }, + { + text: "id: ${if(id, id, 'N/A')}", + type: "TextBlock", + wrap: true, + }, + ], + type: "AdaptiveCard", + version: "1.5", + }, + }, + }, + }, + ], + runtimes: [ + { + type: "OpenApi", + auth: { + type: "None", + }, + spec: { + url: "spec/outputSpec.yaml", + }, + run_for_functions: ["getPets"], + }, + ], + }; + sinon.stub(fs, "readJSON").resolves(originalManifest); + sinon + .stub(fs, "pathExists") + .withArgs(manifestPath) + .resolves(true) + .withArgs(pluginFilePath) + .resolves(false); + + const options: ParseOptions = { + allowMethods: ["get", "post"], + allowResponseSemantics: true, + }; + const [manifest, apiPlugin, warnings] = await ManifestUpdater.updateManifestWithAiPlugin( + manifestPath, + outputSpecPath, + pluginFilePath, + spec, + options + ); + + expect(manifest).to.deep.equal(expectedManifest); + expect(apiPlugin).to.deep.equal(expectedPlugins); + expect(warnings).to.deep.equal([]); + }); }); describe("auth", () => {