From 3aefd31d40651a5caef08e493204e8f9301f67ed Mon Sep 17 00:00:00 2001 From: jeromesimeon Date: Mon, 19 Jul 2021 08:48:10 -0400 Subject: [PATCH] fix(metamodel) Add validation when importing a metamodel + API change Signed-off-by: jeromesimeon --- packages/concerto-cli/lib/commands.js | 12 +- packages/concerto-core/api.txt | 7 +- packages/concerto-core/changelog.txt | 2 +- .../concerto-core/lib/introspect/metamodel.js | 280 +++++++++++++++++- .../concerto-core/lib/introspect/modelfile.js | 9 - .../test/introspect/metamodel.js | 12 +- 6 files changed, 292 insertions(+), 30 deletions(-) diff --git a/packages/concerto-cli/lib/commands.js b/packages/concerto-cli/lib/commands.js index 40d6a6407f..6dc412f68b 100644 --- a/packages/concerto-cli/lib/commands.js +++ b/packages/concerto-cli/lib/commands.js @@ -193,17 +193,13 @@ class Commands { */ static async transform(input) { if (path.extname(input) === '.cto') { - const modelManager = await ModelLoader.loadModelManager([input], {}); - const modelFiles = modelManager.getModelFiles(); - // XXX Pick first model, usually the main one - const lastModelFile = modelFiles[0]; - const metamodel = lastModelFile.toMetaModel(); - // XXX Log here for now - return JSON.stringify(metamodel); + const inputString = fs.readFileSync(input, 'utf8'); + const result = MetaModel.ctoToMetaModel(inputString); + return JSON.stringify(result); } else { const inputString = fs.readFileSync(input, 'utf8'); const json = JSON.parse(inputString); - const result = MetaModel.modelFromMetaModel(json); + const result = MetaModel.ctoFromMetaModel(json); return result; } } diff --git a/packages/concerto-core/api.txt b/packages/concerto-core/api.txt index 388e7c59d0..782106e24f 100644 --- a/packages/concerto-core/api.txt +++ b/packages/concerto-core/api.txt @@ -109,14 +109,18 @@ class ModelFileDownloader { + Promise downloadExternalDependencies(ModelFile[],Object) + Promise runJob(Object,Object) } + + void createMetaModelManager() + + object validateMetaModel() + object fieldToMetaModel() + object relationshipToMetaModel() + object enumPropertyToMetaModel() + object declToMetaModel() + object modelToMetaModel() + + object modelFileToMetaModel() + string fieldFromMetaModel() + string declFromMetaModel() - + string modelFromMetaModel() + + string ctoFromMetaModel() + + object ctoToMetaModel() class ModelFile { + void constructor(ModelManager,string,string) throws IllegalModelException + Boolean isSystemModelFile() @@ -142,7 +146,6 @@ class ModelFile { + string getDefinitions() + string getConcertoVersion() + void isCompatibleVersion() - + object toMetaModel() + boolean hasInstance(object) } class ParticipantDeclaration extends IdentifiedDeclaration { diff --git a/packages/concerto-core/changelog.txt b/packages/concerto-core/changelog.txt index f899e516e7..04ef820c8a 100644 --- a/packages/concerto-core/changelog.txt +++ b/packages/concerto-core/changelog.txt @@ -24,7 +24,7 @@ # Note that the latest public API is documented using JSDocs and is available in api.txt. # -Version 1.0.5 {febf7c3ec5ac5ec2f7f9fa88bd823555} 2021-07-13 +Version 1.0.5 {0717bf67bc6e667299832b5beb40fd90} 2021-07-13 - Add support for Concerto metamodel with import/export to CTO Version 1.0.3 {1fe469fe1a79af5d5a4f5ec7dee6b7d4} 2021-06-25 diff --git a/packages/concerto-core/lib/introspect/metamodel.js b/packages/concerto-core/lib/introspect/metamodel.js index 7beda56732..b75c7d8d78 100644 --- a/packages/concerto-core/lib/introspect/metamodel.js +++ b/packages/concerto-core/lib/introspect/metamodel.js @@ -14,6 +14,246 @@ 'use strict'; +const parser = require('./parser'); +const ModelManager = require('../modelmanager'); +const Factory = require('../factory'); +const Serializer = require('../serializer'); + +const metaModelCto = `/* + * 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. + */ + +namespace concerto.metamodel + +/** + * The metadmodel for Concerto files + */ +abstract concept DecoratorLiteral { +} + +concept DecoratorString extends DecoratorLiteral { + o String value +} + +concept DecoratorNumber extends DecoratorLiteral { + o Double value +} + +concept DecoratorBoolean extends DecoratorLiteral { + o Boolean value +} + +concept TypeIdentifier { + @FormEditor("selectOptions", "types") + o String name default="Concept" +} + +concept DecoratorIdentifier extends DecoratorLiteral { + o TypeIdentifier identifier + o Boolean isArray default=false +} + +concept Decorator { + o String name + o DecoratorLiteral[] arguments optional +} + +concept Identified { +} + +concept IdentifiedBy extends Identified { + o String name +} + +@FormEditor("defaultSubclass","concerto.metamodel.ConceptDeclaration") +abstract concept ClassDeclaration { + @FormEditor("hide", true) + o Decorator[] decorators optional + o Boolean isAbstract default=false + // TODO use regex /^(?!null|true|false)(\\p{Lu}|\\p{Ll}|\\p{Lt}|\\p{Lm}|\\p{Lo}|\\p{Nl}|\\$|_|\\\\u[0-9A-Fa-f]{4})(?:\\p{Lu}|\\p{Ll}|\\p{Lt}|\\p{Lm}|\\p{Lo}|\\p{Nl}|\\$|_|\\\\u[0-9A-Fa-f]{4}|\\p{Mn}|\\p{Mc}|\\p{Nd}|\\p{Pc}|\\u200C|\\u200D)*/u + @FormEditor("title", "Name") + o String name default="ClassName" // regex=/^(?!null|true|false)(\\w|\\d|\\$|_|\\\\u[0-9A-Fa-f]{4})(?:\\w|\\d|\\$|_|\\\\u[0-9A-Fa-f]{4}|\\S|\\u200C|\\u200D)*$/ + o Identified identified optional + @FormEditor("title", "Super Type") + o TypeIdentifier superType optional + o FieldDeclaration[] fields +} + +concept AssetDeclaration extends ClassDeclaration { +} + +concept ParticipantDeclaration extends ClassDeclaration { +} + +concept TransactionDeclaration extends ClassDeclaration { +} + +concept EventDeclaration extends ClassDeclaration { +} + +concept ConceptDeclaration extends ClassDeclaration { +} + +concept EnumDeclaration extends ClassDeclaration { +} + +concept StringDefault { + o String value +} + +concept BooleanDefault { + o Boolean value +} + +concept IntegerDefault { + o Integer value +} + +concept LongDefault { + o Long value +} + +concept DoubleDefault { + o Double value +} + +@FormEditor("defaultSubclass","concerto.metamodel.StringFieldDeclaration") +abstract concept FieldDeclaration { + // TODO Allow regex modifiers e.g. //ui + // regex /^(?!null|true|false)(\\p{Lu}|\\p{Ll}|\\p{Lt}|\\pLm}|\\p{Lo}|\\p{Nl}|\\$|_|\\\\u[0-9A-Fa-f]{4})(?:\\p{Lu}|\\p{Ll}|\\p{Lt}|\\p{Lm}|\\p{Lo}|\\p{Nl}|\\$|_|\\\\u[0-9A-Fa-f]{4}|\\p{Mn}|\\p{Mc}|\\p{Nd}|\\p{Pc}|\\u200C|\\u200D)*/u + // This regex is an approximation of what the parser accepts without using unicode character classes + o String name default="fieldName" // regex=/^(?!null|true|false)(\\w|\\d|\\$|_|\\\\u[0-9A-Fa-f]{4})(?:\\w|\\d|\\$|_|\\\\u[0-9A-Fa-f]{4}|\\S|\\u200C|\\u200D)*$/ + @FormEditor("title", "Is Array?") + o Boolean isArray default=false + @FormEditor("title", "Is Optional?") + o Boolean isOptional default=false + @FormEditor("hide", true) + o Decorator[] decorators optional +} + +concept ObjectFieldDeclaration extends FieldDeclaration { + @FormEditor("hide", true) + o StringDefault defaultValue optional + @FormEditor("title", "Type Name", "selectOptions", "types") + o TypeIdentifier type +} + +concept EnumFieldDeclaration extends FieldDeclaration { +} + +concept BooleanFieldDeclaration extends FieldDeclaration { + @FormEditor("hide", true) + o BooleanDefault defaultValue optional +} + +concept DateTimeFieldDeclaration extends FieldDeclaration { +} + +concept StringFieldDeclaration extends FieldDeclaration { + @FormEditor("hide", true) + o StringDefault defaultValue optional + @FormEditor("hide", true) + o StringRegexValidator validator optional +} + +concept StringRegexValidator { + o String regex +} + +concept DoubleDomainValidator { + o Double lower optional + o Double upper optional +} + +concept IntegerDomainValidator { + o Integer lower optional + o Integer upper optional +} + +concept LongDomainValidator { + o Long lower optional + o Long upper optional +} + +concept DoubleFieldDeclaration extends FieldDeclaration { + o DoubleDefault defaultValue optional + o DoubleDomainValidator validator optional +} + +concept IntegerFieldDeclaration extends FieldDeclaration { + @FormEditor("hide", true) + o IntegerDefault defaultValue optional + @FormEditor("hide", true) + o IntegerDomainValidator validator optional +} + +concept LongFieldDeclaration extends FieldDeclaration { + @FormEditor("hide", true) + o LongDefault defaultValue optional + @FormEditor("hide", true) + o LongDomainValidator validator optional +} + +concept RelationshipDeclaration extends FieldDeclaration { + @FormEditor("title", "Type Name", "selectOptions", "types") + o TypeIdentifier type +} + +abstract concept Import { + o String namespace + o String uri optional +} + +concept ImportAll extends Import { +} + +concept ImportType extends Import { + o TypeIdentifier identifier +} + +concept ModelFile { + o String namespace default="my.namespace" + @FormEditor("hide", true) + o Import[] imports optional + @FormEditor("title", "Classes") + o ClassDeclaration[] declarations optional +} +`; + +/** + * Create a metamodel manager (for validation against the metamodel) + * @return {*} the metamodel manager + */ +function createMetaModelManager() { + const metaModelManager = new ModelManager(); + metaModelManager.addModelFile(metaModelCto, 'concerto.metamodel'); + return metaModelManager; +} + +/** + * Validate against the metamodel + * @param {object} input - the metamodel in JSON + * @return {object} the validated metamodel in JSON + */ +function validateMetaModel(input) { + const metaModelManager = createMetaModelManager(); + const factory = new Factory(metaModelManager); + const serializer = new Serializer(factory, metaModelManager); + // First validate the metaModel + const object = serializer.fromJSON(input); + return serializer.toJSON(object); +} + /** * Create metamodel for a field * @param {object} ast - the AST for the field @@ -200,7 +440,7 @@ function declToMetaModel(ast) { } /** - * Export metamodel + * Export metamodel from an AST * @param {object} ast - the AST for the model * @return {object} the metamodel for this model */ @@ -244,7 +484,20 @@ function modelToMetaModel(ast) { const decl = declToMetaModel(thing); metamodel.declarations.push(decl); } - return metamodel; + + // Last, validate the JSON metaModel + const mm = validateMetaModel(metamodel); + + return mm; +} + +/** + * Export metamodel from a model file + * @param {object} modelFile - the AST for the model + * @return {object} the metamodel for this model + */ +function modelFileToMetaModel(modelFile) { + return modelToMetaModel(modelFile.ast); } /** @@ -348,10 +601,13 @@ function declFromMetaModel(mm) { /** * Create a model string from a metamodel - * @param {object} mm - the metamodel + * @param {object} metaModel - the metamodel * @return {string} the string for that model */ -function modelFromMetaModel(mm) { +function ctoFromMetaModel(metaModel) { + // First, validate the JSON metaModel + const mm = validateMetaModel(metaModel); + let result = ''; result += `namespace ${mm.namespace}`; if (mm.imports && mm.imports.length > 0) { @@ -375,7 +631,19 @@ function modelFromMetaModel(mm) { return result; } +/** + * Export metamodel from a model string + * @param {object} model - the string for the model + * @return {object} the metamodel for this model + */ +function ctoToMetaModel(model) { + const ast = parser.parse(model); + return modelToMetaModel(ast); +} + module.exports = { - modelToMetaModel, - modelFromMetaModel, + metaModelCto, + modelFileToMetaModel, + ctoToMetaModel, + ctoFromMetaModel, }; diff --git a/packages/concerto-core/lib/introspect/modelfile.js b/packages/concerto-core/lib/introspect/modelfile.js index 0bfb6134c0..127a56c439 100644 --- a/packages/concerto-core/lib/introspect/modelfile.js +++ b/packages/concerto-core/lib/introspect/modelfile.js @@ -17,7 +17,6 @@ const packageJson = require('../../package.json'); const semver = require('semver'); const parser = require('./parser'); -const MetaModel = require('./metamodel'); const AssetDeclaration = require('./assetdeclaration'); const EnumDeclaration = require('./enumdeclaration'); const ConceptDeclaration = require('./conceptdeclaration'); @@ -573,14 +572,6 @@ class ModelFile { } } - /** - * Export metamodel - * @return {object} the metamodel for this model file - */ - toMetaModel() { - return MetaModel.modelToMetaModel(this.ast); - } - /** * Populate from an AST * @param {object} ast - the AST obtained from the parser diff --git a/packages/concerto-core/test/introspect/metamodel.js b/packages/concerto-core/test/introspect/metamodel.js index a54c63ccb8..0912de44a2 100644 --- a/packages/concerto-core/test/introspect/metamodel.js +++ b/packages/concerto-core/test/introspect/metamodel.js @@ -29,16 +29,20 @@ describe('MetaModel', () => { const personMetaModel = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../data/model/person.json'), 'utf8')); describe('#toMetaModel', () => { + it('should convert a CTO model to its metamodel', () => { + const mm1 = MetaModel.ctoToMetaModel(personModel); + mm1.should.deep.equal(personMetaModel); + }); - it('should convert CTO file to its metamodel', () => { + it('should convert a ModelFile to its metamodel', () => { const modelManager1 = new ModelManager(); const mf1 = new ModelFile(modelManager1, personModel); - const mm1 = mf1.toMetaModel(); + const mm1 = MetaModel.modelFileToMetaModel(mf1); mm1.should.deep.equal(personMetaModel); - const model2 = MetaModel.modelFromMetaModel(mm1); + const model2 = MetaModel.ctoFromMetaModel(mm1); const modelManager2 = new ModelManager(); const mf2 = new ModelFile(modelManager2, model2); - const mm2 = mf2.toMetaModel(); + const mm2 = MetaModel.modelFileToMetaModel(mf2); mm2.should.deep.equal(personMetaModel); }); });