lib/apiScenario/armUrlParser.ts (197 lines of code) (raw):
import { HttpMethods } from "@azure/core-http";
import { injectable } from "inversify";
import { JsonLoader } from "../swagger/jsonLoader";
import { Operation, Parameter } from "../swagger/swaggerTypes";
import { logger } from "./logger";
export type ArmScopeType =
| "Subscription"
| "ResourceGroup"
| "Tenant"
| "ManagementGroup"
| "Extension";
export type ArmMethodType =
| "Get"
| "GetCollection"
| "CreateOrUpdate"
| "Update"
| "Delete"
| "Head"
| "Action";
export type ArmApiInfo = ReturnType<ArmUrlParser["parseArmApiInfo"]>;
@injectable()
export class ArmUrlParser {
public constructor(private jsonLoader: JsonLoader) {}
public parseArmApiInfo(path: string, method: HttpMethods, operation?: Operation) {
const sp = path.split("/");
if (sp[0] !== "") {
throw new Error(`path must starts with "/": ${path}`);
}
sp.shift();
let lastProviderIdx = sp.length - 1;
while (lastProviderIdx >= 0) {
if (sp[lastProviderIdx].toLowerCase() === "providers") {
break;
}
lastProviderIdx--;
}
if (lastProviderIdx === -1) {
const scopeInfo = this.getArmScopeInfo(sp, path);
const methodInfo = this.getArmMethodInfo(sp, method, path, "", operation);
return {
scopePart: path,
provider: "",
...scopeInfo,
...methodInfo,
};
}
const provider = sp[lastProviderIdx + 1];
if (provider === undefined || provider.length === 0) {
throw new Error(`provider name cannot be detected in path: ${path}`);
}
const providerParamName = this.getParamNameForPathTemplate(provider);
let firstProviderIdx = sp.findIndex((val) => val.toLowerCase() === "providers");
if (firstProviderIdx === -1) {
firstProviderIdx = sp.length;
}
const scopeSlice = sp.slice(0, firstProviderIdx);
const scopePart = `/${scopeSlice.join("/")}`;
const scopeInfo = this.getArmScopeInfo(scopeSlice, path);
const resourceSlice = sp.slice(lastProviderIdx + 2);
const methodInfo = this.getArmMethodInfo(resourceSlice, method, path, provider, operation);
return {
scopePart,
providerParamName,
provider,
...scopeInfo,
...methodInfo,
};
}
private getArmScopeInfo(
scopeSlice: string[],
path: string
): {
scopeType: ArmScopeType | undefined;
subscriptionId?: string;
resourceGroupName?: string;
managementGroupName?: string;
} {
if (scopeSlice.length === 0) {
const managementGroupMatch =
/^\/providers\/Microsoft\.Management\/managementGroups\/(?<mgmtGroupName>[^\/]+)/gi.exec(
path
);
if (managementGroupMatch !== null) {
return { scopeType: "ManagementGroup", managementGroupName: managementGroupMatch[1] };
} else {
return { scopeType: "Tenant" };
}
}
if (scopeSlice.length === 1 && this.getParamNameForPathTemplate(scopeSlice[1]) !== undefined) {
// Special case for extension scope in swagger path template
return { scopeType: undefined };
}
if (
scopeSlice.length >= 4 &&
scopeSlice[0].toLowerCase() === "subscriptions" &&
scopeSlice[2].toLowerCase() === "resourcegroups"
) {
return {
scopeType: "ResourceGroup",
subscriptionId: scopeSlice[1],
resourceGroupName: scopeSlice[3],
};
}
if (scopeSlice.length >= 2 && scopeSlice[0].toLowerCase() === "subscriptions") {
return { scopeType: "Subscription", subscriptionId: scopeSlice[1] };
}
return { scopeType: undefined };
}
private getArmMethodInfo(
resourceSlice: string[],
httpMethod: HttpMethods,
path: string,
provider: string,
operation?: Operation
) {
let resourceUri = path;
let actionName: string | undefined = undefined;
const resourcePart = `/${resourceSlice.join("/")}`;
const resourceTypeArr = resourceSlice.filter((_, idx) => idx % 2 === 0);
const resourceNameArr = resourceSlice.filter((_, idx) => idx % 2 === 1);
const methodTypeMap: { [key in HttpMethods]?: ArmMethodType } = {
PUT: "CreateOrUpdate",
DELETE: "Delete",
GET: resourceSlice.length % 2 === 0 ? "Get" : "GetCollection",
PATCH: "Update",
HEAD: "Head",
POST: "Action",
};
const methodType = methodTypeMap[httpMethod];
if (methodType === undefined) {
throw new Error(`Unsupported http method ${httpMethod} in path: ${resourcePart}`);
}
if (methodType === "Action") {
if (resourceSlice.length % 2 === 0) {
// throw new Error(
// `Invalid ARM action part, should contains odd path segments: ${resourcePart}`
// );
logger.warn(`Invalid ARM action part, should contains odd path segments: ${path}`);
}
actionName = resourceTypeArr.pop();
resourceUri = path.slice(0, path.lastIndexOf("/"));
}
const resourceName = resourceNameArr.join("/");
let resourceTypes: string[] = [provider];
if (operation === undefined) {
resourceTypes[0] = `${provider}/${resourceTypeArr.join("/")}`;
} else {
for (const seg of resourceTypeArr) {
const paramName = this.getParamNameForPathTemplate(seg);
if (paramName === undefined) {
resourceTypes = resourceTypes.map((t) => `${t}/${seg}`);
} else {
const segTypes = this.getPathParamEnumValues(operation, paramName);
resourceTypes = resourceTypes
.map((t) => segTypes.map((s) => `${t}/${s}`))
.reduce((a, b) => a.concat(b), []);
}
}
}
return {
actionName,
methodType,
resourceName,
resourceTypes,
resourceUri,
resourcePart,
};
}
private getPathParamEnumValues(operation: Operation, paramName: string): string[] {
let param: Parameter | undefined = undefined;
for (const p of operation.parameters ?? []) {
const pa = this.jsonLoader.resolveRefObj(p);
if (pa.name === paramName) {
param = pa;
break;
}
}
if (param === undefined) {
throw new Error(
`Parameter name ${paramName} not found for operation ${operation.operationId}`
);
}
if (param.in !== "path") {
throw new Error(
`Parameter ${paramName} is not in path for operation ${operation.operationId}`
);
}
if (param.enum === undefined || param.enum.length === 0) {
throw new Error(
`Parameter ${paramName} without enum definition is not supported for operation ${operation.operationId}`
);
}
return param.enum as string[];
}
private getParamNameForPathTemplate(pathSeg: string | undefined) {
if (pathSeg !== undefined && pathSeg.startsWith("{") && pathSeg.endsWith("}")) {
return pathSeg.substr(0, pathSeg.length - 2);
}
return undefined;
}
}