lib/transform/pathRegexTransformer.ts (141 lines of code) (raw):
import { parse as urlParse } from "url";
import { Key, pathToRegexp } from "path-to-regexp";
import { lowerHttpMethods, Parameter, PathParameter, Schema } from "../swagger/swaggerTypes";
import { xmsParameterizedHost } from "../util/constants";
import { OperationMatch } from "../liveValidation/operationSearcher";
import { resolveNestedDefinitionTransformer } from "./resolveNestedDefinitionTransformer";
import { SpecTransformer, TransformerType } from "./transformer";
import { traverseSwagger } from "./traverseSwagger";
export type RegExpWithKeys = RegExp & {
_keys: string[];
_hostTemplate?: boolean;
_hasMultiPathParam?: boolean;
};
const buildPathRegex = (
hostTemplate: string,
basePathPrefix: string,
path: string,
pathParams: Map<string, PathParameter>
): RegExpWithKeys => {
hostTemplate = hostTemplate.replace("https://", "");
hostTemplate = hostTemplate.replace("http://", "");
if (path.endsWith("/")) {
path = path.substr(0, path.length - 1);
}
const params: string[] = [];
function collectParamName(regResult: string) {
if (regResult) {
const paramName = regResult.replace("{", "").replace("}", "");
if (!params.includes(paramName)) {
params.push(paramName);
}
}
}
// collect all parameter name
const regHostParams = hostTemplate.match(/({[\w-]+})/gi);
if (regHostParams) {
regHostParams.forEach(collectParamName);
}
const regPathParams = path.match(/({[\w-]+})/gi);
if (regPathParams) {
regPathParams.forEach(collectParamName);
}
hostTemplate = hostTemplate.replace(/\(/g, "\\(").replace(/\)/g, "\\)");
path = path.replace(/\(/g, "\\(").replace(/\)/g, "\\)");
/**
* To support parameter name with dash(-), replace the parameter names to array index,
* and will restore the name in the result object regexp later.
* It caused by a design issue in pathToRegexp library that the parameter names must use "word characters" ([A-Za-z0-9_]).
* for more details,see https://github.com/pillarjs/path-to-regexp
**/
let hasMultiPathParam = false;
params.forEach((v, i) => {
if (path.startsWith(`/{${v}}`) && pathParams.get(v)) {
// We allow first param in path as multi path param
path = path.replace("{" + v + "}", "(.*)");
hasMultiPathParam = true;
} else {
hostTemplate = hostTemplate.replace("{" + v + "}", ":" + i);
path = path.replace("{" + v + "}", ":" + i);
}
});
const processedPath = hostTemplate + basePathPrefix + path;
const keys: Key[] = [];
const regexp = pathToRegexp(processedPath, keys, { sensitive: false });
// restore parameter name
const _keys: string[] = [];
keys.forEach((v, i) => {
if (params[v.name as any]) {
_keys[i + 1] = params[v.name as any];
}
});
const regexpWithKeys: RegExpWithKeys = regexp as RegExpWithKeys;
regexpWithKeys._keys = _keys;
if (hostTemplate !== "") {
regexpWithKeys._hostTemplate = true;
}
if (hasMultiPathParam) {
regexpWithKeys._hasMultiPathParam = true;
}
return regexpWithKeys;
};
export const pathRegexTransformer: SpecTransformer = {
type: TransformerType.Spec,
after: [resolveNestedDefinitionTransformer],
transform(spec, { schemaValidator, jsonLoader }) {
let basePathPrefix = spec.basePath ?? "";
if (basePathPrefix.endsWith("/")) {
basePathPrefix = basePathPrefix.substr(0, basePathPrefix.length - 1);
}
const msParameterizedHost = spec[xmsParameterizedHost];
const hostTemplate = msParameterizedHost?.hostTemplate ?? "";
const hostParams = msParameterizedHost?.parameters;
traverseSwagger(spec, {
onPath: (path, pathTemplate) => {
let pathStr = pathTemplate;
const queryIdx = pathTemplate.indexOf("?");
if (queryIdx !== -1) {
// path in x-ms-paths has query part we need to match
const queryMatch = urlParse(pathStr, true).query;
const querySchema: Schema = { type: "object", properties: {}, required: [] };
for (const queryKey of Object.keys(queryMatch)) {
const queryVal = queryMatch[queryKey];
querySchema.required!.push(queryKey);
querySchema.properties![queryKey] = {
enum: typeof queryVal === "string" ? [queryVal] : queryVal,
};
}
path._validateQuery = schemaValidator.compile(querySchema);
pathStr = pathTemplate.substr(0, queryIdx);
}
const pathParams = new Map<string, PathParameter>();
const collectPathParams = (params?: Parameter[]) => {
if (params !== undefined) {
for (const p of params) {
const param = jsonLoader.resolveRefObj(p);
if (param.in === "path") {
pathParams.set(param.name, param);
}
}
}
};
collectPathParams(hostParams);
collectPathParams(path.parameters);
for (const httpMethod of lowerHttpMethods) {
collectPathParams(path[httpMethod]?.parameters);
}
path._pathRegex = buildPathRegex(hostTemplate, basePathPrefix, pathStr, pathParams);
},
onOperation: (operation) => {
if (operation.externalDocs !== undefined) {
operation.externalDocs = undefined;
}
},
onResponse: (response) => {
if (response.examples !== undefined) {
response.examples = undefined;
}
},
});
},
};
export const extractPathParamValue = ({ pathRegex, pathMatch }: OperationMatch) => {
const pathParam: { [key: string]: string } = {};
const _keys = pathRegex._keys;
for (let idx = 1; idx < pathMatch.length; ++idx) {
if (_keys[idx] !== undefined) {
pathParam[_keys[idx]] = decodeURIComponent(pathMatch[idx]);
}
}
return pathParam;
};