Skip to content

Commit

Permalink
perf(spec-parser): add additional project type option (#11026)
Browse files Browse the repository at this point in the history
* perf(spec-parser): add additional project type option

* fix: test case issue

* perf: add allowMethods option, refactor code

---------

Co-authored-by: turenlong <rentu@microsoft.com>
  • Loading branch information
SLdragon and SLdragon authored Mar 11, 2024
1 parent 64d6d81 commit be62238
Show file tree
Hide file tree
Showing 14 changed files with 883 additions and 714 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ describe("copilotPluginGenerator", function () {
assert.equal(result.value.warnings![2].type, WarningType.GenerateCardFailed);
assert.equal(result.value.warnings![3].type, WarningType.OperationOnlyContainsOptionalParam);
assert.equal(result.value.warnings![3].content, "");
assert.isTrue(generateParser.args[0][3].includes(ResponseTemplatesFolderName));
assert.isTrue(generateParser.args[0][3]?.includes(ResponseTemplatesFolderName));
}
});

Expand Down
1 change: 1 addition & 0 deletions packages/spec-parser/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,5 @@ export class ConstantString {
static readonly ParameterDescriptionMaxLens = 128;
static readonly CommandTitleMaxLens = 32;
static readonly ParameterTitleMaxLens = 32;
static readonly SMERequiredParamsMaxNum = 5;
}
1 change: 1 addition & 0 deletions packages/spec-parser/src/index.browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type {
WarningResult,
ParseOptions,
AdaptiveCard,
ProjectType,
} from "./interfaces";
export { ConstantString } from "./constants";
export { Utils } from "./utils";
Expand Down
1 change: 1 addition & 0 deletions packages/spec-parser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export {
ValidateResult,
ParseOptions,
AdaptiveCard,
ProjectType,
} from "./interfaces";

export { ConstantString } from "./constants";
Expand Down
37 changes: 36 additions & 1 deletion packages/spec-parser/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,12 +182,47 @@ export interface CheckParamResult {
}

export interface ParseOptions {
/**
* If true, the parser will not throw an error if an ID is missing the spec file.
*/
allowMissingId?: boolean;

/**
* If true, the parser will allow parsing of Swagger specifications.
*/
allowSwagger?: boolean;

/**
* If true, the parser will allow API Key authentication in the spec file.
*/
allowAPIKeyAuth?: boolean;

/**
* If true, the parser will allow multiple parameters in the spec file.
*/
allowMultipleParameters?: boolean;

/**
* If true, the parser will allow OAuth2 authentication in the spec file.
*/
allowOauth2?: boolean;
isCopilot?: boolean;

/**
* An array of HTTP methods that the parser will allow in the spec file.
*/
allowMethods?: string[];

/**
* The type of project that the parser is being used for.
* Project can be SME/Copilot/TeamsAi
*/
projectType?: ProjectType;
}

export enum ProjectType {
Copilot,
SME,
TeamsAi,
}

export interface APIInfo {
Expand Down
141 changes: 75 additions & 66 deletions packages/spec-parser/src/manifestUpdater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import { OpenAPIV3 } from "openapi-types";
import fs from "fs-extra";
import path from "path";
import { ErrorType, WarningResult } from "./interfaces";
import { ErrorType, ParseOptions, ProjectType, WarningResult } from "./interfaces";
import { Utils } from "./utils";
import { SpecParserError } from "./specParserError";
import { ConstantString } from "./constants";
Expand All @@ -24,7 +24,8 @@ export class ManifestUpdater {
manifestPath: string,
outputSpecPath: string,
apiPluginFilePath: string,
spec: OpenAPIV3.Document
spec: OpenAPIV3.Document,
options: ParseOptions
): Promise<[TeamsAppManifest, PluginManifestSchema]> {
const manifest: TeamsAppManifest = await fs.readJSON(manifestPath);
const apiPluginRelativePath = ManifestUpdater.getRelativePath(manifestPath, apiPluginFilePath);
Expand All @@ -37,7 +38,7 @@ export class ManifestUpdater {
ManifestUpdater.updateManifestDescription(manifest, spec);

const specRelativePath = ManifestUpdater.getRelativePath(manifestPath, outputSpecPath);
const apiPlugin = ManifestUpdater.generatePluginManifestSchema(spec, specRelativePath);
const apiPlugin = ManifestUpdater.generatePluginManifestSchema(spec, specRelativePath, options);

return [manifest, apiPlugin];
}
Expand Down Expand Up @@ -78,7 +79,8 @@ export class ManifestUpdater {

static generatePluginManifestSchema(
spec: OpenAPIV3.Document,
specRelativePath: string
specRelativePath: string,
options: ParseOptions
): PluginManifestSchema {
const functions: FunctionObject[] = [];
const functionNames: string[] = [];
Expand All @@ -90,15 +92,15 @@ export class ManifestUpdater {
if (pathItem) {
const operations = pathItem;
for (const method in operations) {
if (ConstantString.AllOperationMethods.includes(method)) {
if (options.allowMethods!.includes(method)) {
const operationItem = (operations as any)[method] as OpenAPIV3.OperationObject;
if (operationItem) {
const operationId = operationItem.operationId!;
const description = operationItem.description ?? "";
const paramObject = operationItem.parameters as OpenAPIV3.ParameterObject[];
const requestBody = operationItem.requestBody as OpenAPIV3.ParameterObject;

const parameters: FunctionParameters = {
const parameters: Required<FunctionParameters> = {
type: "object",
properties: {},
required: [],
Expand All @@ -110,18 +112,18 @@ export class ManifestUpdater {

const schema = param.schema as OpenAPIV3.SchemaObject;

parameters.properties![param.name] = ManifestUpdater.mapOpenAPISchemaToFuncParam(
parameters.properties[param.name] = ManifestUpdater.mapOpenAPISchemaToFuncParam(
schema,
method,
pathUrl
);

if (param.required) {
parameters.required!.push(param.name);
parameters.required.push(param.name);
}

if (!parameters.properties![param.name].description) {
parameters.properties![param.name].description = param.description ?? "";
if (!parameters.properties[param.name].description) {
parameters.properties[param.name].description = param.description ?? "";
}
}
}
Expand All @@ -132,12 +134,12 @@ export class ManifestUpdater {

if (requestBodySchema.type === "object") {
if (requestBodySchema.required) {
parameters.required!.push(...requestBodySchema.required);
parameters.required.push(...requestBodySchema.required);
}

for (const property in requestBodySchema.properties) {
const schema = requestBodySchema.properties[property] as OpenAPIV3.SchemaObject;
parameters.properties![property] = ManifestUpdater.mapOpenAPISchemaToFuncParam(
parameters.properties[property] = ManifestUpdater.mapOpenAPISchemaToFuncParam(
schema,
method,
pathUrl
Expand Down Expand Up @@ -195,57 +197,65 @@ export class ManifestUpdater {
static async updateManifest(
manifestPath: string,
outputSpecPath: string,
adaptiveCardFolder: string,
spec: OpenAPIV3.Document,
allowMultipleParameters: boolean,
auth?: OpenAPIV3.SecuritySchemeObject,
isMe?: boolean
options: ParseOptions,
adaptiveCardFolder?: string,
auth?: OpenAPIV3.SecuritySchemeObject
): Promise<[TeamsAppManifest, WarningResult[]]> {
try {
const originalManifest: TeamsAppManifest = await fs.readJSON(manifestPath);
const updatedPart: any = {};
const [commands, warnings] = await ManifestUpdater.generateCommands(
spec,
adaptiveCardFolder,
manifestPath,
allowMultipleParameters
);
const composeExtension: IComposeExtension = {
composeExtensionType: "apiBased",
apiSpecificationFile: ManifestUpdater.getRelativePath(manifestPath, outputSpecPath),
commands: commands,
};

if (auth) {
if (Utils.isAPIKeyAuth(auth)) {
auth = auth as OpenAPIV3.ApiKeySecurityScheme;
const safeApiSecretRegistrationId = Utils.getSafeRegistrationIdEnvName(
`${auth.name}_${ConstantString.RegistrationIdPostfix}`
);
(composeExtension as any).authorization = {
authType: "apiSecretServiceAuth",
apiSecretServiceAuthConfiguration: {
apiSecretRegistrationId: `\${{${safeApiSecretRegistrationId}}}`,
},
};
} else if (Utils.isBearerTokenAuth(auth)) {
(composeExtension as any).authorization = {
authType: "microsoftEntra",
microsoftEntraConfiguration: {
supportsSingleSignOn: true,
},
};

updatedPart.webApplicationInfo = {
id: "${{AAD_APP_CLIENT_ID}}",
resource: "api://${{DOMAIN}}/${{AAD_APP_CLIENT_ID}}",
};
updatedPart.composeExtensions = [];
let warnings: WarningResult[] = [];

if (options.projectType === ProjectType.SME) {
const updateResult = await ManifestUpdater.generateCommands(
spec,
manifestPath,
options,
adaptiveCardFolder
);
const commands = updateResult[0];
warnings = updateResult[1];

const composeExtension: IComposeExtension = {
composeExtensionType: "apiBased",
apiSpecificationFile: ManifestUpdater.getRelativePath(manifestPath, outputSpecPath),
commands: commands,
};

if (auth) {
if (Utils.isAPIKeyAuth(auth)) {
auth = auth as OpenAPIV3.ApiKeySecurityScheme;
const safeApiSecretRegistrationId = Utils.getSafeRegistrationIdEnvName(
`${auth.name}_${ConstantString.RegistrationIdPostfix}`
);
(composeExtension as any).authorization = {
authType: "apiSecretServiceAuth",
apiSecretServiceAuthConfiguration: {
apiSecretRegistrationId: `\${{${safeApiSecretRegistrationId}}}`,
},
};
} else if (Utils.isBearerTokenAuth(auth)) {
(composeExtension as any).authorization = {
authType: "microsoftEntra",
microsoftEntraConfiguration: {
supportsSingleSignOn: true,
},
};

updatedPart.webApplicationInfo = {
id: "${{AAD_APP_CLIENT_ID}}",
resource: "api://${{DOMAIN}}/${{AAD_APP_CLIENT_ID}}",
};
}
}

updatedPart.composeExtensions = [composeExtension];
}

updatedPart.description = originalManifest.description;
ManifestUpdater.updateManifestDescription(updatedPart, spec);
updatedPart.composeExtensions = isMe === undefined || isMe === true ? [composeExtension] : [];

const updatedManifest = { ...originalManifest, ...updatedPart };

Expand All @@ -257,9 +267,9 @@ export class ManifestUpdater {

static async generateCommands(
spec: OpenAPIV3.Document,
adaptiveCardFolder: string,
manifestPath: string,
allowMultipleParameters: boolean
options: ParseOptions,
adaptiveCardFolder?: string
): Promise<[IMessagingExtensionCommand[], WarningResult[]]> {
const paths = spec.paths;
const commands: IMessagingExtensionCommand[] = [];
Expand All @@ -272,18 +282,17 @@ export class ManifestUpdater {

// Currently only support GET and POST method
for (const method in operations) {
if (method === ConstantString.PostMethod || method === ConstantString.GetMethod) {
const operationItem = operations[method];
if (options.allowMethods?.includes(method)) {
const operationItem = (operations as any)[method];
if (operationItem) {
const [command, warning] = Utils.parseApiInfo(
operationItem,
allowMultipleParameters
);

const adaptiveCardPath = path.join(adaptiveCardFolder, command.id + ".json");
command.apiResponseRenderingTemplateFile = (await fs.pathExists(adaptiveCardPath))
? ManifestUpdater.getRelativePath(manifestPath, adaptiveCardPath)
: "";
const [command, warning] = Utils.parseApiInfo(operationItem, options);

if (adaptiveCardFolder) {
const adaptiveCardPath = path.join(adaptiveCardFolder, command.id + ".json");
command.apiResponseRenderingTemplateFile = (await fs.pathExists(adaptiveCardPath))
? ManifestUpdater.getRelativePath(manifestPath, adaptiveCardPath)
: "";
}

if (warning) {
warnings.push(warning);
Expand Down
21 changes: 3 additions & 18 deletions packages/spec-parser/src/specFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,15 @@
import { OpenAPIV3 } from "openapi-types";
import { Utils } from "./utils";
import { SpecParserError } from "./specParserError";
import { ErrorType } from "./interfaces";
import { ErrorType, ParseOptions } from "./interfaces";
import { ConstantString } from "./constants";

export class SpecFilter {
static specFilter(
filter: string[],
unResolveSpec: OpenAPIV3.Document,
resolvedSpec: OpenAPIV3.Document,
allowMissingId: boolean,
allowAPIKeyAuth: boolean,
allowMultipleParameters: boolean,
allowOauth2: boolean,
isCopilot: boolean
options: ParseOptions
): OpenAPIV3.Document {
try {
const newSpec = { ...unResolveSpec };
Expand All @@ -26,18 +22,7 @@ export class SpecFilter {
const [method, path] = filterItem.split(" ");
const methodName = method.toLowerCase();

if (
!Utils.isSupportedApi(
methodName,
path,
resolvedSpec,
allowMissingId,
allowAPIKeyAuth,
allowMultipleParameters,
allowOauth2,
isCopilot
)
) {
if (!Utils.isSupportedApi(methodName, path, resolvedSpec, options)) {
continue;
}

Expand Down
Loading

0 comments on commit be62238

Please sign in to comment.