Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 25 additions & 15 deletions packages/concerto-core/src/basemodelmanager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ import decoratorModelModule from './decoratormodelhelper';
import type { ModelFileSource, ModelManagerOptions } from './types';
import type { AstNode } from './introspect/decorated';
type ModelFileInstance = InstanceType<typeof ModelFile>;
type ModelFileInput = string | ModelFileInstance;
type ModelFileInput = string | ModelFileInstance | Record<string, unknown>;

function isModelFile(obj: unknown): obj is ModelFileInstance {
return Boolean(obj) && (obj instanceof ModelFile || (typeof (obj as any).isModelFile === 'function' && (obj as any).isModelFile() === true));
}

function getFileNameFromIdentifier(fileIdentifier) {
const normalizedIdentifier = fileIdentifier.replace(/[\\/]+$/, '');
Expand Down Expand Up @@ -179,17 +183,19 @@ class BaseModelManager {
* 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
* Note: AST objects require an AST-based model manager (such as AstModelManager).
* @param {string|ModelFile|object} modelFile - The Concerto file as a string, AST object (requires an AST-based model manager), or ModelFile
* @param {string} [fileName] - a file name to associate with the model file
* @throws {IllegalModelException}
*/
validateModelFile(modelFile, fileName?) {
if (typeof modelFile === 'string') {
if (isModelFile(modelFile)) {
modelFile.validate();
} else {
const { ast } = this.processFile(fileName, modelFile);
let m = new ModelFile(this, ast as AstNode, modelFile, fileName);
const definitions = typeof modelFile === 'string' ? modelFile : null;
let m = new ModelFile(this, ast as AstNode, definitions, fileName);
m.validate();
} else {
modelFile.validate();
}
}

Expand Down Expand Up @@ -334,7 +340,8 @@ class BaseModelManager {
* 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
* Note: AST objects require an AST-based model manager (such as AstModelManager).
* @param {string|ModelFile|object} modelFile - Model as a string, AST object (requires an AST-based model manager), or ModelFile
* @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}
Expand All @@ -343,10 +350,11 @@ class BaseModelManager {
updateModelFile(modelFile, fileName?, disableValidation?) {
const NAME = 'updateModelFile';
debug(NAME, 'updateModelFile', modelFile, fileName);
if (typeof modelFile === 'string') {
if (!isModelFile(modelFile)) {
const { ast } = this.processFile(fileName, modelFile);
let m = new ModelFile(this, ast as AstNode, modelFile, fileName);
return this.updateModelFile(m,fileName,disableValidation);
const definitions = typeof modelFile === 'string' ? modelFile : null;
let m = new ModelFile(this, ast as AstNode, definitions, fileName);
return this.updateModelFile(m, fileName, disableValidation);
} else {
let existing = this.modelFiles[modelFile.getNamespace()];
if (!existing) {
Expand Down Expand Up @@ -377,7 +385,8 @@ class BaseModelManager {

/**
* Add a set of Concerto files to the model manager.
* @param {string[]|ModelFile[]} modelFiles - An array of models as strings or ModelFile objects.
* Note: AST objects require an AST-based model manager (such as AstModelManager).
* @param {string[]|ModelFile[]|object[]} modelFiles - An array of models as strings, AST objects (requires an AST-based model manager), 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).
Expand All @@ -400,11 +409,12 @@ class BaseModelManager {
}

let m: ModelFileInstance;
if (typeof modelFile === 'string') {
const { ast } = this.processFile(fileName, modelFile);
m = new ModelFile(this, ast as AstNode, modelFile, fileName);
} else {
if (isModelFile(modelFile)) {
Comment thread
Atharva15007 marked this conversation as resolved.
Resolved
m = modelFile;
} else {
const { ast } = this.processFile(fileName, modelFile);
const definitions = typeof modelFile === 'string' ? modelFile : null;
m = new ModelFile(this, ast as AstNode, definitions, fileName);
}
if (!m.getVersion()) {
throw new Error(`Cannot add an unversioned namespace: ${m.getNamespace()}`);
Expand Down
5 changes: 4 additions & 1 deletion packages/concerto-core/src/modelmanager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,12 @@ import type ModelFile from './introspect/modelfile';

// How to create a modelfile from a cto file
const ctoProcessFile = (options: ModelManagerOptions = {}): ((name: string | null, data: unknown) => ModelFileSource) => (name: string | null, data: unknown) => {
if (typeof data !== 'string') {
throw new Error('ModelManager expects CTO model content as a string. For AST objects, use AstModelManager.');
}
// Clone individual properties to avoid options injection to Peggy.
const parserOptions = { skipLocationNodes: options?.skipLocationNodes };
const content = typeof data === 'string' ? data : String(data);
const content = data;
const fileName = name ?? 'UNKNOWN';
return {
ast: Parser.parse(content, fileName, parserOptions),
Expand Down
190 changes: 190 additions & 0 deletions packages/concerto-core/test/astmodelmanager.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

const { AstModelManager } = require('../src/astmodelmanager');
const { IllegalModelException } = require('../src/introspect/illegalmodelexception');
const { TypeNotFoundException } = require('../src/typenotfoundexception');

const chai = require('chai');
chai.should();
Expand Down Expand Up @@ -110,4 +111,193 @@ describe('AstModelManager', () => {
});
});

describe('#addModelFiles', () => {
it('should add multiple model files from AST objects', () => {
const modelContent1 = {
'$class': 'concerto.metamodel@1.0.0.Model',
'namespace': 'org.example.test1@1.0.0',
'imports': [],
'declarations': [
{
'$class': 'concerto.metamodel@1.0.0.ConceptDeclaration',
'name': 'Concept1',
'isAbstract': false,
'properties': [],
'decorators': []
}
]
};
const modelContent2 = {
'$class': 'concerto.metamodel@1.0.0.Model',
'namespace': 'org.example.test2@1.0.0',
'imports': [],
'declarations': [
{
'$class': 'concerto.metamodel@1.0.0.ConceptDeclaration',
'name': 'Concept2',
'isAbstract': false,
'properties': [],
'decorators': []
}
]
};

const added = modelManager.addModelFiles([modelContent1, modelContent2]);
added.should.have.lengthOf(2);
modelManager.getModelFile('org.example.test1@1.0.0').should.not.be.undefined;
modelManager.getModelFile('org.example.test2@1.0.0').should.not.be.undefined;
modelManager.getType('org.example.test1@1.0.0.Concept1').getName().should.equal('Concept1');
modelManager.getType('org.example.test2@1.0.0.Concept2').getName().should.equal('Concept2');
});

it('should rollback added models if a later AST is invalid', () => {
const validModel = {
'$class': 'concerto.metamodel@1.0.0.Model',
'namespace': 'org.example.valid@1.0.0',
'imports': [],
'declarations': [
{
'$class': 'concerto.metamodel@1.0.0.ConceptDeclaration',
'name': 'ValidConcept',
'isAbstract': false,
'properties': [],
'decorators': []
}
]
};
const invalidModel = {
'$class': 'concerto.metamodel@1.0.0.Model',
'namespace': 'org.example.invalid@1.0.0',
'imports': [],
'declarations': [
{
'$class': 'concerto.metamodel@1.0.0.ConceptDeclaration',
'name': 'InvalidConcept',
'isAbstract': false,
'properties': [
{
'$class': 'concerto.metamodel@1.0.0.RelationshipProperty',
'name': 'target',
'type': {
'$class': 'concerto.metamodel@1.0.0.TypeIdentifier',
'name': 'NonExistentType'
},
'isArray': false,
'isOptional': false,
'decorators': []
}
],
'decorators': []
}
]
};

(() => {
modelManager.addModelFiles([validModel, invalidModel]);
}).should.throw(IllegalModelException, /NonExistentType/);

chai.expect(modelManager.getModelFile('org.example.valid@1.0.0')).to.be.undefined;
chai.expect(modelManager.getModelFile('org.example.invalid@1.0.0')).to.be.undefined;
});
});

describe('#updateModelFile', () => {
it('should update a model file from an AST object', () => {
const initialContent = {
'$class': 'concerto.metamodel@1.0.0.Model',
'namespace': 'org.example.update@1.0.0',
'imports': [],
'declarations': [
{
'$class': 'concerto.metamodel@1.0.0.ConceptDeclaration',
'name': 'InitialConcept',
'isAbstract': false,
'properties': [],
'decorators': []
}
]
};

modelManager.addModel(initialContent);
modelManager.getType('org.example.update@1.0.0.InitialConcept').getName().should.equal('InitialConcept');

const updatedContent = {
'$class': 'concerto.metamodel@1.0.0.Model',
'namespace': 'org.example.update@1.0.0',
'imports': [],
'declarations': [
{
'$class': 'concerto.metamodel@1.0.0.ConceptDeclaration',
'name': 'UpdatedConcept',
'isAbstract': false,
'properties': [],
'decorators': []
}
]
};

modelManager.updateModelFile(updatedContent);
modelManager.getType('org.example.update@1.0.0.UpdatedConcept').getName().should.equal('UpdatedConcept');
(() => {
modelManager.getType('org.example.update@1.0.0.InitialConcept');
}).should.throw(TypeNotFoundException, /InitialConcept/);
});
});

describe('#validateModelFile', () => {
it('should validate an AST object model', () => {
const modelContent = {
'$class': 'concerto.metamodel@1.0.0.Model',
'namespace': 'org.example.validate@1.0.0',
'imports': [],
'declarations': [
{
'$class': 'concerto.metamodel@1.0.0.ConceptDeclaration',
'name': 'ValidConcept',
'isAbstract': false,
'properties': [],
'decorators': []
}
]
};

(() => {
modelManager.validateModelFile(modelContent);
}).should.not.throw();
Comment thread
Atharva15007 marked this conversation as resolved.
Resolved
});

it('should throw IllegalModelException for an invalid AST object model', () => {
const invalidContent = {
'$class': 'concerto.metamodel@1.0.0.Model',
'namespace': 'org.example.validate.invalid@1.0.0',
'imports': [],
'declarations': [
{
'$class': 'concerto.metamodel@1.0.0.ConceptDeclaration',
'name': 'InvalidConcept',
'isAbstract': false,
'properties': [
{
'$class': 'concerto.metamodel@1.0.0.RelationshipProperty',
'name': 'target',
'type': {
'$class': 'concerto.metamodel@1.0.0.TypeIdentifier',
'name': 'NonExistentType'
},
'isArray': false,
'isOptional': false,
'decorators': []
}
],
'decorators': []
}
]
};

(() => {
modelManager.validateModelFile(invalidContent);
}).should.throw(IllegalModelException, /NonExistentType/);
});
});

});