diff --git a/server/scripts/parse-template-package.ts b/server/scripts/parse-template-package.ts index 2315973f..3db7d946 100644 --- a/server/scripts/parse-template-package.ts +++ b/server/scripts/parse-template-package.ts @@ -9,7 +9,7 @@ import { getProject } from "../src/parser"; -loadPackage("Buildings"); +loadPackage("Buildings.Templates"); const { options, scheduleOptions } = getOptions(); diff --git a/server/src/parser/index.ts b/server/src/parser/index.ts index 2d60938e..b25fb965 100644 --- a/server/src/parser/index.ts +++ b/server/src/parser/index.ts @@ -6,16 +6,12 @@ export { SystemTypeN as SystemType, Template } from "./template"; /** * - * @param packagePath Absolute path to package + * @param packageName Full class name of package (e.g. "Library.Package.SubPackage") * * @returns Templates */ -export function loadPackage(packagePath: string): templates.SystemTemplateN[] { - // - const parsedPath = path.parse(packagePath); - parser.setPathPrefix(parsedPath.dir); - parser.loadPackage(parsedPath.name); - +export function loadPackage(packageName: string): templates.SystemTemplateN[] { + parser.loadPackage(packageName); return templates.getTemplates().map((t) => t.getSystemTemplate()); } @@ -37,4 +33,4 @@ export function getAllTemplates(): templates.Template[] { export function getProject(): templates.Project { return templates.getProject(); -} \ No newline at end of file +} diff --git a/server/src/parser/loader.ts b/server/src/parser/loader.ts index e479d16d..abcd6717 100644 --- a/server/src/parser/loader.ts +++ b/server/src/parser/loader.ts @@ -6,22 +6,36 @@ import { typeStore } from "./parser"; import config from "../../src/config"; export const TEMPLATE_IDENTIFIER = "__ctrlFlow_template"; -export const MODELICAPATH = [ +export let MODELICA_JSON_PATH = [ `${config.MODELICA_DEPENDENCIES}/template-json/json/`, ]; -function _toModelicaPath(filePath: string) { +// Used for testing: registers additional search paths for Modelica JSON files +export function prependToModelicaJsonPath(paths: string[]) { + MODELICA_JSON_PATH = [...paths, ...MODELICA_JSON_PATH]; +} + +export function getClassNameFromRelativePath(filePath: string) { filePath = filePath.endsWith(".json") ? filePath.slice(0, -5) : filePath; return filePath.replace(/\//g, "."); } +/** + * Finds all entry points that contain the template identifier for a given package. + * - LIMITATION: This function requires that the package uses + * [Directory Hierarchy Mapping](https://specification.modelica.org/maint/3.6/packages.html#directory-hierarchy-mapping) + * @param packageName - The Modelica class name of the package to search for entry points + * @returns An array of objects containing the path and parsed JSON for each entry point found + */ export function findPackageEntryPoints( - prefix: string, - reference: string, + packageName: string, ): { path: string; json: Object | undefined }[] { const entryPoints: { path: string; json: Object | undefined }[] = []; - [prefix, ...MODELICAPATH].forEach((dir) => { - const dirPath = path.resolve(dir, reference); + MODELICA_JSON_PATH.forEach((dir) => { + // We need a top directory to look up for entry points + // so we can simply convert the class name to a relative path + // without adding any file extension. + const dirPath = path.resolve(dir, packageName.replace(/\./g, "/")); if (fs.existsSync(dirPath)) { const cmd = `grep -rl ${dirPath} -e "${TEMPLATE_IDENTIFIER}"`; const response = execSync(cmd).toString(); @@ -32,10 +46,10 @@ export function findPackageEntryPoints( .sort((a, b) => (a.includes("package.json") ? -1 : 1)) .map((p) => path.relative(dir, p)) .map((p) => { - const path = _toModelicaPath(p); + const path = getClassNameFromRelativePath(p); return { path: path, - json: loader(dir, path), + json: loader(path), }; }), ); @@ -46,33 +60,36 @@ export function findPackageEntryPoints( } /** - * Searched the provided directory for a given - * @param prefix directory to search - * @param filePath path to try and find - * - * @returns the found file path or null if not found + * Gets the path to a Modelica JSON file based on the full class name. + * - LIMITATION: This function requires that the library packages use + * [Directory Hierarchy Mapping](https://specification.modelica.org/maint/3.6/packages.html#directory-hierarchy-mapping) + * @param className - The full Modelica class name (e.g. "Library.Package.Class") + * @param dirPath - The directory path to search in + * @returns The file path if found, null otherwise */ -function _findPath(prefix: string, reference: string): string | null { - let filePath = path.parse(reference.replace(/\./g, "/")); +function getPathFromClassName( + className: string, + dirPath: string, +): string | null { + let filePath = path.parse(className.replace(/\./g, "/")); - let jsonFile = path.resolve(prefix, filePath.dir, `${filePath.name}.json`); + let jsonFile = path.resolve(dirPath, filePath.dir, `${filePath.name}.json`); while (!fs.existsSync(jsonFile) && filePath.name) { // check if definition already exists // TODO - construct this path correctly... const curPath = path.relative(filePath.dir, filePath.name); - const modelicaPath = _toModelicaPath(curPath); + const modelicaPath = getClassNameFromRelativePath(curPath); if (typeStore.has(modelicaPath)) { break; } // package definitions break the typical modelica path to file mapping that // is used. A typical modelica path to file path look like: - // 'Template.AirHandlerFans.VAVMultizone' -> 'Template/AirhandlerFans/VAVMultizone + // 'Template.AirHandlerFans.VAVMultizone' -> 'Template/AirhandlerFans/VAVMultizone.json' // We need to support mapping like this as well: - // 'Template.AirHandlerFans -> Template/AirhandlerFans/package' - // 'package' files behave kind of like 'index.html' files + // 'Template.AirHandlerFans -> Template/AirhandlerFans/package.json' jsonFile = path.resolve( - prefix, + dirPath, filePath.dir, filePath.name, "package.json", @@ -81,20 +98,22 @@ function _findPath(prefix: string, reference: string): string | null { break; } filePath = path.parse(filePath.dir); - jsonFile = path.resolve(prefix, filePath.dir, `${filePath.name}.json`); + jsonFile = path.resolve(dirPath, filePath.dir, `${filePath.name}.json`); } return fs.existsSync(jsonFile) ? jsonFile : null; } -// When given a path, loads types. returns null if not found -export function loader(prefix: string, reference: string): Object | undefined { - const modelicaDirs = [prefix, ...MODELICAPATH]; - +/** + * Loads a Modelica JSON file given the full class name. + * @param className The full Modelica class name to load (e.g. "Library.Package.Class") + * @returns The loaded JSON object or undefined if not found + */ +export function loader(className: string): Object | undefined { // TODO: allow modelica paths - if (!reference.startsWith("Modelica")) { - for (const dir of modelicaDirs) { - const jsonFile = _findPath(dir, reference); + if (!className.startsWith("Modelica")) { + for (const dir of MODELICA_JSON_PATH) { + const jsonFile = getPathFromClassName(className, dir); if (jsonFile && fs.existsSync(jsonFile)) { return require(jsonFile); } diff --git a/server/src/parser/parser.ts b/server/src/parser/parser.ts index eeaa5ffc..97211b2f 100644 --- a/server/src/parser/parser.ts +++ b/server/src/parser/parser.ts @@ -1141,28 +1141,26 @@ export class File { } } -let pathPrefix = ""; -export function setPathPrefix(prefix: string) { - pathPrefix = prefix; -} - /** * Extracts the given file into the type store + * @param filePath - The ***relative*** path to the file to load (e.g. "Buildings/Templates/File") */ -export const getFile = (filePath: string) => { - const jsonData = loader(pathPrefix, filePath); +export const getFile = (className: string) => { + const jsonData = loader(className); if (jsonData) { - return new File(jsonData, filePath); + return new File(jsonData, className); } else { // console.log(`Not found: ${filePath}`); } }; -// Searches a package for templates, then loads the file -// creating template instances -export const loadPackage = (filePath: string) => { - const paths = findPackageEntryPoints(pathPrefix, filePath); - +/** + * Searches a package for templates, then loads the file + * creating template instances. + * @param packageName - The full class name of the package to load (e.g. "Library.Package.SubPackage") + */ +export const loadPackage = (packageName: string) => { + const paths = findPackageEntryPoints(packageName); paths?.map(({ json, path }) => new File(json, path)); // Attempt to load project settings from a pre-defined path diff --git a/server/tests/integration/parser/expression.test.ts b/server/tests/integration/parser/expression.test.ts index 20a3d47f..d7a0dc50 100644 --- a/server/tests/integration/parser/expression.test.ts +++ b/server/tests/integration/parser/expression.test.ts @@ -1,6 +1,5 @@ -import { createTestModelicaJson, fullTempDirPath } from "./utils"; import { ModifiersN, getTemplates } from "../../../src/parser/template"; -import { loadPackage, getSystemTypes, Template } from "../../../src/parser/"; +import { loadPackage, Template } from "../../../src/parser/"; import { initializeTestModelicaJson } from "./utils"; import * as parser from "../../../src/parser/parser"; const testModelicaFile = "TestPackage.Template.TestTemplate"; @@ -10,8 +9,8 @@ let template: Template | undefined; describe("Expression", () => { beforeAll(() => { - createTestModelicaJson(); - loadPackage(`${fullTempDirPath}/TestPackage`); + initializeTestModelicaJson(); + loadPackage('TestPackage'); const templates = getTemplates(); template = templates.find( (t) => t.modelicaPath === templatePath, diff --git a/server/tests/integration/parser/loader.test.ts b/server/tests/integration/parser/loader.test.ts index 37d82ec2..e8a4c068 100644 --- a/server/tests/integration/parser/loader.test.ts +++ b/server/tests/integration/parser/loader.test.ts @@ -21,10 +21,10 @@ describe("Parser file loading", () => { }); it("Discovers template files and project options", () => { - const packagePath = "TestPackage"; - const projectOptionsPath = "Buildings.Templates.Data.AllSystems"; - parser.loadPackage(packagePath); - const projectOptionElement = parser.typeStore.find(projectOptionsPath); + const packageName = "TestPackage"; + const projectOptionsClassName = "Buildings.Templates.Data.AllSystems"; + parser.loadPackage(packageName); + const projectOptionElement = parser.typeStore.find(projectOptionsClassName); expect(projectOptionElement).toBeDefined(); }); }); diff --git a/server/tests/integration/parser/modifiers.test.ts b/server/tests/integration/parser/modifiers.test.ts index 1076f896..e91d4bd5 100644 --- a/server/tests/integration/parser/modifiers.test.ts +++ b/server/tests/integration/parser/modifiers.test.ts @@ -1,4 +1,4 @@ -import { createTestModelicaJson, fullTempDirPath } from "./utils"; +import { initializeTestModelicaJson } from "./utils"; import { getTemplates, Option, @@ -21,8 +21,8 @@ const isExpression = (obj: any) => describe("Modifications", () => { beforeAll(() => { - createTestModelicaJson(); - loadPackage(`${fullTempDirPath}/TestPackage`); + initializeTestModelicaJson(); + loadPackage('TestPackage'); const templates = getTemplates(); const template = templates.find( (t) => t.modelicaPath === templatePath, diff --git a/server/tests/integration/parser/path-expansion.test.ts b/server/tests/integration/parser/path-expansion.test.ts index 1fd56ee2..59d57faa 100644 --- a/server/tests/integration/parser/path-expansion.test.ts +++ b/server/tests/integration/parser/path-expansion.test.ts @@ -1,4 +1,4 @@ -import { createTestModelicaJson, fullTempDirPath } from "./utils"; +import { initializeTestModelicaJson } from "./utils"; import { loadPackage, getOptions } from "../../../src/parser/"; import { evaluateExpression } from "../../../src/parser/expression"; @@ -9,8 +9,8 @@ import { evaluateExpression } from "../../../src/parser/expression"; describe("Path Expansion", () => { beforeAll(() => { - createTestModelicaJson(); - loadPackage(`${fullTempDirPath}/TestPackage`); + initializeTestModelicaJson(); + loadPackage('TestPackage'); }); it("Parameter type paths are expanded", () => { diff --git a/server/tests/integration/parser/template.test.ts b/server/tests/integration/parser/template.test.ts index eaacb686..6765c13f 100644 --- a/server/tests/integration/parser/template.test.ts +++ b/server/tests/integration/parser/template.test.ts @@ -1,4 +1,4 @@ -import { createTestModelicaJson, fullTempDirPath } from "./utils"; +import { initializeTestModelicaJson } from "./utils"; import { loadPackage, getSystemTypes, @@ -13,8 +13,8 @@ const NESTED_TEMPLATE_PATH = describe("Template wrapper class functionality", () => { beforeAll(() => { - createTestModelicaJson(); - loadPackage(`${fullTempDirPath}/TestPackage`); + initializeTestModelicaJson(); + loadPackage('TestPackage'); }); it("Extracts two templates and three Template types to be in stores", () => { @@ -170,8 +170,8 @@ const PROJECT_INSTANCE_NAME = "datAll"; describe("'Project' items are extracted", () => { beforeAll(() => { - createTestModelicaJson(); - loadPackage(`${fullTempDirPath}/TestPackage`); + initializeTestModelicaJson(); + loadPackage('TestPackage'); }); it("Project is populated and all project options are included in options", () => { const project = getProject(); diff --git a/server/tests/integration/parser/utils.ts b/server/tests/integration/parser/utils.ts index a0e329a2..0bb937db 100644 --- a/server/tests/integration/parser/utils.ts +++ b/server/tests/integration/parser/utils.ts @@ -1,4 +1,5 @@ import * as parser from "../../../src/parser/parser"; +import { prependToModelicaJsonPath } from '../../../src/parser/loader'; import { createModelicaJson } from "../../../scripts/generate-modelica-json"; // NOTE: if the test modelica package changes it will need to be @@ -21,7 +22,7 @@ export function createTestModelicaJson() { */ export function initializeTestModelicaJson() { createTestModelicaJson(); - parser.setPathPrefix(fullTempDirPath); + prependToModelicaJsonPath([fullTempDirPath]); } type SimpleOption = {