UNPKG

@finos/legend-studio

Version:
349 lines 18.3 kB
/** * Copyright (c) 2020-present, Goldman Sachs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { observable, flow, action, computed, makeObservable, flowResult, } from 'mobx'; import { LogEvent, assertTrue, assertErrorThrown, guaranteeNonNullable, isNonNullable, } from '@finos/legend-shared'; import { LEGEND_STUDIO_APP_EVENT } from '../LegendStudioAppEvent.js'; import { GenerationDirectory, GENERATION_FILE_ROOT_NAME, GenerationFile, getGenerationTreeData, openNode, populateDirectoryTreeNodeChildren, buildGenerationDirectory, reprocessOpenNodes, } from '../shared/FileGenerationTreeUtil.js'; import { ExplorerTreeRootPackageLabel } from '../ExplorerTreeState.js'; import { FileGenerationViewerState } from './FileGenerationViewerState.js'; import { ElementEditorState } from './element-editor-state/ElementEditorState.js'; import { ElementFileGenerationState } from './element-editor-state/ElementFileGenerationState.js'; import { Class, Enumeration, GenerationSpecification, ELEMENT_PATH_DELIMITER, } from '@finos/legend-graph'; import { ExternalFormatState } from './ExternalFormatState.js'; import { generationSpecification_addFileGeneration, generationSpecification_addGenerationElement, } from '../graphModifier/DSLGeneration_GraphModifierHelper.js'; export const DEFAULT_GENERATION_SPECIFICATION_NAME = 'MyGenerationSpecification'; export class GraphGenerationState { editorStore; isRunningGlobalGenerate = false; generatedEntities = new Map(); isClearingGenerationEntities = false; externalFormatState; // NOTE: this will eventually be removed once we also do model/schema import using external format // See https://github.com/finos/legend-studio/issues/866 fileGenerationConfigurations = []; // file generation output rootFileDirectory; filesIndex = new Map(); selectedNode; constructor(editorStore) { makeObservable(this, { isRunningGlobalGenerate: observable, generatedEntities: observable.shallow, isClearingGenerationEntities: observable, fileGenerationConfigurations: observable, externalFormatState: observable, rootFileDirectory: observable, filesIndex: observable, selectedNode: observable.ref, fileGenerationConfigurationOptions: computed, supportedFileGenerationConfigurationsForCurrentElement: computed, setFileGenerationConfigurations: action, processGenerationResult: action, reprocessGenerationFileState: action, reprocessNodeTree: action, onTreeNodeSelect: action, setSelectedNode: action, emptyFileGeneration: action, possiblyAddMissingGenerationSpecifications: flow, fetchAvailableFileGenerationDescriptions: flow, globalGenerate: flow, generateModels: flow, generateFiles: flow, clearGenerations: flow, }); this.editorStore = editorStore; this.rootFileDirectory = new GenerationDirectory(GENERATION_FILE_ROOT_NAME); this.externalFormatState = new ExternalFormatState(editorStore); } get fileGenerationConfigurationOptions() { return this.fileGenerationConfigurations .slice() .sort((a, b) => a.label.localeCompare(b.label)) .map((config) => ({ label: config.label, value: config.key, })); } get supportedFileGenerationConfigurationsForCurrentElement() { if (this.editorStore.currentEditorState instanceof ElementEditorState) { const currentElement = this.editorStore.currentEditorState.element; // Note: For now we only allow classes and enumerations for all types of generations. const extraFileGenerationScopeFilterConfigurations = this.editorStore.pluginManager .getApplicationPlugins() .flatMap((plugin) => plugin.getExtraFileGenerationScopeFilterConfigurations?.() ?? []); return this.fileGenerationConfigurations.filter((generationType) => { const scopeFilters = extraFileGenerationScopeFilterConfigurations.filter((configuration) => configuration.type.toLowerCase() === generationType.key); if (scopeFilters.length) { return scopeFilters.some((scopeFilter) => scopeFilter.filter(currentElement)); } return (currentElement instanceof Class || currentElement instanceof Enumeration); }); } return []; } findGenerationParentPath(genChildPath) { const genEntity = Array.from(this.generatedEntities.entries()).find(([, genEntities]) => genEntities.find((m) => m.path === genChildPath)); return genEntity?.[0]; } setFileGenerationConfigurations(fileGenerationConfigurations) { this.fileGenerationConfigurations = fileGenerationConfigurations; } getFileGenerationConfiguration(type) { return guaranteeNonNullable(this.fileGenerationConfigurations.find((config) => config.key === type), `Can't find configuration description for file generation type '${type}'`); } *fetchAvailableFileGenerationDescriptions() { try { const availableFileGenerationDescriptions = (yield this.editorStore.graphManagerState.graphManager.getAvailableGenerationConfigurationDescriptions()); this.setFileGenerationConfigurations(availableFileGenerationDescriptions); this.editorStore.elementGenerationStates = this.fileGenerationConfigurations.map((config) => new ElementFileGenerationState(this.editorStore, config.key)); } catch (error) { assertErrorThrown(error); this.editorStore.applicationStore.log.error(LogEvent.create(LEGEND_STUDIO_APP_EVENT.GENERATION_FAILURE), error); this.editorStore.applicationStore.notifyError(error); } } /** * Global generation is tied to the generation specification of the project. Every time a generation element * is added, they will be added to the generation specification */ *globalGenerate() { if (this.editorStore.graphState.checkIfApplicationUpdateOperationIsRunning()) { return; } this.isRunningGlobalGenerate = true; try { yield flowResult(this.generateModels()); yield flowResult(this.generateFiles()); } catch (error) { assertErrorThrown(error); this.editorStore.applicationStore.log.error(LogEvent.create(LEGEND_STUDIO_APP_EVENT.GENERATION_FAILURE), error); this.editorStore.graphState.editorStore.applicationStore.notifyError(`${error.message}`); } finally { this.isRunningGlobalGenerate = false; } } *generateModels() { try { this.generatedEntities = new Map(); // reset the map of generated entities const generationSpecs = this.editorStore.graphManagerState.graph.ownGenerationSpecifications; if (!generationSpecs.length) { return; } assertTrue(generationSpecs.length === 1, `Can't generate models: only one generation specification permitted to generate`); const generationSpec = generationSpecs[0]; const generationNodes = generationSpec.generationNodes; for (let i = 0; i < generationNodes.length; i++) { const node = generationNodes[i]; let generatedEntities = []; try { generatedEntities = (yield this.editorStore.graphManagerState.graphManager.generateModel(node.generationElement.value, this.editorStore.graphManagerState.graph)); } catch (error) { assertErrorThrown(error); throw new Error(`Can't generate models: failure occured at step ${i + 1} with specification '${node.generationElement.value.path}'. Error: ${error.message}`); } this.generatedEntities.set(node.generationElement.value.path, generatedEntities); yield flowResult(this.editorStore.graphState.updateGenerationGraphAndApplication()); } } catch (error) { assertErrorThrown(error); this.editorStore.applicationStore.log.error(LogEvent.create(LEGEND_STUDIO_APP_EVENT.GENERATION_FAILURE), error); this.editorStore.graphState.editorStore.applicationStore.notifyError(`${error.message}`); } } /** * Generated file generations in the graph. * NOTE: This method does not update graph and application only the files are generated. */ *generateFiles() { try { this.emptyFileGeneration(); const generationOutputIndex = new Map(); const generationSpecs = this.editorStore.graphManagerState.graph.ownGenerationSpecifications; if (!generationSpecs.length) { return; } assertTrue(generationSpecs.length === 1, `Can't generate models: only one generation specification permitted to generate`); const generationSpec = generationSpecs[0]; const fileGenerations = generationSpec.fileGenerations; // we don't need to keep 'fetching' the main model as it won't grow with each file generation for (const fileGeneration of fileGenerations) { let result = []; try { const mode = this.editorStore.graphState.graphGenerationState.getFileGenerationConfiguration(fileGeneration.value.type).generationMode; result = (yield this.editorStore.graphManagerState.graphManager.generateFile(fileGeneration.value, mode, this.editorStore.graphManagerState.graph)); } catch (error) { assertErrorThrown(error); throw new Error(`Can't generate files using specification '${fileGeneration.value.path}'. Error: ${error.message}`); } generationOutputIndex.set(fileGeneration.value.path, result); } this.processGenerationResult(generationOutputIndex); } catch (error) { assertErrorThrown(error); this.editorStore.applicationStore.log.error(LogEvent.create(LEGEND_STUDIO_APP_EVENT.GENERATION_FAILURE), error); this.editorStore.graphState.editorStore.applicationStore.notifyError(`${error.message}`); } } /** * Used to clear generation entities as well as the generation model */ *clearGenerations() { this.isClearingGenerationEntities = true; this.generatedEntities = new Map(); this.emptyFileGeneration(); yield flowResult(this.editorStore.graphState.updateGenerationGraphAndApplication()); this.isClearingGenerationEntities = false; } /** * Method adds generation specification if * 1. no generation specification has been defined in graph * 2. there exists a generation element */ *possiblyAddMissingGenerationSpecifications() { if (!this.editorStore.graphManagerState.graph.ownGenerationSpecifications .length) { const modelGenerationElements = this.editorStore.pluginManager .getPureGraphManagerPlugins() .flatMap((plugin) => plugin.getExtraModelGenerationElementGetters?.() ?? []) .flatMap((getter) => getter(this.editorStore.graphManagerState.graph)); const fileGenerations = this.editorStore.graphManagerState.graph.ownFileGenerations; if (modelGenerationElements.length || fileGenerations.length) { const generationSpec = new GenerationSpecification(DEFAULT_GENERATION_SPECIFICATION_NAME); modelGenerationElements.forEach((e) => generationSpecification_addGenerationElement(generationSpec, e)); fileGenerations.forEach((e) => generationSpecification_addFileGeneration(generationSpec, e)); // NOTE: add generation specification at the same package as the first generation element found. // we might want to revisit this decision? const specPackage = guaranteeNonNullable([...modelGenerationElements, ...fileGenerations][0]?.package); yield flowResult(this.editorStore.addElement(generationSpec, specPackage.path, false)); } } } // File Generation Tree processGenerationResult(generationOutputIndex) { // empty file index and the directory, we keep the open nodes to reprocess them this.emptyFileGeneration(); const directoryTreeData = this.editorStore.graphState.editorStore.explorerTreeState .fileGenerationTreeData; const openedNodeIds = directoryTreeData ? Array.from(directoryTreeData.nodes.values()) .filter((node) => node.isOpen) .map((node) => node.id) : []; // we read the generation outputs and clean const generationResultIndex = new Map(); Array.from(generationOutputIndex.entries()).forEach((entry) => { const fileGeneration = this.editorStore.graphManagerState.graph.getNullableFileGeneration(entry[0]); const rootFolder = fileGeneration?.generationOutputPath ?? fileGeneration?.path.split(ELEMENT_PATH_DELIMITER).join('_'); const generationOutputs = entry[1]; generationOutputs.forEach((genOutput) => { genOutput.cleanFileName(rootFolder); if (generationResultIndex.has(genOutput.fileName)) { this.editorStore.applicationStore.log.warn(LogEvent.create(LEGEND_STUDIO_APP_EVENT.GENERATION_FAILURE), `Found 2 generation outputs with same path '${genOutput.fileName}'`); } generationResultIndex.set(genOutput.fileName, { generationOutput: genOutput, parentId: fileGeneration?.path, }); }); }); // take generation outputs and put them into the root directory buildGenerationDirectory(this.rootFileDirectory, generationResultIndex, this.filesIndex); this.editorStore.graphState.editorStore.explorerTreeState.setFileGenerationTreeData(getGenerationTreeData(this.rootFileDirectory, ExplorerTreeRootPackageLabel.FILE_GENERATION)); this.editorStore.graphState.editorStore.explorerTreeState.setFileGenerationTreeData(this.reprocessNodeTree(Array.from(generationResultIndex.values()), this.editorStore.graphState.editorStore.explorerTreeState.getFileGenerationTreeData(), openedNodeIds)); this.editorStore.openedEditorStates = this.editorStore.openedEditorStates .map((e) => this.reprocessGenerationFileState(e)) .filter(isNonNullable); const currentEditorState = this.editorStore.currentEditorState; if (currentEditorState instanceof FileGenerationViewerState) { this.editorStore.currentEditorState = this.editorStore.openedEditorStates.find((e) => e instanceof FileGenerationViewerState && e.generatedFile.path === currentEditorState.generatedFile.path); } } reprocessGenerationFileState(editorState) { if (editorState instanceof FileGenerationViewerState) { const fileNode = this.filesIndex.get(editorState.generatedFile.path); if (fileNode) { editorState.generatedFile = fileNode; return editorState; } else { return undefined; } } return editorState; } reprocessNodeTree(generationResult, treeData, openedNodeIds) { reprocessOpenNodes(treeData, this.filesIndex, this.rootFileDirectory, openedNodeIds, true); const selectedFileNodePath = this.selectedNode?.fileNode.path ?? (generationResult.length === 1 ? generationResult[0].generationOutput .fileName : undefined); if (selectedFileNodePath) { const file = this.filesIndex.get(selectedFileNodePath); if (file) { const node = openNode(file, treeData, true); if (node) { this.onTreeNodeSelect(node, treeData, true); } } else { this.selectedNode = undefined; } } return treeData; } onTreeNodeSelect(node, treeData, reprocess) { if (node.childrenIds?.length) { node.isOpen = !node.isOpen; if (node.fileNode instanceof GenerationDirectory) { populateDirectoryTreeNodeChildren(node, treeData); } } if (!reprocess && node.fileNode instanceof GenerationFile) { this.editorStore.openGeneratedFile(node.fileNode); } this.setSelectedNode(node); this.editorStore.graphState.editorStore.explorerTreeState.setFileGenerationTreeData({ ...treeData }); } setSelectedNode(node) { if (this.selectedNode) { this.selectedNode.isSelected = false; } if (node) { node.isSelected = true; } this.selectedNode = node; } emptyFileGeneration() { this.filesIndex = new Map(); this.rootFileDirectory = new GenerationDirectory(GENERATION_FILE_ROOT_NAME); } } //# sourceMappingURL=GraphGenerationState.js.map