@accordproject/concerto-core
Version:
Core Implementation for the Concerto Modeling Language
912 lines (816 loc) • 35.6 kB
JavaScript
/*
* 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.
*/
'use strict';
const fsPath = require('path');
const { DefaultFileLoader, FileDownloader, ModelWriter } = require('@accordproject/concerto-util');
const { MetaModelUtil, MetaModelNamespace } = require('@accordproject/concerto-metamodel');
const Factory = require('./factory');
const Globalize = require('./globalize');
const IllegalModelException = require('./introspect/illegalmodelexception');
const ModelFile = require('./introspect/modelfile');
const ModelUtil = require('./modelutil');
const Serializer = require('./serializer');
const TypeNotFoundException = require('./typenotfoundexception');
const { getRootModel } = require('./rootmodel');
const { getDecoratorModel } = require('./decoratormodel');
const MetamodelException = require('./metamodelexception');
// Types needed for TypeScript generation.
/* eslint-disable no-unused-vars */
/* istanbul ignore next */
if (global === undefined) {
const Declaration = require('./introspect/declaration');
const AssetDeclaration = require('./introspect/assetdeclaration');
const ClassDeclaration = require('./introspect/classdeclaration');
const MapDeclaration = require('./introspect/mapdeclaration');
const ConceptDeclaration = require('./introspect/conceptdeclaration');
const DecoratorFactory = require('./introspect/decoratorfactory');
const EnumDeclaration = require('./introspect/enumdeclaration');
const EventDeclaration = require('./introspect/eventdeclaration');
const ParticipantDeclaration = require('./introspect/participantdeclaration');
const TransactionDeclaration = require('./introspect/transactiondeclaration');
}
/* eslint-enable no-unused-vars */
const debug = require('debug')('concerto:BaseModelManager');
// How to create a modelfile from the external content
const defaultProcessFile = (name, data) => {
return {
ast: data, // AST is input
definitions: null, // No CTO file
fileName: name,
};
};
// default decorator validation configuration
const DEFAULT_DECORATOR_VALIDATION = {
missingDecorator: undefined, // 'error' | 'warn' (see Logger.levels)...,
invalidDecorator: undefined, // 'error' | 'warn' ...
};
// these namespaces are internal and excluded by default by getModelFiles
// and ignored by fromAst
const EXCLUDE_NS = ['concerto@1.0.0', 'concerto', 'concerto.decorator@1.0.0'];
/**
* Manages the Concerto model files.
*
* The structure of {@link Resource}s (Assets, Transactions, Participants) is modelled
* in a set of Concerto files. The contents of these files are managed
* by the {@link ModelManager}. Each Concerto file has a single namespace and contains
* a set of asset, transaction and participant type definitions.
*
* Concerto applications load their Concerto files and then call the {@link ModelManager#addModelFile addModelFile}
* method to register the Concerto file(s) with the ModelManager.
*
* Use the {@link Concerto} class to validate instances.
*
* @memberof module:concerto-core
*/
class BaseModelManager {
/**
* Create the ModelManager.
* @constructor
* @param {object} [options] - ModelManager options, also passed to Serializer
* @param {boolean} [options.strict] - require versioned namespaces and imports
* @param {Object} [options.regExp] - An alternative regular expression engine.
* @param {boolean} [options.metamodelValidation] - When true, modelfiles will be validated
* @param {boolean} [options.addMetamodel] - When true, the Concerto metamodel is added to the model manager
* @param {boolean} [options.enableMapType] - When true, the Concerto Map Type feature is enabled
* @param {boolean} [options.importAliasing] - When true, the Concerto Aliasing feature is enabled
* @param {object} [options.decoratorValidation] - the decorator validation configuration
* @param {string} [options.decoratorValidation.missingDecorator] - the validation log level for missingDecorator decorators: off, warning, error
* @param {string} [options.decoratorValidation.invalidDecorator] - the validation log level for invalidDecorator decorators: off, warning, error
* @param {*} [processFile] - how to obtain a concerto AST from an input to the model manager
*/
constructor(options, processFile) {
this.processFile = processFile ? processFile : defaultProcessFile;
this.modelFiles = {};
this.factory = new Factory(this);
this.serializer = new Serializer(this.factory, this, options);
this.decoratorFactories = [];
this.strict = !!options?.strict;
this.options = options;
this.addDecoratorModel();
this.addRootModel();
this.decoratorValidation = options?.decoratorValidation ? options?.decoratorValidation : DEFAULT_DECORATOR_VALIDATION;
// TODO Remove on release of MapType
// Supports both env var and property based flag
this.enableMapType = !!options?.enableMapType;
this.importAliasing = process?.env?.IMPORT_ALIASING === 'true' || !!options?.importAliasing;
// Cache a copy of the Metamodel ModelFile for use when validating the structure of ModelFiles later.
this.metamodelModelFile = new ModelFile(this, MetaModelUtil.metaModelAst, undefined, MetaModelNamespace);
if(options?.addMetamodel) {
this.addModelFile(this.metamodelModelFile);
}
}
/**
* Returns true
* @returns {boolean} true
*/
isModelManager() {
return true;
}
/**
* Returns the value of the strict option
* @returns {boolean} true if the strict has been set
*/
isStrict() {
return this.strict;
}
/**
* Checks if the import aliasing feature is enabled.
* @returns {boolean} true if the importAliasing has been set
*/
isAliasedTypeEnabled() {
return this.importAliasing;
}
/**
* Adds root types
* @private
*/
addRootModel() {
// create the versioned concerto namespace
const {rootModelAst, rootModelCto, rootModelFile} = getRootModel(true);
const m = new ModelFile(this, rootModelAst, rootModelCto, rootModelFile);
if(this.strict ) {
// add the versioned concerto namespace
this.addModelFile(m, rootModelCto, rootModelFile, true);
}
else {
// add the versioned concerto namespace
this.addModelFile(m, rootModelCto, rootModelFile, true);
// create the unversioned concerto namespace and add
const unversioned = getRootModel(false);
const mUnversioned = new ModelFile(this, unversioned.rootModelAst, unversioned.rootModelCto, unversioned.rootModelFile);
this.addModelFile(mUnversioned, unversioned.rootModelCto, unversioned.rootModelFile, true);
}
}
/**
* Adds decorator types
* @private
*/
addDecoratorModel() {
const {decoratorModelAst, decoratorModelCto, decoratorModelFile} = getDecoratorModel();
const m = new ModelFile(this, decoratorModelAst, decoratorModelCto, decoratorModelFile);
this.addModelFile(m, decoratorModelCto, decoratorModelFile, true);
}
/**
* Visitor design pattern
* @param {Object} visitor - the visitor
* @param {Object} parameters - the parameter
* @return {Object} the result of visiting or null
*/
accept(visitor, parameters) {
return visitor.visit(this, parameters);
}
/**
* Validates a Concerto file (as a string) to the ModelManager.
* Concerto files have a single namespace.
*
* Note that if there are dependencies between multiple files the files
* must be added in dependency order, or the addModelFiles method can be
* used to add a set of files irrespective of dependencies.
* @param {string|ModelFile} modelFile - The Concerto file as a string
* @param {string} [fileName] - a file name to associate with the model file
* @throws {IllegalModelException}
*/
validateModelFile(modelFile, fileName) {
if (typeof modelFile === 'string') {
const { ast } = this.processFile(fileName, modelFile);
let m = new ModelFile(this, ast, modelFile, fileName);
m.validate();
} else {
modelFile.validate();
}
}
/**
* Throws an error with details about the existing namespace.
* @param {ModelFile} modelFile The model file that is trying to declare an existing namespace
* @private
*/
_throwAlreadyExists(modelFile) {
const existingModelFileName = this.modelFiles[modelFile.getNamespace()].getName();
const postfix = existingModelFileName ? ` in file ${existingModelFileName}` : '';
const prefix = modelFile.getName() ? ` specified in file ${modelFile.getName()}` : '';
let errMsg = `Namespace ${modelFile.getNamespace()}${prefix} is already declared${postfix}`;
throw new Error(errMsg);
}
/**
* Adds a Concerto file (as an AST) to the ModelManager.
* Concerto files have a single namespace. If a Concerto file with the
* same namespace has already been added to the ModelManager then it
* will be replaced.
* Note that if there are dependencies between multiple files the files
* must be added in dependency order, or the addModelFiles method can be
* used to add a set of files irrespective of dependencies.
* @param {ModelFile} modelFile - Model as a ModelFile object
* @param {string} [cto] - an optional cto string
* @param {string} [fileName] - an optional file name to associate with the model file
* @param {boolean} [disableValidation] - If true then the model files are not validated
* @throws {IllegalModelException}
* @return {Object} The newly added model file (internal).
*/
addModelFile(modelFile, cto, fileName, disableValidation) {
const NAME = 'addModelFile';
debug(NAME, 'addModelFile', modelFile, fileName);
if(this.isStrict() && !modelFile.getVersion()) {
throw new Error('Cannot add an unversioned namespace when \'strict\' is true');
}
if (!this.modelFiles[modelFile.getNamespace()]) {
if (!disableValidation) {
// Structural validation against the Metamodel
if(this.options?.metamodelValidation){
this.validateAst(modelFile);
}
// Semantic validation of the model file
modelFile.validate();
}
this.modelFiles[modelFile.getNamespace()] = modelFile;
} else {
this._throwAlreadyExists(modelFile);
}
return modelFile;
}
/**
* Check that a modelFile is valid with respect to the metamodel.
*
* @param {ModelFile} modelFile - Model as a ModelFile object
* @throws {MetamodelException} - throws if the ModelFile is invalid
* @private
*/
validateAst(modelFile) {
const { version: modelFileVersion } = ModelUtil.parseNamespace(ModelUtil.getNamespace(modelFile.getAst().$class));
const { version: metamodelVersion } = ModelUtil.parseNamespace(MetaModelNamespace);
if (modelFileVersion !== metamodelVersion){
throw new MetamodelException(`Model file version ${modelFileVersion} does not match metamodel version ${metamodelVersion}`);
}
const alreadyHasMetamodel = !!this.getModelFile(MetaModelNamespace);
if (!alreadyHasMetamodel) {
this.addModelFile(this.metamodelModelFile, undefined, MetaModelNamespace, true);
}
try {
// Use deserialization to validate the AST
this.getSerializer().fromJSON(modelFile.getAst());
} catch (err) {
// Rethrow as a MetamodelException
if (this.isStrict()) {
throw new MetamodelException(err.message);
} else {
console.warn('Invalid metamodel found. This will throw an exception in a future release. ', err.message);
}
}
if (!alreadyHasMetamodel) {
this.deleteModelFile(MetaModelNamespace);
}
}
/**
* Adds a model to the ModelManager.
* Concerto files have a single namespace. If a Concerto file with the
* same namespace has already been added to the ModelManager then it
* will be replaced.
* Note that if there are dependencies between multiple files the files
* must be added in dependency order, or the addModel method can be
* used to add a set of files irrespective of dependencies.
* @param {*} modelInput - Model (as a string or object)
* @param {string} [cto] - an optional cto string
* @param {string} [fileName] - an optional file name to associate with the model file
* @param {boolean} [disableValidation] - If true then the model files are not validated
* @throws {IllegalModelException}
* @return {ModelFile} The newly added model file (internal).
*/
addModel(modelInput, cto, fileName, disableValidation) {
const NAME = 'addModel';
debug(NAME, 'addModel', modelInput, fileName);
const { ast, definitions } = this.processFile(fileName, modelInput);
const finalCto = cto || definitions;
const m = new ModelFile(this, ast, finalCto, fileName);
this.addModelFile(m, finalCto, fileName, disableValidation);
return m;
}
/**
* Updates a Concerto file (as a string) on the ModelManager.
* Concerto files have a single namespace. If a Concerto file with the
* same namespace has already been added to the ModelManager then it
* will be replaced.
* @param {string|ModelFile} modelFile - Model as a string or object
* @param {string} [fileName] - a file name to associate with the model file
* @param {boolean} [disableValidation] - If true then the model files are not validated
* @throws {IllegalModelException}
* @returns {Object} The newly added model file (internal).
*/
updateModelFile(modelFile, fileName, disableValidation) {
const NAME = 'updateModelFile';
debug(NAME, 'updateModelFile', modelFile, fileName);
if (typeof modelFile === 'string') {
const { ast } = this.processFile(fileName, modelFile);
let m = new ModelFile(this, ast, modelFile, fileName);
return this.updateModelFile(m,fileName,disableValidation);
} else {
let existing = this.modelFiles[modelFile.getNamespace()];
if (!existing) {
throw new Error(`Model file for namespace ${modelFile.getNamespace()} not found`);
}
if (!disableValidation) {
modelFile.validate();
}
}
this.modelFiles[modelFile.getNamespace()] = modelFile;
return modelFile;
}
/**
* Remove the Concerto file for a given namespace
* @param {string} namespace - The namespace of the model file to delete.
*/
deleteModelFile(namespace) {
if (!this.modelFiles[namespace]) {
throw new Error('Model file does not exist');
} else {
delete this.modelFiles[namespace];
}
}
/**
* Add a set of Concerto files to the model manager.
* @param {string[]|ModelFile[]} modelFiles - An array of models as strings or ModelFile objects.
* @param {string[]} [fileNames] - A array of file names to associate with the model files
* @param {boolean} [disableValidation] - If true then the model files are not validated
* @returns {Object[]} The newly added model files (internal).
*/
addModelFiles(modelFiles, fileNames, disableValidation) {
const NAME = 'addModelFiles';
debug(NAME, 'addModelFiles', modelFiles, fileNames);
const originalModelFiles = {};
Object.assign(originalModelFiles, this.modelFiles);
let newModelFiles = [];
try {
// create the model files
for (let n = 0; n < modelFiles.length; n++) {
const modelFile = modelFiles[n];
let fileName = null;
if (fileNames) {
fileName = fileNames[n];
}
let m;
if (typeof modelFile === 'string') {
const { ast } = this.processFile(fileName, modelFile);
m = new ModelFile(this, ast, modelFile, fileName);
} else {
m = modelFile;
}
if (!this.modelFiles[m.getNamespace()]) {
this.modelFiles[m.getNamespace()] = m;
newModelFiles.push(m);
} else {
this._throwAlreadyExists(m);
}
}
// re-validate all the model files
if (!disableValidation) {
this.validateModelFiles();
}
// return the model files.
return newModelFiles;
} catch (err) {
this.modelFiles = {};
Object.assign(this.modelFiles, originalModelFiles);
throw err;
} finally {
debug(NAME, newModelFiles);
}
}
/**
* Validates all models files in this model manager
*/
validateModelFiles() {
for (let ns in this.modelFiles) {
this.modelFiles[ns].validate();
}
}
/**
* Downloads all ModelFiles that are external dependencies and adds or
* updates them in this ModelManager.
* @param {Object} [options] - Options object passed to ModelFileLoaders
* @param {FileDownloader} [fileDownloader] - an optional FileDownloader
* @throws {IllegalModelException} if the models fail validation
* @return {Promise} a promise when the download and update operation is completed.
*/
async updateExternalModels(options, fileDownloader) {
const NAME = 'updateExternalModels';
debug(NAME, 'updateExternalModels', options);
if(!fileDownloader) {
fileDownloader = new FileDownloader(new DefaultFileLoader(this.processFile), (file) => MetaModelUtil.getExternalImports(file.ast));
}
const originalModelFiles = {};
Object.assign(originalModelFiles, this.modelFiles);
try {
const externalModels = await fileDownloader.downloadExternalDependencies(this.getModelFiles(), options);
const externalModelFiles = [];
externalModels.forEach((file) => {
const mf = new ModelFile(this, file.ast, file.definitions, file.fileName);
const existing = this.modelFiles[mf.getNamespace()];
if (existing) {
externalModelFiles.push(this.updateModelFile(mf, mf.getName(), true)); // disable validation
} else {
externalModelFiles.push(this.addModelFile(mf, null, mf.getName(), true)); // disable validation
}
});
// now everything is applied, we need to revalidate all models
this.validateModelFiles();
return externalModelFiles;
} catch (err) {
// Restore original files
this.modelFiles = {};
Object.assign(this.modelFiles, originalModelFiles);
throw err;
}
}
/**
* Write all models in this model manager to the specified path in the file system
*
* @param {string} path to a local directory
* @param {Object} [options] - Options object
* @param {boolean} options.includeExternalModels -
* If true, external models are written to the file system. Defaults to true
*/
writeModelsToFileSystem(path, options = {}) {
ModelWriter.writeModelsToFileSystem(this.getModelFiles(), path, options);
}
/**
* Returns the status of the decorator validation options
* @returns {object} returns an object that indicates the log levels for defined and undefined decorators
*/
getDecoratorValidation() {
return this.decoratorValidation;
}
/**
* Get the array of model file instances
* @param {Boolean} [includeConcertoNamespace] - whether to include the concerto namespace
* (default to false)
* @return {ModelFile[]} The ModelFiles registered
* @private
*/
getModelFiles(includeConcertoNamespace) {
let keys = Object.keys(this.modelFiles);
let result = [];
for (let n = 0; n < keys.length; n++) {
const ns = keys[n];
if(includeConcertoNamespace || (!EXCLUDE_NS.includes(ns))) {
result.push(this.modelFiles[ns]);
}
}
return result;
}
/**
* Gets all the Concerto models
* @param {Object} [options] - Options object
* @param {boolean} options.includeExternalModels -
* If true, external models are written to the file system. Defaults to true
* @return {Array<{name:string, content:string}>} the name and content of each CTO file
*/
getModels(options) {
const modelFiles = this.getModelFiles();
let models = [];
const opts = Object.assign({
includeExternalModels: true,
}, options);
modelFiles.forEach(function (file) {
if (file.isExternal() && !opts.includeExternalModels) {
return;
}
let fileName;
if (file.fileName === 'UNKNOWN' || file.fileName === null || !file.fileName) {
fileName = file.namespace + '.cto';
} else {
let fileIdentifier = file.fileName;
fileName = fsPath.basename(fileIdentifier);
}
models.push({ 'name' : fileName, 'content' : file.definitions });
});
return models;
}
/**
* Check that the type is valid and returns the FQN of the type.
* @param {string} context - error reporting context
* @param {string} type - fully qualified type name
* @return {string} - the resolved type name (fully qualified)
* @throws {IllegalModelException} - if the type is not defined
* @private
*/
resolveType(context, type) {
// is the type a primitive?
if (ModelUtil.isPrimitiveType(type)) {
return type;
}
let ns = ModelUtil.getNamespace(type);
let modelFile = this.getModelFile(ns);
if (!modelFile) {
let formatter = Globalize.messageFormatter('modelmanager-resolvetype-nonsfortype');
throw new IllegalModelException(formatter({
type: type,
context: context
}));
}
if (modelFile.isLocalType(type)) {
return type;
}
let formatter = Globalize.messageFormatter('modelmanager-resolvetype-notypeinnsforcontext');
throw new IllegalModelException(formatter({
context: context,
type: type,
namespace: modelFile.getNamespace()
}));
}
/**
* Remove all registered Concerto files
*/
clearModelFiles() {
this.modelFiles = {};
this.addDecoratorModel();
this.addRootModel();
}
/**
* Get the ModelFile associated with a namespace
*
* @param {string} namespace - the namespace containing the ModelFile
* @return {ModelFile} registered ModelFile for the namespace or null
*/
getModelFile(namespace) {
return this.modelFiles[namespace];
}
/**
* Get the ModelFile associated with a file name
*
* @param {string} fileName - the fileName associated with the ModelFile
* @return {ModelFile} registered ModelFile for the namespace or null
* @private
*/
getModelFileByFileName(fileName) {
return this.getModelFiles().filter(mf => mf.getName() === fileName)[0];
}
/**
* Get the namespaces registered with the ModelManager.
* @return {string[]} namespaces - the namespaces that have been registered.
*/
getNamespaces() {
return Object.keys(this.modelFiles);
}
/**
* Look up a type in all registered namespaces.
*
* @param {string} qualifiedName - fully qualified type name.
* @return {ClassDeclaration} - the class declaration for the specified type.
* @throws {TypeNotFoundException} - if the type cannot be found or is a primitive type.
*/
getType(qualifiedName) {
const namespace = ModelUtil.getNamespace(qualifiedName);
const modelFile = this.getModelFile(namespace);
if (!modelFile) {
const formatter = Globalize.messageFormatter('modelmanager-gettype-noregisteredns');
throw new TypeNotFoundException(qualifiedName, formatter({
type: qualifiedName
}));
}
const classDecl = modelFile.getType(qualifiedName);
if (!classDecl) {
const formatter = Globalize.messageFormatter('modelmanager-gettype-notypeinns');
throw new TypeNotFoundException(qualifiedName, formatter({
type: ModelUtil.getShortName(qualifiedName),
namespace: namespace
}));
}
return classDecl;
}
/**
* Get the AssetDeclarations defined in this model manager
* @return {AssetDeclaration[]} the AssetDeclarations defined in the model manager
*/
getAssetDeclarations() {
return this.getModelFiles().reduce((prev, cur) => {
return prev.concat(cur.getAssetDeclarations());
}, []);
}
/**
* Get the TransactionDeclarations defined in this model manager
* @return {TransactionDeclaration[]} the TransactionDeclarations defined in the model manager
*/
getTransactionDeclarations() {
return this.getModelFiles().reduce((prev, cur) => {
return prev.concat(cur.getTransactionDeclarations());
}, []);
}
/**
* Get the EventDeclarations defined in this model manager
* @return {EventDeclaration[]} the EventDeclaration defined in the model manager
*/
getEventDeclarations() {
return this.getModelFiles().reduce((prev, cur) => {
return prev.concat(cur.getEventDeclarations());
}, []);
}
/**
* Get the ParticipantDeclarations defined in this model manager
* @return {ParticipantDeclaration[]} the ParticipantDeclaration defined in the model manager
*/
getParticipantDeclarations() {
return this.getModelFiles().reduce((prev, cur) => {
return prev.concat(cur.getParticipantDeclarations());
}, []);
}
/**
* Get the MapDeclarations defined in this model manager
* @return {MapDeclaration[]} the MapDeclaration defined in the model manager
*/
getMapDeclarations() {
return this.getModelFiles().reduce((prev, cur) => {
return prev.concat(cur.getMapDeclarations());
}, []);
}
/**
* Get the EnumDeclarations defined in this model manager
* @return {EnumDeclaration[]} the EnumDeclaration defined in the model manager
*/
getEnumDeclarations() {
return this.getModelFiles().reduce((prev, cur) => {
return prev.concat(cur.getEnumDeclarations());
}, []);
}
/**
* Get the Concepts defined in this model manager
* @return {ConceptDeclaration[]} the ConceptDeclaration defined in the model manager
*/
getConceptDeclarations() {
return this.getModelFiles().reduce((prev, cur) => {
return prev.concat(cur.getConceptDeclarations());
}, []);
}
/**
* Get a factory for creating new instances of types defined in this model manager.
* @return {Factory} A factory for creating new instances of types defined in this model manager.
*/
getFactory() {
return this.factory;
}
/**
* Get a serializer for serializing instances of types defined in this model manager.
* @return {Serializer} A serializer for serializing instances of types defined in this model manager.
*/
getSerializer() {
return this.serializer;
}
/**
* Get the decorator factories for this model manager.
* @return {DecoratorFactory[]} The decorator factories for this model manager.
*/
getDecoratorFactories() {
return this.decoratorFactories;
}
/**
* Add a decorator factory to this model manager.
* @param {DecoratorFactory} factory The decorator factory to add to this model manager.
*/
addDecoratorFactory(factory) {
this.decoratorFactories.push(factory);
}
/**
* Checks if this fully qualified type name is derived from another.
* @param {string} fqt1 The fully qualified type name to check.
* @param {string} fqt2 The fully qualified type name it is may be derived from.
* @returns {boolean} True if this instance is an instance of the specified fully
* qualified type name, false otherwise.
*/
derivesFrom(fqt1, fqt2) {
// Check to see if this is an exact instance of the specified type.
let typeDeclaration = this.getType(fqt1);
while (typeDeclaration) {
if (typeDeclaration.getFullyQualifiedName() === fqt2) {
return true;
}
typeDeclaration = typeDeclaration.getSuperTypeDeclaration();
}
return false;
}
/**
* Resolve the namespace for names in the metamodel
* @param {object} metaModel - the MetaModel
* @return {object} the resolved metamodel
*/
resolveMetaModel(metaModel) {
const priorModels = this.getAst(false, true);
return MetaModelUtil.resolveLocalNames(priorModels, metaModel);
}
/**
* Populates the model manager from a models metamodel AST
* @param {*} ast the metamodel
* @param {object} [options] - options for the from ast method
* @param {object} [options.disableValidation] - option to disable metamodel validation and just fetch the models, to be used only if the metamodel is already validated
*/
fromAst(ast, options) {
this.clearModelFiles();
ast.models.forEach( model => {
if(!EXCLUDE_NS.includes(model.namespace)) { // excludes the internal namespaces, already added
const modelFile = new ModelFile( this, model );
this.addModelFile( modelFile, null, null, true );
}
});
if (!options?.disableValidation) {
this.validateModelFiles();
}
}
/**
* Get the full ast (metamodel instances) for a modelmanager
* @param {boolean} [resolve] - whether to resolve names
* @param {boolean} [includeConcertoNamespaces] - whether to include the concerto namespaces
* @returns {*} the metamodel
*/
getAst(resolve,includeConcertoNamespaces) {
const result = {
$class: `${MetaModelNamespace}.Models`,
models: [],
};
const modelFiles = this.getModelFiles(includeConcertoNamespaces);
modelFiles.forEach((thisModelFile) => {
let metaModel = thisModelFile.getAst();
if (resolve) {
metaModel = this.resolveMetaModel(metaModel);
}
result.models.push(metaModel);
});
return result;
}
/**
* A function type definition for use as an argument to the filter function
* @callback FilterFunction
* @param {Declaration} declaration
* @returns {boolean} true, if the declaration satisfies the filter function
*/
/**
* Returns a new ModelManager with only the types for which the
* filter function returns true.
*
* ModelFiles with no declarations after filtering will be removed.
*
* @param {FilterFunction} predicate - the filter function over a Declaration object
* @returns {BaseModelManager} - the filtered ModelManager
*/
filter(predicate){
const modelManager = new BaseModelManager({...this.options}, this.processFile);
const removedFqns = []; // the list of FQN of types that have been removed
// remove the types from model files, populating removedFqns
let filteredModels = Object.values(this.modelFiles)
.map((modelFile) => modelFile.filter(predicate, modelManager, removedFqns))
.filter(Boolean);
// remove concerto model files - as these are automatically added
// when we recreate the model manager below
filteredModels = filteredModels.filter(mf => !mf.isSystemModelFile());
// now update filteredModels to remove any imports of removed types
const modelsWithValidImports = filteredModels.map( modelFile => {
const ast = modelFile.getAst();
let modified = false;
removedFqns.forEach( removedFqn => {
const ns = ModelUtil.getNamespace(removedFqn);
const isSystemImport = ns.startsWith('concerto@') || ns === 'concerto';
if(!isSystemImport && modelFile.getImports().includes(removedFqn)) {
const removeName = ModelUtil.getShortName(removedFqn);
const removeNamespace = ModelUtil.getNamespace(removedFqn);
ast.imports = ast.imports.filter(imp => {
const remove = ModelUtil.getShortName(imp.$class) === 'ImportType' &&
imp.name === removeName &&
imp.namespace === removeNamespace;
if(remove) {
modified = true;
}
return !remove;
});
ast.imports.forEach( imp => {
if(imp.namespace === removeNamespace) {
if(ModelUtil.getShortName(imp.$class) === 'ImportTypes') {
imp.types = imp.types.filter((type) => {
const remove = (type === removeName);
if(remove) {
modified = true;
}
return !remove;
});
}
}
});
}
});
if(modified) {
return new ModelFile(this, ast, undefined, modelFile.fileName);
}
else {
return modelFile;
}
});
modelManager.addModelFiles(modelsWithValidImports);
return modelManager;
}
}
module.exports = BaseModelManager;