diff --git a/src/utils/file.js b/src/utils/file.js index d560ed2c..7b3af268 100644 --- a/src/utils/file.js +++ b/src/utils/file.js @@ -1,3 +1,6 @@ +import fs from "fs"; +import path from "path"; + const FILE_EXTENSION_TO_PROGRAMMING_LANGUAGE_MAP = { in: "fortran", sh: "shell", @@ -28,3 +31,30 @@ export function formatFileSize(size, decimals = 2) { const units = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; return parseFloat((size / 1024 ** index).toFixed(decimals)) + " " + units[index]; } + +/** Get list of paths for files in a directory and filter by file extensions if provided. + * @param {string} dirPath - Path to current directory, i.e. $PWD + * @param {string[]} fileExtensions - File extensions to filter, e.g. `.yml` + * @param {boolean} resolvePath - whether to resolve the paths of files + * @returns {string[]} - Array of file paths + */ +export function getFilesInDirectory(dirPath, fileExtensions = [], resolvePath = true) { + let fileNames = fs.readdirSync(dirPath); + if (fileExtensions.length) { + fileNames = fileNames.filter((dirItem) => fileExtensions.includes(path.extname(dirItem))); + } + if (resolvePath) return fileNames.map((fileName) => path.resolve(dirPath, fileName)); + return fileNames; +} + +/** + * Get list of directories contained in current directory. + * @param {string} currentPath - current directory + * @return {*} + */ +export function getDirectories(currentPath) { + return fs + .readdirSync(currentPath, { withFileTypes: true }) + .filter((dirent) => dirent.isDirectory()) + .map((dirent) => dirent.name); +} diff --git a/src/utils/filter.js b/src/utils/filter.js new file mode 100644 index 00000000..254b42a7 --- /dev/null +++ b/src/utils/filter.js @@ -0,0 +1,53 @@ +import lodash from "lodash"; + +/** + * Check if one path matches regular expression or exact string. + * @param {{path: string}} pathObject - Entity or object with path property + * @param {Array<{path: string}|{regex: RegExp}>} filterObjects - Filter conditions + * @return {boolean} + */ +function isPathSupported(pathObject, filterObjects) { + return filterObjects.some((filterObj) => { + if (filterObj.path) { + return filterObj.path === pathObject.path; + } + if (filterObj.regex) { + return filterObj.regex.test(pathObject.path); + } + return false; + }); +} + +/** + * Check if _all_ paths in concatenated path match filtering conditions. + * @param {{path: string}} pathObject - Path object with concatenated path (multipath) + * @param {string} multiPathSeparator - String sequence used for concatenation of paths + * @param {Array<{path: string}|{regex: RegExp}>} filterObjects - Filter conditions + * @return {boolean} + */ +function isMultiPathSupported(pathObject, multiPathSeparator, filterObjects) { + const expandedPaths = pathObject.path.split(multiPathSeparator).map((p) => ({ path: p })); + return expandedPaths.every((expandedPath) => isPathSupported(expandedPath, filterObjects)); +} + +/** + * Filter list of entity paths or entities by paths and regular expressions. + * @param {Object[]} entitiesOrPaths - Array of objects defining entity path + * @param {Array<{ path: string }|{ regex: string }|{ regex: RegExp }>} filterObjects - Array of path or regular expression objects + * @param {string} multiPathSeparator - string by which paths should be split + * @return {Object[]} - filtered entity path objects or entities + */ +export function filterEntityList({ entitiesOrPaths, filterObjects = [], multiPathSeparator = "" }) { + const filterObjects_ = filterObjects.map((o) => (o.regex ? { regex: new RegExp(o.regex) } : o)); + + let filtered; + if (multiPathSeparator) { + filtered = entitiesOrPaths.filter((e) => + isMultiPathSupported(e, multiPathSeparator, filterObjects_), + ); + } else { + filtered = entitiesOrPaths.filter((e) => isPathSupported(e, filterObjects_)); + } + + return lodash.uniqBy(filtered, "path"); +} diff --git a/src/utils/index.js b/src/utils/index.js index 51d365d5..c1978d00 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -3,7 +3,13 @@ import { convertToCompactCSVArrayOfObjects, safeMakeArray } from "./array"; import { cloneClass, extendClass, extendClassStaticProps, extendThis } from "./class"; import { deepClone } from "./clone"; import { refreshCodeMirror } from "./codemirror"; -import { formatFileSize, getProgrammingLanguageFromFileExtension } from "./file"; +import { + formatFileSize, + getDirectories, + getFilesInDirectory, + getProgrammingLanguageFromFileExtension, +} from "./file"; +import { filterEntityList } from "./filter"; import { addUnit, removeUnit, replaceUnit, setNextLinks, setUnitsHead } from "./graph"; import { calculateHashFromObject, @@ -78,4 +84,7 @@ export { JsYamlTypes, JsYamlAllSchemas, renderTextWithSubstitutes, + filterEntityList, + getFilesInDirectory, + getDirectories, }; diff --git a/tests/utils/filter.tests.js b/tests/utils/filter.tests.js new file mode 100644 index 00000000..18d6eab3 --- /dev/null +++ b/tests/utils/filter.tests.js @@ -0,0 +1,74 @@ +import { expect } from "chai"; + +import { filterEntityList } from "../../src/utils/filter"; + +describe("entity filter", () => { + const entities = [ + { name: "A", path: "/root/entity/a" }, + { name: "B", path: "/root/entity/b" }, + { name: "C", path: "/root/entity/c" }, + { name: "D", path: "/root/entity/d" }, + ]; + + it("should filter an entity list with paths", () => { + const filterObjects = [{ path: "/root/entity/b" }, { path: "/root/entity/c" }]; + const filtered = filterEntityList({ filterObjects, entitiesOrPaths: entities }); + const expected = [ + { name: "B", path: "/root/entity/b" }, + { name: "C", path: "/root/entity/c" }, + ]; + expect(filtered).to.have.deep.members(expected); + }); + + it("should filter an entity list with regular expressions", () => { + const filterObjects = [{ regex: /\/root\/entity\/[bc]/ }]; + const filtered = filterEntityList({ filterObjects, entitiesOrPaths: entities }); + const expected = [ + { name: "B", path: "/root/entity/b" }, + { name: "C", path: "/root/entity/c" }, + ]; + expect(filtered).to.have.deep.members(expected); + }); + + it("should filter an entity list with both paths and regular expressions", () => { + const filterObjects = [{ path: "/root/entity/b" }, { regex: /\/root\/entity\/[c]/ }]; + const filtered = filterEntityList({ filterObjects, entitiesOrPaths: entities }); + const expected = [ + { name: "B", path: "/root/entity/b" }, + { name: "C", path: "/root/entity/c" }, + ]; + expect(filtered).to.have.deep.members(expected); + }); + + it("should filter an entity list containing concatenated paths", () => { + const filterObjects = [{ path: "/root/entity/b" }, { path: "/root/entity/c" }]; + const multiPathEntities = [ + { name: "AB", path: "/root/entity/a::/root/entity/b" }, + { name: "BC", path: "/root/entity/b::/root/entity/c" }, + ]; + const multiPathSeparator = "::"; + const filtered = filterEntityList({ + filterObjects, + entitiesOrPaths: multiPathEntities, + multiPathSeparator, + }); + const expected = [{ name: "BC", path: "/root/entity/b::/root/entity/c" }]; + expect(filtered).to.have.deep.members(expected); + }); + + it("should filter an entity list containing concatenated paths using regex", () => { + const filterObjects = [{ path: "/root/entity/b" }, { regex: /\/root\/entity\/[c]/ }]; + const multiPathEntities = [ + { name: "AB", path: "/root/entity/a::/root/entity/b" }, + { name: "BC", path: "/root/entity/b::/root/entity/c" }, + ]; + const multiPathSeparator = "::"; + const filtered = filterEntityList({ + filterObjects, + entitiesOrPaths: multiPathEntities, + multiPathSeparator, + }); + const expected = [{ name: "BC", path: "/root/entity/b::/root/entity/c" }]; + expect(filtered).to.have.deep.members(expected); + }); +});