Skip to content

Commit

Permalink
perf(spec-parser): add browser api, truncate string (#10196)
Browse files Browse the repository at this point in the history
* perf(spec-parser): add browser api, truncate string

* fix: commit missing class file

* fix: commit missing file

* fix: add test cases to make codecov happy

---------

Co-authored-by: turenlong <[email protected]>
  • Loading branch information
SLdragon and SLdragon authored Oct 24, 2023
1 parent 3a10f1c commit 041c93f
Show file tree
Hide file tree
Showing 12 changed files with 1,155 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
"use strict";

import { OpenAPIV3 } from "openapi-types";
import * as util from "util";
import { getResponseJson, isWellKnownName } from "./utils";
import { getResponseJson, isWellKnownName, format } from "./utils";
import {
AdaptiveCard,
ArrayElement,
Expand Down Expand Up @@ -166,10 +165,10 @@ export function generateCardFromResponse(
}

if (schema.oneOf || schema.anyOf || schema.not || schema.allOf) {
throw new Error(util.format(ConstantString.SchemaNotSupported, JSON.stringify(schema)));
throw new Error(format(ConstantString.SchemaNotSupported, JSON.stringify(schema)));
}

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

// Find the first array property in the response schema object with the well-known name
Expand Down
7 changes: 7 additions & 0 deletions packages/fx-core/src/common/spec-parser/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,11 @@ export class ConstantString {
"thumbnail",
"img",
];

static readonly ShortDescriptionMaxLens = 80;
static readonly FullDescriptionMaxLens = 4000;
static readonly CommandDescriptionMaxLens = 128;
static readonly ParameterDescriptionMaxLens = 128;
static readonly CommandTitleMaxLens = 32;
static readonly ParameterTitleMaxLens = 32;
}
8 changes: 8 additions & 0 deletions packages/fx-core/src/common/spec-parser/index.browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
"use strict";

export { SpecParser } from "./specParser.browser";
export { SpecParserError } from "./specParserError";
export { ValidationStatus, WarningType, ErrorType, WarningResult } from "./interfaces";
export { ConstantString } from "./constants";
2 changes: 2 additions & 0 deletions packages/fx-core/src/common/spec-parser/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export enum ErrorType {

ListFailed = "list-failed",
ListOperationMapFailed = "list-operation-map-failed",
listSupportedAPIInfoFailed = "list-supported-api-info-failed",
FilterSpecFailed = "filter-spec-failed",
UpdateManifestFailed = "update-manifest-failed",
GenerateAdaptiveCardFailed = "generate-adaptive-card-failed",
Expand Down Expand Up @@ -181,4 +182,5 @@ export interface APIInfo {
id: string;
parameters: Parameter[];
description: string;
warning?: WarningResult;
}
14 changes: 11 additions & 3 deletions packages/fx-core/src/common/spec-parser/manifestUpdater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { OpenAPIV3 } from "openapi-types";
import fs from "fs-extra";
import path from "path";
import { ErrorType, WarningResult } from "./interfaces";
import { getRelativePath, parseApiInfo } from "./utils";
import { parseApiInfo } from "./utils";
import { SpecParserError } from "./specParserError";
import { ConstantString } from "./constants";
import {
Expand All @@ -33,8 +33,11 @@ export async function updateManifest(

const updatedPart = {
description: {
short: spec.info.title,
full: spec.info.description ?? originalManifest.description.full,
short: spec.info.title.slice(0, ConstantString.ShortDescriptionMaxLens),
full: (spec.info.description ?? originalManifest.description.full)?.slice(
0,
ConstantString.FullDescriptionMaxLens
),
},
composeExtensions: [ComposeExtension],
};
Expand Down Expand Up @@ -87,3 +90,8 @@ export async function generateCommands(

return [commands, warnings];
}

export function getRelativePath(from: string, to: string): string {
const relativePath = path.relative(path.dirname(from), to);
return path.normalize(relativePath).replace(/\\/g, "/");
}
184 changes: 184 additions & 0 deletions packages/fx-core/src/common/spec-parser/specParser.browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
"use strict";

import SwaggerParser from "@apidevtools/swagger-parser";
import { OpenAPIV3 } from "openapi-types";
import {
APIInfo,
ErrorType,
GenerateResult,
ParseOptions,
ValidateResult,
ValidationStatus,
Parameter,
} from "./interfaces";
import { SpecParserError } from "./specParserError";
import { listSupportedAPIs, parseApiInfo, validateSpec } from "./utils";
import { ConstantString } from "./constants";

/**
* A class that parses an OpenAPI specification file and provides methods to validate, list, and generate artifacts.
*/
export class SpecParser {
public readonly pathOrSpec: string | OpenAPIV3.Document;
public readonly parser: SwaggerParser;
public readonly options: ParseOptions;

private apiMap: { [key: string]: OpenAPIV3.PathItemObject } | undefined;
private spec: OpenAPIV3.Document | undefined;
private unResolveSpec: OpenAPIV3.Document | undefined;
private isSwaggerFile: boolean | undefined;

private defaultOptions: ParseOptions = {
allowMissingId: false,
allowSwagger: false,
};

/**
* Creates a new instance of the SpecParser class.
* @param pathOrDoc The path to the OpenAPI specification file or the OpenAPI specification object.
* @param options The options for parsing the OpenAPI specification file.
*/
constructor(pathOrDoc: string | OpenAPIV3.Document, options?: ParseOptions) {
this.pathOrSpec = pathOrDoc;
this.parser = new SwaggerParser();
this.options = {
...this.defaultOptions,
...(options ?? {}),
};
}

/**
* Validates the OpenAPI specification file and returns a validation result.
*
* @returns A validation result object that contains information about any errors or warnings in the specification file.
*/
async validate(): Promise<ValidateResult> {
try {
try {
await this.loadSpec();
await this.parser.validate(this.spec!);
} catch (e) {
return {
status: ValidationStatus.Error,
warnings: [],
errors: [{ type: ErrorType.SpecNotValid, content: (e as Error).toString() }],
};
}

if (!this.options.allowSwagger && this.isSwaggerFile) {
return {
status: ValidationStatus.Error,
warnings: [],
errors: [
{ type: ErrorType.SwaggerNotSupported, content: ConstantString.SwaggerNotSupported },
],
};
}

return validateSpec(this.spec!, this.parser, !!this.isSwaggerFile);
} catch (err) {
throw new SpecParserError((err as Error).toString(), ErrorType.ValidateFailed);
}
}

async listSupportedAPIInfo(): Promise<APIInfo[]> {
try {
await this.loadSpec();
const apiMap = this.getAllSupportedAPIs(this.spec!);
const apiInfos: APIInfo[] = [];
for (const key in apiMap) {
const pathObjectItem = apiMap[key];
const [method, path] = key.split(" ");
const operationId = pathObjectItem.operationId;

// In Browser environment, this api is by default not support api without operationId
if (!operationId) {
continue;
}

const [command, warning] = parseApiInfo(pathObjectItem);

const apiInfo: APIInfo = {
method: method,
path: path,
title: command.title,
id: operationId,
parameters: command.parameters! as Parameter[],
description: command.description!,
};

if (warning) {
apiInfo.warning = warning;
}

apiInfos.push(apiInfo);
}

return apiInfos;
} catch (err) {
throw new SpecParserError((err as Error).toString(), ErrorType.listSupportedAPIInfoFailed);
}
}

/**
* Lists all the OpenAPI operations in the specification file.
* @returns A string array that represents the HTTP method and path of each operation, such as ['GET /pets/{petId}', 'GET /user/{userId}']
* according to copilot plugin spec, only list get and post method without auth
*/
// eslint-disable-next-line @typescript-eslint/require-await
async list(): Promise<string[]> {
throw new Error("Method not implemented.");
}

/**
* List all the OpenAPI operations in the specification file and return a map of operationId and operation path.
* @returns A map of operationId and operation path, such as [{'getPetById': 'GET /pets/{petId}'}, {'getUser': 'GET /user/{userId}'}]
*/
// eslint-disable-next-line @typescript-eslint/require-await
async listOperationMap(): Promise<Map<string, string>> {
throw new Error("Method not implemented.");
}

/**
* Generates and update artifacts from the OpenAPI specification file. Generate Adaptive Cards, update Teams app manifest, and generate a new OpenAPI specification file.
* @param manifestPath A file path of the Teams app manifest file to update.
* @param filter An array of strings that represent the filters to apply when generating the artifacts. If filter is empty, it would process nothing.
* @param outputSpecPath File path of the new OpenAPI specification file to generate. If not specified or empty, no spec file will be generated.
* @param adaptiveCardFolder Folder path where the Adaptive Card files will be generated. If not specified or empty, Adaptive Card files will not be generated.
*/
// eslint-disable-next-line @typescript-eslint/require-await
async generate(
manifestPath: string,
filter: string[],
outputSpecPath: string,
adaptiveCardFolder: string,
signal?: AbortSignal
): Promise<GenerateResult> {
throw new Error("Method not implemented.");
}

private async loadSpec(): Promise<void> {
if (!this.spec) {
this.unResolveSpec = (await this.parser.parse(this.pathOrSpec)) as OpenAPIV3.Document;
if (!this.unResolveSpec.openapi && (this.unResolveSpec as any).swagger === "2.0") {
this.isSwaggerFile = true;
}

const clonedUnResolveSpec = JSON.parse(JSON.stringify(this.unResolveSpec));
this.spec = (await this.parser.dereference(clonedUnResolveSpec)) as OpenAPIV3.Document;
}
}

private getAllSupportedAPIs(spec: OpenAPIV3.Document): {
[key: string]: OpenAPIV3.OperationObject;
} {
if (this.apiMap !== undefined) {
return this.apiMap;
}
const result = listSupportedAPIs(spec, this.options.allowMissingId!);
this.apiMap = result;
return result;
}
}
30 changes: 17 additions & 13 deletions packages/fx-core/src/common/spec-parser/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

import { OpenAPIV3 } from "openapi-types";
import SwaggerParser from "@apidevtools/swagger-parser";
import path from "path";
import { format } from "util";
import { ConstantString } from "./constants";
import {
CheckParamResult,
Expand Down Expand Up @@ -199,11 +197,6 @@ export function updateFirstLetter(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}

export function getRelativePath(from: string, to: string): string {
const relativePath = path.relative(path.dirname(from), to);
return path.normalize(relativePath).replace(/\\/g, "/");
}

export function getResponseJson(
operationObject: OpenAPIV3.OperationObject | undefined
): OpenAPIV3.MediaTypeObject {
Expand Down Expand Up @@ -363,8 +356,8 @@ export function generateParametersFromSchema(
) {
const parameter = {
name: name,
title: updateFirstLetter(name),
description: schema.description ?? "",
title: updateFirstLetter(name).slice(0, ConstantString.ParameterTitleMaxLens),
description: (schema.description ?? "").slice(0, ConstantString.ParameterDescriptionMaxLens),
};
if (isRequired && schema.default === undefined) {
requiredParams.push(parameter);
Expand Down Expand Up @@ -403,8 +396,8 @@ export function parseApiInfo(
paramObject.forEach((param: OpenAPIV3.ParameterObject) => {
const parameter: Parameter = {
name: param.name,
title: updateFirstLetter(param.name),
description: param.description ?? "",
title: updateFirstLetter(param.name).slice(0, ConstantString.ParameterTitleMaxLens),
description: (param.description ?? "").slice(0, ConstantString.ParameterDescriptionMaxLens),
};

const schema = param.schema as OpenAPIV3.SchemaObject;
Expand Down Expand Up @@ -446,10 +439,13 @@ export function parseApiInfo(
const command: IMessagingExtensionCommand = {
context: ["compose"],
type: "query",
title: operationItem.summary ?? "",
title: (operationItem.summary ?? "").slice(0, ConstantString.CommandTitleMaxLens),
id: operationId,
parameters: parameters,
description: operationItem.description ?? "",
description: (operationItem.description ?? "").slice(
0,
ConstantString.CommandDescriptionMaxLens
),
};
let warning: WarningResult | undefined = undefined;

Expand Down Expand Up @@ -554,3 +550,11 @@ export function validateSpec(
errors,
};
}

export function format(str: string, ...args: string[]): string {
let index = 0;
return str.replace(/%s/g, () => {
const arg = args[index++];
return arg !== undefined ? arg : "";
});
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { expect } from "chai";
import * as util from "util";
import "mocha";
import sinon from "sinon";
import {
Expand Down Expand Up @@ -855,7 +854,7 @@ describe("adaptiveCardGenerator", () => {
const parentArrayName = "";

expect(() => generateCardFromResponse(schema as any, name, parentArrayName)).to.throw(
util.format(ConstantString.SchemaNotSupported, JSON.stringify(schema))
utils.format(ConstantString.SchemaNotSupported, JSON.stringify(schema))
);
});

Expand All @@ -867,7 +866,7 @@ describe("adaptiveCardGenerator", () => {
const parentArrayName = "";

expect(() => generateCardFromResponse(schema as any, name, parentArrayName)).to.throw(
util.format(ConstantString.UnknownSchema, JSON.stringify(schema))
utils.format(ConstantString.UnknownSchema, JSON.stringify(schema))
);
});

Expand Down
Loading

0 comments on commit 041c93f

Please sign in to comment.