2011 lines
88 KiB
JavaScript
2011 lines
88 KiB
JavaScript
import SwaggerParser from '@apidevtools/swagger-parser';
|
|
import converter from 'swagger2openapi';
|
|
import jsyaml from 'js-yaml';
|
|
import fs from 'fs-extra';
|
|
import path from 'path';
|
|
|
|
// Copyright (c) Microsoft Corporation.
|
|
/**
|
|
* An enum that represents the types of errors that can occur during validation.
|
|
*/
|
|
var ErrorType;
|
|
(function (ErrorType) {
|
|
ErrorType["SpecNotValid"] = "spec-not-valid";
|
|
ErrorType["RemoteRefNotSupported"] = "remote-ref-not-supported";
|
|
ErrorType["NoServerInformation"] = "no-server-information";
|
|
ErrorType["UrlProtocolNotSupported"] = "url-protocol-not-supported";
|
|
ErrorType["RelativeServerUrlNotSupported"] = "relative-server-url-not-supported";
|
|
ErrorType["NoSupportedApi"] = "no-supported-api";
|
|
ErrorType["NoExtraAPICanBeAdded"] = "no-extra-api-can-be-added";
|
|
ErrorType["ResolveServerUrlFailed"] = "resolve-server-url-failed";
|
|
ErrorType["SwaggerNotSupported"] = "swagger-not-supported";
|
|
ErrorType["MultipleAuthNotSupported"] = "multiple-auth-not-supported";
|
|
ErrorType["SpecVersionNotSupported"] = "spec-version-not-supported";
|
|
ErrorType["ListFailed"] = "list-failed";
|
|
ErrorType["listSupportedAPIInfoFailed"] = "list-supported-api-info-failed";
|
|
ErrorType["FilterSpecFailed"] = "filter-spec-failed";
|
|
ErrorType["UpdateManifestFailed"] = "update-manifest-failed";
|
|
ErrorType["GenerateAdaptiveCardFailed"] = "generate-adaptive-card-failed";
|
|
ErrorType["GenerateFailed"] = "generate-failed";
|
|
ErrorType["ValidateFailed"] = "validate-failed";
|
|
ErrorType["GetSpecFailed"] = "get-spec-failed";
|
|
ErrorType["AuthTypeIsNotSupported"] = "auth-type-is-not-supported";
|
|
ErrorType["MissingOperationId"] = "missing-operation-id";
|
|
ErrorType["PostBodyContainMultipleMediaTypes"] = "post-body-contain-multiple-media-types";
|
|
ErrorType["ResponseContainMultipleMediaTypes"] = "response-contain-multiple-media-types";
|
|
ErrorType["ResponseJsonIsEmpty"] = "response-json-is-empty";
|
|
ErrorType["PostBodySchemaIsNotJson"] = "post-body-schema-is-not-json";
|
|
ErrorType["PostBodyContainsRequiredUnsupportedSchema"] = "post-body-contains-required-unsupported-schema";
|
|
ErrorType["ParamsContainRequiredUnsupportedSchema"] = "params-contain-required-unsupported-schema";
|
|
ErrorType["ParamsContainsNestedObject"] = "params-contains-nested-object";
|
|
ErrorType["RequestBodyContainsNestedObject"] = "request-body-contains-nested-object";
|
|
ErrorType["ExceededRequiredParamsLimit"] = "exceeded-required-params-limit";
|
|
ErrorType["NoParameter"] = "no-parameter";
|
|
ErrorType["NoAPIInfo"] = "no-api-info";
|
|
ErrorType["MethodNotAllowed"] = "method-not-allowed";
|
|
ErrorType["UrlPathNotExist"] = "url-path-not-exist";
|
|
ErrorType["Cancelled"] = "cancelled";
|
|
ErrorType["Unknown"] = "unknown";
|
|
})(ErrorType || (ErrorType = {}));
|
|
/**
|
|
* An enum that represents the types of warnings that can occur during validation.
|
|
*/
|
|
var WarningType;
|
|
(function (WarningType) {
|
|
WarningType["OperationIdMissing"] = "operationid-missing";
|
|
WarningType["GenerateCardFailed"] = "generate-card-failed";
|
|
WarningType["OperationOnlyContainsOptionalParam"] = "operation-only-contains-optional-param";
|
|
WarningType["ConvertSwaggerToOpenAPI"] = "convert-swagger-to-openapi";
|
|
WarningType["Unknown"] = "unknown";
|
|
})(WarningType || (WarningType = {}));
|
|
/**
|
|
* An enum that represents the validation status of an OpenAPI specification file.
|
|
*/
|
|
var ValidationStatus;
|
|
(function (ValidationStatus) {
|
|
ValidationStatus[ValidationStatus["Valid"] = 0] = "Valid";
|
|
ValidationStatus[ValidationStatus["Warning"] = 1] = "Warning";
|
|
ValidationStatus[ValidationStatus["Error"] = 2] = "Error";
|
|
})(ValidationStatus || (ValidationStatus = {}));
|
|
var ProjectType;
|
|
(function (ProjectType) {
|
|
ProjectType[ProjectType["Copilot"] = 0] = "Copilot";
|
|
ProjectType[ProjectType["SME"] = 1] = "SME";
|
|
ProjectType[ProjectType["TeamsAi"] = 2] = "TeamsAi";
|
|
})(ProjectType || (ProjectType = {}));
|
|
|
|
// Copyright (c) Microsoft Corporation.
|
|
class ConstantString {
|
|
}
|
|
ConstantString.CancelledMessage = "Operation is cancelled.";
|
|
ConstantString.NoServerInformation = "No server information is found in the OpenAPI description document.";
|
|
ConstantString.RemoteRefNotSupported = "Remote reference is not supported: %s.";
|
|
ConstantString.MissingOperationId = "Missing operationIds: %s.";
|
|
ConstantString.NoSupportedApi = "No supported API is found in the OpenAPI description document: only GET and POST methods are supported, additionally, there can be at most one required parameter, and no auth is allowed.";
|
|
ConstantString.AdditionalPropertiesNotSupported = "'additionalProperties' is not supported, and will be ignored.";
|
|
ConstantString.SchemaNotSupported = "'oneOf', 'allOf', 'anyOf', and 'not' schema are not supported: %s.";
|
|
ConstantString.UnknownSchema = "Unknown schema: %s.";
|
|
ConstantString.UrlProtocolNotSupported = "Server url is not correct: protocol %s is not supported, you should use https protocol instead.";
|
|
ConstantString.RelativeServerUrlNotSupported = "Server url is not correct: relative server url is not supported.";
|
|
ConstantString.ResolveServerUrlFailed = "Unable to resolve the server URL: please make sure that the environment variable %s is defined.";
|
|
ConstantString.OperationOnlyContainsOptionalParam = "Operation %s contains multiple optional parameters. The first optional parameter is used for this command.";
|
|
ConstantString.ConvertSwaggerToOpenAPI = "The Swagger 2.0 file has been converted to OpenAPI 3.0.";
|
|
ConstantString.SwaggerNotSupported = "Swagger 2.0 is not supported. Please convert to OpenAPI 3.0 manually before proceeding.";
|
|
ConstantString.SpecVersionNotSupported = "Unsupported OpenAPI version %s. Please use version 3.0.x.";
|
|
ConstantString.MultipleAuthNotSupported = "Multiple authentication methods are unsupported. Ensure all selected APIs use identical authentication.";
|
|
ConstantString.UnsupportedSchema = "Unsupported schema in %s %s: %s";
|
|
ConstantString.WrappedCardVersion = "devPreview";
|
|
ConstantString.WrappedCardSchema = "https://developer.microsoft.com/json-schemas/teams/vDevPreview/MicrosoftTeams.ResponseRenderingTemplate.schema.json";
|
|
ConstantString.WrappedCardResponseLayout = "list";
|
|
ConstantString.GetMethod = "get";
|
|
ConstantString.PostMethod = "post";
|
|
ConstantString.AdaptiveCardVersion = "1.5";
|
|
ConstantString.AdaptiveCardSchema = "http://adaptivecards.io/schemas/adaptive-card.json";
|
|
ConstantString.AdaptiveCardType = "AdaptiveCard";
|
|
ConstantString.TextBlockType = "TextBlock";
|
|
ConstantString.ImageType = "Image";
|
|
ConstantString.ContainerType = "Container";
|
|
ConstantString.RegistrationIdPostfix = {
|
|
apiKey: "REGISTRATION_ID",
|
|
oauth2: "CONFIGURATION_ID",
|
|
http: "REGISTRATION_ID",
|
|
openIdConnect: "REGISTRATION_ID",
|
|
};
|
|
ConstantString.ResponseCodeFor20X = [
|
|
"200",
|
|
"201",
|
|
"202",
|
|
"203",
|
|
"204",
|
|
"205",
|
|
"206",
|
|
"207",
|
|
"208",
|
|
"226",
|
|
"default",
|
|
];
|
|
ConstantString.AllOperationMethods = [
|
|
"get",
|
|
"post",
|
|
"put",
|
|
"delete",
|
|
"patch",
|
|
"head",
|
|
"options",
|
|
"trace",
|
|
];
|
|
// TODO: update after investigating the usage of these constants.
|
|
ConstantString.WellknownResultNames = [
|
|
"result",
|
|
"data",
|
|
"items",
|
|
"root",
|
|
"matches",
|
|
"queries",
|
|
"list",
|
|
"output",
|
|
];
|
|
ConstantString.WellknownTitleName = ["title", "name", "summary", "caption", "subject", "label"];
|
|
ConstantString.WellknownSubtitleName = [
|
|
"subtitle",
|
|
"id",
|
|
"uid",
|
|
"description",
|
|
"desc",
|
|
"detail",
|
|
];
|
|
ConstantString.WellknownImageName = [
|
|
"image",
|
|
"icon",
|
|
"avatar",
|
|
"picture",
|
|
"photo",
|
|
"logo",
|
|
"pic",
|
|
"thumbnail",
|
|
"img",
|
|
];
|
|
ConstantString.ShortDescriptionMaxLens = 80;
|
|
ConstantString.FullDescriptionMaxLens = 4000;
|
|
ConstantString.CommandDescriptionMaxLens = 128;
|
|
ConstantString.ParameterDescriptionMaxLens = 128;
|
|
ConstantString.ConversationStarterMaxLens = 50;
|
|
ConstantString.CommandTitleMaxLens = 32;
|
|
ConstantString.ParameterTitleMaxLens = 32;
|
|
ConstantString.SMERequiredParamsMaxNum = 5;
|
|
ConstantString.DefaultPluginId = "plugin_1";
|
|
ConstantString.PluginManifestSchema = "https://aka.ms/json-schemas/copilot-extensions/v2.1/plugin.schema.json";
|
|
|
|
// Copyright (c) Microsoft Corporation.
|
|
class SpecParserError extends Error {
|
|
constructor(message, errorType) {
|
|
super(message);
|
|
this.errorType = errorType;
|
|
}
|
|
}
|
|
|
|
// Copyright (c) Microsoft Corporation.
|
|
class Utils {
|
|
static hasNestedObjectInSchema(schema) {
|
|
if (schema.type === "object") {
|
|
for (const property in schema.properties) {
|
|
const nestedSchema = schema.properties[property];
|
|
if (nestedSchema.type === "object") {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
static containMultipleMediaTypes(bodyObject) {
|
|
return Object.keys((bodyObject === null || bodyObject === void 0 ? void 0 : bodyObject.content) || {}).length > 1;
|
|
}
|
|
static isBearerTokenAuth(authScheme) {
|
|
return authScheme.type === "http" && authScheme.scheme === "bearer";
|
|
}
|
|
static isAPIKeyAuth(authScheme) {
|
|
return authScheme.type === "apiKey";
|
|
}
|
|
static isOAuthWithAuthCodeFlow(authScheme) {
|
|
return !!(authScheme.type === "oauth2" &&
|
|
authScheme.flows &&
|
|
authScheme.flows.authorizationCode);
|
|
}
|
|
static getAuthArray(securities, spec) {
|
|
var _a;
|
|
const result = [];
|
|
const securitySchemas = (_a = spec.components) === null || _a === void 0 ? void 0 : _a.securitySchemes;
|
|
const securitiesArr = securities !== null && securities !== void 0 ? securities : spec.security;
|
|
if (securitiesArr && securitySchemas) {
|
|
for (let i = 0; i < securitiesArr.length; i++) {
|
|
const security = securitiesArr[i];
|
|
const authArray = [];
|
|
for (const name in security) {
|
|
const auth = securitySchemas[name];
|
|
authArray.push({
|
|
authScheme: auth,
|
|
name: name,
|
|
});
|
|
}
|
|
if (authArray.length > 0) {
|
|
result.push(authArray);
|
|
}
|
|
}
|
|
}
|
|
result.sort((a, b) => a[0].name.localeCompare(b[0].name));
|
|
return result;
|
|
}
|
|
static getAuthInfo(spec) {
|
|
let authInfo = undefined;
|
|
for (const url in spec.paths) {
|
|
for (const method in spec.paths[url]) {
|
|
const operation = spec.paths[url][method];
|
|
const authArray = Utils.getAuthArray(operation.security, spec);
|
|
if (authArray && authArray.length > 0) {
|
|
const currentAuth = authArray[0][0];
|
|
if (!authInfo) {
|
|
authInfo = authArray[0][0];
|
|
}
|
|
else if (authInfo.name !== currentAuth.name) {
|
|
throw new SpecParserError(ConstantString.MultipleAuthNotSupported, ErrorType.MultipleAuthNotSupported);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return authInfo;
|
|
}
|
|
static updateFirstLetter(str) {
|
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
}
|
|
static getResponseJson(operationObject) {
|
|
var _a, _b;
|
|
let json = {};
|
|
let multipleMediaType = false;
|
|
for (const code of ConstantString.ResponseCodeFor20X) {
|
|
const responseObject = (_a = operationObject === null || operationObject === void 0 ? void 0 : operationObject.responses) === null || _a === void 0 ? void 0 : _a[code];
|
|
if ((_b = responseObject === null || responseObject === void 0 ? void 0 : responseObject.content) === null || _b === void 0 ? void 0 : _b["application/json"]) {
|
|
multipleMediaType = false;
|
|
json = responseObject.content["application/json"];
|
|
if (Utils.containMultipleMediaTypes(responseObject)) {
|
|
multipleMediaType = true;
|
|
json = {};
|
|
}
|
|
else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return { json, multipleMediaType };
|
|
}
|
|
static convertPathToCamelCase(path) {
|
|
const pathSegments = path.split(/[./{]/);
|
|
const camelCaseSegments = pathSegments.map((segment) => {
|
|
segment = segment.replace(/}/g, "");
|
|
return segment.charAt(0).toUpperCase() + segment.slice(1);
|
|
});
|
|
const camelCasePath = camelCaseSegments.join("");
|
|
return camelCasePath;
|
|
}
|
|
static getUrlProtocol(urlString) {
|
|
try {
|
|
const url = new URL(urlString);
|
|
return url.protocol;
|
|
}
|
|
catch (err) {
|
|
return undefined;
|
|
}
|
|
}
|
|
static resolveEnv(str) {
|
|
const placeHolderReg = /\${{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*}}/g;
|
|
let matches = placeHolderReg.exec(str);
|
|
let newStr = str;
|
|
while (matches != null) {
|
|
const envVar = matches[1];
|
|
const envVal = process.env[envVar];
|
|
if (!envVal) {
|
|
throw new Error(Utils.format(ConstantString.ResolveServerUrlFailed, envVar));
|
|
}
|
|
else {
|
|
newStr = newStr.replace(matches[0], envVal);
|
|
}
|
|
matches = placeHolderReg.exec(str);
|
|
}
|
|
return newStr;
|
|
}
|
|
static checkServerUrl(servers) {
|
|
const errors = [];
|
|
let serverUrl;
|
|
try {
|
|
serverUrl = Utils.resolveEnv(servers[0].url);
|
|
}
|
|
catch (err) {
|
|
errors.push({
|
|
type: ErrorType.ResolveServerUrlFailed,
|
|
content: err.message,
|
|
data: servers,
|
|
});
|
|
return errors;
|
|
}
|
|
const protocol = Utils.getUrlProtocol(serverUrl);
|
|
if (!protocol) {
|
|
// Relative server url is not supported
|
|
errors.push({
|
|
type: ErrorType.RelativeServerUrlNotSupported,
|
|
content: ConstantString.RelativeServerUrlNotSupported,
|
|
data: servers,
|
|
});
|
|
}
|
|
else if (protocol !== "https:") {
|
|
// Http server url is not supported
|
|
const protocolString = protocol.slice(0, -1);
|
|
errors.push({
|
|
type: ErrorType.UrlProtocolNotSupported,
|
|
content: Utils.format(ConstantString.UrlProtocolNotSupported, protocol.slice(0, -1)),
|
|
data: protocolString,
|
|
});
|
|
}
|
|
return errors;
|
|
}
|
|
static validateServer(spec, options) {
|
|
var _a;
|
|
const errors = [];
|
|
let hasTopLevelServers = false;
|
|
let hasPathLevelServers = false;
|
|
let hasOperationLevelServers = false;
|
|
if (spec.servers && spec.servers.length >= 1) {
|
|
hasTopLevelServers = true;
|
|
// for multiple server, we only use the first url
|
|
const serverErrors = Utils.checkServerUrl(spec.servers);
|
|
errors.push(...serverErrors);
|
|
}
|
|
const paths = spec.paths;
|
|
for (const path in paths) {
|
|
const methods = paths[path];
|
|
if ((methods === null || methods === void 0 ? void 0 : methods.servers) && methods.servers.length >= 1) {
|
|
hasPathLevelServers = true;
|
|
const serverErrors = Utils.checkServerUrl(methods.servers);
|
|
errors.push(...serverErrors);
|
|
}
|
|
for (const method in methods) {
|
|
const operationObject = methods[method];
|
|
if (((_a = options.allowMethods) === null || _a === void 0 ? void 0 : _a.includes(method)) && operationObject) {
|
|
if ((operationObject === null || operationObject === void 0 ? void 0 : operationObject.servers) && operationObject.servers.length >= 1) {
|
|
hasOperationLevelServers = true;
|
|
const serverErrors = Utils.checkServerUrl(operationObject.servers);
|
|
errors.push(...serverErrors);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!hasTopLevelServers && !hasPathLevelServers && !hasOperationLevelServers) {
|
|
errors.push({
|
|
type: ErrorType.NoServerInformation,
|
|
content: ConstantString.NoServerInformation,
|
|
});
|
|
}
|
|
return errors;
|
|
}
|
|
static isWellKnownName(name, wellknownNameList) {
|
|
for (let i = 0; i < wellknownNameList.length; i++) {
|
|
name = name.replace(/_/g, "").replace(/-/g, "");
|
|
if (name.toLowerCase().includes(wellknownNameList[i])) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
static generateParametersFromSchema(schema, name, allowMultipleParameters, isRequired = false) {
|
|
var _a, _b;
|
|
const requiredParams = [];
|
|
const optionalParams = [];
|
|
if (schema.type === "string" ||
|
|
schema.type === "integer" ||
|
|
schema.type === "boolean" ||
|
|
schema.type === "number") {
|
|
const parameter = {
|
|
name: name,
|
|
title: Utils.updateFirstLetter(name).slice(0, ConstantString.ParameterTitleMaxLens),
|
|
description: ((_a = schema.description) !== null && _a !== void 0 ? _a : "").slice(0, ConstantString.ParameterDescriptionMaxLens),
|
|
};
|
|
if (allowMultipleParameters) {
|
|
Utils.updateParameterWithInputType(schema, parameter);
|
|
}
|
|
if (isRequired && schema.default === undefined) {
|
|
parameter.isRequired = true;
|
|
requiredParams.push(parameter);
|
|
}
|
|
else {
|
|
optionalParams.push(parameter);
|
|
}
|
|
}
|
|
else if (schema.type === "object") {
|
|
const { properties } = schema;
|
|
for (const property in properties) {
|
|
let isRequired = false;
|
|
if (schema.required && ((_b = schema.required) === null || _b === void 0 ? void 0 : _b.indexOf(property)) >= 0) {
|
|
isRequired = true;
|
|
}
|
|
const [requiredP, optionalP] = Utils.generateParametersFromSchema(properties[property], property, allowMultipleParameters, isRequired);
|
|
requiredParams.push(...requiredP);
|
|
optionalParams.push(...optionalP);
|
|
}
|
|
}
|
|
return [requiredParams, optionalParams];
|
|
}
|
|
static updateParameterWithInputType(schema, param) {
|
|
if (schema.enum) {
|
|
param.inputType = "choiceset";
|
|
param.choices = [];
|
|
for (let i = 0; i < schema.enum.length; i++) {
|
|
param.choices.push({
|
|
title: schema.enum[i],
|
|
value: schema.enum[i],
|
|
});
|
|
}
|
|
}
|
|
else if (schema.type === "string") {
|
|
param.inputType = "text";
|
|
}
|
|
else if (schema.type === "integer" || schema.type === "number") {
|
|
param.inputType = "number";
|
|
}
|
|
else if (schema.type === "boolean") {
|
|
param.inputType = "toggle";
|
|
}
|
|
if (schema.default) {
|
|
param.value = schema.default;
|
|
}
|
|
}
|
|
static parseApiInfo(operationItem, options) {
|
|
var _a, _b;
|
|
const requiredParams = [];
|
|
const optionalParams = [];
|
|
const paramObject = operationItem.parameters;
|
|
if (paramObject) {
|
|
paramObject.forEach((param) => {
|
|
var _a;
|
|
const parameter = {
|
|
name: param.name,
|
|
title: Utils.updateFirstLetter(param.name).slice(0, ConstantString.ParameterTitleMaxLens),
|
|
description: ((_a = param.description) !== null && _a !== void 0 ? _a : "").slice(0, ConstantString.ParameterDescriptionMaxLens),
|
|
};
|
|
const schema = param.schema;
|
|
if (options.allowMultipleParameters && schema) {
|
|
Utils.updateParameterWithInputType(schema, parameter);
|
|
}
|
|
if (param.in !== "header" && param.in !== "cookie") {
|
|
if (param.required && (schema === null || schema === void 0 ? void 0 : schema.default) === undefined) {
|
|
parameter.isRequired = true;
|
|
requiredParams.push(parameter);
|
|
}
|
|
else {
|
|
optionalParams.push(parameter);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
if (operationItem.requestBody) {
|
|
const requestBody = operationItem.requestBody;
|
|
const requestJson = requestBody.content["application/json"];
|
|
if (Object.keys(requestJson).length !== 0) {
|
|
const schema = requestJson.schema;
|
|
const [requiredP, optionalP] = Utils.generateParametersFromSchema(schema, "requestBody", !!options.allowMultipleParameters, requestBody.required);
|
|
requiredParams.push(...requiredP);
|
|
optionalParams.push(...optionalP);
|
|
}
|
|
}
|
|
const operationId = operationItem.operationId;
|
|
const parameters = [...requiredParams, ...optionalParams];
|
|
const command = {
|
|
context: ["compose"],
|
|
type: "query",
|
|
title: ((_a = operationItem.summary) !== null && _a !== void 0 ? _a : "").slice(0, ConstantString.CommandTitleMaxLens),
|
|
id: operationId,
|
|
parameters: parameters,
|
|
description: ((_b = operationItem.description) !== null && _b !== void 0 ? _b : "").slice(0, ConstantString.CommandDescriptionMaxLens),
|
|
};
|
|
return command;
|
|
}
|
|
static format(str, ...args) {
|
|
let index = 0;
|
|
return str.replace(/%s/g, () => {
|
|
const arg = args[index++];
|
|
return arg !== undefined ? arg : "";
|
|
});
|
|
}
|
|
static getSafeRegistrationIdEnvName(authName) {
|
|
if (!authName) {
|
|
return "";
|
|
}
|
|
let safeRegistrationIdEnvName = authName.toUpperCase().replace(/[^A-Z0-9_]/g, "_");
|
|
if (!safeRegistrationIdEnvName.match(/^[A-Z]/)) {
|
|
safeRegistrationIdEnvName = "PREFIX_" + safeRegistrationIdEnvName;
|
|
}
|
|
return safeRegistrationIdEnvName;
|
|
}
|
|
static getServerObject(spec, method, path) {
|
|
const pathObj = spec.paths[path];
|
|
const operationObject = pathObj[method];
|
|
const rootServer = spec.servers && spec.servers[0];
|
|
const methodServer = spec.paths[path].servers && spec.paths[path].servers[0];
|
|
const operationServer = operationObject.servers && operationObject.servers[0];
|
|
const serverUrl = operationServer || methodServer || rootServer;
|
|
return serverUrl;
|
|
}
|
|
}
|
|
|
|
// Copyright (c) Microsoft Corporation.
|
|
class Validator {
|
|
listAPIs() {
|
|
var _a;
|
|
if (this.apiMap) {
|
|
return this.apiMap;
|
|
}
|
|
const paths = this.spec.paths;
|
|
const result = {};
|
|
for (const path in paths) {
|
|
const methods = paths[path];
|
|
for (const method in methods) {
|
|
const operationObject = methods[method];
|
|
if (((_a = this.options.allowMethods) === null || _a === void 0 ? void 0 : _a.includes(method)) && operationObject) {
|
|
const validateResult = this.validateAPI(method, path);
|
|
result[`${method.toUpperCase()} ${path}`] = {
|
|
operation: operationObject,
|
|
isValid: validateResult.isValid,
|
|
reason: validateResult.reason,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
this.apiMap = result;
|
|
return result;
|
|
}
|
|
validateSpecVersion() {
|
|
const result = { errors: [], warnings: [] };
|
|
if (this.spec.openapi >= "3.1.0") {
|
|
result.errors.push({
|
|
type: ErrorType.SpecVersionNotSupported,
|
|
content: Utils.format(ConstantString.SpecVersionNotSupported, this.spec.openapi),
|
|
data: this.spec.openapi,
|
|
});
|
|
}
|
|
return result;
|
|
}
|
|
validateSpecServer() {
|
|
const result = { errors: [], warnings: [] };
|
|
const serverErrors = Utils.validateServer(this.spec, this.options);
|
|
result.errors.push(...serverErrors);
|
|
return result;
|
|
}
|
|
validateSpecNoSupportAPI() {
|
|
const result = { errors: [], warnings: [] };
|
|
const apiMap = this.listAPIs();
|
|
const validAPIs = Object.entries(apiMap).filter(([, value]) => value.isValid);
|
|
if (validAPIs.length === 0) {
|
|
const data = [];
|
|
for (const key in apiMap) {
|
|
const { reason } = apiMap[key];
|
|
const apiInvalidReason = { api: key, reason: reason };
|
|
data.push(apiInvalidReason);
|
|
}
|
|
result.errors.push({
|
|
type: ErrorType.NoSupportedApi,
|
|
content: ConstantString.NoSupportedApi,
|
|
data,
|
|
});
|
|
}
|
|
return result;
|
|
}
|
|
validateSpecOperationId() {
|
|
const result = { errors: [], warnings: [] };
|
|
const apiMap = this.listAPIs();
|
|
// OperationId missing
|
|
const apisMissingOperationId = [];
|
|
for (const key in apiMap) {
|
|
const { operation } = apiMap[key];
|
|
if (!operation.operationId) {
|
|
apisMissingOperationId.push(key);
|
|
}
|
|
}
|
|
if (apisMissingOperationId.length > 0) {
|
|
result.warnings.push({
|
|
type: WarningType.OperationIdMissing,
|
|
content: Utils.format(ConstantString.MissingOperationId, apisMissingOperationId.join(", ")),
|
|
data: apisMissingOperationId,
|
|
});
|
|
}
|
|
return result;
|
|
}
|
|
validateMethodAndPath(method, path) {
|
|
const result = { isValid: true, reason: [] };
|
|
if (this.options.allowMethods && !this.options.allowMethods.includes(method)) {
|
|
result.isValid = false;
|
|
result.reason.push(ErrorType.MethodNotAllowed);
|
|
return result;
|
|
}
|
|
const pathObj = this.spec.paths[path];
|
|
if (!pathObj || !pathObj[method]) {
|
|
result.isValid = false;
|
|
result.reason.push(ErrorType.UrlPathNotExist);
|
|
return result;
|
|
}
|
|
return result;
|
|
}
|
|
validateResponse(method, path) {
|
|
const result = { isValid: true, reason: [] };
|
|
const operationObject = this.spec.paths[path][method];
|
|
const { json, multipleMediaType } = Utils.getResponseJson(operationObject);
|
|
if (this.options.projectType === ProjectType.SME) {
|
|
// only support response body only contains “application/json” content type
|
|
if (multipleMediaType) {
|
|
result.reason.push(ErrorType.ResponseContainMultipleMediaTypes);
|
|
}
|
|
else if (Object.keys(json).length === 0) {
|
|
// response body should not be empty
|
|
result.reason.push(ErrorType.ResponseJsonIsEmpty);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
validateServer(method, path) {
|
|
const result = { isValid: true, reason: [] };
|
|
const serverObj = Utils.getServerObject(this.spec, method, path);
|
|
if (!serverObj) {
|
|
// should contain server URL
|
|
result.reason.push(ErrorType.NoServerInformation);
|
|
}
|
|
else {
|
|
// server url should be absolute url with https protocol
|
|
const serverValidateResult = Utils.checkServerUrl([serverObj]);
|
|
result.reason.push(...serverValidateResult.map((item) => item.type));
|
|
}
|
|
return result;
|
|
}
|
|
validateAuth(method, path) {
|
|
const pathObj = this.spec.paths[path];
|
|
const operationObject = pathObj[method];
|
|
const securities = operationObject.security;
|
|
const authSchemeArray = Utils.getAuthArray(securities, this.spec);
|
|
if (authSchemeArray.length === 0) {
|
|
return { isValid: true, reason: [] };
|
|
}
|
|
if (this.options.allowAPIKeyAuth ||
|
|
this.options.allowOauth2 ||
|
|
this.options.allowBearerTokenAuth) {
|
|
// Currently we don't support multiple auth in one operation
|
|
if (authSchemeArray.length > 0 && authSchemeArray.every((auths) => auths.length > 1)) {
|
|
return {
|
|
isValid: false,
|
|
reason: [ErrorType.MultipleAuthNotSupported],
|
|
};
|
|
}
|
|
for (const auths of authSchemeArray) {
|
|
if (auths.length === 1) {
|
|
if ((this.options.allowAPIKeyAuth && Utils.isAPIKeyAuth(auths[0].authScheme)) ||
|
|
(this.options.allowOauth2 && Utils.isOAuthWithAuthCodeFlow(auths[0].authScheme)) ||
|
|
(this.options.allowBearerTokenAuth && Utils.isBearerTokenAuth(auths[0].authScheme))) {
|
|
return { isValid: true, reason: [] };
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return { isValid: false, reason: [ErrorType.AuthTypeIsNotSupported] };
|
|
}
|
|
checkPostBodySchema(schema, isRequired = false) {
|
|
var _a;
|
|
const paramResult = {
|
|
requiredNum: 0,
|
|
optionalNum: 0,
|
|
isValid: true,
|
|
reason: [],
|
|
};
|
|
if (Object.keys(schema).length === 0) {
|
|
return paramResult;
|
|
}
|
|
const isRequiredWithoutDefault = isRequired && schema.default === undefined;
|
|
const isCopilot = this.projectType === ProjectType.Copilot;
|
|
if (isCopilot && this.hasNestedObjectInSchema(schema)) {
|
|
paramResult.isValid = false;
|
|
paramResult.reason = [ErrorType.RequestBodyContainsNestedObject];
|
|
return paramResult;
|
|
}
|
|
if (schema.type === "string" ||
|
|
schema.type === "integer" ||
|
|
schema.type === "boolean" ||
|
|
schema.type === "number") {
|
|
if (isRequiredWithoutDefault) {
|
|
paramResult.requiredNum = paramResult.requiredNum + 1;
|
|
}
|
|
else {
|
|
paramResult.optionalNum = paramResult.optionalNum + 1;
|
|
}
|
|
}
|
|
else if (schema.type === "object") {
|
|
const { properties } = schema;
|
|
for (const property in properties) {
|
|
let isRequired = false;
|
|
if (schema.required && ((_a = schema.required) === null || _a === void 0 ? void 0 : _a.indexOf(property)) >= 0) {
|
|
isRequired = true;
|
|
}
|
|
const result = this.checkPostBodySchema(properties[property], isRequired);
|
|
paramResult.requiredNum += result.requiredNum;
|
|
paramResult.optionalNum += result.optionalNum;
|
|
paramResult.isValid = paramResult.isValid && result.isValid;
|
|
paramResult.reason.push(...result.reason);
|
|
}
|
|
}
|
|
else {
|
|
if (isRequiredWithoutDefault && !isCopilot) {
|
|
paramResult.isValid = false;
|
|
paramResult.reason.push(ErrorType.PostBodyContainsRequiredUnsupportedSchema);
|
|
}
|
|
}
|
|
return paramResult;
|
|
}
|
|
checkParamSchema(paramObject) {
|
|
const paramResult = {
|
|
requiredNum: 0,
|
|
optionalNum: 0,
|
|
isValid: true,
|
|
reason: [],
|
|
};
|
|
if (!paramObject) {
|
|
return paramResult;
|
|
}
|
|
const isCopilot = this.projectType === ProjectType.Copilot;
|
|
for (let i = 0; i < paramObject.length; i++) {
|
|
const param = paramObject[i];
|
|
const schema = param.schema;
|
|
if (isCopilot && this.hasNestedObjectInSchema(schema)) {
|
|
paramResult.isValid = false;
|
|
paramResult.reason.push(ErrorType.ParamsContainsNestedObject);
|
|
continue;
|
|
}
|
|
const isRequiredWithoutDefault = param.required && schema.default === undefined;
|
|
if (isCopilot) {
|
|
if (isRequiredWithoutDefault) {
|
|
paramResult.requiredNum = paramResult.requiredNum + 1;
|
|
}
|
|
else {
|
|
paramResult.optionalNum = paramResult.optionalNum + 1;
|
|
}
|
|
continue;
|
|
}
|
|
if (param.in === "header" || param.in === "cookie") {
|
|
if (isRequiredWithoutDefault) {
|
|
paramResult.isValid = false;
|
|
paramResult.reason.push(ErrorType.ParamsContainRequiredUnsupportedSchema);
|
|
}
|
|
continue;
|
|
}
|
|
if (schema.type !== "boolean" &&
|
|
schema.type !== "string" &&
|
|
schema.type !== "number" &&
|
|
schema.type !== "integer") {
|
|
if (isRequiredWithoutDefault) {
|
|
paramResult.isValid = false;
|
|
paramResult.reason.push(ErrorType.ParamsContainRequiredUnsupportedSchema);
|
|
}
|
|
continue;
|
|
}
|
|
if (param.in === "query" || param.in === "path") {
|
|
if (isRequiredWithoutDefault) {
|
|
paramResult.requiredNum = paramResult.requiredNum + 1;
|
|
}
|
|
else {
|
|
paramResult.optionalNum = paramResult.optionalNum + 1;
|
|
}
|
|
}
|
|
}
|
|
return paramResult;
|
|
}
|
|
hasNestedObjectInSchema(schema) {
|
|
if (schema.type === "object") {
|
|
for (const property in schema.properties) {
|
|
const nestedSchema = schema.properties[property];
|
|
if (nestedSchema.type === "object") {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Copyright (c) Microsoft Corporation.
|
|
class CopilotValidator extends Validator {
|
|
constructor(spec, options) {
|
|
super();
|
|
this.projectType = ProjectType.Copilot;
|
|
this.options = options;
|
|
this.spec = spec;
|
|
}
|
|
validateSpec() {
|
|
const result = { errors: [], warnings: [] };
|
|
// validate spec version
|
|
let validationResult = this.validateSpecVersion();
|
|
result.errors.push(...validationResult.errors);
|
|
// validate spec server
|
|
validationResult = this.validateSpecServer();
|
|
result.errors.push(...validationResult.errors);
|
|
// validate no supported API
|
|
validationResult = this.validateSpecNoSupportAPI();
|
|
result.errors.push(...validationResult.errors);
|
|
// validate operationId missing
|
|
validationResult = this.validateSpecOperationId();
|
|
result.warnings.push(...validationResult.warnings);
|
|
return result;
|
|
}
|
|
validateAPI(method, path) {
|
|
const result = { isValid: true, reason: [] };
|
|
method = method.toLocaleLowerCase();
|
|
// validate method and path
|
|
const methodAndPathResult = this.validateMethodAndPath(method, path);
|
|
if (!methodAndPathResult.isValid) {
|
|
return methodAndPathResult;
|
|
}
|
|
const operationObject = this.spec.paths[path][method];
|
|
// validate auth
|
|
const authCheckResult = this.validateAuth(method, path);
|
|
result.reason.push(...authCheckResult.reason);
|
|
// validate operationId
|
|
if (!this.options.allowMissingId && !operationObject.operationId) {
|
|
result.reason.push(ErrorType.MissingOperationId);
|
|
}
|
|
// validate server
|
|
const validateServerResult = this.validateServer(method, path);
|
|
result.reason.push(...validateServerResult.reason);
|
|
// validate response
|
|
const validateResponseResult = this.validateResponse(method, path);
|
|
result.reason.push(...validateResponseResult.reason);
|
|
// validate requestBody
|
|
const requestBody = operationObject.requestBody;
|
|
const requestJsonBody = requestBody === null || requestBody === void 0 ? void 0 : requestBody.content["application/json"];
|
|
if (requestJsonBody) {
|
|
const requestBodySchema = requestJsonBody.schema;
|
|
if (requestBodySchema.type !== "object") {
|
|
result.reason.push(ErrorType.PostBodySchemaIsNotJson);
|
|
}
|
|
const requestBodyParamResult = this.checkPostBodySchema(requestBodySchema, requestBody.required);
|
|
result.reason.push(...requestBodyParamResult.reason);
|
|
}
|
|
// validate parameters
|
|
const paramObject = operationObject.parameters;
|
|
const paramResult = this.checkParamSchema(paramObject);
|
|
result.reason.push(...paramResult.reason);
|
|
if (result.reason.length > 0) {
|
|
result.isValid = false;
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
// Copyright (c) Microsoft Corporation.
|
|
class SMEValidator extends Validator {
|
|
constructor(spec, options) {
|
|
super();
|
|
this.projectType = ProjectType.SME;
|
|
this.options = options;
|
|
this.spec = spec;
|
|
}
|
|
validateSpec() {
|
|
const result = { errors: [], warnings: [] };
|
|
// validate spec version
|
|
let validationResult = this.validateSpecVersion();
|
|
result.errors.push(...validationResult.errors);
|
|
// validate spec server
|
|
validationResult = this.validateSpecServer();
|
|
result.errors.push(...validationResult.errors);
|
|
// validate no supported API
|
|
validationResult = this.validateSpecNoSupportAPI();
|
|
result.errors.push(...validationResult.errors);
|
|
// validate operationId missing
|
|
if (this.options.allowMissingId) {
|
|
validationResult = this.validateSpecOperationId();
|
|
result.warnings.push(...validationResult.warnings);
|
|
}
|
|
return result;
|
|
}
|
|
validateAPI(method, path) {
|
|
const result = { isValid: true, reason: [] };
|
|
method = method.toLocaleLowerCase();
|
|
// validate method and path
|
|
const methodAndPathResult = this.validateMethodAndPath(method, path);
|
|
if (!methodAndPathResult.isValid) {
|
|
return methodAndPathResult;
|
|
}
|
|
const operationObject = this.spec.paths[path][method];
|
|
// validate auth
|
|
const authCheckResult = this.validateAuth(method, path);
|
|
result.reason.push(...authCheckResult.reason);
|
|
// validate operationId
|
|
if (!this.options.allowMissingId && !operationObject.operationId) {
|
|
result.reason.push(ErrorType.MissingOperationId);
|
|
}
|
|
// validate server
|
|
const validateServerResult = this.validateServer(method, path);
|
|
result.reason.push(...validateServerResult.reason);
|
|
// validate response
|
|
const validateResponseResult = this.validateResponse(method, path);
|
|
result.reason.push(...validateResponseResult.reason);
|
|
let postBodyResult = {
|
|
requiredNum: 0,
|
|
optionalNum: 0,
|
|
isValid: true,
|
|
reason: [],
|
|
};
|
|
// validate requestBody
|
|
const requestBody = operationObject.requestBody;
|
|
const requestJsonBody = requestBody === null || requestBody === void 0 ? void 0 : requestBody.content["application/json"];
|
|
if (Utils.containMultipleMediaTypes(requestBody)) {
|
|
result.reason.push(ErrorType.PostBodyContainMultipleMediaTypes);
|
|
}
|
|
if (requestJsonBody) {
|
|
const requestBodySchema = requestJsonBody.schema;
|
|
postBodyResult = this.checkPostBodySchema(requestBodySchema, requestBody.required);
|
|
result.reason.push(...postBodyResult.reason);
|
|
}
|
|
// validate parameters
|
|
const paramObject = operationObject.parameters;
|
|
const paramResult = this.checkParamSchema(paramObject);
|
|
result.reason.push(...paramResult.reason);
|
|
// validate total parameters count
|
|
if (paramResult.isValid && postBodyResult.isValid) {
|
|
const paramCountResult = this.validateParamCount(postBodyResult, paramResult);
|
|
result.reason.push(...paramCountResult.reason);
|
|
}
|
|
if (result.reason.length > 0) {
|
|
result.isValid = false;
|
|
}
|
|
return result;
|
|
}
|
|
validateParamCount(postBodyResult, paramResult) {
|
|
const result = { isValid: true, reason: [] };
|
|
const totalRequiredParams = postBodyResult.requiredNum + paramResult.requiredNum;
|
|
const totalParams = totalRequiredParams + postBodyResult.optionalNum + paramResult.optionalNum;
|
|
if (totalRequiredParams > 1) {
|
|
if (!this.options.allowMultipleParameters ||
|
|
totalRequiredParams > SMEValidator.SMERequiredParamsMaxNum) {
|
|
result.reason.push(ErrorType.ExceededRequiredParamsLimit);
|
|
}
|
|
}
|
|
else if (totalParams === 0) {
|
|
result.reason.push(ErrorType.NoParameter);
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
SMEValidator.SMERequiredParamsMaxNum = 5;
|
|
|
|
// Copyright (c) Microsoft Corporation.
|
|
class TeamsAIValidator extends Validator {
|
|
constructor(spec, options) {
|
|
super();
|
|
this.projectType = ProjectType.TeamsAi;
|
|
this.options = options;
|
|
this.spec = spec;
|
|
}
|
|
validateSpec() {
|
|
const result = { errors: [], warnings: [] };
|
|
// validate spec server
|
|
let validationResult = this.validateSpecServer();
|
|
result.errors.push(...validationResult.errors);
|
|
// validate no supported API
|
|
validationResult = this.validateSpecNoSupportAPI();
|
|
result.errors.push(...validationResult.errors);
|
|
return result;
|
|
}
|
|
validateAPI(method, path) {
|
|
const result = { isValid: true, reason: [] };
|
|
method = method.toLocaleLowerCase();
|
|
// validate method and path
|
|
const methodAndPathResult = this.validateMethodAndPath(method, path);
|
|
if (!methodAndPathResult.isValid) {
|
|
return methodAndPathResult;
|
|
}
|
|
const operationObject = this.spec.paths[path][method];
|
|
// validate operationId
|
|
if (!this.options.allowMissingId && !operationObject.operationId) {
|
|
result.reason.push(ErrorType.MissingOperationId);
|
|
}
|
|
// validate server
|
|
const validateServerResult = this.validateServer(method, path);
|
|
result.reason.push(...validateServerResult.reason);
|
|
if (result.reason.length > 0) {
|
|
result.isValid = false;
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
class ValidatorFactory {
|
|
static create(spec, options) {
|
|
var _a;
|
|
const type = (_a = options.projectType) !== null && _a !== void 0 ? _a : ProjectType.SME;
|
|
switch (type) {
|
|
case ProjectType.SME:
|
|
return new SMEValidator(spec, options);
|
|
case ProjectType.Copilot:
|
|
return new CopilotValidator(spec, options);
|
|
case ProjectType.TeamsAi:
|
|
return new TeamsAIValidator(spec, options);
|
|
default:
|
|
throw new Error(`Invalid project type: ${type}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Copyright (c) Microsoft Corporation.
|
|
class SpecFilter {
|
|
static specFilter(filter, unResolveSpec, resolvedSpec, options) {
|
|
var _a;
|
|
try {
|
|
const newSpec = Object.assign({}, unResolveSpec);
|
|
const newPaths = {};
|
|
for (const filterItem of filter) {
|
|
const [method, path] = filterItem.split(" ");
|
|
const methodName = method.toLowerCase();
|
|
const pathObj = (_a = resolvedSpec.paths) === null || _a === void 0 ? void 0 : _a[path];
|
|
if (ConstantString.AllOperationMethods.includes(methodName) &&
|
|
pathObj &&
|
|
pathObj[methodName]) {
|
|
const validator = ValidatorFactory.create(resolvedSpec, options);
|
|
const validateResult = validator.validateAPI(methodName, path);
|
|
if (!validateResult.isValid) {
|
|
continue;
|
|
}
|
|
if (!newPaths[path]) {
|
|
newPaths[path] = Object.assign({}, unResolveSpec.paths[path]);
|
|
for (const m of ConstantString.AllOperationMethods) {
|
|
delete newPaths[path][m];
|
|
}
|
|
}
|
|
newPaths[path][methodName] = unResolveSpec.paths[path][methodName];
|
|
// Add the operationId if missing
|
|
if (!newPaths[path][methodName].operationId) {
|
|
newPaths[path][methodName].operationId = `${methodName}${Utils.convertPathToCamelCase(path)}`;
|
|
}
|
|
}
|
|
}
|
|
newSpec.paths = newPaths;
|
|
return newSpec;
|
|
}
|
|
catch (err) {
|
|
throw new SpecParserError(err.toString(), ErrorType.FilterSpecFailed);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Copyright (c) Microsoft Corporation.
|
|
class AdaptiveCardGenerator {
|
|
static generateAdaptiveCard(operationItem) {
|
|
try {
|
|
const { json } = Utils.getResponseJson(operationItem);
|
|
let cardBody = [];
|
|
let schema = json.schema;
|
|
let jsonPath = "$";
|
|
if (schema && Object.keys(schema).length > 0) {
|
|
jsonPath = AdaptiveCardGenerator.getResponseJsonPathFromSchema(schema);
|
|
if (jsonPath !== "$") {
|
|
schema = schema.properties[jsonPath];
|
|
}
|
|
cardBody = AdaptiveCardGenerator.generateCardFromResponse(schema, "");
|
|
}
|
|
// if no schema, try to use example value
|
|
if (cardBody.length === 0 && (json.examples || json.example)) {
|
|
cardBody = [
|
|
{
|
|
type: ConstantString.TextBlockType,
|
|
text: "${jsonStringify($root)}",
|
|
wrap: true,
|
|
},
|
|
];
|
|
}
|
|
// if no example value, use default success response
|
|
if (cardBody.length === 0) {
|
|
cardBody = [
|
|
{
|
|
type: ConstantString.TextBlockType,
|
|
text: "success",
|
|
wrap: true,
|
|
},
|
|
];
|
|
}
|
|
const fullCard = {
|
|
type: ConstantString.AdaptiveCardType,
|
|
$schema: ConstantString.AdaptiveCardSchema,
|
|
version: ConstantString.AdaptiveCardVersion,
|
|
body: cardBody,
|
|
};
|
|
return [fullCard, jsonPath];
|
|
}
|
|
catch (err) {
|
|
throw new SpecParserError(err.toString(), ErrorType.GenerateAdaptiveCardFailed);
|
|
}
|
|
}
|
|
static generateCardFromResponse(schema, name, parentArrayName = "") {
|
|
if (schema.type === "array") {
|
|
// schema.items can be arbitrary object: schema { type: array, items: {} }
|
|
if (Object.keys(schema.items).length === 0) {
|
|
return [
|
|
{
|
|
type: ConstantString.TextBlockType,
|
|
text: name ? `${name}: \${jsonStringify(${name})}` : "result: ${jsonStringify($root)}",
|
|
wrap: true,
|
|
},
|
|
];
|
|
}
|
|
const obj = AdaptiveCardGenerator.generateCardFromResponse(schema.items, "", name);
|
|
const template = {
|
|
type: ConstantString.ContainerType,
|
|
$data: name ? `\${${name}}` : "${$root}",
|
|
items: Array(),
|
|
};
|
|
template.items.push(...obj);
|
|
return [template];
|
|
}
|
|
// some schema may not contain type but contain properties
|
|
if (schema.type === "object" || (!schema.type && schema.properties)) {
|
|
const { properties } = schema;
|
|
const result = [];
|
|
for (const property in properties) {
|
|
const obj = AdaptiveCardGenerator.generateCardFromResponse(properties[property], name ? `${name}.${property}` : property, parentArrayName);
|
|
result.push(...obj);
|
|
}
|
|
if (schema.additionalProperties) {
|
|
// TODO: better ways to handler warnings.
|
|
console.warn(ConstantString.AdditionalPropertiesNotSupported);
|
|
}
|
|
return result;
|
|
}
|
|
if (schema.type === "string" ||
|
|
schema.type === "integer" ||
|
|
schema.type === "boolean" ||
|
|
schema.type === "number") {
|
|
if (!AdaptiveCardGenerator.isImageUrlProperty(schema, name, parentArrayName)) {
|
|
// string in root: "ddd"
|
|
let text = "result: ${$root}";
|
|
if (name) {
|
|
// object { id: "1" }
|
|
text = `${name}: \${if(${name}, ${name}, 'N/A')}`;
|
|
if (parentArrayName) {
|
|
// object types inside array: { tags: ["id": 1, "name": "name"] }
|
|
text = `${parentArrayName}.${text}`;
|
|
}
|
|
}
|
|
else if (parentArrayName) {
|
|
// string array: photoUrls: ["1", "2"]
|
|
text = `${parentArrayName}: ` + "${$data}";
|
|
}
|
|
return [
|
|
{
|
|
type: ConstantString.TextBlockType,
|
|
text,
|
|
wrap: true,
|
|
},
|
|
];
|
|
}
|
|
else {
|
|
if (name) {
|
|
return [
|
|
{
|
|
type: "Image",
|
|
url: `\${${name}}`,
|
|
$when: `\${${name} != null}`,
|
|
},
|
|
];
|
|
}
|
|
else {
|
|
return [
|
|
{
|
|
type: "Image",
|
|
url: "${$data}",
|
|
$when: "${$data != null}",
|
|
},
|
|
];
|
|
}
|
|
}
|
|
}
|
|
if (schema.oneOf || schema.anyOf || schema.not || schema.allOf) {
|
|
throw new Error(Utils.format(ConstantString.SchemaNotSupported, JSON.stringify(schema)));
|
|
}
|
|
throw new Error(Utils.format(ConstantString.UnknownSchema, JSON.stringify(schema)));
|
|
}
|
|
// Find the first array property in the response schema object with the well-known name
|
|
static getResponseJsonPathFromSchema(schema) {
|
|
if (schema.type === "object" || (!schema.type && schema.properties)) {
|
|
const { properties } = schema;
|
|
for (const property in properties) {
|
|
const schema = properties[property];
|
|
if (schema.type === "array" &&
|
|
Utils.isWellKnownName(property, ConstantString.WellknownResultNames)) {
|
|
return property;
|
|
}
|
|
}
|
|
}
|
|
return "$";
|
|
}
|
|
static isImageUrlProperty(schema, name, parentArrayName) {
|
|
const propertyName = name ? name : parentArrayName;
|
|
return (!!propertyName &&
|
|
schema.type === "string" &&
|
|
Utils.isWellKnownName(propertyName, ConstantString.WellknownImageName) &&
|
|
(propertyName.toLocaleLowerCase().indexOf("url") >= 0 || schema.format === "uri"));
|
|
}
|
|
}
|
|
|
|
// Copyright (c) Microsoft Corporation.
|
|
function wrapAdaptiveCard(card, jsonPath) {
|
|
const result = {
|
|
version: ConstantString.WrappedCardVersion,
|
|
$schema: ConstantString.WrappedCardSchema,
|
|
jsonPath: jsonPath,
|
|
responseLayout: ConstantString.WrappedCardResponseLayout,
|
|
responseCardTemplate: card,
|
|
previewCardTemplate: inferPreviewCardTemplate(card),
|
|
};
|
|
return result;
|
|
}
|
|
function wrapResponseSemantics(card, jsonPath) {
|
|
const props = inferProperties(card);
|
|
const dataPath = jsonPath === "$" ? "$" : "$." + jsonPath;
|
|
const result = {
|
|
data_path: dataPath,
|
|
};
|
|
if (props.title || props.subtitle || props.imageUrl) {
|
|
result.properties = {};
|
|
if (props.title) {
|
|
result.properties.title = "$." + props.title;
|
|
}
|
|
if (props.subtitle) {
|
|
result.properties.subtitle = "$." + props.subtitle;
|
|
}
|
|
if (props.imageUrl) {
|
|
result.properties.url = "$." + props.imageUrl;
|
|
}
|
|
}
|
|
result.static_template = card;
|
|
return result;
|
|
}
|
|
/**
|
|
* Infers the preview card template from an Adaptive Card and a JSON path.
|
|
* The preview card template includes a title and an optional subtitle and image.
|
|
* It populates the preview card template with the first text block that matches
|
|
* each well-known name, in the order of title, subtitle, and image.
|
|
* If no text block matches the title or subtitle, it uses the first two text block as the title and subtitle.
|
|
* If the title is still empty and the subtitle is not empty, it uses subtitle as the title.
|
|
* @param card The Adaptive Card to infer the preview card template from.
|
|
* @param jsonPath The JSON path to the root object in the card body.
|
|
* @returns The inferred preview card template.
|
|
*/
|
|
function inferPreviewCardTemplate(card) {
|
|
const result = {
|
|
title: "result",
|
|
};
|
|
const inferredProperties = inferProperties(card);
|
|
if (inferredProperties.title) {
|
|
result.title = `\${if(${inferredProperties.title}, ${inferredProperties.title}, 'N/A')}`;
|
|
}
|
|
if (inferredProperties.subtitle) {
|
|
result.subtitle = `\${if(${inferredProperties.subtitle}, ${inferredProperties.subtitle}, 'N/A')}`;
|
|
}
|
|
if (inferredProperties.imageUrl) {
|
|
result.image = {
|
|
url: `\${${inferredProperties.imageUrl}}`,
|
|
alt: `\${if(${inferredProperties.imageUrl}, ${inferredProperties.imageUrl}, 'N/A')}`,
|
|
$when: `\${${inferredProperties.imageUrl} != null}`,
|
|
};
|
|
}
|
|
return result;
|
|
}
|
|
function inferProperties(card) {
|
|
var _a;
|
|
const result = {};
|
|
const nameSet = new Set();
|
|
let rootObject;
|
|
if (((_a = card.body[0]) === null || _a === void 0 ? void 0 : _a.type) === ConstantString.ContainerType) {
|
|
rootObject = card.body[0].items;
|
|
}
|
|
else {
|
|
rootObject = card.body;
|
|
}
|
|
for (const element of rootObject) {
|
|
if (element.type === ConstantString.TextBlockType) {
|
|
const textElement = element;
|
|
const index = textElement.text.indexOf("${if(");
|
|
if (index > 0) {
|
|
const text = textElement.text.substring(index);
|
|
const match = text.match(/\${if\(([^,]+),/);
|
|
const property = match ? match[1] : "";
|
|
if (property) {
|
|
nameSet.add(property);
|
|
}
|
|
}
|
|
}
|
|
else if (element.type === ConstantString.ImageType) {
|
|
const imageElement = element;
|
|
const match = imageElement.url.match(/\${([^,]+)}/);
|
|
const property = match ? match[1] : "";
|
|
if (property) {
|
|
nameSet.add(property);
|
|
}
|
|
}
|
|
}
|
|
for (const name of nameSet) {
|
|
if (!result.title && Utils.isWellKnownName(name, ConstantString.WellknownTitleName)) {
|
|
result.title = name;
|
|
nameSet.delete(name);
|
|
}
|
|
else if (!result.subtitle &&
|
|
Utils.isWellKnownName(name, ConstantString.WellknownSubtitleName)) {
|
|
result.subtitle = name;
|
|
nameSet.delete(name);
|
|
}
|
|
else if (!result.imageUrl && Utils.isWellKnownName(name, ConstantString.WellknownImageName)) {
|
|
result.imageUrl = name;
|
|
nameSet.delete(name);
|
|
}
|
|
}
|
|
for (const name of nameSet) {
|
|
if (!result.title) {
|
|
result.title = name;
|
|
nameSet.delete(name);
|
|
}
|
|
else if (!result.subtitle) {
|
|
result.subtitle = name;
|
|
nameSet.delete(name);
|
|
}
|
|
}
|
|
if (!result.title && result.subtitle) {
|
|
result.title = result.subtitle;
|
|
delete result.subtitle;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// Copyright (c) Microsoft Corporation.
|
|
class ManifestUpdater {
|
|
static async updateManifestWithAiPlugin(manifestPath, outputSpecPath, apiPluginFilePath, spec, options, authInfo) {
|
|
const manifest = await fs.readJSON(manifestPath);
|
|
const apiPluginRelativePath = ManifestUpdater.getRelativePath(manifestPath, apiPluginFilePath);
|
|
manifest.copilotExtensions = manifest.copilotExtensions || {};
|
|
// Insert plugins in manifest.json if it is plugin for Copilot.
|
|
if (!options.isGptPlugin) {
|
|
manifest.copilotExtensions.plugins = [
|
|
{
|
|
file: apiPluginRelativePath,
|
|
id: ConstantString.DefaultPluginId,
|
|
},
|
|
];
|
|
ManifestUpdater.updateManifestDescription(manifest, spec);
|
|
}
|
|
const appName = this.removeEnvs(manifest.name.short);
|
|
const specRelativePath = ManifestUpdater.getRelativePath(manifestPath, outputSpecPath);
|
|
const [apiPlugin, warnings] = await ManifestUpdater.generatePluginManifestSchema(spec, specRelativePath, apiPluginFilePath, appName, authInfo, options);
|
|
return [manifest, apiPlugin, warnings];
|
|
}
|
|
static updateManifestDescription(manifest, spec) {
|
|
var _a, _b;
|
|
manifest.description = {
|
|
short: spec.info.title.slice(0, ConstantString.ShortDescriptionMaxLens),
|
|
full: (_b = ((_a = spec.info.description) !== null && _a !== void 0 ? _a : manifest.description.full)) === null || _b === void 0 ? void 0 : _b.slice(0, ConstantString.FullDescriptionMaxLens),
|
|
};
|
|
}
|
|
static checkSchema(schema, method, pathUrl) {
|
|
if (schema.type === "array") {
|
|
const items = schema.items;
|
|
ManifestUpdater.checkSchema(items, method, pathUrl);
|
|
}
|
|
else if (schema.type !== "string" &&
|
|
schema.type !== "boolean" &&
|
|
schema.type !== "integer" &&
|
|
schema.type !== "number") {
|
|
throw new SpecParserError(Utils.format(ConstantString.UnsupportedSchema, method, pathUrl, JSON.stringify(schema)), ErrorType.UpdateManifestFailed);
|
|
}
|
|
}
|
|
static async generatePluginManifestSchema(spec, specRelativePath, apiPluginFilePath, appName, authInfo, options) {
|
|
var _a, _b, _c, _d;
|
|
const warnings = [];
|
|
const functions = [];
|
|
const functionNames = [];
|
|
const conversationStarters = [];
|
|
const paths = spec.paths;
|
|
const pluginAuthObj = {
|
|
type: "None",
|
|
};
|
|
if (authInfo) {
|
|
if (Utils.isOAuthWithAuthCodeFlow(authInfo.authScheme)) {
|
|
pluginAuthObj.type = "OAuthPluginVault";
|
|
}
|
|
else if (Utils.isBearerTokenAuth(authInfo.authScheme)) {
|
|
pluginAuthObj.type = "ApiKeyPluginVault";
|
|
}
|
|
if (pluginAuthObj.type !== "None") {
|
|
const safeRegistrationIdName = Utils.getSafeRegistrationIdEnvName(`${authInfo.name}_${ConstantString.RegistrationIdPostfix[authInfo.authScheme.type]}`);
|
|
pluginAuthObj.reference_id = `\${{${safeRegistrationIdName}}}`;
|
|
}
|
|
}
|
|
for (const pathUrl in paths) {
|
|
const pathItem = paths[pathUrl];
|
|
if (pathItem) {
|
|
const operations = pathItem;
|
|
for (const method in operations) {
|
|
if (options.allowMethods.includes(method)) {
|
|
const operationItem = operations[method];
|
|
const confirmationBodies = [];
|
|
if (operationItem) {
|
|
const operationId = operationItem.operationId;
|
|
const description = (_a = operationItem.description) !== null && _a !== void 0 ? _a : "";
|
|
const summary = operationItem.summary;
|
|
const paramObject = operationItem.parameters;
|
|
const requestBody = operationItem.requestBody;
|
|
if (paramObject) {
|
|
for (let i = 0; i < paramObject.length; i++) {
|
|
const param = paramObject[i];
|
|
const schema = param.schema;
|
|
ManifestUpdater.checkSchema(schema, method, pathUrl);
|
|
confirmationBodies.push(ManifestUpdater.getConfirmationBodyItem(param.name));
|
|
}
|
|
}
|
|
if (requestBody) {
|
|
const requestJsonBody = requestBody.content["application/json"];
|
|
const requestBodySchema = requestJsonBody.schema;
|
|
if (requestBodySchema.type === "object") {
|
|
for (const property in requestBodySchema.properties) {
|
|
const schema = requestBodySchema.properties[property];
|
|
ManifestUpdater.checkSchema(schema, method, pathUrl);
|
|
confirmationBodies.push(ManifestUpdater.getConfirmationBodyItem(property));
|
|
}
|
|
}
|
|
else {
|
|
throw new SpecParserError(Utils.format(ConstantString.UnsupportedSchema, method, pathUrl, JSON.stringify(requestBodySchema)), ErrorType.UpdateManifestFailed);
|
|
}
|
|
}
|
|
const funcObj = {
|
|
name: operationId,
|
|
description: description,
|
|
};
|
|
if (options.allowResponseSemantics) {
|
|
try {
|
|
const { json } = Utils.getResponseJson(operationItem);
|
|
if (json.schema) {
|
|
const [card, jsonPath] = AdaptiveCardGenerator.generateAdaptiveCard(operationItem);
|
|
card.body = card.body.slice(0, 5);
|
|
const responseSemantic = wrapResponseSemantics(card, jsonPath);
|
|
funcObj.capabilities = {
|
|
response_semantics: responseSemantic,
|
|
};
|
|
}
|
|
}
|
|
catch (err) {
|
|
warnings.push({
|
|
type: WarningType.GenerateCardFailed,
|
|
content: err.toString(),
|
|
data: operationId,
|
|
});
|
|
}
|
|
}
|
|
if (options.allowConfirmation && method !== ConstantString.GetMethod) {
|
|
if (!funcObj.capabilities) {
|
|
funcObj.capabilities = {};
|
|
}
|
|
funcObj.capabilities.confirmation = {
|
|
type: "AdaptiveCard",
|
|
title: (_b = operationItem.summary) !== null && _b !== void 0 ? _b : description,
|
|
};
|
|
if (confirmationBodies.length > 0) {
|
|
funcObj.capabilities.confirmation.body = confirmationBodies.join("\n");
|
|
}
|
|
}
|
|
functions.push(funcObj);
|
|
functionNames.push(operationId);
|
|
const conversationStarterStr = (summary !== null && summary !== void 0 ? summary : description).slice(0, ConstantString.ConversationStarterMaxLens);
|
|
if (conversationStarterStr) {
|
|
conversationStarters.push(conversationStarterStr);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let apiPlugin;
|
|
if (await fs.pathExists(apiPluginFilePath)) {
|
|
apiPlugin = await fs.readJSON(apiPluginFilePath);
|
|
}
|
|
else {
|
|
apiPlugin = {
|
|
$schema: ConstantString.PluginManifestSchema,
|
|
schema_version: "v2.1",
|
|
name_for_human: "",
|
|
description_for_human: "",
|
|
namespace: "",
|
|
functions: [],
|
|
runtimes: [],
|
|
};
|
|
}
|
|
apiPlugin.functions = apiPlugin.functions || [];
|
|
for (const func of functions) {
|
|
const index = (_c = apiPlugin.functions) === null || _c === void 0 ? void 0 : _c.findIndex((f) => f.name === func.name);
|
|
if (index === -1) {
|
|
apiPlugin.functions.push(func);
|
|
}
|
|
else {
|
|
apiPlugin.functions[index] = func;
|
|
}
|
|
}
|
|
apiPlugin.runtimes = apiPlugin.runtimes || [];
|
|
const index = apiPlugin.runtimes.findIndex((runtime) => {
|
|
var _a, _b;
|
|
return runtime.spec.url === specRelativePath &&
|
|
runtime.type === "OpenApi" &&
|
|
((_b = (_a = runtime.auth) === null || _a === void 0 ? void 0 : _a.type) !== null && _b !== void 0 ? _b : "None") === pluginAuthObj.type;
|
|
});
|
|
if (index === -1) {
|
|
apiPlugin.runtimes.push({
|
|
type: "OpenApi",
|
|
auth: pluginAuthObj,
|
|
spec: {
|
|
url: specRelativePath,
|
|
},
|
|
run_for_functions: functionNames,
|
|
});
|
|
}
|
|
else {
|
|
apiPlugin.runtimes[index].run_for_functions = functionNames;
|
|
}
|
|
if (!apiPlugin.name_for_human) {
|
|
apiPlugin.name_for_human = appName;
|
|
}
|
|
if (!apiPlugin.namespace) {
|
|
apiPlugin.namespace = ManifestUpdater.removeAllSpecialCharacters(appName);
|
|
}
|
|
if (!apiPlugin.description_for_human) {
|
|
apiPlugin.description_for_human =
|
|
(_d = spec.info.description) !== null && _d !== void 0 ? _d : "<Please add description of the plugin>";
|
|
}
|
|
if (options.allowConversationStarters && conversationStarters.length > 0) {
|
|
if (!apiPlugin.capabilities) {
|
|
apiPlugin.capabilities = {
|
|
localization: {},
|
|
};
|
|
}
|
|
if (!apiPlugin.capabilities.conversation_starters) {
|
|
apiPlugin.capabilities.conversation_starters = conversationStarters
|
|
.slice(0, 5)
|
|
.map((text) => ({ text }));
|
|
}
|
|
}
|
|
return [apiPlugin, warnings];
|
|
}
|
|
static async updateManifest(manifestPath, outputSpecPath, spec, options, adaptiveCardFolder, authInfo) {
|
|
try {
|
|
const originalManifest = await fs.readJSON(manifestPath);
|
|
const updatedPart = {};
|
|
updatedPart.composeExtensions = [];
|
|
let warnings = [];
|
|
if (options.projectType === ProjectType.SME) {
|
|
const updateResult = await ManifestUpdater.generateCommands(spec, manifestPath, options, adaptiveCardFolder);
|
|
const commands = updateResult[0];
|
|
warnings = updateResult[1];
|
|
const composeExtension = {
|
|
composeExtensionType: "apiBased",
|
|
apiSpecificationFile: ManifestUpdater.getRelativePath(manifestPath, outputSpecPath),
|
|
commands: commands,
|
|
};
|
|
if (authInfo) {
|
|
const auth = authInfo.authScheme;
|
|
const safeRegistrationIdName = Utils.getSafeRegistrationIdEnvName(`${authInfo.name}_${ConstantString.RegistrationIdPostfix[authInfo.authScheme.type]}`);
|
|
if (Utils.isAPIKeyAuth(auth) || Utils.isBearerTokenAuth(auth)) {
|
|
const safeApiSecretRegistrationId = Utils.getSafeRegistrationIdEnvName(`${authInfo.name}_${ConstantString.RegistrationIdPostfix[authInfo.authScheme.type]}`);
|
|
composeExtension.authorization = {
|
|
authType: "apiSecretServiceAuth",
|
|
apiSecretServiceAuthConfiguration: {
|
|
apiSecretRegistrationId: `\${{${safeRegistrationIdName}}}`,
|
|
},
|
|
};
|
|
}
|
|
else if (Utils.isOAuthWithAuthCodeFlow(auth)) {
|
|
composeExtension.authorization = {
|
|
authType: "oAuth2.0",
|
|
oAuthConfiguration: {
|
|
oauthConfigurationId: `\${{${safeRegistrationIdName}}}`,
|
|
},
|
|
};
|
|
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);
|
|
const updatedManifest = Object.assign(Object.assign({}, originalManifest), updatedPart);
|
|
return [updatedManifest, warnings];
|
|
}
|
|
catch (err) {
|
|
throw new SpecParserError(err.toString(), ErrorType.UpdateManifestFailed);
|
|
}
|
|
}
|
|
static async generateCommands(spec, manifestPath, options, adaptiveCardFolder) {
|
|
var _a;
|
|
const paths = spec.paths;
|
|
const commands = [];
|
|
const warnings = [];
|
|
if (paths) {
|
|
for (const pathUrl in paths) {
|
|
const pathItem = paths[pathUrl];
|
|
if (pathItem) {
|
|
const operations = pathItem;
|
|
// Currently only support GET and POST method
|
|
for (const method in operations) {
|
|
if ((_a = options.allowMethods) === null || _a === void 0 ? void 0 : _a.includes(method)) {
|
|
const operationItem = operations[method];
|
|
if (operationItem) {
|
|
const command = Utils.parseApiInfo(operationItem, options);
|
|
if (command.parameters &&
|
|
command.parameters.length >= 1 &&
|
|
command.parameters.some((param) => param.isRequired)) {
|
|
command.parameters = command.parameters.filter((param) => param.isRequired);
|
|
}
|
|
else if (command.parameters && command.parameters.length > 0) {
|
|
command.parameters = [command.parameters[0]];
|
|
warnings.push({
|
|
type: WarningType.OperationOnlyContainsOptionalParam,
|
|
content: Utils.format(ConstantString.OperationOnlyContainsOptionalParam, command.id),
|
|
data: command.id,
|
|
});
|
|
}
|
|
if (adaptiveCardFolder) {
|
|
const adaptiveCardPath = path.join(adaptiveCardFolder, command.id + ".json");
|
|
command.apiResponseRenderingTemplateFile = (await fs.pathExists(adaptiveCardPath))
|
|
? ManifestUpdater.getRelativePath(manifestPath, adaptiveCardPath)
|
|
: "";
|
|
}
|
|
commands.push(command);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return [commands, warnings];
|
|
}
|
|
static getRelativePath(from, to) {
|
|
const relativePath = path.relative(path.dirname(from), to);
|
|
return path.normalize(relativePath).replace(/\\/g, "/");
|
|
}
|
|
static removeEnvs(str) {
|
|
const placeHolderReg = /\${{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*}}/g;
|
|
const matches = placeHolderReg.exec(str);
|
|
let newStr = str;
|
|
if (matches != null) {
|
|
newStr = newStr.replace(matches[0], "");
|
|
}
|
|
return newStr;
|
|
}
|
|
static removeAllSpecialCharacters(str) {
|
|
return str.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
}
|
|
static getConfirmationBodyItem(paramName) {
|
|
return `* **${Utils.updateFirstLetter(paramName)}**: {{function.parameters.${paramName}}}`;
|
|
}
|
|
}
|
|
|
|
// Copyright (c) Microsoft Corporation.
|
|
/**
|
|
* A class that parses an OpenAPI specification file and provides methods to validate, list, and generate artifacts.
|
|
*/
|
|
class SpecParser {
|
|
/**
|
|
* 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, options) {
|
|
this.defaultOptions = {
|
|
allowMissingId: true,
|
|
allowSwagger: true,
|
|
allowAPIKeyAuth: false,
|
|
allowBearerTokenAuth: false,
|
|
allowMultipleParameters: false,
|
|
allowOauth2: false,
|
|
allowMethods: ["get", "post"],
|
|
allowConversationStarters: false,
|
|
allowResponseSemantics: false,
|
|
allowConfirmation: false,
|
|
projectType: ProjectType.SME,
|
|
isGptPlugin: false,
|
|
};
|
|
this.pathOrSpec = pathOrDoc;
|
|
this.parser = new SwaggerParser();
|
|
this.options = Object.assign(Object.assign({}, this.defaultOptions), (options !== null && options !== void 0 ? 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() {
|
|
try {
|
|
try {
|
|
await this.loadSpec();
|
|
await this.parser.validate(this.spec);
|
|
}
|
|
catch (e) {
|
|
return {
|
|
status: ValidationStatus.Error,
|
|
warnings: [],
|
|
errors: [{ type: ErrorType.SpecNotValid, content: e.toString() }],
|
|
};
|
|
}
|
|
const errors = [];
|
|
const warnings = [];
|
|
if (!this.options.allowSwagger && this.isSwaggerFile) {
|
|
return {
|
|
status: ValidationStatus.Error,
|
|
warnings: [],
|
|
errors: [
|
|
{ type: ErrorType.SwaggerNotSupported, content: ConstantString.SwaggerNotSupported },
|
|
],
|
|
};
|
|
}
|
|
// Remote reference not supported
|
|
const refPaths = this.parser.$refs.paths();
|
|
// refPaths [0] is the current spec file path
|
|
if (refPaths.length > 1) {
|
|
errors.push({
|
|
type: ErrorType.RemoteRefNotSupported,
|
|
content: Utils.format(ConstantString.RemoteRefNotSupported, refPaths.join(", ")),
|
|
data: refPaths,
|
|
});
|
|
}
|
|
if (!!this.isSwaggerFile && this.options.allowSwagger) {
|
|
warnings.push({
|
|
type: WarningType.ConvertSwaggerToOpenAPI,
|
|
content: ConstantString.ConvertSwaggerToOpenAPI,
|
|
});
|
|
}
|
|
const validator = this.getValidator(this.spec);
|
|
const validationResult = validator.validateSpec();
|
|
warnings.push(...validationResult.warnings);
|
|
errors.push(...validationResult.errors);
|
|
let status = ValidationStatus.Valid;
|
|
if (warnings.length > 0 && errors.length === 0) {
|
|
status = ValidationStatus.Warning;
|
|
}
|
|
else if (errors.length > 0) {
|
|
status = ValidationStatus.Error;
|
|
}
|
|
return {
|
|
status: status,
|
|
warnings: warnings,
|
|
errors: errors,
|
|
};
|
|
}
|
|
catch (err) {
|
|
throw new SpecParserError(err.toString(), ErrorType.ValidateFailed);
|
|
}
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
async listSupportedAPIInfo() {
|
|
throw new Error("Method not implemented.");
|
|
}
|
|
/**
|
|
* 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
|
|
*/
|
|
async list() {
|
|
var _a;
|
|
try {
|
|
await this.loadSpec();
|
|
const spec = this.spec;
|
|
const apiMap = this.getAPIs(spec);
|
|
const result = {
|
|
APIs: [],
|
|
allAPICount: 0,
|
|
validAPICount: 0,
|
|
};
|
|
for (const apiKey in apiMap) {
|
|
const { operation, isValid, reason } = apiMap[apiKey];
|
|
const [method, path] = apiKey.split(" ");
|
|
const operationId = (_a = operation.operationId) !== null && _a !== void 0 ? _a : `${method.toLowerCase()}${Utils.convertPathToCamelCase(path)}`;
|
|
const apiResult = {
|
|
api: apiKey,
|
|
server: "",
|
|
operationId: operationId,
|
|
isValid: isValid,
|
|
reason: reason,
|
|
};
|
|
if (isValid) {
|
|
const serverObj = Utils.getServerObject(spec, method.toLocaleLowerCase(), path);
|
|
if (serverObj) {
|
|
apiResult.server = Utils.resolveEnv(serverObj.url);
|
|
}
|
|
const authArray = Utils.getAuthArray(operation.security, spec);
|
|
for (const auths of authArray) {
|
|
if (auths.length === 1) {
|
|
apiResult.auth = auths[0];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
result.APIs.push(apiResult);
|
|
}
|
|
result.allAPICount = result.APIs.length;
|
|
result.validAPICount = result.APIs.filter((api) => api.isValid).length;
|
|
return result;
|
|
}
|
|
catch (err) {
|
|
if (err instanceof SpecParserError) {
|
|
throw err;
|
|
}
|
|
throw new SpecParserError(err.toString(), ErrorType.ListFailed);
|
|
}
|
|
}
|
|
/**
|
|
* Generate specs according to the filters.
|
|
* @param filter An array of strings that represent the filters to apply when generating the artifacts. If filter is empty, it would process nothing.
|
|
*/
|
|
async getFilteredSpecs(filter, signal) {
|
|
try {
|
|
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
|
|
throw new SpecParserError(ConstantString.CancelledMessage, ErrorType.Cancelled);
|
|
}
|
|
await this.loadSpec();
|
|
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
|
|
throw new SpecParserError(ConstantString.CancelledMessage, ErrorType.Cancelled);
|
|
}
|
|
const newUnResolvedSpec = SpecFilter.specFilter(filter, this.unResolveSpec, this.spec, this.options);
|
|
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
|
|
throw new SpecParserError(ConstantString.CancelledMessage, ErrorType.Cancelled);
|
|
}
|
|
const newSpec = (await this.parser.dereference(newUnResolvedSpec));
|
|
return [newUnResolvedSpec, newSpec];
|
|
}
|
|
catch (err) {
|
|
if (err instanceof SpecParserError) {
|
|
throw err;
|
|
}
|
|
throw new SpecParserError(err.toString(), ErrorType.GetSpecFailed);
|
|
}
|
|
}
|
|
/**
|
|
* 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 pluginFilePath File path of the api plugin file to generate.
|
|
*/
|
|
async generateForCopilot(manifestPath, filter, outputSpecPath, pluginFilePath, signal) {
|
|
const result = {
|
|
allSuccess: true,
|
|
warnings: [],
|
|
};
|
|
try {
|
|
const newSpecs = await this.getFilteredSpecs(filter, signal);
|
|
const newUnResolvedSpec = newSpecs[0];
|
|
const newSpec = newSpecs[1];
|
|
const authInfo = Utils.getAuthInfo(newSpec);
|
|
await this.saveFilterSpec(outputSpecPath, newUnResolvedSpec);
|
|
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
|
|
throw new SpecParserError(ConstantString.CancelledMessage, ErrorType.Cancelled);
|
|
}
|
|
const [updatedManifest, apiPlugin, warnings] = await ManifestUpdater.updateManifestWithAiPlugin(manifestPath, outputSpecPath, pluginFilePath, newSpec, this.options, authInfo);
|
|
result.warnings.push(...warnings);
|
|
await fs.outputJSON(manifestPath, updatedManifest, { spaces: 4 });
|
|
await fs.outputJSON(pluginFilePath, apiPlugin, { spaces: 4 });
|
|
}
|
|
catch (err) {
|
|
if (err instanceof SpecParserError) {
|
|
throw err;
|
|
}
|
|
throw new SpecParserError(err.toString(), ErrorType.GenerateFailed);
|
|
}
|
|
return result;
|
|
}
|
|
/**
|
|
* 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.
|
|
*/
|
|
async generate(manifestPath, filter, outputSpecPath, adaptiveCardFolder, signal) {
|
|
const result = {
|
|
allSuccess: true,
|
|
warnings: [],
|
|
};
|
|
try {
|
|
const newSpecs = await this.getFilteredSpecs(filter, signal);
|
|
const newUnResolvedSpec = newSpecs[0];
|
|
const newSpec = newSpecs[1];
|
|
let authInfo = undefined;
|
|
if (this.options.projectType === ProjectType.SME) {
|
|
authInfo = Utils.getAuthInfo(newSpec);
|
|
}
|
|
await this.saveFilterSpec(outputSpecPath, newUnResolvedSpec);
|
|
if (adaptiveCardFolder) {
|
|
for (const url in newSpec.paths) {
|
|
for (const method in newSpec.paths[url]) {
|
|
// paths object may contain description/summary which is not a http method, so we need to check if it is a operation object
|
|
if (this.options.allowMethods.includes(method)) {
|
|
const operation = newSpec.paths[url][method];
|
|
try {
|
|
const [card, jsonPath] = AdaptiveCardGenerator.generateAdaptiveCard(operation);
|
|
const fileName = path.join(adaptiveCardFolder, `${operation.operationId}.json`);
|
|
const wrappedCard = wrapAdaptiveCard(card, jsonPath);
|
|
await fs.outputJSON(fileName, wrappedCard, { spaces: 2 });
|
|
const dataFileName = path.join(adaptiveCardFolder, `${operation.operationId}.data.json`);
|
|
await fs.outputJSON(dataFileName, {}, { spaces: 2 });
|
|
}
|
|
catch (err) {
|
|
result.allSuccess = false;
|
|
result.warnings.push({
|
|
type: WarningType.GenerateCardFailed,
|
|
content: err.toString(),
|
|
data: operation.operationId,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
|
|
throw new SpecParserError(ConstantString.CancelledMessage, ErrorType.Cancelled);
|
|
}
|
|
const [updatedManifest, warnings] = await ManifestUpdater.updateManifest(manifestPath, outputSpecPath, newSpec, this.options, adaptiveCardFolder, authInfo);
|
|
await fs.outputJSON(manifestPath, updatedManifest, { spaces: 2 });
|
|
result.warnings.push(...warnings);
|
|
}
|
|
catch (err) {
|
|
if (err instanceof SpecParserError) {
|
|
throw err;
|
|
}
|
|
throw new SpecParserError(err.toString(), ErrorType.GenerateFailed);
|
|
}
|
|
return result;
|
|
}
|
|
async loadSpec() {
|
|
if (!this.spec) {
|
|
this.unResolveSpec = (await this.parser.parse(this.pathOrSpec));
|
|
// Convert swagger 2.0 to openapi 3.0
|
|
if (!this.unResolveSpec.openapi && this.unResolveSpec.swagger === "2.0") {
|
|
const specObj = await converter.convert(this.unResolveSpec, {});
|
|
this.unResolveSpec = specObj.openapi;
|
|
this.isSwaggerFile = true;
|
|
}
|
|
const clonedUnResolveSpec = JSON.parse(JSON.stringify(this.unResolveSpec));
|
|
this.spec = (await this.parser.dereference(clonedUnResolveSpec));
|
|
}
|
|
}
|
|
getAPIs(spec) {
|
|
const validator = this.getValidator(spec);
|
|
const apiMap = validator.listAPIs();
|
|
return apiMap;
|
|
}
|
|
getValidator(spec) {
|
|
if (this.validator) {
|
|
return this.validator;
|
|
}
|
|
const validator = ValidatorFactory.create(spec, this.options);
|
|
this.validator = validator;
|
|
return validator;
|
|
}
|
|
async saveFilterSpec(outputSpecPath, unResolvedSpec) {
|
|
let resultStr;
|
|
if (outputSpecPath.endsWith(".yaml") || outputSpecPath.endsWith(".yml")) {
|
|
resultStr = jsyaml.dump(unResolvedSpec);
|
|
}
|
|
else {
|
|
resultStr = JSON.stringify(unResolvedSpec, null, 2);
|
|
}
|
|
await fs.outputFile(outputSpecPath, resultStr);
|
|
}
|
|
}
|
|
|
|
export { AdaptiveCardGenerator, ConstantString, ErrorType, ProjectType, SpecParser, SpecParserError, Utils, ValidationStatus, WarningType };
|
|
//# sourceMappingURL=index.esm2017.mjs.map
|