lib/apiScenario/apiScenarioYamlLoader.ts (90 lines of code) (raw):
import { pathDirName, pathJoin, pathResolve, urlParse } from "@azure-tools/openapi-tools-common";
import { default as AjvInit, ValidateFunction } from "ajv";
import { injectable } from "inversify";
import { DEFAULT_SCHEMA, load as yamlLoad, Type, YAMLException } from "js-yaml";
import { FileLoader } from "../swagger/fileLoader";
import { Loader } from "../swagger/loader";
import { ApiScenarioDefinition } from "./apiScenarioSchema";
import { RawScenarioDefinition, RawStep, ReadmeTag } from "./apiScenarioTypes";
const ajv = new AjvInit({
useDefaults: true,
});
@injectable()
export class ApiScenarioYamlLoader implements Loader<[RawScenarioDefinition, ReadmeTag[]]> {
private fileCache: Map<string, string> = new Map();
private validateApiScenarioFile: ValidateFunction;
public constructor(private fileLoader: FileLoader) {
this.validateApiScenarioFile = ajv.compile(ApiScenarioDefinition);
}
public async load(filePath: string): Promise<[RawScenarioDefinition, ReadmeTag[]]> {
this.fileCache.clear();
const fileContent = await this.fileLoader.load(filePath);
yamlLoad(fileContent, {
schema: DEFAULT_SCHEMA.extend(
new Type("!include", {
kind: "scalar",
resolve: (data: any) => {
if (typeof data === "string") {
data = data.toLowerCase();
if (data.endsWith(".ps1") || data.endsWith(".sh")) {
return true;
}
}
throw new YAMLException(`unsupported include file: ${data}`);
},
construct: (data: string) => {
this.fileCache.set(data, "");
return data;
},
})
),
});
for (const file of this.fileCache.keys()) {
const fileContent = await this.fileLoader.load(
pathResolve(pathJoin(pathDirName(filePath), file))
);
this.fileCache.set(file, fileContent);
}
const filePayload = yamlLoad(fileContent, {
schema: DEFAULT_SCHEMA.extend(
new Type("!include", {
kind: "scalar",
construct: (data: string) => this.fileCache.get(data),
})
),
});
if (!this.validateApiScenarioFile(filePayload)) {
const err = this.validateApiScenarioFile.errors![0];
throw new Error(
`Failed to validate test resource file ${filePath}: ${err.dataPath} ${err.message}`
);
}
const readmeTags: ReadmeTag[] = [];
const tempSet = new Set<string>();
const rawDef = filePayload as RawScenarioDefinition;
const consumeStep = (step: RawStep) => {
if ("readmeTag" in step && step.readmeTag) {
if (!tempSet.has(step.readmeTag)) {
tempSet.add(step.readmeTag);
const match = /(\S+\/readme\.md)(#([a-z][a-z0-9-]+))?/i.exec(step.readmeTag);
if (match) {
const readmeFilePath = urlParse(match[1])
? match[1]
: pathResolve(pathJoin(pathDirName(this.fileLoader.resolvePath(filePath)), match[1]));
readmeTags.push({
name: step.readmeTag,
filePath: readmeFilePath,
tag: match[3],
});
} else {
throw new Error(`Invalid readmeTag: ${step.readmeTag} in step ${step}`);
}
}
}
};
rawDef.prepareSteps?.forEach(consumeStep);
rawDef.scenarios.forEach((scenario) => scenario.steps.forEach(consumeStep));
rawDef.cleanUpSteps?.forEach(consumeStep);
return [rawDef, readmeTags];
}
}