Skip to content

Commit

Permalink
perf(spec-parser): add jsonPath support (#10030)
Browse files Browse the repository at this point in the history
Co-authored-by: turenlong <[email protected]>
  • Loading branch information
SLdragon and SLdragon authored Sep 25, 2023
1 parent 31b4a9d commit 5e7ed96
Show file tree
Hide file tree
Showing 8 changed files with 171 additions and 11 deletions.
31 changes: 27 additions & 4 deletions packages/fx-core/src/common/spec-parser/adaptiveCardGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,27 @@

import { OpenAPIV3 } from "openapi-types";
import * as util from "util";
import { getResponseJson } from "./utils";
import { getResponseJson, isWellknownResultPropertyName } from "./utils";
import { AdaptiveCard, ArrayElement, ErrorType, TextBlockElement } from "./interfaces";
import { ConstantString } from "./constants";
import { SpecParserError } from "./specParserError";

export function generateAdaptiveCard(operationItem: OpenAPIV3.OperationObject): AdaptiveCard {
export function generateAdaptiveCard(
operationItem: OpenAPIV3.OperationObject
): [AdaptiveCard, string] {
try {
const json = getResponseJson(operationItem);

let cardBody: Array<TextBlockElement | ArrayElement> = [];

const schema = json.schema as OpenAPIV3.SchemaObject;
let schema = json.schema as OpenAPIV3.SchemaObject;
let jsonPath = "$";
if (schema && Object.keys(schema).length > 0) {
jsonPath = getResponseJsonPathFromSchema(schema);
if (jsonPath !== "$") {
schema = schema.properties![jsonPath] as OpenAPIV3.SchemaObject;
}

cardBody = generateCardFromResponse(schema, "");
}

Expand Down Expand Up @@ -49,7 +57,7 @@ export function generateAdaptiveCard(operationItem: OpenAPIV3.OperationObject):
body: cardBody,
};

return fullCard;
return [fullCard, jsonPath];
} catch (err) {
throw new SpecParserError((err as Error).toString(), ErrorType.GenerateAdaptiveCardFailed);
}
Expand Down Expand Up @@ -137,3 +145,18 @@ export function generateCardFromResponse(

throw new Error(util.format(ConstantString.UnknownSchema, JSON.stringify(schema)));
}

// Find the first array property in the response schema object with the well-known name
export function getResponseJsonPathFromSchema(schema: OpenAPIV3.SchemaObject): string {
if (schema.type === "object" || (!schema.type && schema.properties)) {
const { properties } = schema;
for (const property in properties) {
const schema = properties[property] as OpenAPIV3.SchemaObject;
if (schema.type === "array" && isWellknownResultPropertyName(property)) {
return property;
}
}
}

return "$";
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import { AdaptiveCard, WrappedAdaptiveCard } from "./interfaces";

export function wrapAdaptiveCard(
card: AdaptiveCard,
jsonPath: string,
title: string,
subtitle: string
): WrappedAdaptiveCard {
const result: WrappedAdaptiveCard = {
version: ConstantString.WrappedCardVersion,
$schema: ConstantString.WrappedCardSchema,
jsonPath: jsonPath,
responseLayout: ConstantString.WrappedCardResponseLayout,
responseCardTemplate: card,
previewCardTemplate: {
Expand Down
12 changes: 12 additions & 0 deletions packages/fx-core/src/common/spec-parser/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,16 @@ export class ConstantString {
"options",
"trace",
];

// TODO: update after investigating the usage of these constants.
static readonly WellknownResultNames = [
"result",
"data",
"items",
"root",
"matches",
"queries",
"list",
"output",
];
}
3 changes: 2 additions & 1 deletion packages/fx-core/src/common/spec-parser/specParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,10 +227,11 @@ export class SpecParser {
if (method === ConstantString.GetMethod || method === ConstantString.PostMethod) {
const operation = newSpec.paths[url]![method] as OpenAPIV3.OperationObject;
try {
const card: AdaptiveCard = generateAdaptiveCard(operation);
const [card, jsonPath] = generateAdaptiveCard(operation);
const fileName = path.join(adaptiveCardFolder, `${operation.operationId!}.json`);
const wrappedCard = wrapAdaptiveCard(
card,
jsonPath,
operation.operationId!,
method.toUpperCase() + " " + url
);
Expand Down
9 changes: 9 additions & 0 deletions packages/fx-core/src/common/spec-parser/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,3 +340,12 @@ export function validateServer(spec: OpenAPIV3.Document): ErrorResult[] {
}
return errors;
}

export function isWellknownResultPropertyName(name: string): boolean {
for (let i = 0; i < ConstantString.WellknownResultNames.length; i++) {
if (name.toLowerCase().includes(ConstantString.WellknownResultNames[i])) {
return true;
}
}
return false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,74 @@ describe("adaptiveCardGenerator", () => {
],
};

const actual = generateAdaptiveCard(operationItem);
const [actual, jsonPath] = generateAdaptiveCard(operationItem);

expect(actual).to.deep.equal(expected);
expect(jsonPath).to.equal("$");
});

it("should generate a card from a object schema with well known array property", () => {
const operationItem = {
responses: {
"200": {
description: "OK",
content: {
"application/json": {
schema: {
type: "object",
properties: {
id: {
type: "string",
},
result: {
type: "array",
items: {
type: "object",
properties: {
name: {
type: "string",
},
age: {
type: "number",
},
},
},
},
},
},
},
},
},
},
} as any;
const expected = {
type: "AdaptiveCard",
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
version: "1.5",
body: [
{
type: "Container",
$data: "${$root}",
items: [
{
type: "TextBlock",
text: "name: ${if(name, name, 'N/A')}",
wrap: true,
},
{
type: "TextBlock",
text: "age: ${if(age, age, 'N/A')}",
wrap: true,
},
],
},
],
};

const [actual, jsonPath] = generateAdaptiveCard(operationItem);

expect(actual).to.deep.equal(expected);
expect(jsonPath).to.equal("result");
});

it("should generate a card from an example value", () => {
Expand Down Expand Up @@ -91,9 +156,10 @@ describe("adaptiveCardGenerator", () => {
],
};

const actual = generateAdaptiveCard(operationItem);
const [actual, jsonPath] = generateAdaptiveCard(operationItem);

expect(actual).to.deep.equal(expected);
expect(jsonPath).to.equal("$");
});

it("should generate a card from a default success response", () => {
Expand All @@ -117,9 +183,10 @@ describe("adaptiveCardGenerator", () => {
],
};

const actual = generateAdaptiveCard(operationItem);
const [actual, jsonPath] = generateAdaptiveCard(operationItem);

expect(actual).to.deep.equal(expected);
expect(jsonPath).to.equal("$");
});

it("should generate a card if no json response", () => {
Expand All @@ -146,9 +213,10 @@ describe("adaptiveCardGenerator", () => {
],
};

const actual = generateAdaptiveCard(operationItem);
const [actual, jsonPath] = generateAdaptiveCard(operationItem);

expect(actual).to.deep.equal(expected);
expect(jsonPath).to.equal("$");
});
});

Expand All @@ -167,9 +235,10 @@ describe("adaptiveCardGenerator", () => {
],
};

const actual = generateAdaptiveCard(schema);
const [actual, jsonPath] = generateAdaptiveCard(schema);

expect(actual).to.deep.equal(expected);
expect(jsonPath).to.equal("$");
});

describe("generateCardFromResponse", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ describe("adaptiveCardWrapper", () => {
const expectedWrappedCard = {
version: ConstantString.WrappedCardVersion,
$schema: ConstantString.WrappedCardSchema,
jsonPath: "$",
responseLayout: ConstantString.WrappedCardResponseLayout,
responseCardTemplate: card,
previewCardTemplate: {
Expand All @@ -45,7 +46,7 @@ describe("adaptiveCardWrapper", () => {
},
};

const wrappedCard = wrapAdaptiveCard(card, "title", "subtitle");
const wrappedCard = wrapAdaptiveCard(card, "$", "title", "subtitle");

expect(util.isDeepStrictEqual(wrappedCard, expectedWrappedCard)).to.be.true;
});
Expand Down
43 changes: 43 additions & 0 deletions packages/fx-core/tests/common/spec-parser/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
updateFirstLetter,
validateServer,
resolveServerUrl,
isWellknownResultPropertyName,
} from "../../../src/common/spec-parser/utils";
import { OpenAPIV3 } from "openapi-types";
import { ConstantString } from "../../../src/common/spec-parser/constants";
Expand Down Expand Up @@ -1446,4 +1447,46 @@ describe("utils", () => {
);
});
});

describe("isWellknownResultPropertyName", () => {
it("should return true for well-known result property names", () => {
expect(isWellknownResultPropertyName("result")).to.be.true;
expect(isWellknownResultPropertyName("data")).to.be.true;
expect(isWellknownResultPropertyName("items")).to.be.true;
expect(isWellknownResultPropertyName("root")).to.be.true;
expect(isWellknownResultPropertyName("matches")).to.be.true;
expect(isWellknownResultPropertyName("queries")).to.be.true;
expect(isWellknownResultPropertyName("list")).to.be.true;
expect(isWellknownResultPropertyName("output")).to.be.true;
});

it("should return true for well-known result property names with different casing", () => {
expect(isWellknownResultPropertyName("Result")).to.be.true;
expect(isWellknownResultPropertyName("DaTa")).to.be.true;
expect(isWellknownResultPropertyName("ITEMS")).to.be.true;
expect(isWellknownResultPropertyName("Root")).to.be.true;
expect(isWellknownResultPropertyName("MaTcHeS")).to.be.true;
expect(isWellknownResultPropertyName("QuErIeS")).to.be.true;
expect(isWellknownResultPropertyName("LiSt")).to.be.true;
expect(isWellknownResultPropertyName("OutPut")).to.be.true;
});

it("should return true for name substring is well-known result property names", () => {
expect(isWellknownResultPropertyName("testResult")).to.be.true;
expect(isWellknownResultPropertyName("carData")).to.be.true;
expect(isWellknownResultPropertyName("productItems")).to.be.true;
expect(isWellknownResultPropertyName("rootValue")).to.be.true;
expect(isWellknownResultPropertyName("matchesResult")).to.be.true;
expect(isWellknownResultPropertyName("DataQueries")).to.be.true;
expect(isWellknownResultPropertyName("productLists")).to.be.true;
expect(isWellknownResultPropertyName("outputData")).to.be.true;
});

it("should return false for non well-known result property names", () => {
expect(isWellknownResultPropertyName("foo")).to.be.false;
expect(isWellknownResultPropertyName("bar")).to.be.false;
expect(isWellknownResultPropertyName("baz")).to.be.false;
expect(isWellknownResultPropertyName("qux")).to.be.false;
});
});
});

0 comments on commit 5e7ed96

Please sign in to comment.