UNPKG

counterfact

Version:

a library for building a fake REST API for testing

151 lines (150 loc) 5.86 kB
function isDirectory(test) { return test !== undefined; } export class ModuleTree { root = { directories: {}, files: {}, isWildcard: false, name: "", rawName: "", }; putDirectory(directory, segments) { const [segment, ...remainingSegments] = segments; if (segment === undefined) { throw new Error("segments array is empty"); } if (remainingSegments.length === 0) { return directory; } const nextDirectory = (directory.directories[segment.toLowerCase()] ??= { directories: {}, files: {}, isWildcard: segment.startsWith("{"), name: segment.replace(/^\{(?<name>.*)\}$/u, "$<name>"), rawName: segment, }); return this.putDirectory(nextDirectory, remainingSegments); } addModuleToDirectory(directory, segments, module) { if (directory === undefined) { return; } const targetDirectory = this.putDirectory(directory, segments); const filename = segments.at(-1); if (filename === undefined) { throw new Error("The file name (the last segment of the URL) is undefined. This is theoretically impossible but TypeScript can't enforce it."); } targetDirectory.files[filename] = { isWildcard: filename.startsWith("{"), module, name: filename.replace(/^\{(?<name>.*)\}$/u, "$<name>"), rawName: filename, }; } add(url, module) { this.addModuleToDirectory(this.root, url.split("/").slice(1), module); } removeModuleFromDirectory(directory, segments) { if (!isDirectory(directory)) { return; } const [segment, ...remainingSegments] = segments; if (segment === undefined) { return; } if (remainingSegments.length === 0) { delete directory.files[segment.toLowerCase()]; return; } this.removeModuleFromDirectory(directory.directories[segment.toLowerCase()], remainingSegments); } remove(url) { const segments = url.split("/").slice(1); this.removeModuleFromDirectory(this.root, segments); } fileModuleDefined(file, method) { return file.module[method] !== undefined; } buildMatch(directory, segment, pathVariables, matchedPath, method) { function normalizedSegment(segment, directory) { for (const file in directory.files) { if (file.toLowerCase() === segment.toLowerCase()) { return file; } } return ""; } const match = directory.files[normalizedSegment(segment, directory)] ?? Object.values(directory.files).find((file) => file.isWildcard && this.fileModuleDefined(file, method)); if (match === undefined) { return undefined; } if (match.isWildcard) { return { ...match, matchedPath: `${matchedPath}/${match.rawName}`, pathVariables: { ...pathVariables, [match.name]: segment, }, }; } return { ...match, matchedPath: `${matchedPath}/${match.rawName}`, pathVariables, }; } matchWithinDirectory(directory, segments, pathVariables, matchedPath, method) { if (segments.length === 0) { return undefined; } const [segment, ...remainingSegments] = segments; if (segment === undefined) { throw new Error("segment cannot be undefined but TypeScript doesn't know that"); } if (remainingSegments.length === 0 || (remainingSegments.length === 1 && remainingSegments[0] === "")) { return this.buildMatch(directory, segment, pathVariables, matchedPath, method); } const exactMatch = directory.directories[segment.toLowerCase()]; if (isDirectory(exactMatch)) { return this.matchWithinDirectory(exactMatch, remainingSegments, pathVariables, `${matchedPath}/${segment}`, method); } const wildcardDirectories = Object.values(directory.directories).filter((subdirectory) => subdirectory.isWildcard); for (const wildcardDirectory of wildcardDirectories) { const match = this.matchWithinDirectory(wildcardDirectory, remainingSegments, { ...pathVariables, [wildcardDirectory.name]: segment, }, `${matchedPath}/${wildcardDirectory.rawName}`, method); if (match !== undefined) { return match; } } return undefined; } match(url, method) { return this.matchWithinDirectory(this.root, url.split("/").slice(1), {}, "", method); } get routes() { const routes = []; function traverse(directory, path) { Object.values(directory.directories).forEach((subdirectory) => { traverse(subdirectory, `${path}/${subdirectory.rawName}`); }); Object.values(directory.files).forEach((file) => { const methods = Object.entries(file.module).map(([method, implementation]) => [method, String(implementation)]); routes.push({ methods: Object.fromEntries(methods), path: `${path}/${file.rawName}`, }); }); } function stripBrackets(string) { return string.replaceAll(/\{|\}/gu, ""); } traverse(this.root, ""); return routes.sort((first, second) => stripBrackets(first.path).localeCompare(stripBrackets(second.path))); } }